@dxos/react-ui-list 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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 +746 -93
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +746 -93
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
- package/dist/types/src/components/Accordion/Accordion.d.ts.map +1 -1
- package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
- package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/types/src/components/Accordion/AccordionRoot.d.ts +1 -1
- package/dist/types/src/components/Accordion/AccordionRoot.d.ts.map +1 -1
- package/dist/types/src/components/Combobox/Combobox.d.ts +105 -0
- package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
- package/dist/types/src/components/Combobox/Combobox.stories.d.ts +12 -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/List/List.d.ts +14 -5
- package/dist/types/src/components/List/List.d.ts.map +1 -1
- package/dist/types/src/components/List/List.stories.d.ts +2 -2
- package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
- package/dist/types/src/components/List/ListItem.d.ts +3 -3
- package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
- package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
- package/dist/types/src/components/List/testing.d.ts.map +1 -1
- package/dist/types/src/components/Listbox/Listbox.d.ts +27 -0
- package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts +12 -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/Picker/Picker.d.ts +49 -0
- package/dist/types/src/components/Picker/Picker.d.ts.map +1 -0
- package/dist/types/src/components/Picker/Picker.stories.d.ts +28 -0
- package/dist/types/src/components/Picker/Picker.stories.d.ts.map +1 -0
- package/dist/types/src/components/Picker/context.d.ts +29 -0
- package/dist/types/src/components/Picker/context.d.ts.map +1 -0
- package/dist/types/src/components/Picker/index.d.ts +3 -0
- package/dist/types/src/components/Picker/index.d.ts.map +1 -0
- package/dist/types/src/components/RowList/RowList.d.ts +61 -0
- package/dist/types/src/components/RowList/RowList.d.ts.map +1 -0
- package/dist/types/src/components/RowList/RowList.stories.d.ts +35 -0
- package/dist/types/src/components/RowList/RowList.stories.d.ts.map +1 -0
- package/dist/types/src/components/RowList/index.d.ts +3 -0
- package/dist/types/src/components/RowList/index.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/helpers.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/util/path.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/components/Combobox/Combobox.stories.tsx +60 -0
- package/src/components/Combobox/Combobox.tsx +387 -0
- package/src/components/Combobox/index.ts +5 -0
- package/src/components/List/List.tsx +11 -2
- package/src/components/Listbox/Listbox.stories.tsx +48 -0
- package/src/components/Listbox/Listbox.tsx +201 -0
- package/src/components/Listbox/index.ts +5 -0
- package/src/components/Picker/Picker.stories.tsx +131 -0
- package/src/components/Picker/Picker.tsx +439 -0
- package/src/components/Picker/context.ts +43 -0
- package/src/components/Picker/index.ts +6 -0
- package/src/components/RowList/RowList.stories.tsx +163 -0
- package/src/components/RowList/RowList.tsx +353 -0
- package/src/components/RowList/index.ts +6 -0
- package/src/components/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-list",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.abd8ff62ef",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -20,9 +20,6 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"types": "dist/types/src/index.d.ts",
|
|
23
|
-
"typesVersions": {
|
|
24
|
-
"*": {}
|
|
25
|
-
},
|
|
26
23
|
"files": [
|
|
27
24
|
"dist",
|
|
28
25
|
"src"
|
|
@@ -31,18 +28,21 @@
|
|
|
31
28
|
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
32
29
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
33
30
|
"@effect-atom/atom-react": "^0.5.0",
|
|
31
|
+
"@fluentui/react-tabster": "9.26.11",
|
|
34
32
|
"@radix-ui/react-accordion": "1.2.3",
|
|
35
33
|
"@radix-ui/react-context": "1.1.1",
|
|
36
34
|
"@radix-ui/react-slot": "1.1.2",
|
|
37
|
-
"@
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
35
|
+
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
36
|
+
"@dxos/debug": "0.8.4-main.abd8ff62ef",
|
|
37
|
+
"@dxos/invariant": "0.8.4-main.abd8ff62ef",
|
|
38
|
+
"@dxos/echo": "0.8.4-main.abd8ff62ef",
|
|
39
|
+
"@dxos/log": "0.8.4-main.abd8ff62ef",
|
|
40
|
+
"@dxos/react-list": "0.8.4-main.abd8ff62ef",
|
|
41
|
+
"@dxos/react-ui": "0.8.4-main.abd8ff62ef",
|
|
42
|
+
"@dxos/react-ui-text-tooltip": "0.8.4-main.abd8ff62ef",
|
|
43
|
+
"@dxos/ui-theme": "0.8.4-main.abd8ff62ef",
|
|
44
|
+
"@dxos/util": "0.8.4-main.abd8ff62ef",
|
|
45
|
+
"@dxos/ui-types": "0.8.4-main.abd8ff62ef"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/react": "~19.2.7",
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"effect": "3.20.0",
|
|
51
51
|
"react": "~19.2.3",
|
|
52
52
|
"react-dom": "~19.2.3",
|
|
53
|
-
"vite": "^
|
|
54
|
-
"@dxos/random": "0.8.4-main.
|
|
55
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
53
|
+
"vite": "^8.0.10",
|
|
54
|
+
"@dxos/random": "0.8.4-main.abd8ff62ef",
|
|
55
|
+
"@dxos/storybook-utils": "0.8.4-main.abd8ff62ef"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"effect": "3.20.0",
|
|
59
59
|
"react": "~19.2.3",
|
|
60
60
|
"react-dom": "~19.2.3",
|
|
61
|
-
"@dxos/ui
|
|
62
|
-
"@dxos/
|
|
61
|
+
"@dxos/react-ui": "0.8.4-main.abd8ff62ef",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.abd8ff62ef"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { random } from '@dxos/random';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { Combobox } from './Combobox';
|
|
12
|
+
|
|
13
|
+
random.seed(1234);
|
|
14
|
+
|
|
15
|
+
const items = random.helpers.uniqueArray(random.commerce.productName, 16).sort();
|
|
16
|
+
|
|
17
|
+
// Simple in-memory substring filter — Combobox is search-domain-agnostic;
|
|
18
|
+
// callers filter however they want and pass only matching children.
|
|
19
|
+
// For fuzzy/ranked filtering, pair with `useSearchListResults` from
|
|
20
|
+
// `@dxos/react-ui-search`.
|
|
21
|
+
const DefaultStory = () => {
|
|
22
|
+
const [query, setQuery] = useState('');
|
|
23
|
+
const filtered = useMemo(() => items.filter((item) => item.toLowerCase().includes(query.toLowerCase())), [query]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Combobox.Root
|
|
27
|
+
placeholder='Nothing selected'
|
|
28
|
+
onValueChange={(value) => {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.log('[Combobox.Root.onValueChange]', value);
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<Combobox.Trigger />
|
|
34
|
+
<Combobox.Content>
|
|
35
|
+
<Combobox.Input placeholder='Search...' value={query} onValueChange={setQuery} />
|
|
36
|
+
<Combobox.List>
|
|
37
|
+
{filtered.map((value) => (
|
|
38
|
+
<Combobox.Item key={value} value={value} label={value} />
|
|
39
|
+
))}
|
|
40
|
+
</Combobox.List>
|
|
41
|
+
<Combobox.Arrow />
|
|
42
|
+
</Combobox.Content>
|
|
43
|
+
</Combobox.Root>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const meta = {
|
|
48
|
+
title: 'ui/react-ui-list/Combobox',
|
|
49
|
+
component: Combobox.Root as any,
|
|
50
|
+
render: DefaultStory,
|
|
51
|
+
decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'p-2' })],
|
|
52
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
53
|
+
|
|
54
|
+
export default meta;
|
|
55
|
+
|
|
56
|
+
type Story = StoryObj<typeof meta>;
|
|
57
|
+
|
|
58
|
+
export const Default: Story = {
|
|
59
|
+
args: {},
|
|
60
|
+
};
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// `Combobox` — popover-list with text input. Generic; no search-domain
|
|
6
|
+
// dependencies. Built on `Picker` (this same package) for the
|
|
7
|
+
// listbox-with-input pattern (registry, virtual highlight, keyboard
|
|
8
|
+
// nav, the two performance-split contexts) and `Popover` from
|
|
9
|
+
// `@dxos/react-ui` for the trigger/content/arrow.
|
|
10
|
+
//
|
|
11
|
+
// Filtering is the caller's responsibility — render only the matching
|
|
12
|
+
// `<Combobox.Item>` children. For fuzzy / search-domain filtering,
|
|
13
|
+
// pair with `useSearchListResults` from `@dxos/react-ui-search`.
|
|
14
|
+
//
|
|
15
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/combobox
|
|
16
|
+
|
|
17
|
+
import { createContext } from '@radix-ui/react-context';
|
|
18
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
19
|
+
import React, {
|
|
20
|
+
type ComponentPropsWithoutRef,
|
|
21
|
+
type ComponentPropsWithRef,
|
|
22
|
+
type PropsWithChildren,
|
|
23
|
+
forwardRef,
|
|
24
|
+
useCallback,
|
|
25
|
+
} from 'react';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
Button,
|
|
29
|
+
type ButtonProps,
|
|
30
|
+
Icon,
|
|
31
|
+
type IconProps,
|
|
32
|
+
Popover,
|
|
33
|
+
type PopoverArrowProps,
|
|
34
|
+
type PopoverContentProps,
|
|
35
|
+
type PopoverVirtualTriggerProps,
|
|
36
|
+
ScrollArea,
|
|
37
|
+
type ThemedClassName,
|
|
38
|
+
useId,
|
|
39
|
+
} from '@dxos/react-ui';
|
|
40
|
+
import { composable, composableProps, mx } from '@dxos/ui-theme';
|
|
41
|
+
|
|
42
|
+
import { Picker, type PickerInputProps, type PickerItemProps } from '../Picker';
|
|
43
|
+
|
|
44
|
+
const COMBOBOX_NAME = 'Combobox';
|
|
45
|
+
const COMBOBOX_CONTENT_NAME = 'ComboboxContent';
|
|
46
|
+
const COMBOBOX_ITEM_NAME = 'ComboboxItem';
|
|
47
|
+
const COMBOBOX_TRIGGER_NAME = 'ComboboxTrigger';
|
|
48
|
+
|
|
49
|
+
//
|
|
50
|
+
// Context — open/value state shared with Trigger and Item.
|
|
51
|
+
//
|
|
52
|
+
|
|
53
|
+
type ComboboxContextValue = {
|
|
54
|
+
modalId: string;
|
|
55
|
+
isCombobox: true;
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
open: boolean;
|
|
58
|
+
onOpenChange: (nextOpen: boolean) => void;
|
|
59
|
+
value: string;
|
|
60
|
+
onValueChange: (nextValue: string) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const [ComboboxProvider, useComboboxContext] = createContext<Partial<ComboboxContextValue>>(COMBOBOX_NAME, {});
|
|
64
|
+
|
|
65
|
+
//
|
|
66
|
+
// Root
|
|
67
|
+
//
|
|
68
|
+
|
|
69
|
+
type ComboboxRootProps = PropsWithChildren<
|
|
70
|
+
Partial<
|
|
71
|
+
ComboboxContextValue & {
|
|
72
|
+
modal: boolean;
|
|
73
|
+
defaultOpen: boolean;
|
|
74
|
+
defaultValue: string;
|
|
75
|
+
placeholder: string;
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
const ComboboxRoot = ({
|
|
81
|
+
children,
|
|
82
|
+
modal,
|
|
83
|
+
modalId: modalIdProp,
|
|
84
|
+
open: openProp,
|
|
85
|
+
defaultOpen,
|
|
86
|
+
onOpenChange: propsOnOpenChange,
|
|
87
|
+
value: valueProp,
|
|
88
|
+
defaultValue,
|
|
89
|
+
onValueChange: propsOnValueChange,
|
|
90
|
+
placeholder,
|
|
91
|
+
}: ComboboxRootProps) => {
|
|
92
|
+
const modalId = useId(COMBOBOX_NAME, modalIdProp);
|
|
93
|
+
const [open = false, onOpenChange] = useControllableState({
|
|
94
|
+
prop: openProp,
|
|
95
|
+
defaultProp: defaultOpen,
|
|
96
|
+
onChange: propsOnOpenChange,
|
|
97
|
+
});
|
|
98
|
+
const [value = '', onValueChange] = useControllableState({
|
|
99
|
+
prop: valueProp,
|
|
100
|
+
defaultProp: defaultValue,
|
|
101
|
+
onChange: propsOnValueChange,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Popover.Root open={open} onOpenChange={onOpenChange} modal={modal}>
|
|
106
|
+
<ComboboxProvider
|
|
107
|
+
isCombobox
|
|
108
|
+
modalId={modalId}
|
|
109
|
+
placeholder={placeholder}
|
|
110
|
+
open={open}
|
|
111
|
+
onOpenChange={onOpenChange}
|
|
112
|
+
value={value}
|
|
113
|
+
onValueChange={onValueChange}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
</ComboboxProvider>
|
|
117
|
+
</Popover.Root>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//
|
|
122
|
+
// Content — Popover.Content + Picker.Root.
|
|
123
|
+
//
|
|
124
|
+
// Filtering is caller-driven: pass already-matching <Combobox.Item> children.
|
|
125
|
+
//
|
|
126
|
+
|
|
127
|
+
type ComboboxContentProps = PopoverContentProps;
|
|
128
|
+
|
|
129
|
+
const ComboboxContent = composable<HTMLDivElement, ComboboxContentProps>(({ children, ...props }, forwardedRef) => {
|
|
130
|
+
const { modalId } = useComboboxContext(COMBOBOX_CONTENT_NAME);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Popover.Content {...composableProps(props, { id: modalId })} ref={forwardedRef}>
|
|
134
|
+
<Popover.Viewport classNames='w-(--radix-popover-trigger-width)'>
|
|
135
|
+
<Picker.Root>{children}</Picker.Root>
|
|
136
|
+
</Popover.Viewport>
|
|
137
|
+
</Popover.Content>
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
ComboboxContent.displayName = COMBOBOX_CONTENT_NAME;
|
|
142
|
+
|
|
143
|
+
//
|
|
144
|
+
// Trigger — the button that opens the popover.
|
|
145
|
+
//
|
|
146
|
+
|
|
147
|
+
type ComboboxTriggerProps = ButtonProps;
|
|
148
|
+
|
|
149
|
+
const ComboboxTrigger = forwardRef<HTMLButtonElement, ComboboxTriggerProps>(
|
|
150
|
+
({ children, onClick, ...props }, forwardedRef) => {
|
|
151
|
+
const { modalId, open, onOpenChange, placeholder, value } = useComboboxContext(COMBOBOX_TRIGGER_NAME);
|
|
152
|
+
const handleClick = useCallback(
|
|
153
|
+
(event: Parameters<Exclude<ButtonProps['onClick'], undefined>>[0]) => {
|
|
154
|
+
onClick?.(event);
|
|
155
|
+
onOpenChange?.(true);
|
|
156
|
+
},
|
|
157
|
+
[onClick, onOpenChange],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Popover.Trigger asChild>
|
|
162
|
+
<Button
|
|
163
|
+
{...props}
|
|
164
|
+
role='combobox'
|
|
165
|
+
aria-expanded={open}
|
|
166
|
+
aria-controls={modalId}
|
|
167
|
+
aria-haspopup='dialog'
|
|
168
|
+
onClick={handleClick}
|
|
169
|
+
ref={forwardedRef}
|
|
170
|
+
>
|
|
171
|
+
{children ?? (
|
|
172
|
+
<>
|
|
173
|
+
<span className={mx('font-normal text-start flex-1 min-w-0 truncate me-2', !value && 'text-subdued')}>
|
|
174
|
+
{value || placeholder}
|
|
175
|
+
</span>
|
|
176
|
+
<Icon icon='ph--caret-down--bold' size={3} />
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
</Button>
|
|
180
|
+
</Popover.Trigger>
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
ComboboxTrigger.displayName = COMBOBOX_TRIGGER_NAME;
|
|
186
|
+
|
|
187
|
+
//
|
|
188
|
+
// VirtualTrigger
|
|
189
|
+
//
|
|
190
|
+
|
|
191
|
+
type ComboboxVirtualTriggerProps = PopoverVirtualTriggerProps;
|
|
192
|
+
|
|
193
|
+
const ComboboxVirtualTrigger = Popover.VirtualTrigger;
|
|
194
|
+
|
|
195
|
+
//
|
|
196
|
+
// Input — text input wired to Picker.Input. Caller controls value.
|
|
197
|
+
//
|
|
198
|
+
|
|
199
|
+
type ComboboxInputProps = ThemedClassName<
|
|
200
|
+
Omit<ComponentPropsWithRef<'input'>, 'value'> & Pick<PickerInputProps, 'value' | 'onValueChange'>
|
|
201
|
+
>;
|
|
202
|
+
|
|
203
|
+
const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(({ classNames, ...props }, forwardedRef) => {
|
|
204
|
+
return (
|
|
205
|
+
<Picker.Input
|
|
206
|
+
{...props}
|
|
207
|
+
classNames={['m-form-chrome mb-0 w-[calc(100%-2*var(--spacing-form-chrome))]', classNames]}
|
|
208
|
+
ref={forwardedRef}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
ComboboxInput.displayName = 'Combobox.Input';
|
|
214
|
+
|
|
215
|
+
//
|
|
216
|
+
// List — scroll wrapper around items.
|
|
217
|
+
//
|
|
218
|
+
|
|
219
|
+
type ComboboxListProps = PropsWithChildren<{ classNames?: string | string[] }>;
|
|
220
|
+
|
|
221
|
+
const ComboboxList = forwardRef<HTMLDivElement, ComboboxListProps>(
|
|
222
|
+
({ classNames, children, ...props }, forwardedRef) => {
|
|
223
|
+
return (
|
|
224
|
+
<ScrollArea.Root
|
|
225
|
+
{...composableProps(props, { classNames: ['py-form-chrome', classNames] })}
|
|
226
|
+
role='listbox'
|
|
227
|
+
centered
|
|
228
|
+
padding
|
|
229
|
+
thin
|
|
230
|
+
ref={forwardedRef}
|
|
231
|
+
>
|
|
232
|
+
<ScrollArea.Viewport>{children}</ScrollArea.Viewport>
|
|
233
|
+
</ScrollArea.Root>
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
ComboboxList.displayName = 'Combobox.List';
|
|
239
|
+
|
|
240
|
+
//
|
|
241
|
+
// Item — wraps Picker.Item; commits value + closes popover on select.
|
|
242
|
+
//
|
|
243
|
+
|
|
244
|
+
type ComboboxItemProps = ThemedClassName<
|
|
245
|
+
PropsWithChildren<{
|
|
246
|
+
/** Unique identifier. */
|
|
247
|
+
value: string;
|
|
248
|
+
/** Display label (used when `children` are not provided). */
|
|
249
|
+
label?: string;
|
|
250
|
+
/** Optional icon id (Phosphor) shown before the label. */
|
|
251
|
+
icon?: string;
|
|
252
|
+
/** Additional class names for the icon. */
|
|
253
|
+
iconClassNames?: IconProps['classNames'];
|
|
254
|
+
/** Show a check icon on the right (commonly used for confirming the picked item). */
|
|
255
|
+
checked?: boolean;
|
|
256
|
+
/** Suffix text after the label. */
|
|
257
|
+
suffix?: string;
|
|
258
|
+
/** Disabled. */
|
|
259
|
+
disabled?: boolean;
|
|
260
|
+
/** Caller-supplied select handler in addition to value-commit. */
|
|
261
|
+
onSelect?: () => void;
|
|
262
|
+
/** Whether to close the popover when this item is selected. Defaults to true. */
|
|
263
|
+
closeOnSelect?: boolean;
|
|
264
|
+
}>
|
|
265
|
+
>;
|
|
266
|
+
|
|
267
|
+
const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
|
|
268
|
+
(
|
|
269
|
+
{
|
|
270
|
+
classNames,
|
|
271
|
+
onSelect,
|
|
272
|
+
value,
|
|
273
|
+
label,
|
|
274
|
+
icon,
|
|
275
|
+
iconClassNames,
|
|
276
|
+
checked,
|
|
277
|
+
suffix,
|
|
278
|
+
disabled,
|
|
279
|
+
closeOnSelect = true,
|
|
280
|
+
children,
|
|
281
|
+
},
|
|
282
|
+
forwardedRef,
|
|
283
|
+
) => {
|
|
284
|
+
const { onValueChange, onOpenChange } = useComboboxContext(COMBOBOX_ITEM_NAME);
|
|
285
|
+
const handleSelect = useCallback<NonNullable<PickerItemProps['onSelect']>>(() => {
|
|
286
|
+
onSelect?.();
|
|
287
|
+
if (value !== undefined) {
|
|
288
|
+
onValueChange?.(value);
|
|
289
|
+
}
|
|
290
|
+
if (closeOnSelect) {
|
|
291
|
+
onOpenChange?.(false);
|
|
292
|
+
}
|
|
293
|
+
}, [onSelect, onValueChange, onOpenChange, value, closeOnSelect]);
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<Picker.Item
|
|
297
|
+
value={value}
|
|
298
|
+
disabled={disabled}
|
|
299
|
+
onSelect={handleSelect}
|
|
300
|
+
ref={forwardedRef}
|
|
301
|
+
classNames={[
|
|
302
|
+
// Full width inside the viewport (no horizontal margin).
|
|
303
|
+
// `px-3 py-1`, `cursor-pointer`, `select-none` and the
|
|
304
|
+
// `dx-hover` / `dx-selected` pairing come from `Picker.Item`'s
|
|
305
|
+
// defaults; we only add the row-shape (flex / icons + label)
|
|
306
|
+
// and the disabled overrides on top.
|
|
307
|
+
'flex w-full gap-2 items-center',
|
|
308
|
+
disabled && 'hover:bg-transparent data-[selected=true]:bg-transparent',
|
|
309
|
+
classNames,
|
|
310
|
+
]}
|
|
311
|
+
>
|
|
312
|
+
{children ?? (
|
|
313
|
+
<>
|
|
314
|
+
{icon && <Icon icon={icon} classNames={iconClassNames} />}
|
|
315
|
+
<span className='w-0 grow truncate'>{label}</span>
|
|
316
|
+
{suffix && <span className='shrink-0 text-description'>{suffix}</span>}
|
|
317
|
+
{checked && <Icon icon='ph--check--regular' />}
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
</Picker.Item>
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
ComboboxItem.displayName = COMBOBOX_ITEM_NAME;
|
|
326
|
+
|
|
327
|
+
//
|
|
328
|
+
// Arrow
|
|
329
|
+
//
|
|
330
|
+
|
|
331
|
+
type ComboboxArrowProps = PopoverArrowProps;
|
|
332
|
+
|
|
333
|
+
const ComboboxArrow = Popover.Arrow;
|
|
334
|
+
|
|
335
|
+
//
|
|
336
|
+
// Empty — passthrough placeholder. No translation; caller supplies copy.
|
|
337
|
+
//
|
|
338
|
+
|
|
339
|
+
type ComboboxEmptyProps = ThemedClassName<PropsWithChildren>;
|
|
340
|
+
|
|
341
|
+
const ComboboxEmpty = forwardRef<HTMLDivElement, ComboboxEmptyProps>(({ classNames, children }, forwardedRef) => {
|
|
342
|
+
return (
|
|
343
|
+
<div ref={forwardedRef} role='status' className={mx(classNames)}>
|
|
344
|
+
{children}
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
ComboboxEmpty.displayName = 'Combobox.Empty';
|
|
350
|
+
|
|
351
|
+
//
|
|
352
|
+
// Portal
|
|
353
|
+
//
|
|
354
|
+
|
|
355
|
+
type ComboboxPortalProps = ComponentPropsWithoutRef<typeof Popover.Portal>;
|
|
356
|
+
|
|
357
|
+
const ComboboxPortal = Popover.Portal;
|
|
358
|
+
|
|
359
|
+
//
|
|
360
|
+
// Combobox
|
|
361
|
+
//
|
|
362
|
+
|
|
363
|
+
export const Combobox = {
|
|
364
|
+
Root: ComboboxRoot,
|
|
365
|
+
Portal: ComboboxPortal,
|
|
366
|
+
Content: ComboboxContent,
|
|
367
|
+
Trigger: ComboboxTrigger,
|
|
368
|
+
VirtualTrigger: ComboboxVirtualTrigger,
|
|
369
|
+
Input: ComboboxInput,
|
|
370
|
+
List: ComboboxList,
|
|
371
|
+
Item: ComboboxItem,
|
|
372
|
+
Arrow: ComboboxArrow,
|
|
373
|
+
Empty: ComboboxEmpty,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export type {
|
|
377
|
+
ComboboxRootProps,
|
|
378
|
+
ComboboxPortalProps,
|
|
379
|
+
ComboboxContentProps,
|
|
380
|
+
ComboboxTriggerProps,
|
|
381
|
+
ComboboxVirtualTriggerProps,
|
|
382
|
+
ComboboxInputProps,
|
|
383
|
+
ComboboxListProps,
|
|
384
|
+
ComboboxItemProps,
|
|
385
|
+
ComboboxArrowProps,
|
|
386
|
+
ComboboxEmptyProps,
|
|
387
|
+
};
|
|
@@ -16,11 +16,20 @@ import {
|
|
|
16
16
|
import { ListRoot, type ListRootProps } from './ListRoot';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Draggable list.
|
|
19
|
+
* Draggable list with per-row drag handles and delete buttons.
|
|
20
20
|
* Ref: https://github.com/atlassian/pragmatic-drag-and-drop
|
|
21
21
|
* Ref: https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/task.tsx
|
|
22
22
|
*
|
|
23
|
-
* @deprecated
|
|
23
|
+
* @deprecated New code should use one of:
|
|
24
|
+
*
|
|
25
|
+
* - `RowList` / `CardList` from this same package — for selectable
|
|
26
|
+
* pickers (master/detail). Correct ARIA + dx-* by construction.
|
|
27
|
+
* - `Mosaic.Stack` / `Mosaic.VirtualStack` from `@dxos/react-ui-mosaic`
|
|
28
|
+
* — for virtualized or drag-reorderable card stacks.
|
|
29
|
+
*
|
|
30
|
+
* This component is retained for the existing reorder-with-delete-button
|
|
31
|
+
* use cases (plugin-meeting, plugin-automation, plugin-zen, etc.) until
|
|
32
|
+
* each is migrated; see `AUDIT.md` Phase 6 for the migration plan.
|
|
24
33
|
*/
|
|
25
34
|
export const List = {
|
|
26
35
|
Root: ListRoot,
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { random } from '@dxos/random';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { Listbox } from './Listbox';
|
|
12
|
+
|
|
13
|
+
random.seed(1234);
|
|
14
|
+
|
|
15
|
+
type StoryItem = { value: string; label: string };
|
|
16
|
+
|
|
17
|
+
const options: StoryItem[] = random.helpers.multiple(
|
|
18
|
+
() => ({ value: random.string.uuid(), label: random.commerce.productName() }) satisfies StoryItem,
|
|
19
|
+
{ count: 16 },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const DefaultStory = () => {
|
|
23
|
+
const [selectedValue, setSelectedValue] = useState<string>();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Listbox.Root value={selectedValue} onValueChange={setSelectedValue}>
|
|
27
|
+
{options.map((option) => (
|
|
28
|
+
<Listbox.Option key={option.value} value={option.value}>
|
|
29
|
+
<Listbox.OptionLabel>{option.label}</Listbox.OptionLabel>
|
|
30
|
+
<Listbox.OptionIndicator />
|
|
31
|
+
</Listbox.Option>
|
|
32
|
+
))}
|
|
33
|
+
</Listbox.Root>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const meta = {
|
|
38
|
+
title: 'ui/react-ui-list/Listbox',
|
|
39
|
+
component: Listbox.Root,
|
|
40
|
+
render: DefaultStory,
|
|
41
|
+
decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'p-2' })],
|
|
42
|
+
} satisfies Meta<typeof Listbox.Root>;
|
|
43
|
+
|
|
44
|
+
export default meta;
|
|
45
|
+
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {};
|