@dxos/react-ui-searchlist 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/browser/index.mjs +716 -408
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +716 -408
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/Combobox/Combobox.d.ts +85 -0
  9. package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
  10. package/dist/types/src/{composites/PopoverCombobox.stories.d.ts → components/Combobox/Combobox.stories.d.ts} +10 -1
  11. package/dist/types/src/components/Combobox/Combobox.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/Combobox/index.d.ts +2 -0
  13. package/dist/types/src/components/Combobox/index.d.ts.map +1 -0
  14. package/dist/types/src/components/{Listbox.d.ts → Listbox/Listbox.d.ts} +6 -6
  15. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
  16. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +21 -0
  17. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -0
  18. package/dist/types/src/components/Listbox/index.d.ts +2 -0
  19. package/dist/types/src/components/Listbox/index.d.ts.map +1 -0
  20. package/dist/types/src/components/SearchList/SearchList.d.ts +90 -0
  21. package/dist/types/src/components/SearchList/SearchList.d.ts.map +1 -0
  22. package/dist/types/src/components/SearchList/SearchList.stories.d.ts +28 -0
  23. package/dist/types/src/components/SearchList/SearchList.stories.d.ts.map +1 -0
  24. package/dist/types/src/components/SearchList/context.d.ts +33 -0
  25. package/dist/types/src/components/SearchList/context.d.ts.map +1 -0
  26. package/dist/types/src/components/SearchList/hooks/index.d.ts +5 -0
  27. package/dist/types/src/components/SearchList/hooks/index.d.ts.map +1 -0
  28. package/dist/types/src/components/SearchList/hooks/useGlobalFilter.d.ts +34 -0
  29. package/dist/types/src/components/SearchList/hooks/useGlobalFilter.d.ts.map +1 -0
  30. package/dist/types/src/components/SearchList/hooks/useSearchListInput.d.ts +12 -0
  31. package/dist/types/src/components/SearchList/hooks/useSearchListInput.d.ts.map +1 -0
  32. package/dist/types/src/components/SearchList/hooks/useSearchListItem.d.ts +10 -0
  33. package/dist/types/src/components/SearchList/hooks/useSearchListItem.d.ts.map +1 -0
  34. package/dist/types/src/components/SearchList/hooks/useSearchListResults.d.ts +36 -0
  35. package/dist/types/src/components/SearchList/hooks/useSearchListResults.d.ts.map +1 -0
  36. package/dist/types/src/components/SearchList/index.d.ts +3 -0
  37. package/dist/types/src/components/SearchList/index.d.ts.map +1 -0
  38. package/dist/types/src/components/index.d.ts +2 -1
  39. package/dist/types/src/components/index.d.ts.map +1 -1
  40. package/dist/types/src/index.d.ts +0 -1
  41. package/dist/types/src/index.d.ts.map +1 -1
  42. package/dist/types/src/translations.d.ts +4 -2
  43. package/dist/types/src/translations.d.ts.map +1 -1
  44. package/dist/types/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +21 -18
  46. package/src/components/Combobox/Combobox.stories.tsx +62 -0
  47. package/src/components/Combobox/Combobox.tsx +343 -0
  48. package/src/components/Combobox/index.ts +5 -0
  49. package/src/components/Listbox/Listbox.stories.tsx +53 -0
  50. package/src/components/{Listbox.tsx → Listbox/Listbox.tsx} +40 -11
  51. package/src/components/Listbox/index.ts +5 -0
  52. package/src/components/SearchList/SearchList.stories.tsx +532 -0
  53. package/src/components/SearchList/SearchList.tsx +554 -0
  54. package/src/components/SearchList/context.ts +43 -0
  55. package/src/components/SearchList/hooks/index.ts +8 -0
  56. package/src/components/SearchList/hooks/useGlobalFilter.tsx +61 -0
  57. package/src/components/SearchList/hooks/useSearchListInput.ts +14 -0
  58. package/src/components/SearchList/hooks/useSearchListItem.ts +14 -0
  59. package/src/components/SearchList/hooks/useSearchListResults.ts +104 -0
  60. package/src/components/SearchList/index.ts +6 -0
  61. package/src/components/index.ts +2 -1
  62. package/src/index.ts +0 -1
  63. package/src/translations.ts +4 -2
  64. package/src/types/command-score.d.ts +16 -0
  65. package/dist/types/src/components/Listbox.d.ts.map +0 -1
  66. package/dist/types/src/components/Listbox.stories.d.ts +0 -16
  67. package/dist/types/src/components/Listbox.stories.d.ts.map +0 -1
  68. package/dist/types/src/components/SearchList.d.ts +0 -47
  69. package/dist/types/src/components/SearchList.d.ts.map +0 -1
  70. package/dist/types/src/components/SearchList.stories.d.ts +0 -16
  71. package/dist/types/src/components/SearchList.stories.d.ts.map +0 -1
  72. package/dist/types/src/composites/PopoverCombobox.d.ts +0 -32
  73. package/dist/types/src/composites/PopoverCombobox.d.ts.map +0 -1
  74. package/dist/types/src/composites/PopoverCombobox.stories.d.ts.map +0 -1
  75. package/dist/types/src/composites/index.d.ts +0 -2
  76. package/dist/types/src/composites/index.d.ts.map +0 -1
  77. package/src/components/Listbox.stories.tsx +0 -73
  78. package/src/components/SearchList.stories.tsx +0 -55
  79. package/src/components/SearchList.tsx +0 -251
  80. package/src/composites/PopoverCombobox.stories.tsx +0 -47
  81. package/src/composites/PopoverCombobox.tsx +0 -209
  82. package/src/composites/index.ts +0 -5
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-searchlist",
3
- "version": "0.8.4-main.c4373fc",
3
+ "version": "0.8.4-main.c85a9c8dae",
4
4
  "description": "A themed ⌘K-style combobox component, triggered by a button (or keyboard shortcut), where values are queried only within the invoked modal.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
- "sideEffects": true,
13
+ "sideEffects": false,
10
14
  "type": "module",
11
15
  "exports": {
12
16
  ".": {
@@ -25,29 +29,28 @@
25
29
  "src"
26
30
  ],
27
31
  "dependencies": {
28
- "@fluentui/react-tabster": "^9.24.2",
29
- "@preact-signals/safe-react": "^0.9.0",
32
+ "@fluentui/react-tabster": "9.26.11",
30
33
  "@radix-ui/react-compose-refs": "1.1.1",
31
34
  "@radix-ui/react-context": "1.1.1",
32
35
  "@radix-ui/react-use-controllable-state": "1.1.0",
33
- "cmdk": "^0.2.0"
36
+ "command-score": "0.1.2"
34
37
  },
35
38
  "devDependencies": {
36
- "@types/react": "~19.2.2",
37
- "@types/react-dom": "~19.2.1",
38
- "react": "~19.2.0",
39
- "react-dom": "~19.2.0",
40
- "vite": "7.1.9",
41
- "@dxos/random": "0.8.4-main.c4373fc",
42
- "@dxos/react-ui": "0.8.4-main.c4373fc",
43
- "@dxos/react-ui-theme": "0.8.4-main.c4373fc",
44
- "@dxos/storybook-utils": "0.8.4-main.c4373fc"
39
+ "@types/react": "~19.2.7",
40
+ "@types/react-dom": "~19.2.3",
41
+ "react": "~19.2.3",
42
+ "react-dom": "~19.2.3",
43
+ "vite": "^7.1.11",
44
+ "@dxos/random": "0.8.4-main.c85a9c8dae",
45
+ "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
46
+ "@dxos/storybook-utils": "0.8.4-main.c85a9c8dae",
47
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae"
45
48
  },
46
49
  "peerDependencies": {
47
- "react": "^19.0.0",
48
- "react-dom": "^19.0.0",
49
- "@dxos/react-ui": "0.8.4-main.c4373fc",
50
- "@dxos/react-ui-theme": "0.8.4-main.c4373fc"
50
+ "react": "~19.2.3",
51
+ "react-dom": "~19.2.3",
52
+ "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
53
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae"
51
54
  },
52
55
  "publishConfig": {
53
56
  "access": "public"
@@ -0,0 +1,62 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React 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
+ import { useSearchListResults } from '../SearchList/hooks';
13
+
14
+ import { Combobox } from './Combobox';
15
+
16
+ faker.seed(1234);
17
+
18
+ const items = faker.helpers.uniqueArray(faker.commerce.productName, 16).sort();
19
+
20
+ const DefaultStory = () => {
21
+ const { results, handleSearch } = useSearchListResults({
22
+ items,
23
+ });
24
+
25
+ return (
26
+ <Combobox.Root
27
+ placeholder='Nothing selected'
28
+ onValueChange={(value) => {
29
+ console.log('[Combobox.Root.onValueChange]', value);
30
+ }}
31
+ >
32
+ <Combobox.Trigger />
33
+ <Combobox.Content onSearch={handleSearch}>
34
+ <Combobox.Input placeholder='Search...' />
35
+ <Combobox.List>
36
+ {results.map((value) => (
37
+ <Combobox.Item key={value} value={value} label={value} />
38
+ ))}
39
+ </Combobox.List>
40
+ <Combobox.Arrow />
41
+ </Combobox.Content>
42
+ </Combobox.Root>
43
+ );
44
+ };
45
+
46
+ const meta = {
47
+ title: 'ui/react-ui-searchlist/Combobox',
48
+ component: Combobox.Root as any,
49
+ render: DefaultStory,
50
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'p-2' })],
51
+ parameters: {
52
+ translations,
53
+ },
54
+ } satisfies Meta<typeof DefaultStory>;
55
+
56
+ export default meta;
57
+
58
+ type Story = StoryObj<typeof meta>;
59
+
60
+ export const Default: Story = {
61
+ args: {},
62
+ };
@@ -0,0 +1,343 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
+ import React, { type PropsWithChildren, forwardRef, useCallback } from 'react';
8
+
9
+ import {
10
+ Button,
11
+ type ButtonProps,
12
+ Icon,
13
+ Popover,
14
+ type PopoverArrowProps,
15
+ type PopoverContentProps,
16
+ type PopoverVirtualTriggerProps,
17
+ } from '@dxos/react-ui';
18
+ import { useId } from '@dxos/react-ui';
19
+ import { mx } from '@dxos/ui-theme';
20
+
21
+ import {
22
+ SearchList,
23
+ type SearchListEmptyProps,
24
+ type SearchListInputProps,
25
+ type SearchListItemProps,
26
+ type SearchListRootProps,
27
+ type SearchListViewportProps,
28
+ } from '../SearchList';
29
+
30
+ const COMBOBOX_NAME = 'Combobox';
31
+ const COMBOBOX_CONTENT_NAME = 'ComboboxContent';
32
+ const COMBOBOX_ITEM_NAME = 'ComboboxItem';
33
+ const COMBOBOX_TRIGGER_NAME = 'ComboboxTrigger';
34
+
35
+ //
36
+ // Context
37
+ //
38
+
39
+ type ComboboxContextValue = {
40
+ modalId: string;
41
+ isCombobox: true;
42
+ placeholder?: string;
43
+ open: boolean;
44
+ onOpenChange: (nextOpen: boolean) => void;
45
+ value: string;
46
+ onValueChange: (nextValue: string) => void;
47
+ };
48
+
49
+ const [ComboboxProvider, useComboboxContext] = createContext<Partial<ComboboxContextValue>>(COMBOBOX_NAME, {});
50
+
51
+ //
52
+ // Root
53
+ //
54
+
55
+ type ComboboxRootProps = PropsWithChildren<
56
+ Partial<ComboboxContextValue & { modal: boolean; defaultOpen: boolean; defaultValue: string; placeholder: string }>
57
+ >;
58
+
59
+ const ComboboxRoot = ({
60
+ modal,
61
+ modalId: propsModalId,
62
+ open: propsOpen,
63
+ defaultOpen,
64
+ onOpenChange: propsOnOpenChange,
65
+ value: propsValue,
66
+ defaultValue,
67
+ onValueChange: propsOnValueChange,
68
+ placeholder,
69
+ children,
70
+ }: ComboboxRootProps) => {
71
+ const modalId = useId(COMBOBOX_NAME, propsModalId);
72
+ const [open = false, onOpenChange] = useControllableState({
73
+ prop: propsOpen,
74
+ onChange: propsOnOpenChange,
75
+ defaultProp: defaultOpen,
76
+ });
77
+ const [value = '', onValueChange] = useControllableState({
78
+ prop: propsValue,
79
+ onChange: propsOnValueChange,
80
+ defaultProp: defaultValue,
81
+ });
82
+
83
+ return (
84
+ <Popover.Root open={open} onOpenChange={onOpenChange} modal={modal}>
85
+ <ComboboxProvider
86
+ isCombobox
87
+ modalId={modalId}
88
+ placeholder={placeholder}
89
+ open={open}
90
+ onOpenChange={onOpenChange}
91
+ value={value}
92
+ onValueChange={onValueChange}
93
+ >
94
+ {children}
95
+ </ComboboxProvider>
96
+ </Popover.Root>
97
+ );
98
+ };
99
+
100
+ //
101
+ // ContentProps
102
+ //
103
+
104
+ type ComboboxContentProps = SearchListRootProps & PopoverContentProps & { label?: string };
105
+
106
+ const ComboboxContent = forwardRef<HTMLDivElement, ComboboxContentProps>(
107
+ (
108
+ {
109
+ side = 'bottom',
110
+ collisionPadding = 48,
111
+ sideOffset,
112
+ align,
113
+ alignOffset,
114
+ avoidCollisions,
115
+ collisionBoundary,
116
+ arrowPadding,
117
+ sticky,
118
+ hideWhenDetached,
119
+ onOpenAutoFocus,
120
+ onCloseAutoFocus,
121
+ onEscapeKeyDown,
122
+ onPointerDownOutside,
123
+ onFocusOutside,
124
+ onInteractOutside,
125
+ forceMount,
126
+ children,
127
+ classNames,
128
+ onSearch,
129
+ value,
130
+ defaultValue,
131
+ debounceMs,
132
+ label,
133
+ },
134
+ forwardedRef,
135
+ ) => {
136
+ const { modalId } = useComboboxContext(COMBOBOX_CONTENT_NAME);
137
+
138
+ return (
139
+ <Popover.Content
140
+ {...{
141
+ side,
142
+ sideOffset,
143
+ align,
144
+ alignOffset,
145
+ avoidCollisions,
146
+ collisionBoundary,
147
+ collisionPadding,
148
+ arrowPadding,
149
+ sticky,
150
+ hideWhenDetached,
151
+ onOpenAutoFocus,
152
+ onCloseAutoFocus,
153
+ onEscapeKeyDown,
154
+ onPointerDownOutside,
155
+ onFocusOutside,
156
+ onInteractOutside,
157
+ forceMount,
158
+ }}
159
+ classNames={[
160
+ 'w-(--radix-popover-trigger-width) max-h-(--radix-popover-content-available-height) grid grid-rows-[min-content_1fr]',
161
+ classNames,
162
+ ]}
163
+ id={modalId}
164
+ ref={forwardedRef}
165
+ >
166
+ <SearchList.Root onSearch={onSearch} value={value} defaultValue={defaultValue} debounceMs={debounceMs}>
167
+ <SearchList.Content>{children}</SearchList.Content>
168
+ </SearchList.Root>
169
+ </Popover.Content>
170
+ );
171
+ },
172
+ );
173
+
174
+ ComboboxContent.displayName = COMBOBOX_CONTENT_NAME;
175
+
176
+ //
177
+ // Trigger
178
+ //
179
+
180
+ type ComboboxTriggerProps = ButtonProps;
181
+
182
+ const ComboboxTrigger = forwardRef<HTMLButtonElement, ComboboxTriggerProps>(
183
+ ({ children, onClick, ...props }, forwardedRef) => {
184
+ const { modalId, open, onOpenChange, placeholder, value } = useComboboxContext(COMBOBOX_TRIGGER_NAME);
185
+ const handleClick = useCallback(
186
+ (event: Parameters<Exclude<ButtonProps['onClick'], undefined>>[0]) => {
187
+ onClick?.(event);
188
+ onOpenChange?.(true);
189
+ },
190
+ [onClick, onOpenChange],
191
+ );
192
+
193
+ return (
194
+ <Popover.Trigger asChild>
195
+ <Button
196
+ {...props}
197
+ role='combobox'
198
+ aria-expanded={open}
199
+ aria-controls={modalId}
200
+ aria-haspopup='dialog'
201
+ onClick={handleClick}
202
+ ref={forwardedRef}
203
+ >
204
+ {children ?? (
205
+ <>
206
+ <span className={mx('font-normal text-start flex-1 min-w-0 truncate me-2', !value && 'text-subdued')}>
207
+ {value || placeholder}
208
+ </span>
209
+ <Icon icon='ph--caret-down--bold' size={3} />
210
+ </>
211
+ )}
212
+ </Button>
213
+ </Popover.Trigger>
214
+ );
215
+ },
216
+ );
217
+
218
+ ComboboxTrigger.displayName = COMBOBOX_TRIGGER_NAME;
219
+
220
+ //
221
+ // VirtualTrigger
222
+ //
223
+
224
+ type ComboboxVirtualTriggerProps = PopoverVirtualTriggerProps;
225
+
226
+ const ComboboxVirtualTrigger = Popover.VirtualTrigger;
227
+
228
+ //
229
+ // Input
230
+ //
231
+
232
+ type ComboboxInputProps = SearchListInputProps;
233
+
234
+ const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(({ classNames, ...props }, forwardedRef) => {
235
+ return (
236
+ <SearchList.Input
237
+ {...props}
238
+ classNames={['m-form-chrome mb-0 w-[calc(100%-2*var(--spacing-form-chrome))]', classNames]}
239
+ ref={forwardedRef}
240
+ />
241
+ );
242
+ });
243
+
244
+ //
245
+ // List
246
+ //
247
+
248
+ type ComboboxListProps = SearchListViewportProps;
249
+
250
+ const ComboboxList = forwardRef<HTMLDivElement, ComboboxListProps>(({ classNames, ...props }, forwardedRef) => {
251
+ return <SearchList.Viewport {...props} classNames={['py-form-chrome', classNames]} ref={forwardedRef} />;
252
+ });
253
+
254
+ //
255
+ // Item
256
+ //
257
+
258
+ type ComboboxItemProps = SearchListItemProps & {
259
+ /** Whether to close the popover when this item is selected. Defaults to true. */
260
+ closeOnSelect?: boolean;
261
+ };
262
+
263
+ const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
264
+ ({ classNames, onSelect, value, closeOnSelect = true, ...props }, forwardedRef) => {
265
+ const { onValueChange, onOpenChange } = useComboboxContext(COMBOBOX_ITEM_NAME);
266
+ const handleSelect = useCallback<NonNullable<SearchListItemProps['onSelect']>>(() => {
267
+ onSelect?.();
268
+ if (value !== undefined) {
269
+ onValueChange?.(value);
270
+ }
271
+ if (closeOnSelect) {
272
+ onOpenChange?.(false);
273
+ }
274
+ }, [onSelect, onValueChange, onOpenChange, value, closeOnSelect]);
275
+
276
+ return (
277
+ <SearchList.Item
278
+ {...props}
279
+ value={value}
280
+ classNames={['mx-form-chrome px-form-chrome', classNames]}
281
+ onSelect={handleSelect}
282
+ ref={forwardedRef}
283
+ />
284
+ );
285
+ },
286
+ );
287
+
288
+ ComboboxItem.displayName = COMBOBOX_ITEM_NAME;
289
+
290
+ //
291
+ // Arrow
292
+ //
293
+
294
+ type ComboboxArrowProps = PopoverArrowProps;
295
+
296
+ const ComboboxArrow = Popover.Arrow;
297
+
298
+ //
299
+ // Empty
300
+ //
301
+
302
+ type ComboboxEmptyProps = SearchListEmptyProps;
303
+
304
+ const ComboboxEmpty = SearchList.Empty;
305
+
306
+ //
307
+ // Combobox
308
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox
309
+ //
310
+
311
+ //
312
+ // Portal
313
+ //
314
+
315
+ type ComboboxPortalProps = React.ComponentPropsWithoutRef<typeof Popover.Portal>;
316
+
317
+ const ComboboxPortal = Popover.Portal;
318
+
319
+ export const Combobox = {
320
+ Root: ComboboxRoot,
321
+ Portal: ComboboxPortal,
322
+ Content: ComboboxContent,
323
+ Trigger: ComboboxTrigger,
324
+ VirtualTrigger: ComboboxVirtualTrigger,
325
+ Input: ComboboxInput,
326
+ List: ComboboxList,
327
+ Item: ComboboxItem,
328
+ Arrow: ComboboxArrow,
329
+ Empty: ComboboxEmpty,
330
+ };
331
+
332
+ export type {
333
+ ComboboxRootProps,
334
+ ComboboxPortalProps,
335
+ ComboboxContentProps,
336
+ ComboboxTriggerProps,
337
+ ComboboxVirtualTriggerProps,
338
+ ComboboxInputProps,
339
+ ComboboxListProps,
340
+ ComboboxItemProps,
341
+ ComboboxArrowProps,
342
+ ComboboxEmptyProps,
343
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './Combobox';
@@ -0,0 +1,53 @@
1
+ //
2
+ // Copyright 2023 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 { Listbox } from './Listbox';
14
+
15
+ faker.seed(1234);
16
+
17
+ type StoryItem = { value: string; label: string };
18
+
19
+ const options: StoryItem[] = faker.helpers.multiple(
20
+ () => ({ value: faker.string.uuid(), label: faker.commerce.productName() }) satisfies StoryItem,
21
+ { count: 16 },
22
+ );
23
+
24
+ const DefaultStory = () => {
25
+ const [selectedValue, setSelectedValue] = useState<string>();
26
+
27
+ return (
28
+ <Listbox.Root value={selectedValue} onValueChange={setSelectedValue}>
29
+ {options.map((option) => (
30
+ <Listbox.Option key={option.value} value={option.value}>
31
+ <Listbox.OptionLabel>{option.label}</Listbox.OptionLabel>
32
+ <Listbox.OptionIndicator />
33
+ </Listbox.Option>
34
+ ))}
35
+ </Listbox.Root>
36
+ );
37
+ };
38
+
39
+ const meta = {
40
+ title: 'ui/react-ui-searchlist/Listbox',
41
+ component: Listbox.Root,
42
+ render: DefaultStory,
43
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'p-2' })],
44
+ parameters: {
45
+ translations,
46
+ },
47
+ } satisfies Meta<typeof Listbox.Root>;
48
+
49
+ export default meta;
50
+
51
+ type Story = StoryObj<typeof meta>;
52
+
53
+ export const Default: Story = {};
@@ -9,25 +9,22 @@ import { useControllableState } from '@radix-ui/react-use-controllable-state';
9
9
  import React, { type ComponentPropsWithRef, forwardRef, useCallback, useEffect, useRef } from 'react';
10
10
 
11
11
  import { Icon, type IconProps, type ThemedClassName } from '@dxos/react-ui';
12
- import { mx } from '@dxos/react-ui-theme';
12
+ import { mx } from '@dxos/ui-theme';
13
13
 
14
- import { commandItem, searchListItem } from './SearchList';
14
+ const commandItem = 'flex items-center overflow-hidden';
15
15
 
16
16
  const LISTBOX_NAME = 'Listbox';
17
17
  const LISTBOX_OPTION_NAME = 'ListboxOption';
18
18
  const LISTBOX_OPTION_LABEL_NAME = 'ListboxOptionLabel';
19
19
  const LISTBOX_OPTION_INDICATOR_NAME = 'ListboxOptionIndicator';
20
20
 
21
+ //
22
+ // Context
23
+ //
24
+
21
25
  type ListboxScopedProps<P> = P & { __listboxScope?: Scope };
22
26
  type ListboxOptionScopedProps<P> = P & { __listboxOptionScope?: Scope };
23
27
 
24
- type ListboxRootProps = ThemedClassName<ComponentPropsWithRef<'ul'>> & {
25
- value?: string;
26
- defaultValue?: string;
27
- onValueChange?: (value: string) => void;
28
- autoFocus?: boolean;
29
- };
30
-
31
28
  type ListboxOptionProps = ThemedClassName<ComponentPropsWithRef<'li'>> & {
32
29
  value: string;
33
30
  };
@@ -51,6 +48,17 @@ const [ListboxProvider, useListboxContext] = createListboxContext<ListboxContext
51
48
  const [ListboxOptionProvider, useListboxOptionContext] =
52
49
  createListboxOptionContext<ListboxOptionContextValue>(LISTBOX_OPTION_NAME);
53
50
 
51
+ //
52
+ // Root
53
+ //
54
+
55
+ type ListboxRootProps = ThemedClassName<ComponentPropsWithRef<'ul'>> & {
56
+ value?: string;
57
+ defaultValue?: string;
58
+ onValueChange?: (value: string) => void;
59
+ autoFocus?: boolean;
60
+ };
61
+
54
62
  // TODO(thure): Note that this overlaps significantly with the the `SelectableListbox` story of `List.tsx` in `react-ui`,
55
63
  // making this an exemplar of `List` specifying standard `role="listbox"` interactivity, though it is here because it
56
64
  // coheres with SearchList’s styles and norms. This can be promoted to `react-ui`, but doing so should involve clearing
@@ -92,7 +100,7 @@ const ListboxRoot = forwardRef<HTMLUListElement, ListboxRootProps>(
92
100
  <ul
93
101
  role='listbox'
94
102
  {...rootProps}
95
- className={mx('p-cardSpacingChrome', classNames)}
103
+ className={mx('w-full p-form-chrome', classNames)}
96
104
  ref={rootRef}
97
105
  {...arrowGroup}
98
106
  >
@@ -105,6 +113,10 @@ const ListboxRoot = forwardRef<HTMLUListElement, ListboxRootProps>(
105
113
 
106
114
  ListboxRoot.displayName = LISTBOX_NAME;
107
115
 
116
+ //
117
+ // Option
118
+ //
119
+
108
120
  const ListboxOption = forwardRef<HTMLLIElement, ListboxOptionProps>(
109
121
  (props: ListboxScopedProps<ListboxOptionProps>, forwardedRef) => {
110
122
  const { __listboxScope, children, classNames, value, ...rootProps } = props;
@@ -123,7 +135,12 @@ const ListboxOption = forwardRef<HTMLLIElement, ListboxOptionProps>(
123
135
  {...rootProps}
124
136
  aria-selected={isSelected}
125
137
  tabIndex={0}
126
- className={mx('dx-focus-ring', commandItem, searchListItem, classNames)}
138
+ className={mx(
139
+ 'dx-focus-ring',
140
+ 'py-1 px-2 rounded-xs select-none cursor-pointer data-[selected=true]:bg-hover-overlay hover:bg-hover-overlay',
141
+ commandItem,
142
+ classNames,
143
+ )}
127
144
  onClick={handleSelect}
128
145
  onKeyDown={({ key }) => {
129
146
  if (['Enter', ' '].includes(key)) {
@@ -141,6 +158,10 @@ const ListboxOption = forwardRef<HTMLLIElement, ListboxOptionProps>(
141
158
 
142
159
  ListboxOption.displayName = LISTBOX_OPTION_NAME;
143
160
 
161
+ //
162
+ // OptionLabel
163
+ //
164
+
144
165
  const ListboxOptionLabel = forwardRef<HTMLDivElement, ThemedClassName<ComponentPropsWithRef<'div'>>>(
145
166
  ({ children, classNames, ...rootProps }, forwardedRef) => {
146
167
  return (
@@ -155,6 +176,10 @@ ListboxOptionLabel.displayName = LISTBOX_OPTION_LABEL_NAME;
155
176
 
156
177
  type ListboxOptionIndicatorProps = Omit<IconProps, 'icon'> & Partial<Pick<IconProps, 'icon'>>;
157
178
 
179
+ //
180
+ // OptionIndicator
181
+ //
182
+
158
183
  const ListboxOptionIndicator = forwardRef<SVGSVGElement, ListboxOptionIndicatorProps>(
159
184
  (props: ListboxOptionScopedProps<ListboxOptionIndicatorProps>, forwardedRef) => {
160
185
  const { __listboxOptionScope, classNames, ...rootProps } = props;
@@ -173,6 +198,10 @@ const ListboxOptionIndicator = forwardRef<SVGSVGElement, ListboxOptionIndicatorP
173
198
 
174
199
  ListboxOptionIndicator.displayName = LISTBOX_OPTION_INDICATOR_NAME;
175
200
 
201
+ //
202
+ // Listbox
203
+ //
204
+
176
205
  export const Listbox = {
177
206
  Root: ListboxRoot,
178
207
  Option: ListboxOption,
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './Listbox';