@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.
- package/README.md +1 -1
- package/dist/lib/browser/index.mjs +716 -408
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +716 -408
- 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 +85 -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/SearchList.d.ts +90 -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 -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 +4 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -18
- package/src/components/Combobox/Combobox.stories.tsx +62 -0
- package/src/components/Combobox/Combobox.tsx +343 -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} +40 -11
- package/src/components/Listbox/index.ts +5 -0
- package/src/components/SearchList/SearchList.stories.tsx +532 -0
- package/src/components/SearchList/SearchList.tsx +554 -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 -1
- package/src/index.ts +0 -1
- package/src/translations.ts +4 -2
- package/src/types/command-score.d.ts +16 -0
- 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 +0 -47
- package/dist/types/src/components/SearchList.d.ts.map +0 -1
- package/dist/types/src/components/SearchList.stories.d.ts +0 -16
- 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.stories.tsx +0 -55
- 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,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-searchlist",
|
|
3
|
-
"version": "0.8.4-main.
|
|
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":
|
|
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": "
|
|
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
|
-
"
|
|
36
|
+
"command-score": "0.1.2"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
|
-
"@types/react": "~19.2.
|
|
37
|
-
"@types/react-dom": "~19.2.
|
|
38
|
-
"react": "~19.2.
|
|
39
|
-
"react-dom": "~19.2.
|
|
40
|
-
"vite": "7.1.
|
|
41
|
-
"@dxos/random": "0.8.4-main.
|
|
42
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
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": "
|
|
48
|
-
"react-dom": "
|
|
49
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
50
|
-
"@dxos/
|
|
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,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/
|
|
12
|
+
import { mx } from '@dxos/ui-theme';
|
|
13
13
|
|
|
14
|
-
|
|
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-
|
|
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(
|
|
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,
|