@dxos/react-ui-list 0.8.3 → 0.8.4-main.1c7ec43d41

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 (106) hide show
  1. package/dist/lib/browser/index.mjs +1374 -739
  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 +1374 -739
  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/components/Accordion/Accordion.d.ts +1 -1
  8. package/dist/types/src/components/Accordion/Accordion.d.ts.map +1 -1
  9. package/dist/types/src/components/Accordion/Accordion.stories.d.ts +7 -4
  10. package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Accordion/AccordionItem.d.ts +1 -1
  12. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  13. package/dist/types/src/components/Accordion/AccordionRoot.d.ts +1 -1
  14. package/dist/types/src/components/Accordion/AccordionRoot.d.ts.map +1 -1
  15. package/dist/types/src/components/Combobox/Combobox.d.ts +105 -0
  16. package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
  17. package/dist/types/src/components/Combobox/Combobox.stories.d.ts +12 -0
  18. package/dist/types/src/components/Combobox/Combobox.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/Combobox/index.d.ts +2 -0
  20. package/dist/types/src/components/Combobox/index.d.ts.map +1 -0
  21. package/dist/types/src/components/List/List.d.ts +21 -10
  22. package/dist/types/src/components/List/List.d.ts.map +1 -1
  23. package/dist/types/src/components/List/List.stories.d.ts +14 -5
  24. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/List/ListItem.d.ts +12 -13
  26. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  27. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  28. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  29. package/dist/types/src/components/List/testing.d.ts +1 -1
  30. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  31. package/dist/types/src/components/Listbox/Listbox.d.ts +27 -0
  32. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
  33. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +12 -0
  34. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Listbox/index.d.ts +2 -0
  36. package/dist/types/src/components/Listbox/index.d.ts.map +1 -0
  37. package/dist/types/src/components/Picker/Picker.d.ts +49 -0
  38. package/dist/types/src/components/Picker/Picker.d.ts.map +1 -0
  39. package/dist/types/src/components/Picker/Picker.stories.d.ts +28 -0
  40. package/dist/types/src/components/Picker/Picker.stories.d.ts.map +1 -0
  41. package/dist/types/src/components/Picker/context.d.ts +29 -0
  42. package/dist/types/src/components/Picker/context.d.ts.map +1 -0
  43. package/dist/types/src/components/Picker/index.d.ts +3 -0
  44. package/dist/types/src/components/Picker/index.d.ts.map +1 -0
  45. package/dist/types/src/components/RowList/RowList.d.ts +61 -0
  46. package/dist/types/src/components/RowList/RowList.d.ts.map +1 -0
  47. package/dist/types/src/components/RowList/RowList.stories.d.ts +35 -0
  48. package/dist/types/src/components/RowList/RowList.stories.d.ts.map +1 -0
  49. package/dist/types/src/components/RowList/index.d.ts +3 -0
  50. package/dist/types/src/components/RowList/index.d.ts.map +1 -0
  51. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  52. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  53. package/dist/types/src/components/Tree/Tree.stories.d.ts +18 -7
  54. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  55. package/dist/types/src/components/Tree/TreeContext.d.ts +24 -10
  56. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  57. package/dist/types/src/components/Tree/TreeItem.d.ts +32 -10
  58. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  59. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
  60. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  61. package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
  62. package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
  63. package/dist/types/src/components/Tree/helpers.d.ts.map +1 -1
  64. package/dist/types/src/components/Tree/index.d.ts +2 -0
  65. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Tree/testing.d.ts +3 -3
  67. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  68. package/dist/types/src/components/index.d.ts +4 -0
  69. package/dist/types/src/components/index.d.ts.map +1 -1
  70. package/dist/types/src/util/path.d.ts.map +1 -1
  71. package/dist/types/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +35 -33
  73. package/src/components/Accordion/Accordion.stories.tsx +8 -10
  74. package/src/components/Accordion/Accordion.tsx +1 -1
  75. package/src/components/Accordion/AccordionItem.tsx +7 -5
  76. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  77. package/src/components/Combobox/Combobox.stories.tsx +60 -0
  78. package/src/components/Combobox/Combobox.tsx +387 -0
  79. package/src/components/Combobox/index.ts +5 -0
  80. package/src/components/List/List.stories.tsx +46 -32
  81. package/src/components/List/List.tsx +15 -14
  82. package/src/components/List/ListItem.tsx +91 -57
  83. package/src/components/List/ListRoot.tsx +4 -4
  84. package/src/components/List/testing.ts +7 -7
  85. package/src/components/Listbox/Listbox.stories.tsx +48 -0
  86. package/src/components/Listbox/Listbox.tsx +201 -0
  87. package/src/components/Listbox/index.ts +5 -0
  88. package/src/components/Picker/Picker.stories.tsx +131 -0
  89. package/src/components/Picker/Picker.tsx +368 -0
  90. package/src/components/Picker/context.ts +43 -0
  91. package/src/components/Picker/index.ts +6 -0
  92. package/src/components/RowList/RowList.stories.tsx +163 -0
  93. package/src/components/RowList/RowList.tsx +353 -0
  94. package/src/components/RowList/index.ts +6 -0
  95. package/src/components/Tree/Tree.stories.tsx +175 -83
  96. package/src/components/Tree/Tree.tsx +43 -40
  97. package/src/components/Tree/TreeContext.tsx +21 -9
  98. package/src/components/Tree/TreeItem.tsx +222 -135
  99. package/src/components/Tree/TreeItemHeading.tsx +15 -12
  100. package/src/components/Tree/TreeItemToggle.tsx +29 -19
  101. package/src/components/Tree/index.ts +2 -0
  102. package/src/components/Tree/testing.ts +10 -9
  103. package/src/components/index.ts +4 -0
  104. package/dist/lib/node/index.cjs +0 -886
  105. package/dist/lib/node/index.cjs.map +0 -7
  106. package/dist/lib/node/meta.json +0 -1
@@ -2,40 +2,43 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type Meta, type StoryObj } from '@storybook/react';
8
- import { Schema } from 'effect';
9
- import React from 'react';
10
-
11
- import { live } from '@dxos/live-object';
12
- import { ghostHover, mx } from '@dxos/react-ui-theme';
13
- import { withLayout, withTheme } from '@dxos/storybook-utils';
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import * as Schema from 'effect/Schema';
8
+ import React, { useContext, useMemo } from 'react';
9
+
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { withRegistry } from '@dxos/storybook-utils';
12
+ import { mx } from '@dxos/ui-theme';
14
13
  import { arrayMove } from '@dxos/util';
15
14
 
16
15
  import { List, type ListRootProps } from './List';
17
- import { createList, TestItemSchema, type TestItemType } from './testing';
16
+ import { TestItemSchema, type TestItemType, type TestList, createList } from './testing';
18
17
 
19
18
  // TODO(burdon): var-icon-size.
20
- const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2rem] rounded';
21
-
22
- const meta: Meta = {
23
- title: 'ui/react-ui-list/List',
24
- decorators: [withTheme, withLayout({ fullscreen: true })],
25
- };
19
+ const grid = 'grid grid-cols-[32px_1fr_32px] min-h-[2rem] rounded-sm';
26
20
 
27
- export default meta;
21
+ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
22
+ const registry = useContext(RegistryContext);
23
+ const listAtom = useMemo(() => Atom.make<TestList>(createList(100)).pipe(Atom.keepAlive), []);
24
+ const list = useAtomValue(listAtom);
25
+ const items = list.items;
28
26
 
29
- const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
30
27
  const handleSelect = (item: TestItemType) => {
31
28
  console.log('select', item);
32
29
  };
33
30
  const handleDelete = (item: TestItemType) => {
34
- const idx = items.findIndex((i) => i.id === item.id);
35
- items.splice(idx, 1);
31
+ const prev = registry.get(listAtom);
32
+ registry.set(listAtom, {
33
+ ...prev,
34
+ items: prev.items.filter((i) => i.id !== item.id),
35
+ });
36
36
  };
37
37
  const handleMove = (from: number, to: number) => {
38
- arrayMove(items, from, to);
38
+ const prev = registry.get(listAtom);
39
+ const newItems = [...prev.items];
40
+ arrayMove(newItems, from, to);
41
+ registry.set(listAtom, { ...prev, items: newItems });
39
42
  };
40
43
 
41
44
  return (
@@ -48,9 +51,9 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
48
51
  <div className='flex items-center text-sm'>Items</div>
49
52
  </div>
50
53
 
51
- <div role='list' className='w-full h-full overflow-auto'>
54
+ <div role='list' className='h-full w-full overflow-auto'>
52
55
  {items?.map((item) => (
53
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
56
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
54
57
  <List.ItemDragHandle />
55
58
  <List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
56
59
  <List.ItemDeleteButton onClick={() => handleDelete(item)} />
@@ -66,7 +69,7 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
66
69
 
67
70
  <List.ItemDragPreview<TestItemType>>
68
71
  {({ item }) => (
69
- <List.ItemWrapper classNames={mx(grid, 'bg-modalSurface border border-separator')}>
72
+ <List.ItemWrapper classNames={mx(grid, 'bg-modal-surface border border-separator')}>
70
73
  <List.ItemDragHandle />
71
74
  <div className='flex items-center'>{item.name}</div>
72
75
  </List.ItemWrapper>
@@ -78,13 +81,17 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
78
81
  );
79
82
  };
80
83
 
81
- const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
84
+ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
85
+ const listAtom = useMemo(() => Atom.make<TestList>(createList(100)).pipe(Atom.keepAlive), []);
86
+ const list = useAtomValue(listAtom);
87
+ const items = list.items;
88
+
82
89
  return (
83
90
  <List.Root<TestItemType> dragPreview items={items} {...props}>
84
91
  {({ items }) => (
85
- <div role='list' className='w-full h-full overflow-auto'>
92
+ <div role='list' className='h-full w-full overflow-auto'>
86
93
  {items?.map((item) => (
87
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
94
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
88
95
  <List.ItemDragHandle />
89
96
  <List.ItemTitle>{item.name}</List.ItemTitle>
90
97
  <List.ItemDeleteButton />
@@ -96,20 +103,27 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
96
103
  );
97
104
  };
98
105
 
99
- const list = live(createList(100));
106
+ const meta = {
107
+ title: 'ui/react-ui-list/List',
108
+ component: List.Root,
109
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withRegistry],
110
+ parameters: {
111
+ layout: 'fullscreen',
112
+ },
113
+ } satisfies Meta<typeof List.Root>;
114
+
115
+ export default meta;
100
116
 
101
- export const Default: StoryObj<ListRootProps<TestItemType>> = {
117
+ export const Default: StoryObj<typeof DefaultStory> = {
102
118
  render: DefaultStory,
103
119
  args: {
104
- items: list.items,
105
120
  isItem: Schema.is(TestItemSchema),
106
121
  },
107
122
  };
108
123
 
109
- export const Simple: StoryObj<ListRootProps<TestItemType>> = {
124
+ export const Simple: StoryObj<typeof SimpleStory> = {
110
125
  render: SimpleStory,
111
126
  args: {
112
- items: list.items,
113
127
  isItem: Schema.is(TestItemSchema),
114
128
  },
115
129
  };
@@ -3,11 +3,9 @@
3
3
  //
4
4
 
5
5
  import {
6
- IconButton,
7
- type IconButtonProps,
8
6
  ListItem,
7
+ ListItemIconButton,
9
8
  ListItemDeleteButton,
10
- ListItemButton,
11
9
  ListItemDragHandle,
12
10
  ListItemDragPreview,
13
11
  type ListItemProps,
@@ -17,17 +15,21 @@ import {
17
15
  } from './ListItem';
18
16
  import { ListRoot, type ListRootProps } from './ListRoot';
19
17
 
20
- // TODO(burdon): Multi-select model.
21
- // TODO(burdon): Key nav.
22
- // TODO(burdon): Animation.
23
- // TODO(burdon): Constrain axis.
24
- // TODO(burdon): Tree view.
25
- // TODO(burdon): Fix autoscroll while dragging.
26
-
27
18
  /**
28
- * Draggable list.
19
+ * Draggable list with per-row drag handles and delete buttons.
29
20
  * Ref: https://github.com/atlassian/pragmatic-drag-and-drop
30
21
  * Ref: https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/task.tsx
22
+ *
23
+ * @deprecated New code should use one of:
24
+ *
25
+ * - `RowList` / `CardList` from this same package — for selectable
26
+ * pickers (master/detail). Correct ARIA + dx-* by construction.
27
+ * - `Mosaic.Stack` / `Mosaic.VirtualStack` from `@dxos/react-ui-mosaic`
28
+ * — for virtualized or drag-reorderable card stacks.
29
+ *
30
+ * This component is retained for the existing reorder-with-delete-button
31
+ * use cases (plugin-meeting, plugin-automation, plugin-zen, etc.) until
32
+ * each is migrated; see `AUDIT.md` Phase 6 for the migration plan.
31
33
  */
32
34
  export const List = {
33
35
  Root: ListRoot,
@@ -35,12 +37,11 @@ export const List = {
35
37
  ItemDragPreview: ListItemDragPreview,
36
38
  ItemWrapper: ListItemWrapper,
37
39
  ItemDragHandle: ListItemDragHandle,
40
+ ItemIconButton: ListItemIconButton,
38
41
  ItemDeleteButton: ListItemDeleteButton,
39
- ItemButton: ListItemButton,
40
42
  ItemTitle: ListItemTitle,
41
- IconButton,
42
43
  };
43
44
 
44
45
  type ListItem = ListItemRecord;
45
46
 
46
- export type { ListRootProps, ListItemProps, IconButtonProps, ListItem, ListItemRecord };
47
+ export type { ListRootProps, ListItemProps, ListItem, ListItemRecord };
@@ -2,22 +2,22 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
6
- import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
7
- import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
8
5
  import {
9
6
  type Edge,
10
7
  attachClosestEdge,
11
8
  extractClosestEdge,
12
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
13
  import { createContext } from '@radix-ui/react-context';
14
+ import { Slot } from '@radix-ui/react-slot';
14
15
  import React, {
15
16
  type ComponentProps,
16
17
  type HTMLAttributes,
17
- type MutableRefObject,
18
18
  type PropsWithChildren,
19
19
  type ReactNode,
20
- forwardRef,
20
+ RefObject,
21
21
  useEffect,
22
22
  useRef,
23
23
  useState,
@@ -25,8 +25,14 @@ import React, {
25
25
  import { createPortal } from 'react-dom';
26
26
 
27
27
  import { invariant } from '@dxos/invariant';
28
- import { Icon, type ThemedClassName, ListItem as NaturalListItem } from '@dxos/react-ui';
29
- import { mx } from '@dxos/react-ui-theme';
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';
30
36
 
31
37
  import { useListContext } from './ListRoot';
32
38
 
@@ -56,7 +62,7 @@ const stateStyles: { [Key in ItemDragState['type']]?: HTMLAttributes<HTMLDivElem
56
62
 
57
63
  type ListItemContext<T extends ListItemRecord> = {
58
64
  item: T;
59
- dragHandleRef: MutableRefObject<HTMLElement | null>;
65
+ dragHandleRef: RefObject<HTMLButtonElement | null>;
60
66
  };
61
67
 
62
68
  /**
@@ -72,22 +78,34 @@ export const [ListItemProvider, useListItemContext] = createContext<ListItemCont
72
78
  );
73
79
 
74
80
  export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
75
- PropsWithChildren<{
76
- item: T;
77
- }> &
78
- HTMLAttributes<HTMLDivElement>
81
+ PropsWithChildren<
82
+ {
83
+ item: T;
84
+ asChild?: boolean;
85
+ selected?: boolean;
86
+ } & HTMLAttributes<HTMLDivElement>
87
+ >
79
88
  >;
80
89
 
81
90
  /**
82
91
  * Draggable list item.
83
92
  */
84
- export const ListItem = <T extends ListItemRecord>({ children, classNames, item, ...props }: ListItemProps<T>) => {
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';
85
102
  const { isItem, readonly, dragPreview, setState: setRootState } = useListContext(LIST_ITEM_NAME);
86
- const ref = useRef<HTMLDivElement | null>(null);
87
- const dragHandleRef = useRef<HTMLElement | null>(null);
103
+ const rootRef = useRef<HTMLDivElement | null>(null);
104
+ const dragHandleRef = useRef<HTMLButtonElement | null>(null);
88
105
  const [state, setState] = useState<ItemDragState>(idle);
106
+
89
107
  useEffect(() => {
90
- const element = ref.current;
108
+ const element = rootRef.current;
91
109
  invariant(element);
92
110
  return combine(
93
111
  //
@@ -142,6 +160,9 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
142
160
  const closestEdge = extractClosestEdge(self.data);
143
161
  setState({ type: 'is-dragging-over', closestEdge });
144
162
  },
163
+ onDragLeave: () => {
164
+ setState(idle);
165
+ },
145
166
  onDrag: ({ self }) => {
146
167
  const closestEdge = extractClosestEdge(self.data);
147
168
  setState((current) => {
@@ -151,9 +172,6 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
151
172
  return { type: 'is-dragging-over', closestEdge };
152
173
  });
153
174
  },
154
- onDragLeave: () => {
155
- setState(idle);
156
- },
157
175
  onDrop: () => {
158
176
  setState(idle);
159
177
  },
@@ -163,19 +181,18 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
163
181
 
164
182
  return (
165
183
  <ListItemProvider item={item} dragHandleRef={dragHandleRef}>
166
- <div role='none' className='relative'>
167
- <div
168
- ref={ref}
169
- role='listitem'
170
- className={mx('flex overflow-hidden', classNames, stateStyles[state.type])}
171
- {...props}
172
- >
173
- {children}
174
- </div>
175
- {state.type === 'is-dragging-over' && state.closestEdge && (
176
- <NaturalListItem.DropIndicator edge={state.closestEdge} />
177
- )}
178
- </div>
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
+ )}
179
196
  </ListItemProvider>
180
197
  );
181
198
  };
@@ -184,51 +201,66 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
184
201
  // List item components
185
202
  //
186
203
 
187
- export type IconButtonProps = ThemedClassName<ComponentProps<'button'>> & { icon: string };
188
-
189
- export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
190
- ({ classNames, icon, ...props }, forwardedRef) => {
191
- return (
192
- <button ref={forwardedRef} className={mx('flex items-center justify-center', classNames)} {...props}>
193
- <Icon icon={icon} classNames='cursor-pointer' size={4} />
194
- </button>
195
- );
196
- },
197
- );
198
-
199
- export const ListItemDeleteButton = ({
204
+ export const ListItemIconButton = ({
200
205
  autoHide = true,
206
+ iconOnly = true,
207
+ variant = 'ghost',
201
208
  classNames,
202
209
  disabled,
203
- icon = 'ph--x--regular',
204
210
  ...props
205
- }: Partial<Pick<IconButtonProps, 'icon'>> & Omit<IconButtonProps, 'icon'> & { autoHide?: boolean }) => {
206
- const { state } = useListContext('DELETE_BUTTON');
211
+ }: IconButtonProps & { autoHide?: boolean }) => {
212
+ const { state } = useListContext('ITEM_BUTTON');
207
213
  const isDisabled = state.type !== 'idle' || disabled;
208
214
  return (
209
215
  <IconButton
210
- icon={icon}
216
+ {...props}
211
217
  disabled={isDisabled}
218
+ iconOnly={iconOnly}
219
+ variant={variant}
212
220
  classNames={[classNames, autoHide && disabled && 'hidden']}
213
- {...props}
214
221
  />
215
222
  );
216
223
  };
217
224
 
218
- export const ListItemButton = ({
225
+ // TODO(burdon): Generalize to action button.
226
+ export const ListItemDeleteButton = ({
219
227
  autoHide = true,
220
228
  classNames,
221
229
  disabled,
230
+ icon = 'ph--x--regular',
231
+ label,
222
232
  ...props
223
- }: IconButtonProps & { autoHide?: boolean }) => {
224
- const { state } = useListContext('ITEM_BUTTON');
233
+ }: Partial<Pick<IconButtonProps, 'icon'>> &
234
+ Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
235
+ const { state } = useListContext('DELETE_BUTTON');
225
236
  const isDisabled = state.type !== 'idle' || disabled;
226
- return <IconButton disabled={isDisabled} classNames={[classNames, autoHide && disabled && 'hidden']} {...props} />;
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
+ );
227
249
  };
228
250
 
229
251
  export const ListItemDragHandle = ({ disabled }: Pick<IconButtonProps, 'disabled'>) => {
230
252
  const { dragHandleRef } = useListItemContext('DRAG_HANDLE');
231
- return <IconButton ref={dragHandleRef as any} icon='ph--dots-six-vertical--regular' disabled={disabled} />;
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
+ );
232
264
  };
233
265
 
234
266
  export const ListItemDragPreview = <T extends ListItemRecord>({
@@ -241,7 +273,9 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
241
273
  };
242
274
 
243
275
  export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
244
- <div className={mx('flex is-full gap-2', classNames)}>{children}</div>
276
+ <div role='none' className={mx('flex w-full gap-2', classNames)}>
277
+ {children}
278
+ </div>
245
279
  );
246
280
 
247
281
  export const ListItemTitle = ({
@@ -249,7 +283,7 @@ export const ListItemTitle = ({
249
283
  children,
250
284
  ...props
251
285
  }: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
252
- <div className={mx('flex grow items-center truncate', classNames)} {...props}>
286
+ <div role='none' className={mx('flex grow items-center truncate', classNames)} {...props}>
253
287
  {children}
254
288
  </div>
255
289
  );
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
5
  import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
7
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
8
  import { createContext } from '@radix-ui/react-context';
9
9
  import React, { type ReactNode, useCallback, useEffect, useState } from 'react';
10
10
 
11
- import { idle, type ItemDragState, type ListItemRecord } from './ListItem';
11
+ import { type ItemDragState, type ListItemRecord, idle } from './ListItem';
12
12
 
13
13
  type ListContext<T extends ListItemRecord> = {
14
14
  // TODO(burdon): Rename drag state.
@@ -26,14 +26,14 @@ export const [ListProvider, useListContext] = createContext<ListContext<any>>(LI
26
26
 
27
27
  export type ListRendererProps<T extends ListItemRecord> = {
28
28
  state: ListContext<T>['state'];
29
- items: T[];
29
+ items: readonly T[];
30
30
  };
31
31
 
32
32
  const defaultGetId = <T extends ListItemRecord>(item: T) => (item as any)?.id;
33
33
 
34
34
  export type ListRootProps<T extends ListItemRecord> = {
35
35
  children?: (props: ListRendererProps<T>) => ReactNode;
36
- items?: T[];
36
+ items?: readonly T[];
37
37
  onMove?: (fromIndex: number, toIndex: number) => void;
38
38
  } & Pick<ListContext<T>, 'isItem' | 'getId' | 'readonly' | 'dragPreview'>;
39
39
 
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import * as Schema from 'effect/Schema';
6
6
 
7
- import { ObjectId } from '@dxos/echo-schema';
8
- import { faker } from '@dxos/random';
7
+ import { Obj } from '@dxos/echo';
8
+ import { random } from '@dxos/random';
9
9
 
10
10
  export const TestItemSchema = Schema.Struct({
11
- id: ObjectId,
11
+ id: Obj.ID,
12
12
  name: Schema.String,
13
13
  });
14
14
 
@@ -21,10 +21,10 @@ export const TestList = Schema.Struct({
21
21
  export type TestList = Schema.Schema.Type<typeof TestList>;
22
22
 
23
23
  export const createList = (n = 10): TestList => ({
24
- items: faker.helpers.multiple(
24
+ items: random.helpers.multiple(
25
25
  () => ({
26
- id: faker.string.uuid(),
27
- name: faker.commerce.productName(),
26
+ id: random.string.uuid(),
27
+ name: random.commerce.productName(),
28
28
  }),
29
29
  { count: n },
30
30
  ),
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2023 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 { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { Listbox } from './Listbox';
12
+
13
+ random.seed(1234);
14
+
15
+ type StoryItem = { value: string; label: string };
16
+
17
+ const options: StoryItem[] = random.helpers.multiple(
18
+ () => ({ value: random.string.uuid(), label: random.commerce.productName() }) satisfies StoryItem,
19
+ { count: 16 },
20
+ );
21
+
22
+ const DefaultStory = () => {
23
+ const [selectedValue, setSelectedValue] = useState<string>();
24
+
25
+ return (
26
+ <Listbox.Root value={selectedValue} onValueChange={setSelectedValue}>
27
+ {options.map((option) => (
28
+ <Listbox.Option key={option.value} value={option.value}>
29
+ <Listbox.OptionLabel>{option.label}</Listbox.OptionLabel>
30
+ <Listbox.OptionIndicator />
31
+ </Listbox.Option>
32
+ ))}
33
+ </Listbox.Root>
34
+ );
35
+ };
36
+
37
+ const meta = {
38
+ title: 'ui/react-ui-list/Listbox',
39
+ component: Listbox.Root,
40
+ render: DefaultStory,
41
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'p-2' })],
42
+ } satisfies Meta<typeof Listbox.Root>;
43
+
44
+ export default meta;
45
+
46
+ type Story = StoryObj<typeof meta>;
47
+
48
+ export const Default: Story = {};