@dxos/react-ui-list 0.8.4-main.74a063c4e0 → 0.8.4-main.765dc60934
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +745 -99
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +745 -99
- 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 +1 -1
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +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/TreeItemHeading.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 +19 -19
- package/src/components/Accordion/AccordionItem.tsx +1 -3
- 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.stories.tsx +2 -2
- package/src/components/List/List.tsx +11 -2
- package/src/components/List/ListItem.tsx +2 -4
- 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 +368 -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 +350 -0
- package/src/components/RowList/index.ts +6 -0
- package/src/components/Tree/Tree.tsx +1 -1
- package/src/components/Tree/TreeItem.tsx +2 -6
- package/src/components/Tree/TreeItemHeading.tsx +3 -1
- 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.765dc60934",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"exports": {
|
|
@@ -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/react-ui-text-tooltip": "0.8.4-main.
|
|
44
|
-
"@dxos/ui-
|
|
45
|
-
"@dxos/
|
|
35
|
+
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
36
|
+
"@dxos/debug": "0.8.4-main.765dc60934",
|
|
37
|
+
"@dxos/echo": "0.8.4-main.765dc60934",
|
|
38
|
+
"@dxos/invariant": "0.8.4-main.765dc60934",
|
|
39
|
+
"@dxos/log": "0.8.4-main.765dc60934",
|
|
40
|
+
"@dxos/react-ui": "0.8.4-main.765dc60934",
|
|
41
|
+
"@dxos/react-ui-text-tooltip": "0.8.4-main.765dc60934",
|
|
42
|
+
"@dxos/ui-theme": "0.8.4-main.765dc60934",
|
|
43
|
+
"@dxos/ui-types": "0.8.4-main.765dc60934",
|
|
44
|
+
"@dxos/react-list": "0.8.4-main.765dc60934",
|
|
45
|
+
"@dxos/util": "0.8.4-main.765dc60934"
|
|
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/
|
|
55
|
-
"@dxos/
|
|
53
|
+
"vite": "^8.0.13",
|
|
54
|
+
"@dxos/random": "0.8.4-main.765dc60934",
|
|
55
|
+
"@dxos/storybook-utils": "0.8.4-main.765dc60934"
|
|
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/react-ui": "0.8.4-main.
|
|
62
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
61
|
+
"@dxos/react-ui": "0.8.4-main.765dc60934",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.765dc60934"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
@@ -59,9 +59,7 @@ export type AccordionItemBodyProps = ThemedClassName<PropsWithChildren>;
|
|
|
59
59
|
export const AccordionItemBody = ({ children, classNames }: AccordionItemBodyProps) => {
|
|
60
60
|
return (
|
|
61
61
|
<AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down'>
|
|
62
|
-
<div
|
|
63
|
-
{children}
|
|
64
|
-
</div>
|
|
62
|
+
<div className={mx('p-2', classNames)}>{children}</div>
|
|
65
63
|
</AccordionPrimitive.Content>
|
|
66
64
|
);
|
|
67
65
|
};
|
|
@@ -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
|
+
};
|
|
@@ -46,7 +46,7 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
46
46
|
{({ items }) => (
|
|
47
47
|
<>
|
|
48
48
|
<div className='flex flex-col w-full'>
|
|
49
|
-
<div
|
|
49
|
+
<div className={grid}>
|
|
50
50
|
<div />
|
|
51
51
|
<div className='flex items-center text-sm'>Items</div>
|
|
52
52
|
</div>
|
|
@@ -61,7 +61,7 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
61
61
|
))}
|
|
62
62
|
</div>
|
|
63
63
|
|
|
64
|
-
<div
|
|
64
|
+
<div className={grid}>
|
|
65
65
|
<div />
|
|
66
66
|
<div className='flex items-center text-sm'>{items?.length} Items</div>
|
|
67
67
|
</div>
|
|
@@ -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,
|
|
@@ -273,9 +273,7 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
|
|
|
273
273
|
};
|
|
274
274
|
|
|
275
275
|
export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
|
|
276
|
-
<div
|
|
277
|
-
{children}
|
|
278
|
-
</div>
|
|
276
|
+
<div className={mx('flex w-full gap-2', classNames)}>{children}</div>
|
|
279
277
|
);
|
|
280
278
|
|
|
281
279
|
export const ListItemTitle = ({
|
|
@@ -283,7 +281,7 @@ export const ListItemTitle = ({
|
|
|
283
281
|
children,
|
|
284
282
|
...props
|
|
285
283
|
}: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
|
|
286
|
-
<div
|
|
284
|
+
<div className={mx('flex grow items-center truncate', classNames)} {...props}>
|
|
287
285
|
{children}
|
|
288
286
|
</div>
|
|
289
287
|
);
|
|
@@ -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 = {};
|