@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.
Files changed (101) hide show
  1. package/dist/lib/browser/index.mjs +1349 -718
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +1349 -718
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
  8. package/dist/types/src/components/Accordion/Accordion.d.ts.map +1 -1
  9. package/dist/types/src/components/Accordion/Accordion.stories.d.ts +0 -3
  10. package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  12. package/dist/types/src/components/Accordion/AccordionRoot.d.ts +1 -1
  13. package/dist/types/src/components/Accordion/AccordionRoot.d.ts.map +1 -1
  14. package/dist/types/src/components/Combobox/Combobox.d.ts +105 -0
  15. package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
  16. package/dist/types/src/components/Combobox/Combobox.stories.d.ts +12 -0
  17. package/dist/types/src/components/Combobox/Combobox.stories.d.ts.map +1 -0
  18. package/dist/types/src/components/Combobox/index.d.ts +2 -0
  19. package/dist/types/src/components/Combobox/index.d.ts.map +1 -0
  20. package/dist/types/src/components/List/List.d.ts +19 -8
  21. package/dist/types/src/components/List/List.d.ts.map +1 -1
  22. package/dist/types/src/components/List/List.stories.d.ts +2 -2
  23. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/List/ListItem.d.ts +10 -8
  25. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  26. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  27. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  28. package/dist/types/src/components/List/testing.d.ts +1 -1
  29. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  30. package/dist/types/src/components/Listbox/Listbox.d.ts +27 -0
  31. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
  32. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +12 -0
  33. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Listbox/index.d.ts +2 -0
  35. package/dist/types/src/components/Listbox/index.d.ts.map +1 -0
  36. package/dist/types/src/components/Picker/Picker.d.ts +49 -0
  37. package/dist/types/src/components/Picker/Picker.d.ts.map +1 -0
  38. package/dist/types/src/components/Picker/Picker.stories.d.ts +28 -0
  39. package/dist/types/src/components/Picker/Picker.stories.d.ts.map +1 -0
  40. package/dist/types/src/components/Picker/context.d.ts +29 -0
  41. package/dist/types/src/components/Picker/context.d.ts.map +1 -0
  42. package/dist/types/src/components/Picker/index.d.ts +3 -0
  43. package/dist/types/src/components/Picker/index.d.ts.map +1 -0
  44. package/dist/types/src/components/RowList/RowList.d.ts +61 -0
  45. package/dist/types/src/components/RowList/RowList.d.ts.map +1 -0
  46. package/dist/types/src/components/RowList/RowList.stories.d.ts +35 -0
  47. package/dist/types/src/components/RowList/RowList.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/RowList/index.d.ts +3 -0
  49. package/dist/types/src/components/RowList/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  51. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  52. package/dist/types/src/components/Tree/Tree.stories.d.ts +9 -28
  53. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Tree/TreeContext.d.ts +24 -10
  55. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  56. package/dist/types/src/components/Tree/TreeItem.d.ts +25 -4
  57. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  58. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
  59. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  60. package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
  61. package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
  62. package/dist/types/src/components/Tree/helpers.d.ts.map +1 -1
  63. package/dist/types/src/components/Tree/index.d.ts +2 -0
  64. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  65. package/dist/types/src/components/Tree/testing.d.ts +3 -3
  66. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +4 -0
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/util/path.d.ts.map +1 -1
  70. package/dist/types/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +34 -31
  72. package/src/components/Accordion/Accordion.stories.tsx +5 -8
  73. package/src/components/Accordion/AccordionItem.tsx +3 -4
  74. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  75. package/src/components/Combobox/Combobox.stories.tsx +60 -0
  76. package/src/components/Combobox/Combobox.tsx +387 -0
  77. package/src/components/Combobox/index.ts +5 -0
  78. package/src/components/List/List.stories.tsx +34 -22
  79. package/src/components/List/List.tsx +14 -10
  80. package/src/components/List/ListItem.tsx +60 -40
  81. package/src/components/List/ListRoot.tsx +3 -3
  82. package/src/components/List/testing.ts +7 -7
  83. package/src/components/Listbox/Listbox.stories.tsx +48 -0
  84. package/src/components/Listbox/Listbox.tsx +201 -0
  85. package/src/components/Listbox/index.ts +5 -0
  86. package/src/components/Picker/Picker.stories.tsx +131 -0
  87. package/src/components/Picker/Picker.tsx +439 -0
  88. package/src/components/Picker/context.ts +43 -0
  89. package/src/components/Picker/index.ts +6 -0
  90. package/src/components/RowList/RowList.stories.tsx +163 -0
  91. package/src/components/RowList/RowList.tsx +353 -0
  92. package/src/components/RowList/index.ts +6 -0
  93. package/src/components/Tree/Tree.stories.tsx +153 -64
  94. package/src/components/Tree/Tree.tsx +43 -40
  95. package/src/components/Tree/TreeContext.tsx +21 -9
  96. package/src/components/Tree/TreeItem.tsx +214 -127
  97. package/src/components/Tree/TreeItemHeading.tsx +10 -8
  98. package/src/components/Tree/TreeItemToggle.tsx +29 -18
  99. package/src/components/Tree/index.ts +2 -0
  100. package/src/components/Tree/testing.ts +10 -9
  101. 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.a4bbb77",
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": "^1.4.0",
28
- "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
29
- "@preact-signals/safe-react": "^0.9.0",
30
- "@preact/signals-core": "^1.12.1",
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
- "@dxos/debug": "0.8.4-main.a4bbb77",
34
- "@dxos/echo-schema": "0.8.4-main.a4bbb77",
35
- "@dxos/invariant": "0.8.4-main.a4bbb77",
36
- "@dxos/live-object": "0.8.4-main.a4bbb77",
37
- "@dxos/react-ui": "0.8.4-main.a4bbb77",
38
- "@dxos/log": "0.8.4-main.a4bbb77",
39
- "@dxos/react-ui-text-tooltip": "0.8.4-main.a4bbb77",
40
- "@dxos/react-ui-theme": "0.8.4-main.a4bbb77",
41
- "@dxos/react-ui-types": "0.8.4-main.a4bbb77",
42
- "@dxos/util": "0.8.4-main.a4bbb77"
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.0",
46
- "@types/react-dom": "~19.2.0",
47
- "effect": "3.18.3",
48
- "react": "~19.2.0",
49
- "react-dom": "~19.2.0",
50
- "vite": "7.1.9",
51
- "@dxos/storybook-utils": "0.8.4-main.a4bbb77",
52
- "@dxos/random": "0.8.4-main.a4bbb77"
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": "^3.13.3",
56
- "react": "^19.0.0",
57
- "react-dom": "^19.0.0",
58
- "@dxos/react-ui": "0.8.4-main.a4bbb77",
59
- "@dxos/react-ui-theme": "0.8.4-main.a4bbb77"
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 { faker } from '@dxos/random';
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
- faker.seed(1);
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: faker.lorem.paragraphs(3),
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/react-ui-theme';
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 is-full text-start'>
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-slideUp data-[state=open]:animate-slideDown'>
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/react-ui-theme';
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
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './Combobox';