@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
@@ -0,0 +1,348 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, {
7
+ type ComponentProps,
8
+ type CSSProperties,
9
+ type MouseEvent,
10
+ type PropsWithChildren,
11
+ type ReactNode,
12
+ type RefCallback,
13
+ useCallback,
14
+ } from 'react';
15
+
16
+ import {
17
+ IconBlock,
18
+ IconButton,
19
+ type IconButtonProps,
20
+ ListItem as NaturalListItem,
21
+ type ThemedClassName,
22
+ ToggleIconButton,
23
+ useTranslation,
24
+ } from '@dxos/react-ui';
25
+ import { mx, osTranslations } from '@dxos/ui-theme';
26
+
27
+ import { useListGrid, useReorderItem } from '../../aspects';
28
+ import { type ListItemRecord, useOrderedListContext } from './OrderedListRoot';
29
+
30
+ const ORDERED_LIST_ITEM_NAME = 'OrderedListItem';
31
+
32
+ type OrderedListItemContextValue = {
33
+ id: string;
34
+ expanded: boolean;
35
+ toggle: () => void;
36
+ canDrag: boolean;
37
+ handleRef: RefCallback<HTMLElement>;
38
+ /** ARIA wiring for the controlled disclosure panel. */
39
+ triggerProps: ReturnType<NonNullable<ReturnType<typeof useOrderedListContext>['disclosure']['bind']>>['triggerProps'];
40
+ panelProps: ReturnType<NonNullable<ReturnType<typeof useOrderedListContext>['disclosure']['bind']>>['panelProps'];
41
+ };
42
+
43
+ const [OrderedListItemProvider, useOrderedListItemContext] =
44
+ createContext<OrderedListItemContextValue>(ORDERED_LIST_ITEM_NAME);
45
+
46
+ export type OrderedListItemProps<T extends ListItemRecord> = ThemedClassName<
47
+ PropsWithChildren<{
48
+ id: string;
49
+ /** The record handed to the underlying reorder hook (kept for back-compat with callers). */
50
+ item: T;
51
+ /** Defaults to true; false disables the drag handle. */
52
+ canDrag?: boolean;
53
+ /** Apply the row-hover affordance. Defaults to false. */
54
+ hover?: boolean;
55
+ /**
56
+ * Visually highlights the row as the "currently active" entry — sets `aria-current="true"`
57
+ * paired with `dx-current` styling. `aria-current` is the right grammar for an active row
58
+ * inside a `list`-mode container (listbox/option semantics live on `Listbox`); used for
59
+ * surfaces like the `Mixer` where the active layer needs visual highlight without
60
+ * adopting listbox role semantics.
61
+ */
62
+ selected?: boolean;
63
+ /** Optional click handler bound to the outer row element. */
64
+ onClick?: (event: MouseEvent<HTMLDivElement>) => void;
65
+ /** Inline style merged onto the outer element. Used for grid templates produced by `useListGrid`. */
66
+ style?: CSSProperties;
67
+ }>
68
+ >;
69
+
70
+ /**
71
+ * A single reorderable item. Calls `useReorderItem` to wire pragmatic-dnd refs + state,
72
+ * resolves disclosure state from the root context, and exposes both via item context for
73
+ * the sub-components (`OrderedListDragHandle`, `OrderedListTitle`, `OrderedListExpandCaret`).
74
+ *
75
+ * The outer element applies only structural concerns (`relative` + state classes); the
76
+ * layout (flex/grid) is controlled by the caller via `classNames` so master-detail rows
77
+ * and bare reorderable rows can share the same component.
78
+ */
79
+ export const OrderedListItem = <T extends ListItemRecord>({
80
+ id,
81
+ canDrag = true,
82
+ hover = false,
83
+ selected,
84
+ onClick,
85
+ classNames,
86
+ style,
87
+ children,
88
+ }: OrderedListItemProps<T>) => {
89
+ const { reorder, disclosure, navigation } = useOrderedListContext(ORDERED_LIST_ITEM_NAME);
90
+ const { rowRef, handleRef, closestEdge, state } = useReorderItem(reorder, id);
91
+ const { expanded, toggle, triggerProps, panelProps } = disclosure.bind(id);
92
+
93
+ return (
94
+ <OrderedListItemProvider
95
+ id={id}
96
+ expanded={expanded}
97
+ toggle={toggle}
98
+ canDrag={canDrag}
99
+ handleRef={handleRef}
100
+ triggerProps={triggerProps}
101
+ panelProps={panelProps}
102
+ >
103
+ <div
104
+ ref={rowRef as RefCallback<HTMLDivElement>}
105
+ {...navigation.itemProps()}
106
+ style={style}
107
+ aria-current={selected || undefined}
108
+ onClick={onClick}
109
+ className={mx(
110
+ 'relative dx-current',
111
+ hover && 'dx-hover',
112
+ state.type === 'dragging' && 'opacity-50',
113
+ classNames,
114
+ )}
115
+ >
116
+ {children}
117
+ {closestEdge && <NaturalListItem.DropIndicator edge={closestEdge} />}
118
+ </div>
119
+ </OrderedListItemProvider>
120
+ );
121
+ };
122
+
123
+ /**
124
+ * Drag handle. Disabled when the list is readonly or the item opts out via `canDrag={false}`.
125
+ * The button is the only element that initiates drag — pragmatic-dnd's `dragHandle:` option
126
+ * scopes the source surface to this ref.
127
+ */
128
+ export const OrderedListDragHandle = () => {
129
+ const { readonly } = useOrderedListContext('OrderedListDragHandle');
130
+ const { canDrag, handleRef } = useOrderedListItemContext('OrderedListDragHandle');
131
+ const { t } = useTranslation(osTranslations);
132
+ const disabled = readonly || !canDrag;
133
+ return (
134
+ <IconButton
135
+ variant='ghost'
136
+ disabled={disabled}
137
+ noTooltip
138
+ icon='ph--dots-six-vertical--regular'
139
+ iconOnly
140
+ label={t('drag-handle.label')}
141
+ ref={handleRef as RefCallback<HTMLButtonElement>}
142
+ />
143
+ );
144
+ };
145
+
146
+ /**
147
+ * Clickable title; clicking toggles the item's expanded state. Carries a stable id so the
148
+ * expanded panel can name itself via `aria-labelledby`.
149
+ */
150
+ export const OrderedListTitle = ({
151
+ classNames,
152
+ children,
153
+ onClick,
154
+ ...props
155
+ }: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => {
156
+ const { triggerProps } = useOrderedListItemContext('OrderedListTitle');
157
+ const handleClick = useCallback(
158
+ (event: MouseEvent<HTMLDivElement>) => {
159
+ onClick?.(event);
160
+ triggerProps.onClick(event);
161
+ },
162
+ [onClick, triggerProps],
163
+ );
164
+ return (
165
+ <div
166
+ {...props}
167
+ // The title row is also the disclosure trigger, so it carries the trigger's
168
+ // `id` + `aria-expanded` + `aria-controls` for assistive tech.
169
+ id={triggerProps.id}
170
+ aria-expanded={triggerProps['aria-expanded']}
171
+ aria-controls={triggerProps['aria-controls']}
172
+ className={mx('flex grow items-center truncate cursor-pointer', classNames)}
173
+ onClick={handleClick}
174
+ >
175
+ {children}
176
+ </div>
177
+ );
178
+ };
179
+
180
+ /**
181
+ * Generic action icon button. Anchored in a `var(--dx-rail-item)` IconBlock so it shares a
182
+ * centerline with the title row regardless of expand state. Use for inline row actions
183
+ * (mute, edit, copy, etc.); pair with `OrderedListDeleteButton` for the delete affordance.
184
+ */
185
+ export const OrderedListIconButton = ({
186
+ autoHide = false,
187
+ disabled,
188
+ classNames,
189
+ ...props
190
+ }: IconButtonProps & { autoHide?: boolean }) => (
191
+ <IconBlock>
192
+ <IconButton
193
+ {...props}
194
+ variant='ghost'
195
+ iconOnly
196
+ disabled={disabled}
197
+ classNames={[classNames, autoHide && disabled && 'hidden']}
198
+ />
199
+ </IconBlock>
200
+ );
201
+
202
+ /**
203
+ * Delete icon button. Anchored in a `var(--dx-rail-item)` IconBlock so it shares a centerline
204
+ * with the title row regardless of expand state. No `my-[1px]` nudge: the central column's
205
+ * outline is `ring-1` (see `OrderedListDetailItem`) so layout is exact.
206
+ */
207
+ export const OrderedListDeleteButton = ({
208
+ autoHide = false,
209
+ icon = 'ph--x--regular',
210
+ label,
211
+ disabled,
212
+ classNames,
213
+ ...props
214
+ }: Partial<Pick<IconButtonProps, 'icon'>> &
215
+ Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
216
+ const { t } = useTranslation(osTranslations);
217
+ return (
218
+ <OrderedListIconButton
219
+ {...props}
220
+ autoHide={autoHide}
221
+ disabled={disabled}
222
+ icon={icon}
223
+ label={label ?? t('delete.label')}
224
+ classNames={classNames}
225
+ />
226
+ );
227
+ };
228
+
229
+ /**
230
+ * Expand/collapse caret; reflects and toggles the item's expanded state via the disclosure
231
+ * trigger's `aria-expanded` + `aria-controls`.
232
+ */
233
+ export const OrderedListExpandCaret = ({ onClick, ...props }: Partial<IconButtonProps>) => {
234
+ const { t } = useTranslation(osTranslations);
235
+ const { expanded, toggle, triggerProps } = useOrderedListItemContext('OrderedListExpandCaret');
236
+ const handleClick = useCallback(
237
+ (event: MouseEvent<HTMLButtonElement>) => {
238
+ toggle();
239
+ onClick?.(event);
240
+ },
241
+ [toggle, onClick],
242
+ );
243
+ return (
244
+ <ToggleIconButton
245
+ iconOnly
246
+ variant='ghost'
247
+ active={expanded}
248
+ icon='ph--caret-right--regular'
249
+ label={t('toggle-expand.label')}
250
+ // Disclosure semantics are carried here for AT users that interact with the caret
251
+ // rather than the title.
252
+ aria-expanded={triggerProps['aria-expanded']}
253
+ aria-controls={triggerProps['aria-controls']}
254
+ onClick={handleClick}
255
+ {...props}
256
+ />
257
+ );
258
+ };
259
+
260
+ export type OrderedListDetailItemProps<T extends ListItemRecord> = ThemedClassName<
261
+ PropsWithChildren<{
262
+ id: string;
263
+ /** The record handed to the underlying reorder hook (kept for back-compat with callers). */
264
+ item: T;
265
+ /** Defaults to true; false disables the drag handle. */
266
+ canDrag?: boolean;
267
+ /** Title content shown in the clickable name row (clicking toggles expansion). */
268
+ title: ReactNode;
269
+ titleClassNames?: ThemedClassName<any>['classNames'];
270
+ /** Inline actions placed in the name row before the expand caret (e.g. a visibility toggle). */
271
+ actions?: ReactNode;
272
+ /** Action(s) placed outside the bordered column, flanking it (e.g. a delete button). */
273
+ trailing?: ReactNode;
274
+ /** When false, hides the expand caret and detail panel. Defaults to true. */
275
+ expandable?: boolean;
276
+ }>
277
+ >;
278
+
279
+ /**
280
+ * Master-detail row: a drag handle and trailing action flank a `ring-1`-outlined central
281
+ * column whose title row (title + inline actions + expand caret) toggles an inline detail
282
+ * panel (children).
283
+ *
284
+ * Outline uses `ring-1` (rendered as box-shadow) rather than `border` so the column's
285
+ * content area is the full `var(--dx-rail-item)` height — handles, title, caret, and
286
+ * trailing all sit on the same baseline without per-pixel nudges.
287
+ */
288
+ export const OrderedListDetailItem = <T extends ListItemRecord>({
289
+ id,
290
+ item,
291
+ canDrag,
292
+ title,
293
+ titleClassNames,
294
+ actions,
295
+ trailing,
296
+ expandable = true,
297
+ classNames,
298
+ children,
299
+ }: OrderedListDetailItemProps<T>) => {
300
+ const grid = useListGrid({ trailing: !!trailing });
301
+ return (
302
+ <OrderedListItem
303
+ id={id}
304
+ item={item}
305
+ canDrag={canDrag}
306
+ // The grid template is inline so the row's three slots (handle / card / trailing)
307
+ // land in fixed-width tracks that share a baseline with the title row inside the card.
308
+ // See useListGrid for the rationale.
309
+ style={grid.rowProps.style}
310
+ classNames={mx(grid.rowProps.className, 'pb-1', classNames)}
311
+ >
312
+ <OrderedListDragHandle />
313
+ <div className='flex flex-col ring-1 ring-subdued-separator rounded-sm overflow-hidden'>
314
+ <div className='flex items-center min-h-[var(--dx-rail-item)]'>
315
+ {expandable ? (
316
+ <OrderedListTitle classNames={mx('px-2', titleClassNames)}>{title}</OrderedListTitle>
317
+ ) : (
318
+ // When the row is not expandable, render a plain (non-toggling) title so a click
319
+ // doesn't mutate hidden disclosure state. Mirrors `OrderedListTitle`'s structure
320
+ // minus the trigger plumbing.
321
+ <div className={mx('flex grow items-center truncate px-2', titleClassNames)}>{title}</div>
322
+ )}
323
+ {actions}
324
+ {expandable && <OrderedListExpandCaret />}
325
+ </div>
326
+ {expandable && <DetailPanel>{children}</DetailPanel>}
327
+ </div>
328
+ {trailing}
329
+ </OrderedListItem>
330
+ );
331
+ };
332
+
333
+ /**
334
+ * Read-only panel renderer that consumes the item's disclosure state from context. Kept as
335
+ * a small sub-component so the panel's `id` + `role=region` + `aria-labelledby` come from a
336
+ * single source — and so a closed item doesn't pay for rendering an empty panel.
337
+ */
338
+ const DetailPanel = ({ children }: PropsWithChildren) => {
339
+ const { expanded, panelProps } = useOrderedListItemContext('OrderedListDetailItem.Panel');
340
+ if (!expanded || !children) {
341
+ return null;
342
+ }
343
+ return (
344
+ <div {...panelProps} className='px-2 pb-2'>
345
+ {children}
346
+ </div>
347
+ );
348
+ };
@@ -0,0 +1,173 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type PropsWithChildren, type ReactNode, useMemo } from 'react';
7
+
8
+ import {
9
+ ScrollArea,
10
+ type ScrollAreaRootProps,
11
+ type ThemedClassName,
12
+ composable,
13
+ composableProps,
14
+ } from '@dxos/react-ui';
15
+ import { mx } from '@dxos/ui-theme';
16
+
17
+ import {
18
+ type ReorderActive,
19
+ type ReorderListController,
20
+ type UseListNavigationReturn,
21
+ type UseListDisclosureReturn,
22
+ useListDisclosure,
23
+ useListNavigation,
24
+ useReorderAutoScroll,
25
+ useReorderList,
26
+ } from '../../aspects';
27
+
28
+ export type ListItemRecord = any;
29
+
30
+ const ORDERED_LIST_NAME = 'OrderedList';
31
+
32
+ type OrderedListContextValue<T extends ListItemRecord> = {
33
+ reorder: ReorderListController<T>;
34
+ disclosure: UseListDisclosureReturn;
35
+ navigation: UseListNavigationReturn;
36
+ readonly?: boolean;
37
+ active: ReorderActive<T>;
38
+ /**
39
+ * Stable id accessor reused by items that want to look up their record (e.g. the
40
+ * `OrderedListItem` <-> `useReorderItem` plumbing).
41
+ */
42
+ getId: (item: T) => string;
43
+ };
44
+
45
+ const [OrderedListProvider, useOrderedListContext] = createContext<OrderedListContextValue<any>>(ORDERED_LIST_NAME);
46
+
47
+ export { useOrderedListContext };
48
+
49
+ export type OrderedListRootProps<T extends ListItemRecord> = ThemedClassName<{
50
+ items: readonly T[];
51
+ /**
52
+ * Type guard reserved for backwards compatibility with the deprecated `List` API. The
53
+ * aspect layer doesn't need it (payloads are scoped via the list's internal id) — values
54
+ * passed here are currently ignored. Will be removed when call-sites migrate.
55
+ */
56
+ isItem?: (item: any) => boolean;
57
+ /**
58
+ * Stable id accessor. When omitted, the hook falls back to reference equality, which
59
+ * breaks after a pragmatic-dnd round-trip serialises the payload — supply a `getId` for
60
+ * any list whose items are plain values rather than ECHO refs.
61
+ */
62
+ getId?: (item: T) => string;
63
+ onMove?: (fromIndex: number, toIndex: number) => void;
64
+ readonly?: boolean;
65
+ /** Controlled expanded item id (single-expand). */
66
+ expandedId?: string;
67
+ defaultExpandedId?: string;
68
+ onExpandedChange?: (id: string | undefined) => void;
69
+ children: (props: { items: readonly T[] }) => ReactNode;
70
+ }>;
71
+
72
+ const defaultGetId = <T extends ListItemRecord>(item: T) => (item as any)?.id;
73
+ const noopMove = () => {};
74
+
75
+ /**
76
+ * Reorderable, single-expandable master-detail list. Wraps the aspect hooks:
77
+ *
78
+ * - `useReorderList` — drag-and-drop reorder via pragmatic-dnd.
79
+ * - `useListDisclosure` (single mode) — single-expand state machine.
80
+ * - `useListNavigation` (list mode) — Tabster keyboard nav across items.
81
+ *
82
+ * Owns the drag-handle / delete / expand-caret chrome plus expand state. Renders no DOM
83
+ * itself; `OrderedListContent` is the container.
84
+ */
85
+ export const OrderedListRoot = <T extends ListItemRecord>({
86
+ items,
87
+ getId = defaultGetId,
88
+ onMove = noopMove,
89
+ readonly,
90
+ expandedId,
91
+ defaultExpandedId,
92
+ onExpandedChange,
93
+ children,
94
+ }: OrderedListRootProps<T>) => {
95
+ const { controller, active } = useReorderList<T>({
96
+ items,
97
+ getId,
98
+ onMove,
99
+ readonly,
100
+ });
101
+
102
+ const disclosure = useListDisclosure({
103
+ mode: 'single',
104
+ value: expandedId,
105
+ defaultValue: defaultExpandedId,
106
+ onValueChange: (next) => onExpandedChange?.(next),
107
+ });
108
+
109
+ const navigation = useListNavigation({ mode: 'list' });
110
+
111
+ // Memoise the context value so identity-stable items don't re-render on aspect re-renders
112
+ // that don't affect their bindings (e.g. an unrelated drag-state change).
113
+ const context = useMemo(
114
+ () => ({
115
+ reorder: controller,
116
+ disclosure,
117
+ navigation,
118
+ readonly,
119
+ active,
120
+ getId,
121
+ }),
122
+ [controller, disclosure, navigation, readonly, active, getId],
123
+ );
124
+
125
+ return <OrderedListProvider {...context}>{children({ items })}</OrderedListProvider>;
126
+ };
127
+
128
+ /**
129
+ * Container for the list. Applies the navigation aspect's `containerProps` so role,
130
+ * aria-orientation, Tabster attributes, and focus-on-entry are wired in one place.
131
+ */
132
+ export const OrderedListContent = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => {
133
+ const { navigation } = useOrderedListContext('OrderedList.Content');
134
+ return (
135
+ <div {...navigation.containerProps} className={mx('flex flex-col', classNames)}>
136
+ {children}
137
+ </div>
138
+ );
139
+ };
140
+
141
+ /**
142
+ * Optional ScrollArea wrapper for the list. Mirrors `Listbox.Viewport`. Include when the
143
+ * list needs to fill a constrained pane and scroll independently; omit for static lists
144
+ * that flow with their parent.
145
+ *
146
+ * Wires `useReorderAutoScroll` on the inner viewport so pragmatic-dnd auto-scrolls the
147
+ * container when a drag hovers near its edges — long lists can be reordered without
148
+ * scrolling manually first.
149
+ */
150
+ type OrderedListViewportProps = Pick<ScrollAreaRootProps, 'thin' | 'padding' | 'centered'>;
151
+
152
+ export const OrderedListViewport = composable<HTMLDivElement, OrderedListViewportProps>((props, forwardedRef) => {
153
+ const { thin, padding, centered, children, ...rest } = props as PropsWithChildren<
154
+ OrderedListViewportProps & Record<string, unknown>
155
+ >;
156
+ // Callback ref so registration fires on attach and cleanup on detach — `useEffect` on a
157
+ // ref object would miss the element entirely (ref mutations don't re-run effects).
158
+ const autoScrollRef = useReorderAutoScroll();
159
+ return (
160
+ <ScrollArea.Root
161
+ {...composableProps<HTMLDivElement>(rest, { classNames: 'dx-container' })}
162
+ {...{ thin, padding, centered }}
163
+ orientation='vertical'
164
+ ref={forwardedRef}
165
+ >
166
+ <ScrollArea.Viewport ref={autoScrollRef}>{children}</ScrollArea.Viewport>
167
+ </ScrollArea.Root>
168
+ );
169
+ });
170
+
171
+ OrderedListViewport.displayName = 'OrderedList.Viewport';
172
+
173
+ export type { OrderedListViewportProps };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './OrderedList';
@@ -316,6 +316,8 @@ const RawTreeItem = <T extends { id: string } = any>({
316
316
  aria-current={current ? ('' as 'page') : undefined}
317
317
  classNames={mx(
318
318
  'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-current-surface',
319
+ // Highlight the row while a descendant marks an open popover anchor (e.g. inline rename).
320
+ 'has-[[data-popover-anchor]]:bg-current-surface',
319
321
  hoverableControls,
320
322
  hoverableFocusedKeyboardControls,
321
323
  hoverableFocusedWithinControls,
@@ -4,8 +4,7 @@
4
4
 
5
5
  import React, { type KeyboardEvent, type MouseEvent, forwardRef, memo, useCallback } from 'react';
6
6
 
7
- import { Button, Icon, type Label, Tag, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
- import { TextTooltip } from '@dxos/react-ui-text-tooltip';
7
+ import { Button, Icon, type Label, Tag, TextTooltip, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
8
  import { getStyles } from '@dxos/ui-theme';
10
9
 
11
10
  // TODO(wittjosiah): Consider whether there should be a separate disabled prop which was visually distinct
@@ -4,8 +4,7 @@
4
4
 
5
5
  export * from './Accordion';
6
6
  export * from './Combobox';
7
- export * from './List';
8
7
  export * from './Listbox';
8
+ export * from './OrderedList';
9
9
  export * from './Picker';
10
- export * from './RowList';
11
10
  export * from './Tree';
package/src/index.ts CHANGED
@@ -2,5 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './aspects';
5
6
  export * from './components';
6
7
  export * from './util';
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // https://github.com/testing-library/jest-dom#with-vitest
6
+ import '@testing-library/jest-dom/vitest';
7
+
8
+ // https://github.com/jsdom/jsdom/issues/3368#issuecomment-1396749033
9
+ import ResizeObserver from 'resize-observer-polyfill';
10
+
11
+ global.ResizeObserver = ResizeObserver;
@@ -1,40 +0,0 @@
1
- import { ListItem, type ListItemProps, type ListItemRecord } from './ListItem';
2
- import { type ListRootProps } from './ListRoot';
3
- /**
4
- * Draggable list with per-row drag handles and delete buttons.
5
- * Ref: https://github.com/atlassian/pragmatic-drag-and-drop
6
- * Ref: https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/task.tsx
7
- *
8
- * @deprecated New code should use one of:
9
- *
10
- * - `RowList` / `CardList` from this same package — for selectable
11
- * pickers (master/detail). Correct ARIA + dx-* by construction.
12
- * - `Mosaic.Stack` / `Mosaic.VirtualStack` from `@dxos/react-ui-mosaic`
13
- * — for virtualized or drag-reorderable card stacks.
14
- *
15
- * This component is retained for the existing reorder-with-delete-button
16
- * use cases (plugin-meeting, plugin-automation, plugin-zen, etc.) until
17
- * each is migrated; see `AUDIT.md` Phase 6 for the migration plan.
18
- */
19
- export declare const List: {
20
- Root: <T extends unknown>({ children, items, isItem, getId, onMove, ...props }: ListRootProps<T>) => import("react").JSX.Element;
21
- Item: <T extends unknown>({ children, classNames, item, asChild, selected, ...props }: ListItemProps<T>) => import("react").JSX.Element;
22
- ItemDragPreview: <T extends unknown>({ children, }: {
23
- children: ({ item }: {
24
- item: T;
25
- }) => import("react").ReactNode;
26
- }) => import("react").ReactPortal | null;
27
- ItemWrapper: ({ classNames, children }: import("@dxos/ui-types").ThemedClassName<import("react").PropsWithChildren>) => import("react").JSX.Element;
28
- ItemDragHandle: ({ disabled }: Pick<import("@dxos/react-ui").IconButtonProps, 'disabled'>) => import("react").JSX.Element;
29
- ItemIconButton: ({ autoHide, iconOnly, variant, classNames, disabled, ...props }: import("@dxos/react-ui").IconButtonProps & {
30
- autoHide?: boolean;
31
- }) => import("react").JSX.Element;
32
- ItemDeleteButton: ({ autoHide, classNames, disabled, icon, label, ...props }: Partial<Pick<import("@dxos/react-ui").IconButtonProps, 'icon'>> & Omit<import("@dxos/react-ui").IconButtonProps, 'icon' | 'label'> & {
33
- autoHide?: boolean;
34
- label?: string;
35
- }) => import("react").JSX.Element;
36
- ItemTitle: ({ classNames, children, ...props }: import("@dxos/ui-types").ThemedClassName<import("react").PropsWithChildren<import("react").ComponentProps<'div'>>>) => import("react").JSX.Element;
37
- };
38
- type ListItem = ListItemRecord;
39
- export type { ListRootProps, ListItemProps, ListItem, ListItemRecord };
40
- //# sourceMappingURL=List.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"List.d.ts","sourceRoot":"","sources":["../../../../../src/components/List/List.tsx"],"names":[],"mappings":"AAIA,OAAO,EACL,QAAQ,EAKR,KAAK,aAAa,EAClB,KAAK,cAAc,EAGpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,IAAI;IACf,IAAI;IACJ,IAAI;IACJ,eAAe;;;;;IACf,WAAW;IACX,cAAc;IACd,cAAc;;;IACd,gBAAgB;;;;IAChB,SAAS;CACV,CAAC;AAEF,KAAK,QAAQ,GAAG,cAAc,CAAC;AAE/B,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC"}
@@ -1,18 +0,0 @@
1
- import { type StoryObj } from '@storybook/react-vite';
2
- import React from 'react';
3
- import { type ListRootProps } from './List';
4
- import { type TestItemType } from './testing';
5
- declare const DefaultStory: (props: Omit<ListRootProps<TestItemType>, 'items'>) => React.JSX.Element;
6
- declare const SimpleStory: (props: Omit<ListRootProps<TestItemType>, 'items'>) => React.JSX.Element;
7
- declare const meta: {
8
- title: string;
9
- component: <T extends unknown>({ children, items, isItem, getId, onMove, ...props }: ListRootProps<T>) => React.JSX.Element;
10
- decorators: import("@storybook/react").Decorator[];
11
- parameters: {
12
- layout: string;
13
- };
14
- };
15
- export default meta;
16
- export declare const Default: StoryObj<typeof DefaultStory>;
17
- export declare const Simple: StoryObj<typeof SimpleStory>;
18
- //# sourceMappingURL=List.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"List.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/List/List.stories.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjE,OAAO,KAA8B,MAAM,OAAO,CAAC;AAOnD,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAkB,KAAK,YAAY,EAA6B,MAAM,WAAW,CAAC;AAKzF,QAAA,MAAM,YAAY,UAAW,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,sBA6DtE,CAAC;AAEF,QAAA,MAAM,WAAW,UAAW,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,sBAoBrE,CAAC;AAEF,QAAA,MAAM,IAAI;;;;;QAKN,MAAM;;CAEwB,CAAC;eAEpB,IAAI;AAEnB,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC,OAAO,YAAY,CAKjD,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,QAAQ,CAAC,OAAO,WAAW,CAK/C,CAAC"}