@dxos/react-ui-searchlist 0.8.3 → 0.8.4-main.1068cf700f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +741 -265
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +741 -265
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Combobox/Combobox.d.ts +84 -0
- package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
- package/dist/types/src/components/Combobox/Combobox.stories.d.ts +21 -0
- package/dist/types/src/components/Combobox/Combobox.stories.d.ts.map +1 -0
- package/dist/types/src/components/Combobox/index.d.ts +2 -0
- package/dist/types/src/components/Combobox/index.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/Listbox.d.ts +31 -0
- package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts +21 -0
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/index.d.ts +2 -0
- package/dist/types/src/components/Listbox/index.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/SearchList.d.ts +88 -0
- package/dist/types/src/components/SearchList/SearchList.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/SearchList.stories.d.ts +28 -0
- package/dist/types/src/components/SearchList/SearchList.stories.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/context.d.ts +33 -0
- package/dist/types/src/components/SearchList/context.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/hooks/index.d.ts +5 -0
- package/dist/types/src/components/SearchList/hooks/index.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/hooks/useGlobalFilter.d.ts +34 -0
- package/dist/types/src/components/SearchList/hooks/useGlobalFilter.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/hooks/useSearchListInput.d.ts +12 -0
- package/dist/types/src/components/SearchList/hooks/useSearchListInput.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/hooks/useSearchListItem.d.ts +10 -0
- package/dist/types/src/components/SearchList/hooks/useSearchListItem.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/hooks/useSearchListResults.d.ts +36 -0
- package/dist/types/src/components/SearchList/hooks/useSearchListResults.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/index.d.ts +3 -0
- package/dist/types/src/components/SearchList/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +7 -6
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +24 -20
- package/src/components/Combobox/Combobox.stories.tsx +62 -0
- package/src/components/Combobox/Combobox.tsx +348 -0
- package/src/components/Combobox/index.ts +5 -0
- package/src/components/Listbox/Listbox.stories.tsx +53 -0
- package/src/components/Listbox/Listbox.tsx +214 -0
- package/src/components/Listbox/index.ts +5 -0
- package/src/components/SearchList/SearchList.stories.tsx +532 -0
- package/src/components/SearchList/SearchList.tsx +560 -0
- package/src/components/SearchList/context.ts +43 -0
- package/src/components/SearchList/hooks/index.ts +8 -0
- package/src/components/SearchList/hooks/useGlobalFilter.tsx +61 -0
- package/src/components/SearchList/hooks/useSearchListInput.ts +14 -0
- package/src/components/SearchList/hooks/useSearchListItem.ts +14 -0
- package/src/components/SearchList/hooks/useSearchListResults.ts +104 -0
- package/src/components/SearchList/index.ts +6 -0
- package/src/components/index.ts +2 -0
- package/src/index.ts +1 -2
- package/src/translations.ts +8 -4
- package/src/types/command-score.d.ts +16 -0
- package/dist/lib/node/index.cjs +0 -324
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/types/src/components/SearchList.d.ts +0 -44
- package/dist/types/src/components/SearchList.d.ts.map +0 -1
- package/dist/types/src/components/SearchList.stories.d.ts +0 -15
- package/dist/types/src/components/SearchList.stories.d.ts.map +0 -1
- package/dist/types/src/composites/PopoverCombobox.d.ts +0 -32
- package/dist/types/src/composites/PopoverCombobox.d.ts.map +0 -1
- package/dist/types/src/composites/PopoverCombobox.stories.d.ts +0 -28
- package/dist/types/src/composites/PopoverCombobox.stories.d.ts.map +0 -1
- package/dist/types/src/composites/index.d.ts +0 -2
- package/dist/types/src/composites/index.d.ts.map +0 -1
- package/src/components/SearchList.stories.tsx +0 -47
- package/src/components/SearchList.tsx +0 -250
- package/src/composites/PopoverCombobox.stories.tsx +0 -44
- package/src/composites/PopoverCombobox.tsx +0 -186
- package/src/composites/index.ts +0 -5
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { faker } from '@dxos/random';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { translations } from '../../translations';
|
|
12
|
+
|
|
13
|
+
import { useSearchListInput, useSearchListItem, useSearchListResults } from './hooks';
|
|
14
|
+
import { SearchList } from './SearchList';
|
|
15
|
+
|
|
16
|
+
faker.seed(1234);
|
|
17
|
+
|
|
18
|
+
type StoryItem = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
icon?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const defaultItems: StoryItem[] = faker.helpers.uniqueArray(faker.commerce.productName, 16).map((label) => ({
|
|
25
|
+
id: faker.string.uuid(),
|
|
26
|
+
label,
|
|
27
|
+
icon: 'ph--file--regular',
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
//
|
|
31
|
+
// Default Story - Basic composition with SearchList.Item
|
|
32
|
+
//
|
|
33
|
+
|
|
34
|
+
type DefaultStoryProps = {
|
|
35
|
+
items?: StoryItem[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DefaultStory = ({ items = defaultItems }: DefaultStoryProps) => {
|
|
39
|
+
const { results, handleSearch } = useSearchListResults({ items });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
43
|
+
<SearchList.Content classNames='bs-[400px]'>
|
|
44
|
+
<SearchList.Input placeholder='Search items...' autoFocus />
|
|
45
|
+
<SearchList.Viewport>
|
|
46
|
+
{results.length > 0 ? (
|
|
47
|
+
results.map((item) => (
|
|
48
|
+
<SearchList.Item
|
|
49
|
+
key={item.id}
|
|
50
|
+
value={item.id}
|
|
51
|
+
label={item.label}
|
|
52
|
+
icon={item.icon}
|
|
53
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id, item.label)}
|
|
54
|
+
/>
|
|
55
|
+
))
|
|
56
|
+
) : (
|
|
57
|
+
<SearchList.Empty>No results found</SearchList.Empty>
|
|
58
|
+
)}
|
|
59
|
+
</SearchList.Viewport>
|
|
60
|
+
</SearchList.Content>
|
|
61
|
+
</SearchList.Root>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
//
|
|
66
|
+
// Controlled Story - Controlled query state
|
|
67
|
+
//
|
|
68
|
+
|
|
69
|
+
const ControlledStory = ({ items = defaultItems }: DefaultStoryProps) => {
|
|
70
|
+
const [query, setQuery] = useState('');
|
|
71
|
+
const [results, setResults] = useState<StoryItem[]>(items);
|
|
72
|
+
|
|
73
|
+
const handleSearch = (searchQuery: string) => {
|
|
74
|
+
if (!searchQuery) {
|
|
75
|
+
setResults(items);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const filtered = items.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
79
|
+
setResults(filtered);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleQueryChange = (newQuery: string) => {
|
|
83
|
+
setQuery(newQuery);
|
|
84
|
+
handleSearch(newQuery);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className='is-full bs-[400px] flex flex-col gap-2'>
|
|
89
|
+
<div className='text-sm text-description'>Controlled query: "{query}"</div>
|
|
90
|
+
<SearchList.Root onSearch={handleSearch} value={query}>
|
|
91
|
+
<SearchList.Input placeholder='Controlled search...' onChange={(e) => handleQueryChange(e.target.value)} />
|
|
92
|
+
<SearchList.Content>
|
|
93
|
+
<SearchList.Viewport>
|
|
94
|
+
{results.map((item) => (
|
|
95
|
+
<SearchList.Item
|
|
96
|
+
key={item.id}
|
|
97
|
+
value={item.id}
|
|
98
|
+
label={item.label}
|
|
99
|
+
icon={item.icon}
|
|
100
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id)}
|
|
101
|
+
/>
|
|
102
|
+
))}
|
|
103
|
+
</SearchList.Viewport>
|
|
104
|
+
</SearchList.Content>
|
|
105
|
+
</SearchList.Root>
|
|
106
|
+
<button className='pli-2 plb-1 rounded bg-accentSurface text-accentText' onClick={() => handleQueryChange('')}>
|
|
107
|
+
Clear Query
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//
|
|
114
|
+
// Custom Rendering Story - Custom components in Content using useSearchItem hook
|
|
115
|
+
//
|
|
116
|
+
|
|
117
|
+
type CustomItemProps = {
|
|
118
|
+
value: string;
|
|
119
|
+
label: string;
|
|
120
|
+
description: string;
|
|
121
|
+
onSelect?: () => void;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const CustomItem = ({ value, label, description, onSelect }: CustomItemProps) => {
|
|
125
|
+
const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
|
|
126
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
127
|
+
const isSelected = selectedValue === value;
|
|
128
|
+
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
registerItem(value, ref.current, onSelect);
|
|
131
|
+
return () => unregisterItem(value);
|
|
132
|
+
}, [value, onSelect, registerItem, unregisterItem]);
|
|
133
|
+
|
|
134
|
+
// Scroll into view when selected.
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (isSelected && ref.current) {
|
|
137
|
+
ref.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
138
|
+
}
|
|
139
|
+
}, [isSelected]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
ref={ref}
|
|
144
|
+
role='option'
|
|
145
|
+
aria-selected={isSelected}
|
|
146
|
+
data-selected={isSelected}
|
|
147
|
+
className={`p-2 border-be border-separator cursor-pointer ${isSelected ? 'bg-hoverOverlay' : 'hover:bg-hoverOverlay'}`}
|
|
148
|
+
onClick={onSelect}
|
|
149
|
+
>
|
|
150
|
+
<div className='font-medium'>{label}</div>
|
|
151
|
+
<div className='text-xs text-description'>{description}</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const CustomRenderingStory = ({ items = defaultItems }: DefaultStoryProps) => {
|
|
157
|
+
const { results, handleSearch } = useSearchListResults({ items });
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className='is-full bs-[400px] flex flex-col'>
|
|
161
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
162
|
+
<SearchList.Input placeholder='Search with custom rendering...' autoFocus />
|
|
163
|
+
<SearchList.Content>
|
|
164
|
+
<SearchList.Viewport>
|
|
165
|
+
{results.map((item) => (
|
|
166
|
+
<CustomItem
|
|
167
|
+
key={item.id}
|
|
168
|
+
value={item.id}
|
|
169
|
+
label={item.label}
|
|
170
|
+
description={`ID: ${item.id}`}
|
|
171
|
+
onSelect={() => console.log('[CustomItem.onSelect]', item.id, item.label)}
|
|
172
|
+
/>
|
|
173
|
+
))}
|
|
174
|
+
</SearchList.Viewport>
|
|
175
|
+
</SearchList.Content>
|
|
176
|
+
</SearchList.Root>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
//
|
|
182
|
+
// With Empty Story - Show Empty component when no results
|
|
183
|
+
//
|
|
184
|
+
|
|
185
|
+
const WithEmptyStory = () => {
|
|
186
|
+
const [hasSearched, setHasSearched] = useState(false);
|
|
187
|
+
|
|
188
|
+
const handleSearch = (query: string) => {
|
|
189
|
+
setHasSearched(!!query);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className='is-full bs-[400px] flex flex-col'>
|
|
194
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
195
|
+
<SearchList.Input placeholder='Try searching for anything...' />
|
|
196
|
+
<SearchList.Content>
|
|
197
|
+
{hasSearched ? (
|
|
198
|
+
<SearchList.Empty classNames='text-center text-description p-4'>
|
|
199
|
+
<div className='text-lg'>🔍</div>
|
|
200
|
+
<div>No results found</div>
|
|
201
|
+
<div className='text-xs'>Try a different search term</div>
|
|
202
|
+
</SearchList.Empty>
|
|
203
|
+
) : (
|
|
204
|
+
<SearchList.Empty classNames='text-center text-description p-4'>
|
|
205
|
+
<div>Start typing to search</div>
|
|
206
|
+
</SearchList.Empty>
|
|
207
|
+
)}
|
|
208
|
+
</SearchList.Content>
|
|
209
|
+
</SearchList.Root>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
//
|
|
215
|
+
// Without Viewport Story - Content without scrolling
|
|
216
|
+
//
|
|
217
|
+
|
|
218
|
+
const WithoutViewportStory = ({ items = defaultItems }: DefaultStoryProps) => {
|
|
219
|
+
const { results, handleSearch } = useSearchListResults({ items });
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className='is-full bs-[300px] flex flex-col'>
|
|
223
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
224
|
+
<SearchList.Input placeholder='Search without viewport (no scroll)...' classNames='shrink-0' />
|
|
225
|
+
<SearchList.Content>
|
|
226
|
+
{results.map((item) => (
|
|
227
|
+
<SearchList.Item
|
|
228
|
+
key={item.id}
|
|
229
|
+
value={item.id}
|
|
230
|
+
label={item.label}
|
|
231
|
+
icon={item.icon}
|
|
232
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id)}
|
|
233
|
+
/>
|
|
234
|
+
))}
|
|
235
|
+
</SearchList.Content>
|
|
236
|
+
</SearchList.Root>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
//
|
|
242
|
+
// With Icons Story - Various icon configurations
|
|
243
|
+
//
|
|
244
|
+
|
|
245
|
+
const iconsItems: StoryItem[] = [
|
|
246
|
+
{ id: '1', label: 'Document', icon: 'ph--file-text--regular' },
|
|
247
|
+
{ id: '2', label: 'Folder', icon: 'ph--folder--regular' },
|
|
248
|
+
{ id: '3', label: 'Image', icon: 'ph--image--regular' },
|
|
249
|
+
{ id: '4', label: 'Settings', icon: 'ph--gear--regular' },
|
|
250
|
+
{ id: '5', label: 'No icon item' },
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const WithIconsStory = () => {
|
|
254
|
+
return (
|
|
255
|
+
<div className='is-full flex flex-col'>
|
|
256
|
+
<SearchList.Root>
|
|
257
|
+
<SearchList.Input placeholder='Search items with icons...' />
|
|
258
|
+
<SearchList.Content>
|
|
259
|
+
{iconsItems.map((item) => (
|
|
260
|
+
<SearchList.Item
|
|
261
|
+
key={item.id}
|
|
262
|
+
value={item.id}
|
|
263
|
+
label={item.label}
|
|
264
|
+
icon={item.icon}
|
|
265
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id)}
|
|
266
|
+
/>
|
|
267
|
+
))}
|
|
268
|
+
</SearchList.Content>
|
|
269
|
+
</SearchList.Root>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
//
|
|
275
|
+
// Custom Input Story - Demonstrate using hooks for custom input
|
|
276
|
+
//
|
|
277
|
+
|
|
278
|
+
const CustomInput = () => {
|
|
279
|
+
const { query, onQueryChange, selectedValue, onSelectedValueChange, getItemValues, triggerSelect } =
|
|
280
|
+
useSearchListInput();
|
|
281
|
+
|
|
282
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
283
|
+
const values = getItemValues();
|
|
284
|
+
if (values.length === 0) {
|
|
285
|
+
if (event.key === 'Escape') {
|
|
286
|
+
onQueryChange('');
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const currentIndex = selectedValue !== undefined ? values.indexOf(selectedValue) : -1;
|
|
292
|
+
|
|
293
|
+
switch (event.key) {
|
|
294
|
+
case 'ArrowDown': {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
const nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, values.length - 1);
|
|
297
|
+
const nextValue = values[nextIndex];
|
|
298
|
+
if (nextValue !== undefined) {
|
|
299
|
+
onSelectedValueChange(nextValue);
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case 'ArrowUp': {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
const prevIndex = currentIndex === -1 ? values.length - 1 : Math.max(currentIndex - 1, 0);
|
|
306
|
+
const prevValue = values[prevIndex];
|
|
307
|
+
if (prevValue !== undefined) {
|
|
308
|
+
onSelectedValueChange(prevValue);
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'Enter': {
|
|
313
|
+
if (selectedValue !== undefined) {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
triggerSelect();
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'Escape': {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
if (selectedValue !== undefined) {
|
|
322
|
+
onSelectedValueChange(undefined);
|
|
323
|
+
} else {
|
|
324
|
+
onQueryChange('');
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div className='flex gap-2 items-center p-2 bg-input rounded'>
|
|
333
|
+
<input
|
|
334
|
+
type='text'
|
|
335
|
+
value={query}
|
|
336
|
+
onChange={(ev) => onQueryChange(ev.target.value)}
|
|
337
|
+
onKeyDown={handleKeyDown}
|
|
338
|
+
placeholder='Custom input...'
|
|
339
|
+
className='bg-transparent outline-none grow'
|
|
340
|
+
/>
|
|
341
|
+
{query && (
|
|
342
|
+
<button onClick={() => onQueryChange('')} className='text-description hover:text-baseText'>
|
|
343
|
+
✕
|
|
344
|
+
</button>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const CustomInputStory = ({ items = defaultItems }: DefaultStoryProps) => {
|
|
351
|
+
const { results, handleSearch } = useSearchListResults({ items });
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className='is-full bs-[400px] flex flex-col border border-separator'>
|
|
355
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
356
|
+
<CustomInput />
|
|
357
|
+
<SearchList.Content>
|
|
358
|
+
<SearchList.Viewport>
|
|
359
|
+
{results.map((item) => (
|
|
360
|
+
<SearchList.Item
|
|
361
|
+
key={item.id}
|
|
362
|
+
value={item.id}
|
|
363
|
+
label={item.label}
|
|
364
|
+
icon={item.icon}
|
|
365
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id)}
|
|
366
|
+
/>
|
|
367
|
+
))}
|
|
368
|
+
</SearchList.Viewport>
|
|
369
|
+
</SearchList.Content>
|
|
370
|
+
</SearchList.Root>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
//
|
|
376
|
+
// With Disabled Items Story
|
|
377
|
+
//
|
|
378
|
+
|
|
379
|
+
const disabledItems: StoryItem[] = [
|
|
380
|
+
{ id: '1', label: 'Available item 1', icon: 'ph--check--regular' },
|
|
381
|
+
{ id: '2', label: 'Disabled item (cannot select)', icon: 'ph--prohibit--regular' },
|
|
382
|
+
{ id: '3', label: 'Available item 2', icon: 'ph--check--regular' },
|
|
383
|
+
{ id: '4', label: 'Disabled item 2', icon: 'ph--prohibit--regular' },
|
|
384
|
+
{ id: '5', label: 'Available item 3', icon: 'ph--check--regular' },
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const WithDisabledItemsStory = () => {
|
|
388
|
+
return (
|
|
389
|
+
<div className='is-full flex flex-col'>
|
|
390
|
+
<SearchList.Root>
|
|
391
|
+
<SearchList.Input placeholder='Arrow keys skip disabled items...' autoFocus />
|
|
392
|
+
<SearchList.Content>
|
|
393
|
+
{disabledItems.map((item, index) => (
|
|
394
|
+
<SearchList.Item
|
|
395
|
+
key={item.id}
|
|
396
|
+
value={item.id}
|
|
397
|
+
label={item.label}
|
|
398
|
+
icon={item.icon}
|
|
399
|
+
disabled={index === 1 || index === 3}
|
|
400
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id)}
|
|
401
|
+
/>
|
|
402
|
+
))}
|
|
403
|
+
</SearchList.Content>
|
|
404
|
+
</SearchList.Root>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
//
|
|
410
|
+
// With Groups Story
|
|
411
|
+
//
|
|
412
|
+
|
|
413
|
+
type GroupedItem = StoryItem & { category: string };
|
|
414
|
+
|
|
415
|
+
const groupedItems: GroupedItem[] = [
|
|
416
|
+
{ id: '1', label: 'Document 1', icon: 'ph--file-text--regular', category: 'Documents' },
|
|
417
|
+
{ id: '2', label: 'Document 2', icon: 'ph--file-text--regular', category: 'Documents' },
|
|
418
|
+
{ id: '3', label: 'Image 1', icon: 'ph--image--regular', category: 'Images' },
|
|
419
|
+
{ id: '4', label: 'Image 2', icon: 'ph--image--regular', category: 'Images' },
|
|
420
|
+
{ id: '5', label: 'Settings', icon: 'ph--gear--regular', category: 'Other' },
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const WithGroupsStory = () => {
|
|
424
|
+
const { results, handleSearch } = useSearchListResults({ items: groupedItems });
|
|
425
|
+
|
|
426
|
+
// Group items by category.
|
|
427
|
+
const grouped = results.reduce(
|
|
428
|
+
(acc, item) => {
|
|
429
|
+
if (!acc[item.category]) {
|
|
430
|
+
acc[item.category] = [];
|
|
431
|
+
}
|
|
432
|
+
acc[item.category].push(item);
|
|
433
|
+
return acc;
|
|
434
|
+
},
|
|
435
|
+
{} as Record<string, GroupedItem[]>,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<div className='is-full bs-[400px] flex flex-col'>
|
|
440
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
441
|
+
<SearchList.Input placeholder='Search grouped items...' autoFocus />
|
|
442
|
+
<SearchList.Content>
|
|
443
|
+
<SearchList.Viewport>
|
|
444
|
+
{Object.entries(grouped).map(([category, items]) => (
|
|
445
|
+
<SearchList.Group key={category} heading={category}>
|
|
446
|
+
{items.map((item) => (
|
|
447
|
+
<SearchList.Item
|
|
448
|
+
key={item.id}
|
|
449
|
+
value={item.id}
|
|
450
|
+
label={item.label}
|
|
451
|
+
icon={item.icon}
|
|
452
|
+
onSelect={() => console.log('[SearchList.Item.onSelect]', item.id, item.label)}
|
|
453
|
+
/>
|
|
454
|
+
))}
|
|
455
|
+
</SearchList.Group>
|
|
456
|
+
))}
|
|
457
|
+
{results.length === 0 && <SearchList.Empty>No results found</SearchList.Empty>}
|
|
458
|
+
</SearchList.Viewport>
|
|
459
|
+
</SearchList.Content>
|
|
460
|
+
</SearchList.Root>
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
//
|
|
466
|
+
// Meta
|
|
467
|
+
//
|
|
468
|
+
|
|
469
|
+
const meta = {
|
|
470
|
+
title: 'ui/react-ui-searchlist/SearchList',
|
|
471
|
+
component: SearchList.Root as any,
|
|
472
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
473
|
+
parameters: {
|
|
474
|
+
layout: 'fullscreen',
|
|
475
|
+
translations,
|
|
476
|
+
},
|
|
477
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
478
|
+
|
|
479
|
+
export default meta;
|
|
480
|
+
|
|
481
|
+
type Story = StoryObj<typeof meta>;
|
|
482
|
+
|
|
483
|
+
export const Default: Story = {
|
|
484
|
+
render: DefaultStory,
|
|
485
|
+
args: {
|
|
486
|
+
items: defaultItems,
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
export const Controlled: Story = {
|
|
491
|
+
render: ControlledStory,
|
|
492
|
+
args: {
|
|
493
|
+
items: defaultItems,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export const CustomRendering: Story = {
|
|
498
|
+
render: CustomRenderingStory,
|
|
499
|
+
args: {
|
|
500
|
+
items: defaultItems,
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export const WithEmpty: Story = {
|
|
505
|
+
render: WithEmptyStory,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
export const WithoutViewport: Story = {
|
|
509
|
+
render: WithoutViewportStory,
|
|
510
|
+
args: {
|
|
511
|
+
items: defaultItems,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
export const WithIcons: Story = {
|
|
516
|
+
render: WithIconsStory,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export const CustomInputExample: Story = {
|
|
520
|
+
render: CustomInputStory,
|
|
521
|
+
args: {
|
|
522
|
+
items: defaultItems,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export const WithDisabledItems: Story = {
|
|
527
|
+
render: WithDisabledItemsStory,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export const WithGroups: Story = {
|
|
531
|
+
render: WithGroupsStory,
|
|
532
|
+
};
|