@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
@@ -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,5 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- export * from './List';
@@ -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 /> };