Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 435aef4

Browse files
authored
feat: implemented multi-select component (#117)
* fix: creation of multi-select component * chore: added select-switch-multiselect to the build
1 parent c960f15 commit 435aef4

14 files changed

+1040
-2
lines changed

src/client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./primitives/Markdown";
33
export * from "./primitives/MarkdownEditor";
44
export * from "./components/Form";
55
export * from "./components/GenericProgressForm";
6+
export * from "./components/MultipleSelect";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { Meta, Story, Controls, Canvas } from "@storybook/blocks";
2+
3+
import * as MultipleSelectStories from "./MultipleSelect.stories";
4+
5+
<Meta
6+
of={MultipleSelectStories}
7+
parameters={{
8+
layout: "centered",
9+
docs: {
10+
story: {
11+
inline: true,
12+
iframeHeight: 200,
13+
},
14+
},
15+
}}
16+
/>
17+
18+
# MultipleSelect
19+
20+
A flexible multi-select component with grouped options, exclusive selections, and collapsible sections.
21+
22+
## Props
23+
24+
### Component Props
25+
26+
- **`options`**: `MultipleSelectGroup[]` - Array of option groups
27+
- **`defaultValue`**: `Record<string, string[]>` - Initial selections per group
28+
- **`placeholder`**: `string` - Text shown when nothing selected if defaultValue then placeholder will be ignored
29+
- **`className`**: `string` - Popover container CSS classes
30+
- **`variants`**: `object` - Style customizations with the following options:
31+
- **`color`**: `"default" | "grey"` - Controls the overall color scheme
32+
- **`size`**: `"default" | "sm"` - Controls component sizing
33+
- **`rounded`**: `"default"` - Controls border radius styling
34+
- **`itemsPosition`**: `"start" | "end" | "center"` - Controls alignment of items in the list
35+
- **`headerPosition`**: `"start" | "end" | "center"` - Controls alignment of group headers
36+
- **`triggerTextColor`**: `"default" | "red" | "green"` - Controls the trigger text color
37+
- **`itemsColor`**: `"default" | "light-grey"` - Controls the color of items and their icons
38+
- **`onChange`**: `(values: Record<string, string[]>) => void` - Selection change handler
39+
40+
### Group Properties (`MultipleSelectGroup`)
41+
42+
- **`groupLabel?`**: `string` - Group name (omit for ungrouped items)
43+
- **`multiple?`**: `boolean` - Allow multiple selections (default: true)
44+
- **`collapsible?`**: `boolean` - Enable group toggle
45+
- **`items`**: `MultipleSelectItem[]` - Array of options
46+
47+
### Item Properties (`MultipleSelectItem`)
48+
49+
- **`value`**: `string` - Unique identifier
50+
- **`label`**: `string` - Display text
51+
- **`exclusive?`**: `boolean` - Clear other selections when chosen
52+
- **`exclusiveScope?`**: `"group" | "global"` - Clear scope for exclusive items
53+
- **`iconType?`**: `IconType` - Optional icon identifier check `@/primitives/Icon story`
54+
- **`icon?`**: `React.ComponentType` - Optional icon component used if no iconType is provided
55+
- **`itemClassName?`**: `string` - Item-specific CSS classes
56+
57+
---
58+
59+
<Canvas of={MultipleSelectStories.FilterExample}>
60+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", padding: "1rem" }}>
61+
<Story
62+
of={MultipleSelectStories.FilterExample}
63+
parameters={{
64+
docs: {
65+
canvas: { sourceState: "shown" },
66+
},
67+
}}
68+
/>
69+
</div>
70+
</Canvas>
71+
<Controls of={MultipleSelectStories.FilterExample} />
72+
73+
## Stories
74+
75+
Below are several usage examples showcasing common patterns and advanced features.
76+
77+
### 1. Basic
78+
79+
<Canvas of={MultipleSelectStories.Basic}>
80+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", padding: "1rem" }}>
81+
<Story
82+
of={MultipleSelectStories.Basic}
83+
parameters={{
84+
layout: "centered",
85+
docs: {
86+
canvas: { sourceState: "shown" },
87+
},
88+
}}
89+
/>
90+
</div>
91+
</Canvas>
92+
93+
**Description**
94+
A single ungrouped set of items (no group label) with simple placeholder text.
95+
96+
- **Options**: 3 items, no exclusivity.
97+
- **`onChange`** logs the selection to console.
98+
99+
### 2. OrderByExample
100+
101+
<Canvas of={MultipleSelectStories.OrderByExample}>
102+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", padding: "1rem" }}>
103+
<Story
104+
of={MultipleSelectStories.OrderByExample}
105+
parameters={{
106+
layout: "centered",
107+
docs: {
108+
canvas: { sourceState: "shown" },
109+
},
110+
}}
111+
/>
112+
</div>
113+
</Canvas>
114+
115+
**Description** Demonstrates **exclusive items** across two single-select groups (“Order by time” and
116+
“Order by name”). Each item has `exclusive: true, exclusiveScope: 'global'`, ensuring that if you pick
117+
“Recent,” it clears any “A-Z” or “Z-A” selection.
118+
119+
- **`defaultValue`** sets the initial selection to `["Recent"]` in the “ORDER BY TIME” group.
120+
- **`variants`** example usage for adjusting text color, alignment, etc.
121+
122+
### 3. FilterExample
123+
124+
<Canvas of={MultipleSelectStories.FilterExample}>
125+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", padding: "1rem" }}>
126+
<Story
127+
of={MultipleSelectStories.FilterExample}
128+
parameters={{
129+
layout: "centered",
130+
docs: {
131+
canvas: { sourceState: "shown" },
132+
},
133+
}}
134+
/>
135+
</div>
136+
</Canvas>
137+
138+
**Description** A **filter** scenario with an **“All”** exclusive item for resetting.
139+
140+
- “All” is in an **ungrouped** set with `exclusive: true, exclusiveScope: 'global'`.
141+
- Two collapsible groups, “Network” and “Status,” each with `multiple: true`.
142+
- **`defaultValue`** starts with “All” selected in the ungrouped items.
143+
144+
### 4. MixedSelectionTypes
145+
146+
<Canvas of={MultipleSelectStories.MixedSelectionTypes}>
147+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", padding: "1rem" }}>
148+
<Story
149+
of={MultipleSelectStories.MixedSelectionTypes}
150+
parameters={{
151+
layout: "centered",
152+
docs: {
153+
canvas: { sourceState: "shown" },
154+
},
155+
}}
156+
/>
157+
</div>
158+
</Canvas>
159+
160+
**Description** Shows a combination of:
161+
162+
- A group with an **exclusive** item called “Reset All.”
163+
- Another group that’s **multi-select** with standard options.
164+
165+
This approach is useful when you have a special action (like “Reset” or “Clear All”) plus normal multi-select items.
166+
167+
### 5. WithVariants
168+
169+
<Canvas of={MultipleSelectStories.WithVariants}>
170+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", padding: "1rem" }}>
171+
<Story
172+
of={MultipleSelectStories.WithVariants}
173+
parameters={{
174+
layout: "centered",
175+
docs: {
176+
canvas: { sourceState: "shown" },
177+
},
178+
}}
179+
/>
180+
</div>
181+
</Canvas>
182+
183+
**Description**
184+
Showcases using **all variant props** (`triggerTextColor`, `headerPosition`, `itemsPosition`, etc.) or whatever your **MultipleSelect** has to customize.
185+
186+
- You can set text color, alignment, plus a collapsible group.
187+
- Useful to see how to pass styling variants from **tailwind-variants** to the component.
188+
189+
---
190+
191+
## Tips & Best Practices
192+
193+
1. **Use `defaultValue`** for initial selections, or manage selections outside if you want it to be “controlled.”
194+
2. **`exclusive`** items are great for “select all,” “select none,” or “reset.”
195+
3. **Collapsible groups** let you nest large sets of items without overwhelming the user.
196+
197+
---
198+
199+
## Conclusion
200+
201+
`MultipleSelect` is highly **configurable** for your needs: from a simple single group with no advanced logic, to multi-group filters, collapsible sections, exclusive “all” items, or complex order-by pickers.
202+
203+
Check out the source code & stories for additional usage details.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { action } from "@storybook/addon-actions";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import { MultipleSelect } from "./MultipleSelect";
5+
6+
const onChange = action("onChange");
7+
8+
const meta = {
9+
title: "Components/MultipleSelect",
10+
component: MultipleSelect,
11+
parameters: {
12+
layout: "centered",
13+
},
14+
} satisfies Meta<typeof MultipleSelect>;
15+
16+
export default meta;
17+
type Story = StoryObj<typeof MultipleSelect>;
18+
19+
// Basic example with single group
20+
export const Basic: Story = {
21+
args: {
22+
options: [
23+
{
24+
items: [
25+
{ value: "1", label: "Option 1" },
26+
{ value: "2", label: "Option 2" },
27+
{ value: "3", label: "Option 3" },
28+
],
29+
},
30+
],
31+
placeholder: "Select options",
32+
onChange: (values) => onChange(values),
33+
},
34+
};
35+
36+
// Example with exclusive options (like order by)
37+
export const OrderByExample: Story = {
38+
args: {
39+
options: [
40+
{
41+
groupLabel: "ORDER BY TIME",
42+
multiple: false,
43+
items: ["Recent", "Oldest"].map((value) => ({
44+
label: value,
45+
value,
46+
exclusive: true,
47+
exclusiveScope: "global",
48+
})),
49+
},
50+
{
51+
groupLabel: "ORDER BY NAME",
52+
multiple: false,
53+
items: ["A-Z", "Z-A"].map((value) => ({
54+
label: value,
55+
value,
56+
exclusive: true,
57+
exclusiveScope: "global",
58+
})),
59+
},
60+
],
61+
defaultValue: { "ORDER BY TIME": ["Recent"] },
62+
variants: {
63+
triggerTextColor: "green",
64+
headerPosition: "end",
65+
itemsPosition: "end",
66+
},
67+
placeholder: "Order by",
68+
className: "w-40",
69+
onChange: (values) => onChange(values),
70+
},
71+
};
72+
73+
// Example with filters including "All" option and collapsible groups
74+
export const FilterExample: Story = {
75+
args: {
76+
options: [
77+
{
78+
multiple: false,
79+
items: [
80+
{
81+
label: "All",
82+
value: "All-id",
83+
exclusive: true,
84+
exclusiveScope: "global",
85+
},
86+
],
87+
},
88+
{
89+
groupLabel: "Network",
90+
multiple: true,
91+
collapsible: true,
92+
items: [
93+
{ label: "Rounds on Ethereum", value: "1" },
94+
{ label: "Rounds on Polygon", value: "137" },
95+
{ label: "Rounds on Optimism", value: "10" },
96+
],
97+
},
98+
{
99+
groupLabel: "Status",
100+
multiple: true,
101+
collapsible: true,
102+
items: [
103+
{ label: "Active", value: "active" },
104+
{ label: "Taking Applications", value: "applications" },
105+
{ label: "Finished", value: "finished" },
106+
],
107+
},
108+
],
109+
defaultValue: { ungrouped: ["All-id"] },
110+
variants: { triggerTextColor: "red" },
111+
placeholder: "Filter by",
112+
className: "w-64",
113+
onChange: (values) => onChange(values),
114+
},
115+
};
116+
117+
// Example with mixed exclusive and non-exclusive options
118+
export const MixedSelectionTypes: Story = {
119+
args: {
120+
options: [
121+
{
122+
groupLabel: "Special Actions",
123+
items: [
124+
{
125+
label: "Reset All",
126+
value: "reset",
127+
exclusive: true,
128+
exclusiveScope: "global",
129+
},
130+
],
131+
},
132+
{
133+
groupLabel: "Regular Options",
134+
multiple: true,
135+
items: [
136+
{ label: "Option 1", value: "1" },
137+
{ label: "Option 2", value: "2" },
138+
{ label: "Option 3", value: "3" },
139+
],
140+
},
141+
],
142+
placeholder: "Select options",
143+
onChange: (values) => onChange(values),
144+
},
145+
};
146+
147+
// Example with all variant options
148+
export const WithVariants: Story = {
149+
args: {
150+
options: [
151+
{
152+
groupLabel: "Group 1",
153+
multiple: true,
154+
collapsible: true,
155+
items: [
156+
{ label: "Option 1", value: "1" },
157+
{ label: "Option 2", value: "2" },
158+
],
159+
},
160+
],
161+
162+
variants: {
163+
triggerTextColor: "green",
164+
headerPosition: "end",
165+
itemsPosition: "end",
166+
},
167+
168+
placeholder: "Select with variants",
169+
onChange: (values) => onChange(values),
170+
className: "w-40",
171+
},
172+
};

0 commit comments

Comments
 (0)