@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
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
type Edge,
|
|
7
|
-
attachClosestEdge,
|
|
8
|
-
extractClosestEdge,
|
|
9
|
-
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
10
|
-
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
11
|
-
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
12
|
-
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
|
13
|
-
import { createContext } from '@radix-ui/react-context';
|
|
14
|
-
import { Slot } from '@radix-ui/react-slot';
|
|
15
|
-
import React, {
|
|
16
|
-
type ComponentProps,
|
|
17
|
-
type HTMLAttributes,
|
|
18
|
-
type PropsWithChildren,
|
|
19
|
-
type ReactNode,
|
|
20
|
-
RefObject,
|
|
21
|
-
useEffect,
|
|
22
|
-
useRef,
|
|
23
|
-
useState,
|
|
24
|
-
} from 'react';
|
|
25
|
-
import { createPortal } from 'react-dom';
|
|
26
|
-
|
|
27
|
-
import { invariant } from '@dxos/invariant';
|
|
28
|
-
import {
|
|
29
|
-
IconButton,
|
|
30
|
-
type IconButtonProps,
|
|
31
|
-
ListItem as NaturalListItem,
|
|
32
|
-
type ThemedClassName,
|
|
33
|
-
useTranslation,
|
|
34
|
-
} from '@dxos/react-ui';
|
|
35
|
-
import { mx, osTranslations } from '@dxos/ui-theme';
|
|
36
|
-
|
|
37
|
-
import { useListContext } from './ListRoot';
|
|
38
|
-
|
|
39
|
-
export type ListItemRecord = any;
|
|
40
|
-
|
|
41
|
-
export type ItemDragState =
|
|
42
|
-
| {
|
|
43
|
-
type: 'idle';
|
|
44
|
-
}
|
|
45
|
-
| {
|
|
46
|
-
type: 'preview';
|
|
47
|
-
container: HTMLElement;
|
|
48
|
-
}
|
|
49
|
-
| {
|
|
50
|
-
type: 'is-dragging';
|
|
51
|
-
}
|
|
52
|
-
| {
|
|
53
|
-
type: 'is-dragging-over';
|
|
54
|
-
closestEdge: Edge | null;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const idle: ItemDragState = { type: 'idle' };
|
|
58
|
-
|
|
59
|
-
const stateStyles: { [Key in ItemDragState['type']]?: HTMLAttributes<HTMLDivElement>['className'] } = {
|
|
60
|
-
'is-dragging': 'opacity-50',
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
type ListItemContext<T extends ListItemRecord> = {
|
|
64
|
-
item: T;
|
|
65
|
-
dragHandleRef: RefObject<HTMLButtonElement | null>;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Default context defined for ListItemDragPreview, which is defined outside of ListItem.
|
|
70
|
-
*/
|
|
71
|
-
const defaultContext: ListItemContext<any> = {} as any;
|
|
72
|
-
|
|
73
|
-
const LIST_ITEM_NAME = 'ListItem';
|
|
74
|
-
|
|
75
|
-
export const [ListItemProvider, useListItemContext] = createContext<ListItemContext<any>>(
|
|
76
|
-
LIST_ITEM_NAME,
|
|
77
|
-
defaultContext,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
|
|
81
|
-
PropsWithChildren<
|
|
82
|
-
{
|
|
83
|
-
item: T;
|
|
84
|
-
asChild?: boolean;
|
|
85
|
-
selected?: boolean;
|
|
86
|
-
} & HTMLAttributes<HTMLDivElement>
|
|
87
|
-
>
|
|
88
|
-
>;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Draggable list item.
|
|
92
|
-
*/
|
|
93
|
-
export const ListItem = <T extends ListItemRecord>({
|
|
94
|
-
children,
|
|
95
|
-
classNames,
|
|
96
|
-
item,
|
|
97
|
-
asChild,
|
|
98
|
-
selected,
|
|
99
|
-
...props
|
|
100
|
-
}: ListItemProps<T>) => {
|
|
101
|
-
const Comp = asChild ? Slot : 'div';
|
|
102
|
-
const { isItem, readonly, dragPreview, setState: setRootState } = useListContext(LIST_ITEM_NAME);
|
|
103
|
-
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
104
|
-
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
|
105
|
-
const [state, setState] = useState<ItemDragState>(idle);
|
|
106
|
-
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
const element = rootRef.current;
|
|
109
|
-
invariant(element);
|
|
110
|
-
return combine(
|
|
111
|
-
//
|
|
112
|
-
// https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about#draggable
|
|
113
|
-
//
|
|
114
|
-
draggable({
|
|
115
|
-
element,
|
|
116
|
-
dragHandle: dragHandleRef.current!,
|
|
117
|
-
canDrag: () => !readonly,
|
|
118
|
-
getInitialData: () => item as any,
|
|
119
|
-
onGenerateDragPreview: dragPreview
|
|
120
|
-
? ({ nativeSetDragImage, source }) => {
|
|
121
|
-
const rect = source.element.getBoundingClientRect();
|
|
122
|
-
setCustomNativeDragPreview({
|
|
123
|
-
nativeSetDragImage,
|
|
124
|
-
getOffset: ({ container }) => {
|
|
125
|
-
const { height } = container.getBoundingClientRect();
|
|
126
|
-
return { x: 20, y: height / 2 };
|
|
127
|
-
},
|
|
128
|
-
render: ({ container }) => {
|
|
129
|
-
container.style.width = rect.width + 'px';
|
|
130
|
-
setState({ type: 'preview', container });
|
|
131
|
-
setRootState({ type: 'preview', container, item });
|
|
132
|
-
return () => {}; // TODO(burdon): Cleanup.
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
: undefined,
|
|
137
|
-
onDragStart: () => {
|
|
138
|
-
setState({ type: 'is-dragging' });
|
|
139
|
-
setRootState({ type: 'is-dragging', item });
|
|
140
|
-
},
|
|
141
|
-
onDrop: () => {
|
|
142
|
-
setState(idle);
|
|
143
|
-
setRootState(idle);
|
|
144
|
-
},
|
|
145
|
-
}),
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
// https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about#drop-target-for-elements
|
|
149
|
-
//
|
|
150
|
-
dropTargetForElements({
|
|
151
|
-
element,
|
|
152
|
-
canDrop: ({ source }) => {
|
|
153
|
-
return (source.element !== element && isItem?.(source.data)) ?? false;
|
|
154
|
-
},
|
|
155
|
-
getData: ({ input }) => {
|
|
156
|
-
return attachClosestEdge(item as any, { element, input, allowedEdges: ['top', 'bottom'] });
|
|
157
|
-
},
|
|
158
|
-
getIsSticky: () => true,
|
|
159
|
-
onDragEnter: ({ self }) => {
|
|
160
|
-
const closestEdge = extractClosestEdge(self.data);
|
|
161
|
-
setState({ type: 'is-dragging-over', closestEdge });
|
|
162
|
-
},
|
|
163
|
-
onDragLeave: () => {
|
|
164
|
-
setState(idle);
|
|
165
|
-
},
|
|
166
|
-
onDrag: ({ self }) => {
|
|
167
|
-
const closestEdge = extractClosestEdge(self.data);
|
|
168
|
-
setState((current) => {
|
|
169
|
-
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
|
170
|
-
return current;
|
|
171
|
-
}
|
|
172
|
-
return { type: 'is-dragging-over', closestEdge };
|
|
173
|
-
});
|
|
174
|
-
},
|
|
175
|
-
onDrop: () => {
|
|
176
|
-
setState(idle);
|
|
177
|
-
},
|
|
178
|
-
}),
|
|
179
|
-
);
|
|
180
|
-
}, [item]);
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<ListItemProvider item={item} dragHandleRef={dragHandleRef}>
|
|
184
|
-
<Comp
|
|
185
|
-
{...props}
|
|
186
|
-
role='listitem'
|
|
187
|
-
aria-selected={selected}
|
|
188
|
-
className={mx('relative p-1 dx-selected dx-hover', classNames, stateStyles[state.type])}
|
|
189
|
-
ref={rootRef}
|
|
190
|
-
>
|
|
191
|
-
{children}
|
|
192
|
-
</Comp>
|
|
193
|
-
{state.type === 'is-dragging-over' && state.closestEdge && (
|
|
194
|
-
<NaturalListItem.DropIndicator edge={state.closestEdge} />
|
|
195
|
-
)}
|
|
196
|
-
</ListItemProvider>
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
// List item components
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
export const ListItemIconButton = ({
|
|
205
|
-
autoHide = true,
|
|
206
|
-
iconOnly = true,
|
|
207
|
-
variant = 'ghost',
|
|
208
|
-
classNames,
|
|
209
|
-
disabled,
|
|
210
|
-
...props
|
|
211
|
-
}: IconButtonProps & { autoHide?: boolean }) => {
|
|
212
|
-
const { state } = useListContext('ITEM_BUTTON');
|
|
213
|
-
const isDisabled = state.type !== 'idle' || disabled;
|
|
214
|
-
return (
|
|
215
|
-
<IconButton
|
|
216
|
-
{...props}
|
|
217
|
-
disabled={isDisabled}
|
|
218
|
-
iconOnly={iconOnly}
|
|
219
|
-
variant={variant}
|
|
220
|
-
classNames={[classNames, autoHide && disabled && 'hidden']}
|
|
221
|
-
/>
|
|
222
|
-
);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
// TODO(burdon): Generalize to action button.
|
|
226
|
-
export const ListItemDeleteButton = ({
|
|
227
|
-
autoHide = true,
|
|
228
|
-
classNames,
|
|
229
|
-
disabled,
|
|
230
|
-
icon = 'ph--x--regular',
|
|
231
|
-
label,
|
|
232
|
-
...props
|
|
233
|
-
}: Partial<Pick<IconButtonProps, 'icon'>> &
|
|
234
|
-
Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
|
|
235
|
-
const { state } = useListContext('DELETE_BUTTON');
|
|
236
|
-
const isDisabled = state.type !== 'idle' || disabled;
|
|
237
|
-
const { t } = useTranslation(osTranslations);
|
|
238
|
-
return (
|
|
239
|
-
<IconButton
|
|
240
|
-
{...props}
|
|
241
|
-
variant='ghost'
|
|
242
|
-
disabled={isDisabled}
|
|
243
|
-
icon={icon}
|
|
244
|
-
iconOnly
|
|
245
|
-
label={label ?? t('delete.label')}
|
|
246
|
-
classNames={[classNames, autoHide && disabled && 'hidden']}
|
|
247
|
-
/>
|
|
248
|
-
);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
export const ListItemDragHandle = ({ disabled }: Pick<IconButtonProps, 'disabled'>) => {
|
|
252
|
-
const { dragHandleRef } = useListItemContext('DRAG_HANDLE');
|
|
253
|
-
const { t } = useTranslation(osTranslations);
|
|
254
|
-
return (
|
|
255
|
-
<IconButton
|
|
256
|
-
variant='ghost'
|
|
257
|
-
disabled={disabled}
|
|
258
|
-
icon='ph--dots-six-vertical--regular'
|
|
259
|
-
iconOnly
|
|
260
|
-
label={t('drag-handle.label')}
|
|
261
|
-
ref={dragHandleRef}
|
|
262
|
-
/>
|
|
263
|
-
);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
export const ListItemDragPreview = <T extends ListItemRecord>({
|
|
267
|
-
children,
|
|
268
|
-
}: {
|
|
269
|
-
children: ({ item }: { item: T }) => ReactNode;
|
|
270
|
-
}) => {
|
|
271
|
-
const { state } = useListContext('DRAG_PREVIEW');
|
|
272
|
-
return state?.type === 'preview' ? createPortal(children({ item: state.item }), state.container) : null;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
|
|
276
|
-
<div className={mx('flex w-full gap-2', classNames)}>{children}</div>
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
export const ListItemTitle = ({
|
|
280
|
-
classNames,
|
|
281
|
-
children,
|
|
282
|
-
...props
|
|
283
|
-
}: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
|
|
284
|
-
<div className={mx('flex grow items-center truncate', classNames)} {...props}>
|
|
285
|
-
{children}
|
|
286
|
-
</div>
|
|
287
|
-
);
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
6
|
-
import { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index';
|
|
7
|
-
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
8
|
-
import { createContext } from '@radix-ui/react-context';
|
|
9
|
-
import React, { type ReactNode, useCallback, useEffect, useState } from 'react';
|
|
10
|
-
|
|
11
|
-
import { type ItemDragState, type ListItemRecord, idle } from './ListItem';
|
|
12
|
-
|
|
13
|
-
type ListContext<T extends ListItemRecord> = {
|
|
14
|
-
// TODO(burdon): Rename drag state.
|
|
15
|
-
state: ItemDragState & { item?: T };
|
|
16
|
-
setState: (state: ItemDragState & { item?: T }) => void;
|
|
17
|
-
readonly?: boolean;
|
|
18
|
-
dragPreview?: boolean;
|
|
19
|
-
isItem?: (item: any) => boolean;
|
|
20
|
-
getId?: (item: T) => string; // TODO(burdon): Require if T doesn't conform to type.
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const LIST_NAME = 'List';
|
|
24
|
-
|
|
25
|
-
export const [ListProvider, useListContext] = createContext<ListContext<any>>(LIST_NAME);
|
|
26
|
-
|
|
27
|
-
export type ListRendererProps<T extends ListItemRecord> = {
|
|
28
|
-
state: ListContext<T>['state'];
|
|
29
|
-
items: readonly T[];
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const defaultGetId = <T extends ListItemRecord>(item: T) => (item as any)?.id;
|
|
33
|
-
|
|
34
|
-
export type ListRootProps<T extends ListItemRecord> = {
|
|
35
|
-
children?: (props: ListRendererProps<T>) => ReactNode;
|
|
36
|
-
items?: readonly T[];
|
|
37
|
-
onMove?: (fromIndex: number, toIndex: number) => void;
|
|
38
|
-
} & Pick<ListContext<T>, 'isItem' | 'getId' | 'readonly' | 'dragPreview'>;
|
|
39
|
-
|
|
40
|
-
export const ListRoot = <T extends ListItemRecord>({
|
|
41
|
-
children,
|
|
42
|
-
items,
|
|
43
|
-
isItem,
|
|
44
|
-
getId = defaultGetId,
|
|
45
|
-
onMove,
|
|
46
|
-
...props
|
|
47
|
-
}: ListRootProps<T>) => {
|
|
48
|
-
const isEqual = useCallback(
|
|
49
|
-
(a: T, b: T) => {
|
|
50
|
-
const idA = getId?.(a);
|
|
51
|
-
const idB = getId?.(b);
|
|
52
|
-
|
|
53
|
-
if (idA !== undefined && idB !== undefined) {
|
|
54
|
-
return idA === idB;
|
|
55
|
-
} else {
|
|
56
|
-
// Fallback for primitive values or when getId fails.
|
|
57
|
-
// NOTE(ZaymonFC): After drag and drop, pragmatic internally serializes drop targets which breaks reference equality.
|
|
58
|
-
// You must provide an `getId` function that returns a stable identifier for your items.
|
|
59
|
-
return a === b;
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
[getId],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const [state, setState] = useState<ListContext<T>['state']>(idle);
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (!items) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return monitorForElements({
|
|
72
|
-
canMonitor: ({ source }) => isItem?.(source.data) ?? false,
|
|
73
|
-
onDrop: ({ location, source }) => {
|
|
74
|
-
const target = location.current.dropTargets[0];
|
|
75
|
-
if (!target) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sourceData = source.data;
|
|
80
|
-
const targetData = target.data;
|
|
81
|
-
if (!isItem?.(sourceData) || !isItem?.(targetData)) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const sourceIdx = items.findIndex((item) => isEqual(item, sourceData as T));
|
|
86
|
-
const targetIdx = items.findIndex((item) => isEqual(item, targetData as T));
|
|
87
|
-
if (targetIdx < 0 || sourceIdx < 0) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const closestEdgeOfTarget = extractClosestEdge(targetData);
|
|
91
|
-
const destinationIndex = getReorderDestinationIndex({
|
|
92
|
-
closestEdgeOfTarget,
|
|
93
|
-
startIndex: sourceIdx,
|
|
94
|
-
indexOfTarget: targetIdx,
|
|
95
|
-
axis: 'vertical',
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
onMove?.(sourceIdx, destinationIndex);
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
}, [items, isEqual, onMove]);
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<ListProvider {...{ state, setState, isItem, ...props }}>{children?.({ state, items: items ?? [] })}</ListProvider>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import * as Schema from 'effect/Schema';
|
|
6
|
-
|
|
7
|
-
import { Obj } from '@dxos/echo';
|
|
8
|
-
import { random } from '@dxos/random';
|
|
9
|
-
|
|
10
|
-
export const TestItemSchema = Schema.Struct({
|
|
11
|
-
id: Obj.ID,
|
|
12
|
-
name: Schema.String,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export type TestItemType = Schema.Schema.Type<typeof TestItemSchema>;
|
|
16
|
-
|
|
17
|
-
export const TestList = Schema.Struct({
|
|
18
|
-
items: Schema.mutable(Schema.Array(TestItemSchema)),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export type TestList = Schema.Schema.Type<typeof TestList>;
|
|
22
|
-
|
|
23
|
-
export const createList = (n = 10): TestList => ({
|
|
24
|
-
items: random.helpers.multiple(
|
|
25
|
-
() => ({
|
|
26
|
-
id: random.string.uuid(),
|
|
27
|
-
name: random.commerce.productName(),
|
|
28
|
-
}),
|
|
29
|
-
{ count: n },
|
|
30
|
-
),
|
|
31
|
-
});
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2026 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React, { useState } from 'react';
|
|
7
|
-
|
|
8
|
-
import { random } from '@dxos/random';
|
|
9
|
-
import { Input, Panel, Toolbar } from '@dxos/react-ui';
|
|
10
|
-
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
-
|
|
12
|
-
import { Row, RowList } from './RowList';
|
|
13
|
-
|
|
14
|
-
random.seed(1);
|
|
15
|
-
|
|
16
|
-
type TestItem = { id: string; name: string; description: string };
|
|
17
|
-
|
|
18
|
-
const allItems: TestItem[] = Array.from({ length: 24 }, (_, i) => ({
|
|
19
|
-
id: `item-${i}`,
|
|
20
|
-
name: random.commerce.productName(),
|
|
21
|
-
description: random.lorem.sentences(2),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
// Single configurable story for the basic-listbox variants
|
|
26
|
-
// (Default / Thin / WithDisabled). MasterDetail and WithToolbar
|
|
27
|
-
// diverge structurally and keep their own render functions per
|
|
28
|
-
// AUDIT.md §11.
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
type StoryArgs = {
|
|
32
|
-
/** Items to render. Defaults to the full 24-item catalog. */
|
|
33
|
-
items?: TestItem[];
|
|
34
|
-
/** Forwards to `RowList.Viewport thin`. */
|
|
35
|
-
thin?: boolean;
|
|
36
|
-
/** Forwards to `RowList.Viewport padding`. */
|
|
37
|
-
padding?: boolean;
|
|
38
|
-
/** Index into `items` that should render disabled. */
|
|
39
|
-
disabledIndex?: number;
|
|
40
|
-
/** Render the description line under each row's name. */
|
|
41
|
-
showDescription?: boolean;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const DefaultStory = ({
|
|
45
|
-
items = allItems,
|
|
46
|
-
thin = false,
|
|
47
|
-
padding = false,
|
|
48
|
-
disabledIndex,
|
|
49
|
-
showDescription = true,
|
|
50
|
-
}: StoryArgs = {}) => {
|
|
51
|
-
const [selected, setSelected] = useState<string | undefined>(items[0]?.id);
|
|
52
|
-
return (
|
|
53
|
-
<RowList.Root selectedId={selected} onSelectChange={setSelected}>
|
|
54
|
-
<RowList.Viewport thin={thin} padding={padding}>
|
|
55
|
-
<RowList.Content aria-label='Items'>
|
|
56
|
-
{items.map((item, i) => {
|
|
57
|
-
const disabled = i === disabledIndex;
|
|
58
|
-
return (
|
|
59
|
-
<Row key={item.id} id={item.id} disabled={disabled}>
|
|
60
|
-
<div className='font-medium'>
|
|
61
|
-
{item.name}
|
|
62
|
-
{disabled && ' (disabled)'}
|
|
63
|
-
</div>
|
|
64
|
-
{showDescription && <div className='text-sm text-description line-clamp-1'>{item.description}</div>}
|
|
65
|
-
</Row>
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
68
|
-
</RowList.Content>
|
|
69
|
-
</RowList.Viewport>
|
|
70
|
-
</RowList.Root>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
// Master/detail — list is one pane of a layout.
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
const MasterDetailStory = () => {
|
|
79
|
-
const [selected, setSelected] = useState<string | undefined>(allItems[0].id);
|
|
80
|
-
const detail = allItems.find(({ id }) => id === selected);
|
|
81
|
-
return (
|
|
82
|
-
<div className='dx-container grid grid-cols-[20rem_1fr] divide-x divide-separator'>
|
|
83
|
-
<RowList.Root selectedId={selected} onSelectChange={setSelected}>
|
|
84
|
-
<RowList.Viewport>
|
|
85
|
-
<RowList.Content aria-label='Items'>
|
|
86
|
-
{allItems.map((item) => (
|
|
87
|
-
<Row key={item.id} id={item.id}>
|
|
88
|
-
<div className='font-medium'>{item.name}</div>
|
|
89
|
-
</Row>
|
|
90
|
-
))}
|
|
91
|
-
</RowList.Content>
|
|
92
|
-
</RowList.Viewport>
|
|
93
|
-
</RowList.Root>
|
|
94
|
-
<div role='region' aria-label='Detail' className='dx-container p-4 overflow-auto'>
|
|
95
|
-
{detail && (
|
|
96
|
-
<>
|
|
97
|
-
<h2 className='text-lg font-semibold'>{detail.name}</h2>
|
|
98
|
-
<p className='text-description mt-2'>{detail.description}</p>
|
|
99
|
-
</>
|
|
100
|
-
)}
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
// Toolbar + viewport siblings — Root is headless, so layout is the
|
|
108
|
-
// caller's responsibility. `Panel` is the canonical chrome wrapper.
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
const WithToolbarStory = () => {
|
|
112
|
-
const [selected, setSelected] = useState<string | undefined>(allItems[0].id);
|
|
113
|
-
const [filter, setFilter] = useState('');
|
|
114
|
-
const filtered = allItems.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()));
|
|
115
|
-
return (
|
|
116
|
-
<RowList.Root selectedId={selected} onSelectChange={setSelected}>
|
|
117
|
-
<Panel.Root>
|
|
118
|
-
<Panel.Toolbar asChild>
|
|
119
|
-
<Toolbar.Root>
|
|
120
|
-
<Input.Root>
|
|
121
|
-
<Input.Label srOnly>Filter items</Input.Label>
|
|
122
|
-
<Input.TextInput
|
|
123
|
-
placeholder='Filter…'
|
|
124
|
-
value={filter}
|
|
125
|
-
onChange={(event) => setFilter(event.target.value)}
|
|
126
|
-
/>
|
|
127
|
-
</Input.Root>
|
|
128
|
-
</Toolbar.Root>
|
|
129
|
-
</Panel.Toolbar>
|
|
130
|
-
<Panel.Content asChild>
|
|
131
|
-
<RowList.Viewport>
|
|
132
|
-
<RowList.Content aria-label='Items'>
|
|
133
|
-
{filtered.map((item) => (
|
|
134
|
-
<Row key={item.id} id={item.id}>
|
|
135
|
-
{item.name}
|
|
136
|
-
</Row>
|
|
137
|
-
))}
|
|
138
|
-
</RowList.Content>
|
|
139
|
-
</RowList.Viewport>
|
|
140
|
-
</Panel.Content>
|
|
141
|
-
</Panel.Root>
|
|
142
|
-
</RowList.Root>
|
|
143
|
-
);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const meta = {
|
|
147
|
-
title: 'ui/react-ui-list/RowList',
|
|
148
|
-
render: (args) => <DefaultStory {...args} />,
|
|
149
|
-
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
150
|
-
parameters: {
|
|
151
|
-
layout: 'fullscreen',
|
|
152
|
-
},
|
|
153
|
-
} satisfies Meta<StoryArgs>;
|
|
154
|
-
|
|
155
|
-
export default meta;
|
|
156
|
-
|
|
157
|
-
type Story = StoryObj<StoryArgs>;
|
|
158
|
-
|
|
159
|
-
export const Default: Story = {};
|
|
160
|
-
export const Thin: Story = { args: { thin: true, padding: true, showDescription: false } };
|
|
161
|
-
export const WithDisabled: Story = { args: { items: allItems.slice(0, 6), disabledIndex: 2 } };
|
|
162
|
-
export const MasterDetail: Story = { render: () => <MasterDetailStory /> };
|
|
163
|
-
export const WithToolbar: Story = { render: () => <WithToolbarStory /> };
|