import clsx from "clsx";
import { twMerge } from "tailwind-merge";
export type UnknownRecord = Record<string, unknown>;
export type GroupData = UnknownRecord & {
[key: string]: unknown;
};
export type OptionData = UnknownRecord & {
[key: string]: unknown;
};
export type SelectItemOptionsType = OptionData[] | GroupData[];
interface ListChangeTargetOptions {
name: string;
id: string;
value: unknown;
}
interface ListChangeEvent {
originalEvent: React.SyntheticEvent;
value: unknown;
stopPropagation(): void;
preventDefault(): void;
target: ListChangeTargetOptions;
}
export interface ListProps
extends Omit<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>,
"onChange"
> {
listBoxStyle?: React.CSSProperties | undefined;
listGroupStyle?: React.CSSProperties | undefined;
listItemStyle?: React.CSSProperties | undefined;
listStyle?: React.CSSProperties | undefined;
optionGroupChildren?: string | undefined;
optionGroupLabel?: string | undefined;
optionLabel: string;
options?: SelectItemOptionsType | undefined;
value?: string | null;
onChange?: (event: ListChangeEvent) => void;
}
interface OptionGroupItem {
optionGroup: GroupData;
group: boolean;
index: number;
label: unknown;
}
export type MixedOptionArray = Array<OptionGroupItem | OptionData>;
export const List = ({
listBoxStyle,
listGroupStyle,
listItemStyle,
listStyle,
optionGroupChildren,
optionGroupLabel,
optionLabel,
options,
value,
onChange,
...props
}: ListProps) => {
const handleSelect = (
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
option: UnknownRecord,
) => {
const optionValue = option[optionLabel];
const changeEvent: ListChangeEvent = {
originalEvent: event,
value: optionValue,
stopPropagation: () => event.stopPropagation(),
preventDefault: () => event.preventDefault(),
target: {
name: option.label?.toString() || "",
id: "list",
value: optionValue,
},
};
onChange?.(changeEvent);
};
const flatOptions = (options: GroupData[] | undefined) => {
return (options || []).reduce(
(result: MixedOptionArray, option: GroupData, index: number) => {
result.push({
optionGroup: option,
group: true,
index,
label: option[optionGroupLabel as keyof GroupData],
});
const groupChildren = optionGroupChildren
? option[optionGroupChildren as keyof GroupData]
: null;
if (Array.isArray(groupChildren)) {
groupChildren.forEach((o: OptionData) => {
result.push(o);
});
}
return result;
},
[] as MixedOptionArray,
);
};
const createItem = (option: OptionData | OptionGroupItem, index: number) => {
const isOptionGroupItem = (
item: OptionData | OptionGroupItem,
): item is OptionGroupItem => {
return "group" in item && item.group === true;
};
if (isOptionGroupItem(option)) {
const groupContent =
option.optionGroup[optionGroupLabel as keyof GroupData];
const key = `group_${index}_${String(groupContent)}`;
return (
<li
key={key}
role-slot="list-group"
aria-label={String(groupContent || "")}
className={clsx("font-bold dark:text-gray-400 py-2 px-4 text-sm")}
style={listGroupStyle}
>
{String(groupContent || " ")}
</li>
);
}
const optionData = option as OptionData;
const optionKey = optionData[optionLabel as keyof OptionData];
const key = `option_${index}_${String(optionKey)}`;
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
handleSelect(e, optionData);
};
const optionValue = optionData[optionLabel];
const isSelected = String(optionValue) === String(value);
return (
<li
key={key}
role-slot="list-item"
className={twMerge(
clsx("py-2 px-4 hover:bg-gray-200 text-gray-600 cursor-pointer", {
"bg-blue-500 text-white hover:bg-blue-600": isSelected,
}),
)}
id={`lo_id_${index}`}
style={listItemStyle}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect(
e as
| React.MouseEvent<HTMLElement>
| React.KeyboardEvent<HTMLElement>,
optionData,
);
}
}}
>
{String(optionKey || "")}
</li>
);
};
const createList = () => {
const finalOptions = optionGroupLabel
? flatOptions(options as GroupData[] | undefined)
: (options as OptionData[]);
return (
<ul role-slot="listbox" style={listBoxStyle}>
{finalOptions?.map((option, index) => createItem(option, index))}
</ul>
);
};
const list = createList();
return (
<div style={listStyle} data-slot="list" {...props}>
{list}
</div>
);
};