@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,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 };
|
|
@@ -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
|
package/src/components/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -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"}
|