@dxos/react-ui-list 0.9.0 → 0.9.1-main.c7dcc2e112

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 (110) hide show
  1. package/dist/lib/browser/index.mjs +993 -521
  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 +993 -521
  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/aspects/index.d.ts +6 -0
  8. package/dist/types/src/aspects/index.d.ts.map +1 -0
  9. package/dist/types/src/aspects/useListDisclosure.d.ts +60 -0
  10. package/dist/types/src/aspects/useListDisclosure.d.ts.map +1 -0
  11. package/dist/types/src/aspects/useListDisclosure.test.d.ts +2 -0
  12. package/dist/types/src/aspects/useListDisclosure.test.d.ts.map +1 -0
  13. package/dist/types/src/aspects/useListGrid.d.ts +30 -0
  14. package/dist/types/src/aspects/useListGrid.d.ts.map +1 -0
  15. package/dist/types/src/aspects/useListGrid.test.d.ts +2 -0
  16. package/dist/types/src/aspects/useListGrid.test.d.ts.map +1 -0
  17. package/dist/types/src/aspects/useListNavigation.d.ts +68 -0
  18. package/dist/types/src/aspects/useListNavigation.d.ts.map +1 -0
  19. package/dist/types/src/aspects/useListNavigation.test.d.ts +2 -0
  20. package/dist/types/src/aspects/useListNavigation.test.d.ts.map +1 -0
  21. package/dist/types/src/aspects/useListSelection.d.ts +48 -0
  22. package/dist/types/src/aspects/useListSelection.d.ts.map +1 -0
  23. package/dist/types/src/aspects/useListSelection.test.d.ts +2 -0
  24. package/dist/types/src/aspects/useListSelection.test.d.ts.map +1 -0
  25. package/dist/types/src/aspects/useReorder.d.ts +103 -0
  26. package/dist/types/src/aspects/useReorder.d.ts.map +1 -0
  27. package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
  28. package/dist/types/src/components/Accordion/AccordionItem.d.ts +5 -3
  29. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  30. package/dist/types/src/components/Accordion/AccordionRoot.d.ts +1 -1
  31. package/dist/types/src/components/Accordion/AccordionRoot.d.ts.map +1 -1
  32. package/dist/types/src/components/Listbox/Listbox.d.ts +60 -20
  33. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -1
  34. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +27 -3
  35. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/OrderedList/OrderedList.d.ts +49 -0
  37. package/dist/types/src/components/OrderedList/OrderedList.d.ts.map +1 -0
  38. package/dist/types/src/components/OrderedList/OrderedList.stories.d.ts +11 -0
  39. package/dist/types/src/components/OrderedList/OrderedList.stories.d.ts.map +1 -0
  40. package/dist/types/src/components/OrderedList/OrderedList.test.d.ts +2 -0
  41. package/dist/types/src/components/OrderedList/OrderedList.test.d.ts.map +1 -0
  42. package/dist/types/src/components/OrderedList/OrderedListItem.d.ts +94 -0
  43. package/dist/types/src/components/OrderedList/OrderedListItem.d.ts.map +1 -0
  44. package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts +73 -0
  45. package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts.map +1 -0
  46. package/dist/types/src/components/OrderedList/index.d.ts +2 -0
  47. package/dist/types/src/components/OrderedList/index.d.ts.map +1 -0
  48. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  49. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  50. package/dist/types/src/components/index.d.ts +1 -2
  51. package/dist/types/src/components/index.d.ts.map +1 -1
  52. package/dist/types/src/index.d.ts +1 -0
  53. package/dist/types/src/index.d.ts.map +1 -1
  54. package/dist/types/src/vitest-setup.d.ts +2 -0
  55. package/dist/types/src/vitest-setup.d.ts.map +1 -0
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +18 -15
  58. package/src/aspects/index.ts +9 -0
  59. package/src/aspects/useListDisclosure.test.ts +72 -0
  60. package/src/aspects/useListDisclosure.ts +160 -0
  61. package/src/aspects/useListGrid.test.ts +41 -0
  62. package/src/aspects/useListGrid.ts +61 -0
  63. package/src/aspects/useListNavigation.test.ts +44 -0
  64. package/src/aspects/useListNavigation.ts +160 -0
  65. package/src/aspects/useListSelection.test.ts +101 -0
  66. package/src/aspects/useListSelection.ts +162 -0
  67. package/src/aspects/useReorder.ts +370 -0
  68. package/src/components/Accordion/Accordion.stories.tsx +1 -1
  69. package/src/components/Accordion/AccordionItem.tsx +11 -6
  70. package/src/components/Accordion/AccordionRoot.tsx +4 -1
  71. package/src/components/Listbox/Listbox.stories.tsx +171 -21
  72. package/src/components/Listbox/Listbox.tsx +302 -145
  73. package/src/components/OrderedList/OrderedList.stories.tsx +379 -0
  74. package/src/components/OrderedList/OrderedList.test.tsx +59 -0
  75. package/src/components/OrderedList/OrderedList.tsx +63 -0
  76. package/src/components/OrderedList/OrderedListItem.tsx +348 -0
  77. package/src/components/OrderedList/OrderedListRoot.tsx +173 -0
  78. package/src/components/OrderedList/index.ts +5 -0
  79. package/src/components/Tree/TreeItem.tsx +2 -0
  80. package/src/components/Tree/TreeItemHeading.tsx +1 -2
  81. package/src/components/index.ts +1 -2
  82. package/src/index.ts +1 -0
  83. package/src/vitest-setup.ts +11 -0
  84. package/dist/types/src/components/List/List.d.ts +0 -40
  85. package/dist/types/src/components/List/List.d.ts.map +0 -1
  86. package/dist/types/src/components/List/List.stories.d.ts +0 -18
  87. package/dist/types/src/components/List/List.stories.d.ts.map +0 -1
  88. package/dist/types/src/components/List/ListItem.d.ts +0 -49
  89. package/dist/types/src/components/List/ListItem.d.ts.map +0 -1
  90. package/dist/types/src/components/List/ListRoot.d.ts +0 -29
  91. package/dist/types/src/components/List/ListRoot.d.ts.map +0 -1
  92. package/dist/types/src/components/List/index.d.ts +0 -2
  93. package/dist/types/src/components/List/index.d.ts.map +0 -1
  94. package/dist/types/src/components/List/testing.d.ts +0 -15
  95. package/dist/types/src/components/List/testing.d.ts.map +0 -1
  96. package/dist/types/src/components/RowList/RowList.d.ts +0 -61
  97. package/dist/types/src/components/RowList/RowList.d.ts.map +0 -1
  98. package/dist/types/src/components/RowList/RowList.stories.d.ts +0 -35
  99. package/dist/types/src/components/RowList/RowList.stories.d.ts.map +0 -1
  100. package/dist/types/src/components/RowList/index.d.ts +0 -3
  101. package/dist/types/src/components/RowList/index.d.ts.map +0 -1
  102. package/src/components/List/List.stories.tsx +0 -129
  103. package/src/components/List/List.tsx +0 -47
  104. package/src/components/List/ListItem.tsx +0 -287
  105. package/src/components/List/ListRoot.tsx +0 -106
  106. package/src/components/List/index.ts +0 -5
  107. package/src/components/List/testing.ts +0 -31
  108. package/src/components/RowList/RowList.stories.tsx +0 -163
  109. package/src/components/RowList/RowList.tsx +0 -350
  110. package/src/components/RowList/index.ts +0 -6
@@ -9,7 +9,10 @@ import React, { type ReactNode } from 'react';
9
9
  import { type ThemedClassName } from '@dxos/react-ui';
10
10
  import { mx } from '@dxos/ui-theme';
11
11
 
12
- import { type ListItemRecord } from '../List';
12
+ // Records flowing through this compound carry caller-defined shapes (any record with an
13
+ // optional `id` is acceptable; `getId` defaults to reading `.id`). Typed as `any` so the
14
+ // generic parameter remains the caller's source of truth.
15
+ type ListItemRecord = any;
13
16
 
14
17
  type AccordionContext<T extends ListItemRecord> = {
15
18
  getId: (item: T) => string;
@@ -1,48 +1,198 @@
1
1
  //
2
- // Copyright 2023 DXOS.org
2
+ // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useState } from 'react';
7
7
 
8
8
  import { random } from '@dxos/random';
9
+ import { Input, Panel, Toolbar } from '@dxos/react-ui';
9
10
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
11
 
11
12
  import { Listbox } from './Listbox';
12
13
 
13
- random.seed(1234);
14
+ random.seed(1);
14
15
 
15
- type StoryItem = { value: string; label: string };
16
+ type TestItem = { id: string; name: string; description: string };
16
17
 
17
- const options: StoryItem[] = random.helpers.multiple(
18
- () => ({ value: random.string.uuid(), label: random.commerce.productName() }) satisfies StoryItem,
19
- { count: 16 },
20
- );
18
+ const allItems: TestItem[] = Array.from({ length: 24 }, (_, i) => ({
19
+ id: `item-${i}`,
20
+ name: random.commerce.productName(),
21
+ description: random.lorem.sentences(2),
22
+ }));
23
+
24
+ //
25
+ // Configurable basic story (Default / Thin / WithDisabled / Compact share this body).
26
+ //
27
+
28
+ type StoryArgs = {
29
+ /** Items to render. Defaults to the full 24-item catalog. */
30
+ items?: TestItem[];
31
+ /** Forwards to `Listbox.Viewport thin`. */
32
+ thin?: boolean;
33
+ /** Forwards to `Listbox.Viewport padding`. */
34
+ padding?: boolean;
35
+ /** Index into `items` that should render disabled. */
36
+ disabledIndex?: number;
37
+ /** Render the description line under each row's name. */
38
+ showDescription?: boolean;
39
+ };
40
+
41
+ const DefaultStory = ({
42
+ items = allItems,
43
+ thin = false,
44
+ padding = false,
45
+ disabledIndex,
46
+ showDescription = true,
47
+ }: StoryArgs = {}) => {
48
+ const [selected, setSelected] = useState<string | undefined>(items[0]?.id);
49
+ return (
50
+ <Listbox.Root value={selected} onValueChange={setSelected}>
51
+ <Listbox.Viewport thin={thin} padding={padding}>
52
+ <Listbox.Content aria-label='Items'>
53
+ {items.map((item, i) => {
54
+ const disabled = i === disabledIndex;
55
+ return (
56
+ <Listbox.Item key={item.id} id={item.id} disabled={disabled}>
57
+ <div className='flex flex-col gap-0.5 overflow-hidden'>
58
+ <div className='font-medium'>
59
+ {item.name}
60
+ {disabled && ' (disabled)'}
61
+ </div>
62
+ {showDescription && <div className='text-sm text-description line-clamp-1'>{item.description}</div>}
63
+ </div>
64
+ </Listbox.Item>
65
+ );
66
+ })}
67
+ </Listbox.Content>
68
+ </Listbox.Viewport>
69
+ </Listbox.Root>
70
+ );
71
+ };
72
+
73
+ //
74
+ // Master/detail — list is one pane of a layout.
75
+ //
76
+
77
+ const MasterDetailStory = () => {
78
+ const [selected, setSelected] = useState<string | undefined>(allItems[0].id);
79
+ const detail = allItems.find(({ id }) => id === selected);
80
+ return (
81
+ <div className='dx-container grid grid-cols-[20rem_1fr] divide-x divide-separator'>
82
+ <Listbox.Root value={selected} onValueChange={setSelected}>
83
+ <Listbox.Viewport>
84
+ <Listbox.Content aria-label='Items'>
85
+ {allItems.map((item) => (
86
+ <Listbox.Item key={item.id} id={item.id}>
87
+ <div className='font-medium'>{item.name}</div>
88
+ </Listbox.Item>
89
+ ))}
90
+ </Listbox.Content>
91
+ </Listbox.Viewport>
92
+ </Listbox.Root>
93
+ <div role='region' aria-label='Detail' className='dx-container p-4 overflow-auto'>
94
+ {detail && (
95
+ <>
96
+ <h2 className='text-lg font-semibold'>{detail.name}</h2>
97
+ <p className='text-description mt-2'>{detail.description}</p>
98
+ </>
99
+ )}
100
+ </div>
101
+ </div>
102
+ );
103
+ };
21
104
 
22
- const DefaultStory = () => {
23
- const [selectedValue, setSelectedValue] = useState<string>();
105
+ //
106
+ // Toolbar + viewport siblings — Root is headless, so layout is the caller's responsibility.
107
+ // `Panel` is the canonical chrome wrapper.
108
+ //
24
109
 
110
+ const WithToolbarStory = () => {
111
+ const [selected, setSelected] = useState<string | undefined>(allItems[0].id);
112
+ const [filter, setFilter] = useState('');
113
+ const filtered = allItems.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()));
25
114
  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
- ))}
115
+ <Listbox.Root value={selected} onValueChange={setSelected}>
116
+ <Panel.Root>
117
+ <Panel.Toolbar asChild>
118
+ <Toolbar.Root>
119
+ <Input.Root>
120
+ <Input.Label srOnly>Filter items</Input.Label>
121
+ <Input.TextInput
122
+ placeholder='Filter…'
123
+ value={filter}
124
+ onChange={(event) => setFilter(event.target.value)}
125
+ />
126
+ </Input.Root>
127
+ </Toolbar.Root>
128
+ </Panel.Toolbar>
129
+ <Panel.Content asChild>
130
+ <Listbox.Viewport>
131
+ <Listbox.Content aria-label='Items'>
132
+ {filtered.map((item) => (
133
+ <Listbox.Item key={item.id} id={item.id}>
134
+ {item.name}
135
+ </Listbox.Item>
136
+ ))}
137
+ </Listbox.Content>
138
+ </Listbox.Viewport>
139
+ </Panel.Content>
140
+ </Panel.Root>
33
141
  </Listbox.Root>
34
142
  );
35
143
  };
36
144
 
145
+ //
146
+ // Popover variant — no Viewport (caller's popover/dialog owns scroll). Uses the
147
+ // `Listbox.ItemLabel` + `Listbox.Indicator` slots for a confirmatory checkmark.
148
+ //
149
+
150
+ type Option = { value: string; label: string };
151
+
152
+ const popoverOptions: Option[] = random.helpers.multiple(
153
+ () => ({ value: random.string.uuid(), label: random.commerce.productName() }) satisfies Option,
154
+ { count: 8 },
155
+ );
156
+
157
+ const PopoverStory = () => {
158
+ const [selected, setSelected] = useState<string | undefined>(popoverOptions[0]?.value);
159
+ return (
160
+ <div className='max-w-xs p-2 ring-1 ring-subdued-separator rounded'>
161
+ <Listbox.Root value={selected} onValueChange={setSelected}>
162
+ <Listbox.Content aria-label='Models'>
163
+ {popoverOptions.map((option) => (
164
+ <Listbox.Item
165
+ key={option.value}
166
+ id={option.value}
167
+ // Compact / popover styling (the previous standalone `Listbox.Option` look).
168
+ classNames='px-2 py-1 dx-focus-ring rounded-xs'
169
+ >
170
+ <Listbox.ItemLabel>{option.label}</Listbox.ItemLabel>
171
+ <Listbox.Indicator />
172
+ </Listbox.Item>
173
+ ))}
174
+ </Listbox.Content>
175
+ </Listbox.Root>
176
+ </div>
177
+ );
178
+ };
179
+
37
180
  const meta = {
38
181
  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>;
182
+ render: (args) => <DefaultStory {...args} />,
183
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
184
+ parameters: {
185
+ layout: 'fullscreen',
186
+ },
187
+ } satisfies Meta<StoryArgs>;
43
188
 
44
189
  export default meta;
45
190
 
46
- type Story = StoryObj<typeof meta>;
191
+ type Story = StoryObj<StoryArgs>;
47
192
 
48
193
  export const Default: Story = {};
194
+ export const Thin: Story = { args: { thin: true, padding: true, showDescription: false } };
195
+ export const WithDisabled: Story = { args: { items: allItems.slice(0, 6), disabledIndex: 2 } };
196
+ export const MasterDetail: Story = { render: () => <MasterDetailStory /> };
197
+ export const WithToolbar: Story = { render: () => <WithToolbarStory /> };
198
+ export const Popover: Story = { render: () => <PopoverStory /> };