@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.
- package/dist/lib/browser/index.mjs +993 -521
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +993 -521
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/aspects/index.d.ts +6 -0
- package/dist/types/src/aspects/index.d.ts.map +1 -0
- package/dist/types/src/aspects/useListDisclosure.d.ts +60 -0
- package/dist/types/src/aspects/useListDisclosure.d.ts.map +1 -0
- package/dist/types/src/aspects/useListDisclosure.test.d.ts +2 -0
- package/dist/types/src/aspects/useListDisclosure.test.d.ts.map +1 -0
- package/dist/types/src/aspects/useListGrid.d.ts +30 -0
- package/dist/types/src/aspects/useListGrid.d.ts.map +1 -0
- package/dist/types/src/aspects/useListGrid.test.d.ts +2 -0
- package/dist/types/src/aspects/useListGrid.test.d.ts.map +1 -0
- package/dist/types/src/aspects/useListNavigation.d.ts +68 -0
- package/dist/types/src/aspects/useListNavigation.d.ts.map +1 -0
- package/dist/types/src/aspects/useListNavigation.test.d.ts +2 -0
- package/dist/types/src/aspects/useListNavigation.test.d.ts.map +1 -0
- package/dist/types/src/aspects/useListSelection.d.ts +48 -0
- package/dist/types/src/aspects/useListSelection.d.ts.map +1 -0
- package/dist/types/src/aspects/useListSelection.test.d.ts +2 -0
- package/dist/types/src/aspects/useListSelection.test.d.ts.map +1 -0
- package/dist/types/src/aspects/useReorder.d.ts +103 -0
- package/dist/types/src/aspects/useReorder.d.ts.map +1 -0
- package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
- package/dist/types/src/components/Accordion/AccordionItem.d.ts +5 -3
- 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/Listbox/Listbox.d.ts +60 -20
- package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -1
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts +27 -3
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -1
- package/dist/types/src/components/OrderedList/OrderedList.d.ts +49 -0
- package/dist/types/src/components/OrderedList/OrderedList.d.ts.map +1 -0
- package/dist/types/src/components/OrderedList/OrderedList.stories.d.ts +11 -0
- package/dist/types/src/components/OrderedList/OrderedList.stories.d.ts.map +1 -0
- package/dist/types/src/components/OrderedList/OrderedList.test.d.ts +2 -0
- package/dist/types/src/components/OrderedList/OrderedList.test.d.ts.map +1 -0
- package/dist/types/src/components/OrderedList/OrderedListItem.d.ts +94 -0
- package/dist/types/src/components/OrderedList/OrderedListItem.d.ts.map +1 -0
- package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts +73 -0
- package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts.map +1 -0
- package/dist/types/src/components/OrderedList/index.d.ts +2 -0
- package/dist/types/src/components/OrderedList/index.d.ts.map +1 -0
- 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/index.d.ts +1 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/vitest-setup.d.ts +2 -0
- package/dist/types/src/vitest-setup.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -15
- package/src/aspects/index.ts +9 -0
- package/src/aspects/useListDisclosure.test.ts +72 -0
- package/src/aspects/useListDisclosure.ts +160 -0
- package/src/aspects/useListGrid.test.ts +41 -0
- package/src/aspects/useListGrid.ts +61 -0
- package/src/aspects/useListNavigation.test.ts +44 -0
- package/src/aspects/useListNavigation.ts +160 -0
- package/src/aspects/useListSelection.test.ts +101 -0
- package/src/aspects/useListSelection.ts +162 -0
- package/src/aspects/useReorder.ts +370 -0
- package/src/components/Accordion/Accordion.stories.tsx +1 -1
- package/src/components/Accordion/AccordionItem.tsx +11 -6
- package/src/components/Accordion/AccordionRoot.tsx +4 -1
- package/src/components/Listbox/Listbox.stories.tsx +171 -21
- package/src/components/Listbox/Listbox.tsx +302 -145
- package/src/components/OrderedList/OrderedList.stories.tsx +379 -0
- package/src/components/OrderedList/OrderedList.test.tsx +59 -0
- package/src/components/OrderedList/OrderedList.tsx +63 -0
- package/src/components/OrderedList/OrderedListItem.tsx +348 -0
- package/src/components/OrderedList/OrderedListRoot.tsx +173 -0
- package/src/components/OrderedList/index.ts +5 -0
- package/src/components/Tree/TreeItem.tsx +2 -0
- package/src/components/Tree/TreeItemHeading.tsx +1 -2
- package/src/components/index.ts +1 -2
- package/src/index.ts +1 -0
- package/src/vitest-setup.ts +11 -0
- package/dist/types/src/components/List/List.d.ts +0 -40
- package/dist/types/src/components/List/List.d.ts.map +0 -1
- package/dist/types/src/components/List/List.stories.d.ts +0 -18
- package/dist/types/src/components/List/List.stories.d.ts.map +0 -1
- package/dist/types/src/components/List/ListItem.d.ts +0 -49
- package/dist/types/src/components/List/ListItem.d.ts.map +0 -1
- package/dist/types/src/components/List/ListRoot.d.ts +0 -29
- package/dist/types/src/components/List/ListRoot.d.ts.map +0 -1
- package/dist/types/src/components/List/index.d.ts +0 -2
- package/dist/types/src/components/List/index.d.ts.map +0 -1
- package/dist/types/src/components/List/testing.d.ts +0 -15
- package/dist/types/src/components/List/testing.d.ts.map +0 -1
- package/dist/types/src/components/RowList/RowList.d.ts +0 -61
- package/dist/types/src/components/RowList/RowList.d.ts.map +0 -1
- package/dist/types/src/components/RowList/RowList.stories.d.ts +0 -35
- package/dist/types/src/components/RowList/RowList.stories.d.ts.map +0 -1
- package/dist/types/src/components/RowList/index.d.ts +0 -3
- package/dist/types/src/components/RowList/index.d.ts.map +0 -1
- package/src/components/List/List.stories.tsx +0 -129
- package/src/components/List/List.tsx +0 -47
- package/src/components/List/ListItem.tsx +0 -287
- package/src/components/List/ListRoot.tsx +0 -106
- package/src/components/List/index.ts +0 -5
- package/src/components/List/testing.ts +0 -31
- package/src/components/RowList/RowList.stories.tsx +0 -163
- package/src/components/RowList/RowList.tsx +0 -350
- package/src/components/RowList/index.ts +0 -6
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useCallback, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { mx, osTranslations } from '@dxos/ui-theme';
|
|
11
|
+
import { arrayMove } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
import { OrderedList } from './OrderedList';
|
|
14
|
+
|
|
15
|
+
type Item = { id: string; label: string };
|
|
16
|
+
|
|
17
|
+
const initialItems: Item[] = [
|
|
18
|
+
{ id: 'a', label: 'Alpha' },
|
|
19
|
+
{ id: 'b', label: 'Bravo' },
|
|
20
|
+
{ id: 'c', label: 'Charlie' },
|
|
21
|
+
{ id: 'd', label: 'Delta' },
|
|
22
|
+
{ id: 'e', label: 'Echo' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const isItem = (value: any): value is Item => !!value && typeof value === 'object' && typeof value.id === 'string';
|
|
26
|
+
|
|
27
|
+
//
|
|
28
|
+
// Simple — a static list with title only. No drag, no toggle, no delete. The lowest-noise
|
|
29
|
+
// shape `OrderedList` supports; useful when the chrome (drag/expand/delete) is overkill.
|
|
30
|
+
//
|
|
31
|
+
|
|
32
|
+
const SimpleStory = () => {
|
|
33
|
+
const [items] = useState<Item[]>(initialItems);
|
|
34
|
+
return (
|
|
35
|
+
<OrderedList.Root<Item> items={items} isItem={isItem} getId={(item) => item.id}>
|
|
36
|
+
{({ items: resolved }) => (
|
|
37
|
+
<OrderedList.Content>
|
|
38
|
+
{resolved.map((item) => (
|
|
39
|
+
<OrderedList.Item key={item.id} id={item.id} item={item} hover classNames='px-3 py-2'>
|
|
40
|
+
{item.label}
|
|
41
|
+
</OrderedList.Item>
|
|
42
|
+
))}
|
|
43
|
+
</OrderedList.Content>
|
|
44
|
+
)}
|
|
45
|
+
</OrderedList.Root>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//
|
|
50
|
+
// Scrollable — Simple variant wrapped in `OrderedList.Viewport` so it fills its parent
|
|
51
|
+
// pane and scrolls independently. Use this shape when the list lives inside a constrained
|
|
52
|
+
// container (settings panel, sidebar) and may overflow.
|
|
53
|
+
//
|
|
54
|
+
|
|
55
|
+
const longItems: Item[] = Array.from({ length: 40 }, (_, i) => ({
|
|
56
|
+
id: `item-${i}`,
|
|
57
|
+
label: `Item ${i + 1}`,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const ScrollableStory = () => {
|
|
61
|
+
return (
|
|
62
|
+
<OrderedList.Root<Item> items={longItems} isItem={isItem} getId={(item) => item.id}>
|
|
63
|
+
{({ items: resolved }) => (
|
|
64
|
+
<OrderedList.Viewport>
|
|
65
|
+
<OrderedList.Content>
|
|
66
|
+
{resolved.map((item) => (
|
|
67
|
+
<OrderedList.Item key={item.id} id={item.id} item={item} hover classNames='px-3 py-2'>
|
|
68
|
+
{item.label}
|
|
69
|
+
</OrderedList.Item>
|
|
70
|
+
))}
|
|
71
|
+
</OrderedList.Content>
|
|
72
|
+
</OrderedList.Viewport>
|
|
73
|
+
)}
|
|
74
|
+
</OrderedList.Root>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//
|
|
79
|
+
// Draggable / Ordered — drag handle + title, reorder via `onMove`. No expand, no delete.
|
|
80
|
+
// The canonical "user-curates-the-order" shape (matches the ordered ArrayField pattern).
|
|
81
|
+
//
|
|
82
|
+
|
|
83
|
+
const DraggableStory = () => {
|
|
84
|
+
const [items, setItems] = useState<Item[]>(longItems);
|
|
85
|
+
|
|
86
|
+
const handleMove = useCallback((fromIndex: number, toIndex: number) => {
|
|
87
|
+
setItems((prev) => {
|
|
88
|
+
const next = [...prev];
|
|
89
|
+
arrayMove(next, fromIndex, toIndex);
|
|
90
|
+
return next;
|
|
91
|
+
});
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<OrderedList.Root<Item> items={items} isItem={isItem} getId={(item) => item.id} onMove={handleMove}>
|
|
96
|
+
{({ items: resolved }) => (
|
|
97
|
+
<OrderedList.Viewport>
|
|
98
|
+
<OrderedList.Content>
|
|
99
|
+
{resolved.map((item) => (
|
|
100
|
+
<OrderedList.Item
|
|
101
|
+
key={item.id}
|
|
102
|
+
id={item.id}
|
|
103
|
+
item={item}
|
|
104
|
+
hover
|
|
105
|
+
classNames='grid grid-cols-[var(--dx-rail-item)_1fr] items-center gap-1'
|
|
106
|
+
>
|
|
107
|
+
<OrderedList.DragHandle />
|
|
108
|
+
<OrderedList.Title>{item.label}</OrderedList.Title>
|
|
109
|
+
</OrderedList.Item>
|
|
110
|
+
))}
|
|
111
|
+
</OrderedList.Content>
|
|
112
|
+
</OrderedList.Viewport>
|
|
113
|
+
)}
|
|
114
|
+
</OrderedList.Root>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
//
|
|
119
|
+
// Checkbox + Delete — checkbox-led row that records a `done` flag, plus an `OrderedList.DeleteButton`
|
|
120
|
+
// in the trailing slot. No drag (the order is intrinsic). Matches todo-list shapes
|
|
121
|
+
// (`plugin-sidekick/ActionItems` etc.).
|
|
122
|
+
//
|
|
123
|
+
|
|
124
|
+
type TodoItem = Item & { done: boolean };
|
|
125
|
+
|
|
126
|
+
const initialTodos: TodoItem[] = initialItems.map((item) => ({ ...item, done: false }));
|
|
127
|
+
|
|
128
|
+
const CheckboxWithDeleteStory = () => {
|
|
129
|
+
const [items, setItems] = useState<TodoItem[]>(initialTodos);
|
|
130
|
+
|
|
131
|
+
const handleToggle = useCallback((id: string, checked: boolean) => {
|
|
132
|
+
setItems((prev) => prev.map((item) => (item.id === id ? { ...item, done: checked } : item)));
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const handleDelete = useCallback((id: string) => {
|
|
136
|
+
setItems((prev) => prev.filter((item) => item.id !== id));
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<OrderedList.Root<TodoItem> items={items} isItem={isItem} getId={(item) => item.id}>
|
|
141
|
+
{({ items: resolved }) => (
|
|
142
|
+
<OrderedList.Content>
|
|
143
|
+
{resolved.map((item) => (
|
|
144
|
+
<OrderedList.Item
|
|
145
|
+
key={item.id}
|
|
146
|
+
id={item.id}
|
|
147
|
+
item={item}
|
|
148
|
+
hover
|
|
149
|
+
classNames='grid grid-cols-[var(--dx-rail-item)_1fr_var(--dx-rail-item)] items-center gap-1 px-2'
|
|
150
|
+
>
|
|
151
|
+
<Input.Root>
|
|
152
|
+
<Input.Checkbox checked={item.done} onCheckedChange={(next) => handleToggle(item.id, next === true)} />
|
|
153
|
+
</Input.Root>
|
|
154
|
+
<OrderedList.Title classNames={mx(item.done && 'line-through text-subdued')}>
|
|
155
|
+
{item.label}
|
|
156
|
+
</OrderedList.Title>
|
|
157
|
+
<OrderedList.DeleteButton onClick={() => handleDelete(item.id)} />
|
|
158
|
+
</OrderedList.Item>
|
|
159
|
+
))}
|
|
160
|
+
</OrderedList.Content>
|
|
161
|
+
)}
|
|
162
|
+
</OrderedList.Root>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//
|
|
167
|
+
// Draggable with Toggle — drag handle + clickable title that opens an inline detail panel,
|
|
168
|
+
// plus a trailing delete button. The full master-detail editor shape — same as the
|
|
169
|
+
// canonical `OrderedList.DetailItem` and the way `PipelineProperties` / `FieldList` /
|
|
170
|
+
// the ordered `ArrayField` consume the compound.
|
|
171
|
+
//
|
|
172
|
+
|
|
173
|
+
const DraggableWithToggleStory = () => {
|
|
174
|
+
const { t } = useTranslation(osTranslations);
|
|
175
|
+
const [items, setItems] = useState<Item[]>(initialItems);
|
|
176
|
+
const [expandedId, setExpandedId] = useState<string>();
|
|
177
|
+
|
|
178
|
+
const handleMove = useCallback((fromIndex: number, toIndex: number) => {
|
|
179
|
+
setItems((prev) => {
|
|
180
|
+
const next = [...prev];
|
|
181
|
+
arrayMove(next, fromIndex, toIndex);
|
|
182
|
+
return next;
|
|
183
|
+
});
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
const handleDelete = useCallback((id: string) => {
|
|
187
|
+
setItems((prev) => prev.filter((item) => item.id !== id));
|
|
188
|
+
setExpandedId((current) => (current === id ? undefined : current));
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<OrderedList.Root<Item>
|
|
193
|
+
items={items}
|
|
194
|
+
isItem={isItem}
|
|
195
|
+
getId={(item) => item.id}
|
|
196
|
+
onMove={handleMove}
|
|
197
|
+
expandedId={expandedId}
|
|
198
|
+
onExpandedChange={setExpandedId}
|
|
199
|
+
>
|
|
200
|
+
{({ items: resolved }) => (
|
|
201
|
+
<OrderedList.Content>
|
|
202
|
+
{resolved.map((item) => (
|
|
203
|
+
<OrderedList.DetailItem<Item>
|
|
204
|
+
key={item.id}
|
|
205
|
+
id={item.id}
|
|
206
|
+
item={item}
|
|
207
|
+
title={item.label}
|
|
208
|
+
trailing={
|
|
209
|
+
<OrderedList.DeleteButton
|
|
210
|
+
label={t('delete.label')}
|
|
211
|
+
onClick={() => handleDelete(item.id)}
|
|
212
|
+
data-testid={`delete-${item.id}`}
|
|
213
|
+
/>
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
<div data-testid={`panel-${item.id}`} className='p-2'>
|
|
217
|
+
Details for {item.label}
|
|
218
|
+
</div>
|
|
219
|
+
</OrderedList.DetailItem>
|
|
220
|
+
))}
|
|
221
|
+
</OrderedList.Content>
|
|
222
|
+
)}
|
|
223
|
+
</OrderedList.Root>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
//
|
|
228
|
+
// Nested — a parent `OrderedList.DetailItem` whose detail panel contains another
|
|
229
|
+
// `OrderedList`. Exercises Radix context shadowing (each `<OrderedList.Root>` provides its
|
|
230
|
+
// own reorder/disclosure/nav controllers) and pragmatic-dnd's per-list `canDrop` filter
|
|
231
|
+
// (so a sub-item can't drop into the parent list, and vice versa).
|
|
232
|
+
//
|
|
233
|
+
|
|
234
|
+
type Group = { id: string; label: string; subItems: Item[] };
|
|
235
|
+
|
|
236
|
+
const initialGroups: Group[] = [
|
|
237
|
+
{
|
|
238
|
+
id: 'g-a',
|
|
239
|
+
label: 'Vowels',
|
|
240
|
+
subItems: [
|
|
241
|
+
{ id: 'sa-1', label: 'A' },
|
|
242
|
+
{ id: 'sa-2', label: 'E' },
|
|
243
|
+
{ id: 'sa-3', label: 'I' },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'g-b',
|
|
248
|
+
label: 'Numbers',
|
|
249
|
+
subItems: [
|
|
250
|
+
{ id: 'sb-1', label: 'One' },
|
|
251
|
+
{ id: 'sb-2', label: 'Two' },
|
|
252
|
+
{ id: 'sb-3', label: 'Three' },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'g-c',
|
|
257
|
+
label: 'Colors',
|
|
258
|
+
subItems: [
|
|
259
|
+
{ id: 'sc-1', label: 'Red' },
|
|
260
|
+
{ id: 'sc-2', label: 'Green' },
|
|
261
|
+
{ id: 'sc-3', label: 'Blue' },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const isGroup = (value: any): value is Group => isItem(value) && Array.isArray((value as Group).subItems);
|
|
267
|
+
|
|
268
|
+
const NestedStory = () => {
|
|
269
|
+
const [groups, setGroups] = useState<Group[]>(initialGroups);
|
|
270
|
+
const [expandedId, setExpandedId] = useState<string>();
|
|
271
|
+
|
|
272
|
+
const handleGroupMove = useCallback((fromIndex: number, toIndex: number) => {
|
|
273
|
+
setGroups((prev) => {
|
|
274
|
+
const next = [...prev];
|
|
275
|
+
arrayMove(next, fromIndex, toIndex);
|
|
276
|
+
return next;
|
|
277
|
+
});
|
|
278
|
+
}, []);
|
|
279
|
+
|
|
280
|
+
const handleGroupDelete = useCallback((id: string) => {
|
|
281
|
+
setGroups((prev) => prev.filter((group) => group.id !== id));
|
|
282
|
+
setExpandedId((current) => (current === id ? undefined : current));
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
// Per-group sub-item reorder. Each nested list owns its own onMove that updates the
|
|
286
|
+
// matching group; the parent list's `onMove` (above) is independent.
|
|
287
|
+
const handleSubMove = useCallback(
|
|
288
|
+
(groupId: string) => (fromIndex: number, toIndex: number) => {
|
|
289
|
+
setGroups((prev) =>
|
|
290
|
+
prev.map((group) => {
|
|
291
|
+
if (group.id !== groupId) {
|
|
292
|
+
return group;
|
|
293
|
+
}
|
|
294
|
+
const subItems = [...group.subItems];
|
|
295
|
+
arrayMove(subItems, fromIndex, toIndex);
|
|
296
|
+
return { ...group, subItems };
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
[],
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const handleSubDelete = useCallback((groupId: string, subId: string) => {
|
|
304
|
+
setGroups((prev) =>
|
|
305
|
+
prev.map((group) =>
|
|
306
|
+
group.id === groupId ? { ...group, subItems: group.subItems.filter((sub) => sub.id !== subId) } : group,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}, []);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<OrderedList.Root<Group>
|
|
313
|
+
items={groups}
|
|
314
|
+
isItem={isGroup}
|
|
315
|
+
getId={(group) => group.id}
|
|
316
|
+
onMove={handleGroupMove}
|
|
317
|
+
expandedId={expandedId}
|
|
318
|
+
onExpandedChange={setExpandedId}
|
|
319
|
+
>
|
|
320
|
+
{({ items: resolved }) => (
|
|
321
|
+
<OrderedList.Content>
|
|
322
|
+
{resolved.map((group) => (
|
|
323
|
+
<OrderedList.DetailItem<Group>
|
|
324
|
+
key={group.id}
|
|
325
|
+
id={group.id}
|
|
326
|
+
item={group}
|
|
327
|
+
title={group.label}
|
|
328
|
+
trailing={<OrderedList.DeleteButton onClick={() => handleGroupDelete(group.id)} />}
|
|
329
|
+
>
|
|
330
|
+
<OrderedList.Root<Item>
|
|
331
|
+
items={group.subItems}
|
|
332
|
+
isItem={isItem}
|
|
333
|
+
getId={(sub) => sub.id}
|
|
334
|
+
onMove={handleSubMove(group.id)}
|
|
335
|
+
>
|
|
336
|
+
{({ items: subs }) => (
|
|
337
|
+
<OrderedList.Content>
|
|
338
|
+
{subs.map((sub) => (
|
|
339
|
+
<OrderedList.Item
|
|
340
|
+
key={sub.id}
|
|
341
|
+
id={sub.id}
|
|
342
|
+
item={sub}
|
|
343
|
+
hover
|
|
344
|
+
classNames='grid grid-cols-[var(--dx-rail-item)_1fr_var(--dx-rail-item)] items-center gap-1'
|
|
345
|
+
>
|
|
346
|
+
<OrderedList.DragHandle />
|
|
347
|
+
<OrderedList.Title>{sub.label}</OrderedList.Title>
|
|
348
|
+
<OrderedList.DeleteButton onClick={() => handleSubDelete(group.id, sub.id)} />
|
|
349
|
+
</OrderedList.Item>
|
|
350
|
+
))}
|
|
351
|
+
</OrderedList.Content>
|
|
352
|
+
)}
|
|
353
|
+
</OrderedList.Root>
|
|
354
|
+
</OrderedList.DetailItem>
|
|
355
|
+
))}
|
|
356
|
+
</OrderedList.Content>
|
|
357
|
+
)}
|
|
358
|
+
</OrderedList.Root>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const meta: Meta = {
|
|
363
|
+
title: 'ui/react-ui-list/OrderedList',
|
|
364
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export default meta;
|
|
368
|
+
|
|
369
|
+
type Story = StoryObj;
|
|
370
|
+
|
|
371
|
+
// `Default` shows the lowest-noise shape of the compound (no drag / no expand / no
|
|
372
|
+
// delete). The composed-story tests below import the specific variant they exercise
|
|
373
|
+
// rather than reading `Default`.
|
|
374
|
+
export const Default: Story = { render: () => <SimpleStory /> };
|
|
375
|
+
export const Scrollable: Story = { render: () => <ScrollableStory /> };
|
|
376
|
+
export const Draggable: Story = { render: () => <DraggableStory /> };
|
|
377
|
+
export const CheckboxWithDelete: Story = { render: () => <CheckboxWithDeleteStory /> };
|
|
378
|
+
export const DraggableWithToggle: Story = { render: () => <DraggableWithToggleStory /> };
|
|
379
|
+
export const Nested: Story = { render: () => <NestedStory /> };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { composeStories } from '@storybook/react';
|
|
6
|
+
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import * as stories from './OrderedList.stories';
|
|
11
|
+
|
|
12
|
+
// Tests exercise the master-detail editor variant directly — the most feature-rich shape
|
|
13
|
+
// of the compound (drag handle + clickable title + expand caret + detail panel + delete).
|
|
14
|
+
const { DraggableWithToggle } = composeStories(stories);
|
|
15
|
+
|
|
16
|
+
describe('OrderedList', () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
cleanup();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('renders all items', () => {
|
|
22
|
+
render(<DraggableWithToggle />);
|
|
23
|
+
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('Bravo')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('Charlie')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('clicking a title expands and collapses it', () => {
|
|
29
|
+
render(<DraggableWithToggle />);
|
|
30
|
+
expect(screen.queryByTestId('panel-a')).not.toBeInTheDocument();
|
|
31
|
+
fireEvent.click(screen.getByText('Alpha'));
|
|
32
|
+
expect(screen.getByTestId('panel-a')).toBeInTheDocument();
|
|
33
|
+
fireEvent.click(screen.getByText('Alpha'));
|
|
34
|
+
expect(screen.queryByTestId('panel-a')).not.toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('expanding one collapses the previously expanded (single-expand)', () => {
|
|
38
|
+
render(<DraggableWithToggle />);
|
|
39
|
+
fireEvent.click(screen.getByText('Alpha'));
|
|
40
|
+
expect(screen.getByTestId('panel-a')).toBeInTheDocument();
|
|
41
|
+
fireEvent.click(screen.getByText('Bravo'));
|
|
42
|
+
expect(screen.queryByTestId('panel-a')).not.toBeInTheDocument();
|
|
43
|
+
expect(screen.getByTestId('panel-b')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('caret toggles expansion', () => {
|
|
47
|
+
render(<DraggableWithToggle />);
|
|
48
|
+
const row = screen.getByText('Charlie').closest('[role="listitem"]')!;
|
|
49
|
+
fireEvent.click(within(row as HTMLElement).getByRole('button', { name: /toggle-expand/i }));
|
|
50
|
+
expect(screen.getByTestId('panel-c')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('delete removes the item', () => {
|
|
54
|
+
render(<DraggableWithToggle />);
|
|
55
|
+
fireEvent.click(screen.getByTestId('delete-b'));
|
|
56
|
+
expect(screen.queryByText('Bravo')).not.toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
OrderedListDeleteButton,
|
|
7
|
+
OrderedListDetailItem,
|
|
8
|
+
type OrderedListDetailItemProps,
|
|
9
|
+
OrderedListDragHandle,
|
|
10
|
+
OrderedListExpandCaret,
|
|
11
|
+
OrderedListIconButton,
|
|
12
|
+
OrderedListItem,
|
|
13
|
+
type OrderedListItemProps,
|
|
14
|
+
OrderedListTitle,
|
|
15
|
+
} from './OrderedListItem';
|
|
16
|
+
import {
|
|
17
|
+
OrderedListContent,
|
|
18
|
+
OrderedListRoot,
|
|
19
|
+
type OrderedListRootProps,
|
|
20
|
+
OrderedListViewport,
|
|
21
|
+
type OrderedListViewportProps,
|
|
22
|
+
} from './OrderedListRoot';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reorderable, single-expandable master-detail list.
|
|
26
|
+
*
|
|
27
|
+
* `DetailItem` encapsulates the common master-detail row (drag handle + bordered column with a
|
|
28
|
+
* name row that toggles an inline detail panel + a trailing action). Compose the lower-level
|
|
29
|
+
* `Item` / `DragHandle` / `Title` / `ExpandCaret` / `DeleteButton` directly for other layouts.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <OrderedList.Root items={…} isItem={…} getId={…} onMove={…} expandedId={…} onExpandedChange={…}>
|
|
33
|
+
* {({ items }) => (
|
|
34
|
+
* <OrderedList.Content>
|
|
35
|
+
* {items.map((item) => (
|
|
36
|
+
* <OrderedList.DetailItem
|
|
37
|
+
* key={item.id}
|
|
38
|
+
* id={item.id}
|
|
39
|
+
* item={item}
|
|
40
|
+
* title={item.label}
|
|
41
|
+
* trailing={<OrderedList.DeleteButton onClick={…} />}
|
|
42
|
+
* >
|
|
43
|
+
* {detail}
|
|
44
|
+
* </OrderedList.DetailItem>
|
|
45
|
+
* ))}
|
|
46
|
+
* </OrderedList.Content>
|
|
47
|
+
* )}
|
|
48
|
+
* </OrderedList.Root>
|
|
49
|
+
*/
|
|
50
|
+
export const OrderedList = {
|
|
51
|
+
Root: OrderedListRoot,
|
|
52
|
+
Viewport: OrderedListViewport,
|
|
53
|
+
Content: OrderedListContent,
|
|
54
|
+
Item: OrderedListItem,
|
|
55
|
+
DetailItem: OrderedListDetailItem,
|
|
56
|
+
DragHandle: OrderedListDragHandle,
|
|
57
|
+
Title: OrderedListTitle,
|
|
58
|
+
IconButton: OrderedListIconButton,
|
|
59
|
+
DeleteButton: OrderedListDeleteButton,
|
|
60
|
+
ExpandCaret: OrderedListExpandCaret,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type { OrderedListRootProps, OrderedListItemProps, OrderedListDetailItemProps, OrderedListViewportProps };
|