@dxos/react-ui-list 0.8.4-main.a4bbb77 → 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 +1349 -718
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1349 -718
- 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 +0 -3
- 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 +19 -8
- 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 +10 -8
- package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
- package/dist/types/src/components/List/ListRoot.d.ts +2 -2
- package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
- package/dist/types/src/components/List/testing.d.ts +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 +10 -6
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +9 -28
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeContext.d.ts +24 -10
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts +25 -4
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
- package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
- package/dist/types/src/components/Tree/helpers.d.ts.map +1 -1
- package/dist/types/src/components/Tree/index.d.ts +2 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing.d.ts +3 -3
- 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 +34 -31
- package/src/components/Accordion/Accordion.stories.tsx +5 -8
- package/src/components/Accordion/AccordionItem.tsx +3 -4
- package/src/components/Accordion/AccordionRoot.tsx +1 -1
- 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 +34 -22
- package/src/components/List/List.tsx +14 -10
- package/src/components/List/ListItem.tsx +60 -40
- package/src/components/List/ListRoot.tsx +3 -3
- package/src/components/List/testing.ts +7 -7
- 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/Tree/Tree.stories.tsx +153 -64
- package/src/components/Tree/Tree.tsx +43 -40
- package/src/components/Tree/TreeContext.tsx +21 -9
- package/src/components/Tree/TreeItem.tsx +214 -127
- package/src/components/Tree/TreeItemHeading.tsx +10 -8
- package/src/components/Tree/TreeItemToggle.tsx +29 -18
- package/src/components/Tree/index.ts +2 -0
- package/src/components/Tree/testing.ts +10 -9
- package/src/components/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
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",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"type": "module",
|
|
@@ -16,47 +20,46 @@
|
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"types": "dist/types/src/index.d.ts",
|
|
19
|
-
"typesVersions": {
|
|
20
|
-
"*": {}
|
|
21
|
-
},
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|
|
24
25
|
"src"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"@atlaskit/pragmatic-drag-and-drop": "
|
|
28
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
28
|
+
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
29
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
30
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
31
|
+
"@fluentui/react-tabster": "9.26.11",
|
|
31
32
|
"@radix-ui/react-accordion": "1.2.3",
|
|
32
33
|
"@radix-ui/react-context": "1.1.1",
|
|
33
|
-
"@
|
|
34
|
-
"@
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/log": "0.8.4-main.
|
|
39
|
-
"@dxos/react-
|
|
40
|
-
"@dxos/react-ui
|
|
41
|
-
"@dxos/react-ui-
|
|
42
|
-
"@dxos/
|
|
34
|
+
"@radix-ui/react-slot": "1.1.2",
|
|
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"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
|
-
"@types/react": "~19.2.
|
|
46
|
-
"@types/react-dom": "~19.2.
|
|
47
|
-
"effect": "3.
|
|
48
|
-
"react": "~19.2.
|
|
49
|
-
"react-dom": "~19.2.
|
|
50
|
-
"vite": "
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
48
|
+
"@types/react": "~19.2.7",
|
|
49
|
+
"@types/react-dom": "~19.2.3",
|
|
50
|
+
"effect": "3.20.0",
|
|
51
|
+
"react": "~19.2.3",
|
|
52
|
+
"react-dom": "~19.2.3",
|
|
53
|
+
"vite": "^8.0.10",
|
|
54
|
+
"@dxos/random": "0.8.4-main.abd8ff62ef",
|
|
55
|
+
"@dxos/storybook-utils": "0.8.4-main.abd8ff62ef"
|
|
53
56
|
},
|
|
54
57
|
"peerDependencies": {
|
|
55
|
-
"effect": "
|
|
56
|
-
"react": "
|
|
57
|
-
"react-dom": "
|
|
58
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
59
|
-
"@dxos/
|
|
58
|
+
"effect": "3.20.0",
|
|
59
|
+
"react": "~19.2.3",
|
|
60
|
+
"react-dom": "~19.2.3",
|
|
61
|
+
"@dxos/react-ui": "0.8.4-main.abd8ff62ef",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.abd8ff62ef"
|
|
60
63
|
},
|
|
61
64
|
"publishConfig": {
|
|
62
65
|
"access": "public"
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
8
|
+
import { random } from '@dxos/random';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
10
|
|
|
11
11
|
import { Accordion } from './Accordion';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
random.seed(1);
|
|
14
14
|
|
|
15
15
|
type TestItem = { id: string; name: string; text: string };
|
|
16
16
|
|
|
17
17
|
const items: TestItem[] = Array.from({ length: 10 }, (_, i) => ({
|
|
18
18
|
id: i.toString(),
|
|
19
19
|
name: `Item ${i}`,
|
|
20
|
-
text:
|
|
20
|
+
text: random.lorem.paragraphs(3),
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
23
|
const DefaultStory = () => {
|
|
@@ -42,10 +42,7 @@ const DefaultStory = () => {
|
|
|
42
42
|
const meta = {
|
|
43
43
|
title: 'ui/react-ui-list/Accordion',
|
|
44
44
|
render: DefaultStory,
|
|
45
|
-
decorators: [withTheme],
|
|
46
|
-
parameters: {
|
|
47
|
-
layout: 'column',
|
|
48
|
-
},
|
|
45
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
49
46
|
} satisfies Meta<typeof Accordion>;
|
|
50
47
|
|
|
51
48
|
export default meta;
|
|
@@ -7,10 +7,9 @@ import { createContext } from '@radix-ui/react-context';
|
|
|
7
7
|
import React, { type PropsWithChildren } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Icon, type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
-
import { mx } from '@dxos/
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
12
|
import { type ListItemRecord } from '../List';
|
|
13
|
-
|
|
14
13
|
import { useAccordionContext } from './AccordionRoot';
|
|
15
14
|
|
|
16
15
|
const ACCORDION_ITEM_NAME = 'AccordionItem';
|
|
@@ -43,7 +42,7 @@ export type AccordionItemHeaderProps = ThemedClassName<AccordionPrimitive.Accord
|
|
|
43
42
|
export const AccordionItemHeader = ({ classNames, children, ...props }: AccordionItemHeaderProps) => {
|
|
44
43
|
return (
|
|
45
44
|
<AccordionPrimitive.Header {...props} className={mx(classNames)}>
|
|
46
|
-
<AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset
|
|
45
|
+
<AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset w-full text-start'>
|
|
47
46
|
{children}
|
|
48
47
|
<Icon
|
|
49
48
|
icon='ph--caret-right--regular'
|
|
@@ -59,7 +58,7 @@ export type AccordionItemBodyProps = ThemedClassName<PropsWithChildren>;
|
|
|
59
58
|
|
|
60
59
|
export const AccordionItemBody = ({ children, classNames }: AccordionItemBodyProps) => {
|
|
61
60
|
return (
|
|
62
|
-
<AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-
|
|
61
|
+
<AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down'>
|
|
63
62
|
<div role='none' className={mx('p-2', classNames)}>
|
|
64
63
|
{children}
|
|
65
64
|
</div>
|
|
@@ -7,7 +7,7 @@ import { createContext } from '@radix-ui/react-context';
|
|
|
7
7
|
import React, { type ReactNode } from 'react';
|
|
8
8
|
|
|
9
9
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
-
import { mx } from '@dxos/
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
12
|
import { type ListItemRecord } from '../List';
|
|
13
13
|
|
|
@@ -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
|
+
};
|