@dxos/react-ui-searchlist 0.8.4-main.a4bbb77 → 0.8.4-main.ae835ea
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 +193 -216
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +193 -216
- 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 +44 -0
- package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
- package/dist/types/src/{composites/PopoverCombobox.stories.d.ts → components/Combobox/Combobox.stories.d.ts} +10 -1
- 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.d.ts → Listbox/Listbox.d.ts} +6 -6
- 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.d.ts → SearchList/SearchList.d.ts} +5 -28
- package/dist/types/src/components/SearchList/SearchList.d.ts.map +1 -0
- package/dist/types/src/components/{SearchList.stories.d.ts → SearchList/SearchList.stories.d.ts} +9 -0
- package/dist/types/src/components/SearchList/SearchList.stories.d.ts.map +1 -0
- package/dist/types/src/components/SearchList/index.d.ts +2 -0
- package/dist/types/src/components/SearchList/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +2 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +3 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/components/Combobox/Combobox.stories.tsx +57 -0
- package/src/components/Combobox/Combobox.tsx +335 -0
- package/src/components/Combobox/index.ts +5 -0
- package/src/components/Listbox/Listbox.stories.tsx +53 -0
- package/src/components/{Listbox.tsx → Listbox/Listbox.tsx} +33 -9
- package/src/components/Listbox/index.ts +5 -0
- package/src/components/{SearchList.stories.tsx → SearchList/SearchList.stories.tsx} +17 -8
- package/src/components/SearchList/SearchList.tsx +163 -0
- package/src/components/SearchList/index.ts +5 -0
- package/src/components/index.ts +2 -1
- package/src/index.ts +0 -1
- package/src/translations.ts +3 -1
- package/dist/types/src/components/Listbox.d.ts.map +0 -1
- package/dist/types/src/components/Listbox.stories.d.ts +0 -16
- package/dist/types/src/components/Listbox.stories.d.ts.map +0 -1
- package/dist/types/src/components/SearchList.d.ts.map +0 -1
- 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.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/Listbox.stories.tsx +0 -73
- package/src/components/SearchList.tsx +0 -251
- package/src/composites/PopoverCombobox.stories.tsx +0 -47
- package/src/composites/PopoverCombobox.tsx +0 -209
- package/src/composites/index.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-searchlist",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.ae835ea",
|
|
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",
|
|
@@ -33,21 +33,21 @@
|
|
|
33
33
|
"cmdk": "^0.2.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@types/react": "~19.2.
|
|
37
|
-
"@types/react-dom": "~19.2.
|
|
36
|
+
"@types/react": "~19.2.2",
|
|
37
|
+
"@types/react-dom": "~19.2.2",
|
|
38
38
|
"react": "~19.2.0",
|
|
39
39
|
"react-dom": "~19.2.0",
|
|
40
40
|
"vite": "7.1.9",
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
41
|
+
"@dxos/random": "0.8.4-main.ae835ea",
|
|
42
|
+
"@dxos/react-ui": "0.8.4-main.ae835ea",
|
|
43
|
+
"@dxos/react-ui-theme": "0.8.4-main.ae835ea",
|
|
44
|
+
"@dxos/storybook-utils": "0.8.4-main.ae835ea"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": "^19.0.0",
|
|
48
48
|
"react-dom": "^19.0.0",
|
|
49
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
50
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
49
|
+
"@dxos/react-ui": "0.8.4-main.ae835ea",
|
|
50
|
+
"@dxos/react-ui-theme": "0.8.4-main.ae835ea"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|
|
13
|
+
import { Combobox } from './Combobox';
|
|
14
|
+
|
|
15
|
+
faker.seed(1234);
|
|
16
|
+
|
|
17
|
+
const items = faker.helpers.uniqueArray(faker.commerce.productName, 16).sort();
|
|
18
|
+
|
|
19
|
+
const DefaultStory = () => {
|
|
20
|
+
return (
|
|
21
|
+
<Combobox.Root
|
|
22
|
+
placeholder='Nothing selected'
|
|
23
|
+
onValueChange={(value) => {
|
|
24
|
+
console.log('[Combobox.Root.onValueChange]', value);
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<Combobox.Trigger />
|
|
28
|
+
<Combobox.Content filter={(value, search) => (value.includes(search) ? 1 : 0)}>
|
|
29
|
+
<Combobox.Input placeholder='Search...' />
|
|
30
|
+
<Combobox.List>
|
|
31
|
+
{items.map((value) => (
|
|
32
|
+
<Combobox.Item key={value}>{value}</Combobox.Item>
|
|
33
|
+
))}
|
|
34
|
+
</Combobox.List>
|
|
35
|
+
<Combobox.Arrow />
|
|
36
|
+
</Combobox.Content>
|
|
37
|
+
</Combobox.Root>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const meta = {
|
|
42
|
+
title: 'ui/react-ui-searchlist/Combobox',
|
|
43
|
+
component: Combobox.Root as any,
|
|
44
|
+
render: DefaultStory,
|
|
45
|
+
decorators: [withTheme, withLayout({ container: 'column', classNames: 'p-2' })],
|
|
46
|
+
parameters: {
|
|
47
|
+
translations,
|
|
48
|
+
},
|
|
49
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
50
|
+
|
|
51
|
+
export default meta;
|
|
52
|
+
|
|
53
|
+
type Story = StoryObj<typeof meta>;
|
|
54
|
+
|
|
55
|
+
export const Default: Story = {
|
|
56
|
+
args: {},
|
|
57
|
+
};
|
|
@@ -0,0 +1,335 @@
|
|
|
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, staticPlaceholderText } from '@dxos/react-ui-theme';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
SearchList,
|
|
23
|
+
type SearchListContentProps,
|
|
24
|
+
type SearchListEmptyProps,
|
|
25
|
+
type SearchListInputProps,
|
|
26
|
+
type SearchListItemProps,
|
|
27
|
+
type SearchListRootProps,
|
|
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;
|
|
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
|
+
...props
|
|
129
|
+
},
|
|
130
|
+
forwardedRef,
|
|
131
|
+
) => {
|
|
132
|
+
const { modalId } = useComboboxContext(COMBOBOX_CONTENT_NAME);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Popover.Content
|
|
136
|
+
{...{
|
|
137
|
+
side,
|
|
138
|
+
sideOffset,
|
|
139
|
+
align,
|
|
140
|
+
alignOffset,
|
|
141
|
+
avoidCollisions,
|
|
142
|
+
collisionBoundary,
|
|
143
|
+
collisionPadding,
|
|
144
|
+
arrowPadding,
|
|
145
|
+
sticky,
|
|
146
|
+
hideWhenDetached,
|
|
147
|
+
onOpenAutoFocus,
|
|
148
|
+
onCloseAutoFocus,
|
|
149
|
+
onEscapeKeyDown,
|
|
150
|
+
onPointerDownOutside,
|
|
151
|
+
onFocusOutside,
|
|
152
|
+
onInteractOutside,
|
|
153
|
+
forceMount,
|
|
154
|
+
}}
|
|
155
|
+
classNames={[
|
|
156
|
+
'is-[--radix-popover-trigger-width] max-bs-[--radix-popover-content-available-height] grid grid-rows-[min-content_1fr]',
|
|
157
|
+
classNames,
|
|
158
|
+
]}
|
|
159
|
+
id={modalId}
|
|
160
|
+
ref={forwardedRef}
|
|
161
|
+
>
|
|
162
|
+
<SearchList.Root {...props} classNames='contents density-fine' role='none'>
|
|
163
|
+
{children}
|
|
164
|
+
</SearchList.Root>
|
|
165
|
+
</Popover.Content>
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
ComboboxContent.displayName = COMBOBOX_CONTENT_NAME;
|
|
171
|
+
|
|
172
|
+
//
|
|
173
|
+
// Trigger
|
|
174
|
+
//
|
|
175
|
+
|
|
176
|
+
type ComboboxTriggerProps = ButtonProps;
|
|
177
|
+
|
|
178
|
+
const ComboboxTrigger = forwardRef<HTMLButtonElement, ComboboxTriggerProps>(
|
|
179
|
+
({ children, onClick, ...props }, forwardedRef) => {
|
|
180
|
+
const { modalId, open, onOpenChange, placeholder, value } = useComboboxContext(COMBOBOX_TRIGGER_NAME);
|
|
181
|
+
const handleClick = useCallback(
|
|
182
|
+
(event: Parameters<Exclude<ButtonProps['onClick'], undefined>>[0]) => {
|
|
183
|
+
onClick?.(event);
|
|
184
|
+
onOpenChange?.(true);
|
|
185
|
+
},
|
|
186
|
+
[onClick, onOpenChange],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<Popover.Trigger asChild>
|
|
191
|
+
<Button
|
|
192
|
+
{...props}
|
|
193
|
+
role='combobox'
|
|
194
|
+
aria-expanded={open}
|
|
195
|
+
aria-controls={modalId}
|
|
196
|
+
aria-haspopup='dialog'
|
|
197
|
+
onClick={handleClick}
|
|
198
|
+
ref={forwardedRef}
|
|
199
|
+
>
|
|
200
|
+
{children ?? (
|
|
201
|
+
<>
|
|
202
|
+
<span
|
|
203
|
+
className={mx('font-normal text-start flex-1 min-is-0 truncate mie-2', !value && staticPlaceholderText)}
|
|
204
|
+
>
|
|
205
|
+
{value || placeholder}
|
|
206
|
+
</span>
|
|
207
|
+
<Icon icon='ph--caret-down--bold' size={3} />
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</Button>
|
|
211
|
+
</Popover.Trigger>
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
ComboboxTrigger.displayName = COMBOBOX_TRIGGER_NAME;
|
|
217
|
+
|
|
218
|
+
//
|
|
219
|
+
// VirtualTrigger
|
|
220
|
+
//
|
|
221
|
+
|
|
222
|
+
type ComboboxVirtualTriggerProps = PopoverVirtualTriggerProps;
|
|
223
|
+
|
|
224
|
+
const ComboboxVirtualTrigger = Popover.VirtualTrigger;
|
|
225
|
+
|
|
226
|
+
//
|
|
227
|
+
// Input
|
|
228
|
+
//
|
|
229
|
+
|
|
230
|
+
type ComboboxInputProps = SearchListInputProps;
|
|
231
|
+
|
|
232
|
+
const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(({ classNames, ...props }, forwardedRef) => {
|
|
233
|
+
return (
|
|
234
|
+
<SearchList.Input
|
|
235
|
+
{...props}
|
|
236
|
+
classNames={[
|
|
237
|
+
'mli-cardSpacingChrome mbs-cardSpacingChrome mbe-0 is-[calc(100%-2*var(--dx-cardSpacingChrome))]',
|
|
238
|
+
classNames,
|
|
239
|
+
]}
|
|
240
|
+
ref={forwardedRef}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
//
|
|
246
|
+
// List
|
|
247
|
+
//
|
|
248
|
+
|
|
249
|
+
type ComboboxListProps = SearchListContentProps;
|
|
250
|
+
|
|
251
|
+
const ComboboxList = forwardRef<HTMLDivElement, ComboboxListProps>(({ classNames, ...props }, forwardedRef) => {
|
|
252
|
+
return (
|
|
253
|
+
<SearchList.Content
|
|
254
|
+
{...props}
|
|
255
|
+
classNames={['min-bs-0 overflow-y-auto plb-cardSpacingChrome', classNames]}
|
|
256
|
+
ref={forwardedRef}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
//
|
|
262
|
+
// Item
|
|
263
|
+
//
|
|
264
|
+
|
|
265
|
+
type ComboboxItemProps = SearchListItemProps;
|
|
266
|
+
|
|
267
|
+
const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
|
|
268
|
+
({ classNames, onSelect, ...props }, forwardedRef) => {
|
|
269
|
+
const { onValueChange, onOpenChange } = useComboboxContext(COMBOBOX_ITEM_NAME);
|
|
270
|
+
const handleSelect = useCallback<NonNullable<SearchListItemProps['onSelect']>>(
|
|
271
|
+
(nextValue) => {
|
|
272
|
+
onSelect?.(nextValue);
|
|
273
|
+
onValueChange?.(nextValue);
|
|
274
|
+
onOpenChange?.(false);
|
|
275
|
+
},
|
|
276
|
+
[onSelect, onValueChange, onOpenChange],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<SearchList.Item
|
|
281
|
+
{...props}
|
|
282
|
+
classNames={['mli-cardSpacingChrome pli-cardSpacingChrome', classNames]}
|
|
283
|
+
onSelect={handleSelect}
|
|
284
|
+
ref={forwardedRef}
|
|
285
|
+
/>
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
ComboboxItem.displayName = COMBOBOX_ITEM_NAME;
|
|
291
|
+
|
|
292
|
+
//
|
|
293
|
+
// Arrow
|
|
294
|
+
//
|
|
295
|
+
|
|
296
|
+
type ComboboxArrowProps = PopoverArrowProps;
|
|
297
|
+
|
|
298
|
+
const ComboboxArrow = Popover.Arrow;
|
|
299
|
+
|
|
300
|
+
//
|
|
301
|
+
// Empty
|
|
302
|
+
//
|
|
303
|
+
|
|
304
|
+
type ComboboxEmptyProps = SearchListEmptyProps;
|
|
305
|
+
|
|
306
|
+
const ComboboxEmpty = SearchList.Empty;
|
|
307
|
+
|
|
308
|
+
//
|
|
309
|
+
// Combobox
|
|
310
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox
|
|
311
|
+
//
|
|
312
|
+
|
|
313
|
+
export const Combobox = {
|
|
314
|
+
Root: ComboboxRoot,
|
|
315
|
+
Content: ComboboxContent,
|
|
316
|
+
Trigger: ComboboxTrigger,
|
|
317
|
+
VirtualTrigger: ComboboxVirtualTrigger,
|
|
318
|
+
Input: ComboboxInput,
|
|
319
|
+
List: ComboboxList,
|
|
320
|
+
Item: ComboboxItem,
|
|
321
|
+
Arrow: ComboboxArrow,
|
|
322
|
+
Empty: ComboboxEmpty,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export type {
|
|
326
|
+
ComboboxRootProps,
|
|
327
|
+
ComboboxContentProps,
|
|
328
|
+
ComboboxTriggerProps,
|
|
329
|
+
ComboboxVirtualTriggerProps,
|
|
330
|
+
ComboboxInputProps,
|
|
331
|
+
ComboboxListProps,
|
|
332
|
+
ComboboxItemProps,
|
|
333
|
+
ComboboxArrowProps,
|
|
334
|
+
ComboboxEmptyProps,
|
|
335
|
+
};
|
|
@@ -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({ container: '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 = {};
|
|
@@ -11,23 +11,20 @@ import React, { type ComponentPropsWithRef, forwardRef, useCallback, useEffect,
|
|
|
11
11
|
import { Icon, type IconProps, type ThemedClassName } from '@dxos/react-ui';
|
|
12
12
|
import { mx } from '@dxos/react-ui-theme';
|
|
13
13
|
|
|
14
|
-
import { commandItem, searchListItem } from '
|
|
14
|
+
import { commandItem, searchListItem } from '../SearchList';
|
|
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('is-full p-cardSpacingChrome', 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;
|
|
@@ -141,6 +153,10 @@ const ListboxOption = forwardRef<HTMLLIElement, ListboxOptionProps>(
|
|
|
141
153
|
|
|
142
154
|
ListboxOption.displayName = LISTBOX_OPTION_NAME;
|
|
143
155
|
|
|
156
|
+
//
|
|
157
|
+
// OptionLabel
|
|
158
|
+
//
|
|
159
|
+
|
|
144
160
|
const ListboxOptionLabel = forwardRef<HTMLDivElement, ThemedClassName<ComponentPropsWithRef<'div'>>>(
|
|
145
161
|
({ children, classNames, ...rootProps }, forwardedRef) => {
|
|
146
162
|
return (
|
|
@@ -155,6 +171,10 @@ ListboxOptionLabel.displayName = LISTBOX_OPTION_LABEL_NAME;
|
|
|
155
171
|
|
|
156
172
|
type ListboxOptionIndicatorProps = Omit<IconProps, 'icon'> & Partial<Pick<IconProps, 'icon'>>;
|
|
157
173
|
|
|
174
|
+
//
|
|
175
|
+
// OptionIndicator
|
|
176
|
+
//
|
|
177
|
+
|
|
158
178
|
const ListboxOptionIndicator = forwardRef<SVGSVGElement, ListboxOptionIndicatorProps>(
|
|
159
179
|
(props: ListboxOptionScopedProps<ListboxOptionIndicatorProps>, forwardedRef) => {
|
|
160
180
|
const { __listboxOptionScope, classNames, ...rootProps } = props;
|
|
@@ -173,6 +193,10 @@ const ListboxOptionIndicator = forwardRef<SVGSVGElement, ListboxOptionIndicatorP
|
|
|
173
193
|
|
|
174
194
|
ListboxOptionIndicator.displayName = LISTBOX_OPTION_INDICATOR_NAME;
|
|
175
195
|
|
|
196
|
+
//
|
|
197
|
+
// Listbox
|
|
198
|
+
//
|
|
199
|
+
|
|
176
200
|
export const Listbox = {
|
|
177
201
|
Root: ListboxRoot,
|
|
178
202
|
Option: ListboxOption,
|
|
@@ -6,10 +6,14 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
8
|
import { faker } from '@dxos/random';
|
|
9
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { translations } from '../../translations';
|
|
10
12
|
|
|
11
13
|
import { SearchList } from './SearchList';
|
|
12
14
|
|
|
15
|
+
faker.seed(1234);
|
|
16
|
+
|
|
13
17
|
type StoryItems = Record<string, string>;
|
|
14
18
|
|
|
15
19
|
const defaultItems: StoryItems = faker.helpers
|
|
@@ -26,11 +30,15 @@ type StoryProps = {
|
|
|
26
30
|
|
|
27
31
|
const DefaultStory = ({ items = defaultItems }: StoryProps) => {
|
|
28
32
|
return (
|
|
29
|
-
<SearchList.Root filter={(value, search) => (items[value].includes(search) ? 1 : 0)}>
|
|
30
|
-
<SearchList.Input
|
|
33
|
+
<SearchList.Root filter={(value, search) => (items[value].toLowerCase().includes(search.toLowerCase()) ? 1 : 0)}>
|
|
34
|
+
<SearchList.Input />
|
|
31
35
|
<SearchList.Content>
|
|
32
36
|
{Object.entries(items).map(([value, label]) => (
|
|
33
|
-
<SearchList.Item
|
|
37
|
+
<SearchList.Item
|
|
38
|
+
key={value}
|
|
39
|
+
value={value}
|
|
40
|
+
onSelect={(value) => console.log('[SearchList.Item.onSelect]', value)}
|
|
41
|
+
>
|
|
34
42
|
{label}
|
|
35
43
|
</SearchList.Item>
|
|
36
44
|
))}
|
|
@@ -43,13 +51,14 @@ const meta = {
|
|
|
43
51
|
title: 'ui/react-ui-searchlist/SearchList',
|
|
44
52
|
component: SearchList.Root as any,
|
|
45
53
|
render: DefaultStory,
|
|
46
|
-
decorators: [withTheme],
|
|
54
|
+
decorators: [withTheme, withLayout({ container: 'column', classNames: 'p-2' })],
|
|
55
|
+
parameters: {
|
|
56
|
+
translations,
|
|
57
|
+
},
|
|
47
58
|
} satisfies Meta<typeof DefaultStory>;
|
|
48
59
|
|
|
49
60
|
export default meta;
|
|
50
61
|
|
|
51
62
|
type Story = StoryObj<typeof meta>;
|
|
52
63
|
|
|
53
|
-
export const Default: Story = {
|
|
54
|
-
args: {},
|
|
55
|
-
};
|
|
64
|
+
export const Default: Story = {};
|