Skip to content

Commit a0b5fc8

Browse files
TMZZ031130fu050409
andauthored
feat(list): add List component (#26)
Resolved #6 --------- Co-authored-by: 苏向夜 <[email protected]>
1 parent d61b43c commit a0b5fc8

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

.changes/add-list-component.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@matechat/react": patch:feat
3+
---
4+
5+
Add `List` component in `list.tsx` to support grouped or normal lists.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./bubble";
22
export * from "./button";
3+
export * from "./list";
34
export * from "./prompt";
45
export * from "./sender";

src/list.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import clsx from "clsx";
2+
import { twMerge } from "tailwind-merge";
3+
4+
export type SelectOptionsType = SelectOption[] | OptionGroup[];
5+
6+
export interface SelectOption {
7+
label?: string;
8+
value?: string;
9+
className?: string;
10+
[key: string]: unknown;
11+
}
12+
13+
export interface OptionGroup {
14+
label?: string;
15+
[key: string]: unknown;
16+
}
17+
18+
export interface ListProps extends React.ComponentProps<"div"> {
19+
value: string | undefined;
20+
options: SelectOptionsType | undefined;
21+
className?: string;
22+
optionGroupChildren?: string;
23+
optionGroupLabel?: string;
24+
optionGroupTemplate?: (group: OptionGroup) => React.ReactNode;
25+
optionLabel?: string;
26+
optionValue?: string;
27+
onSelected?: (value: string) => void;
28+
}
29+
30+
export function List({
31+
value,
32+
options,
33+
className,
34+
optionGroupChildren = "items",
35+
optionGroupLabel = "label",
36+
optionGroupTemplate,
37+
optionLabel = "label",
38+
optionValue = "value",
39+
onSelected,
40+
...props
41+
}: ListProps) {
42+
const renderOption = (option: SelectOption) => (
43+
<button
44+
type="button"
45+
tabIndex={-1}
46+
data-slot="list-item"
47+
key={
48+
(option[optionValue] as string | number) ??
49+
(option[optionLabel] as string | number)
50+
}
51+
className={twMerge(
52+
clsx(
53+
"cursor-pointer px-4 py-2 text-sm hover:bg-blue-300 block w-full text-left",
54+
value === option[optionValue] &&
55+
"bg-blue-500 hover:bg-blue-500 font-semibold",
56+
),
57+
)}
58+
onClick={() => onSelected?.(option[optionValue] as string)}
59+
onKeyDown={(e) => {
60+
if (e.key === "Enter" || e.key === " ") {
61+
e.preventDefault();
62+
onSelected?.(option[optionValue] as string);
63+
}
64+
}}
65+
>
66+
{option[optionLabel] as React.ReactNode}
67+
</button>
68+
);
69+
70+
const renderGroup = (group: OptionGroup) => {
71+
const children = (group?.[optionGroupChildren] as SelectOption[]) ?? [];
72+
73+
return (
74+
<div
75+
key={
76+
(group[optionGroupLabel] as string | number) ??
77+
(group.code as string | number)
78+
}
79+
>
80+
<div
81+
data-solt="list-label"
82+
className="px-3 py-2 bg-gray-100 font-medium text-sm flex items-center gap-2"
83+
>
84+
{optionGroupTemplate
85+
? (optionGroupTemplate(group) as React.ReactNode)
86+
: (group[optionGroupLabel] as React.ReactNode)}
87+
</div>
88+
{children.map(renderOption)}
89+
</div>
90+
);
91+
};
92+
93+
const isGrouped =
94+
Array.isArray(options) &&
95+
options.length > 0 &&
96+
typeof options[0] === "object" &&
97+
optionGroupChildren &&
98+
Array.isArray(options[0][optionGroupChildren]);
99+
100+
return (
101+
<div
102+
data-slot="list"
103+
className={twMerge(clsx("w-full max-w-xs overflow-y-auto", className))}
104+
{...props}
105+
>
106+
{Array.isArray(options) &&
107+
(isGrouped
108+
? (options as OptionGroup[]).map(renderGroup)
109+
: (options as SelectOption[]).map(renderOption))}
110+
</div>
111+
);
112+
}

0 commit comments

Comments
 (0)