@dxos/react-ui-list 0.8.4-main.fffef41 → 0.9.0

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 (99) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +1360 -730
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +1360 -730
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
  9. package/dist/types/src/components/Accordion/Accordion.d.ts.map +1 -1
  10. package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  12. package/dist/types/src/components/Accordion/AccordionRoot.d.ts +1 -1
  13. package/dist/types/src/components/Accordion/AccordionRoot.d.ts.map +1 -1
  14. package/dist/types/src/components/Combobox/Combobox.d.ts +105 -0
  15. package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
  16. package/dist/types/src/components/Combobox/Combobox.stories.d.ts +12 -0
  17. package/dist/types/src/components/Combobox/Combobox.stories.d.ts.map +1 -0
  18. package/dist/types/src/components/Combobox/index.d.ts +2 -0
  19. package/dist/types/src/components/Combobox/index.d.ts.map +1 -0
  20. package/dist/types/src/components/List/List.d.ts +19 -8
  21. package/dist/types/src/components/List/List.d.ts.map +1 -1
  22. package/dist/types/src/components/List/List.stories.d.ts +2 -2
  23. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/List/ListItem.d.ts +10 -8
  25. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  26. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  27. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  28. package/dist/types/src/components/List/testing.d.ts +2 -2
  29. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  30. package/dist/types/src/components/Listbox/Listbox.d.ts +27 -0
  31. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
  32. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +12 -0
  33. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Listbox/index.d.ts +2 -0
  35. package/dist/types/src/components/Listbox/index.d.ts.map +1 -0
  36. package/dist/types/src/components/Picker/Picker.d.ts +49 -0
  37. package/dist/types/src/components/Picker/Picker.d.ts.map +1 -0
  38. package/dist/types/src/components/Picker/Picker.stories.d.ts +28 -0
  39. package/dist/types/src/components/Picker/Picker.stories.d.ts.map +1 -0
  40. package/dist/types/src/components/Picker/context.d.ts +29 -0
  41. package/dist/types/src/components/Picker/context.d.ts.map +1 -0
  42. package/dist/types/src/components/Picker/index.d.ts +3 -0
  43. package/dist/types/src/components/Picker/index.d.ts.map +1 -0
  44. package/dist/types/src/components/RowList/RowList.d.ts +61 -0
  45. package/dist/types/src/components/RowList/RowList.d.ts.map +1 -0
  46. package/dist/types/src/components/RowList/RowList.stories.d.ts +35 -0
  47. package/dist/types/src/components/RowList/RowList.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/RowList/index.d.ts +3 -0
  49. package/dist/types/src/components/RowList/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  51. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  52. package/dist/types/src/components/Tree/Tree.stories.d.ts +9 -28
  53. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Tree/TreeContext.d.ts +25 -8
  55. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  56. package/dist/types/src/components/Tree/TreeItem.d.ts +20 -3
  57. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  58. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -0
  59. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  60. package/dist/types/src/components/Tree/helpers.d.ts.map +1 -1
  61. package/dist/types/src/components/Tree/index.d.ts +2 -0
  62. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  63. package/dist/types/src/components/Tree/testing.d.ts +3 -3
  64. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  65. package/dist/types/src/components/index.d.ts +4 -0
  66. package/dist/types/src/components/index.d.ts.map +1 -1
  67. package/dist/types/src/util/path.d.ts.map +1 -1
  68. package/dist/types/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +35 -32
  70. package/src/components/Accordion/Accordion.stories.tsx +6 -6
  71. package/src/components/Accordion/AccordionItem.tsx +4 -7
  72. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  73. package/src/components/Combobox/Combobox.stories.tsx +60 -0
  74. package/src/components/Combobox/Combobox.tsx +388 -0
  75. package/src/components/Combobox/index.ts +5 -0
  76. package/src/components/List/List.stories.tsx +36 -24
  77. package/src/components/List/List.tsx +14 -10
  78. package/src/components/List/ListItem.tsx +57 -39
  79. package/src/components/List/ListRoot.tsx +3 -3
  80. package/src/components/List/testing.ts +6 -6
  81. package/src/components/Listbox/Listbox.stories.tsx +48 -0
  82. package/src/components/Listbox/Listbox.tsx +201 -0
  83. package/src/components/Listbox/index.ts +5 -0
  84. package/src/components/Picker/Picker.stories.tsx +131 -0
  85. package/src/components/Picker/Picker.tsx +369 -0
  86. package/src/components/Picker/context.ts +43 -0
  87. package/src/components/Picker/index.ts +6 -0
  88. package/src/components/RowList/RowList.stories.tsx +163 -0
  89. package/src/components/RowList/RowList.tsx +350 -0
  90. package/src/components/RowList/index.ts +6 -0
  91. package/src/components/Tree/Tree.stories.tsx +156 -64
  92. package/src/components/Tree/Tree.tsx +41 -43
  93. package/src/components/Tree/TreeContext.tsx +22 -7
  94. package/src/components/Tree/TreeItem.tsx +189 -108
  95. package/src/components/Tree/TreeItemHeading.tsx +35 -7
  96. package/src/components/Tree/TreeItemToggle.tsx +5 -5
  97. package/src/components/Tree/index.ts +2 -0
  98. package/src/components/Tree/testing.ts +9 -8
  99. package/src/components/index.ts +4 -0
@@ -2,30 +2,160 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
5
  import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
6
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
7
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
7
8
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import React, { useEffect } from 'react';
9
+ import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
9
10
 
10
- import { type Live, live } from '@dxos/live-object';
11
- import { faker } from '@dxos/random';
11
+ import { random } from '@dxos/random';
12
12
  import { Icon } from '@dxos/react-ui';
13
13
  import { withTheme } from '@dxos/react-ui/testing';
14
+ import { withRegistry } from '@dxos/storybook-utils';
14
15
 
15
16
  import { Path } from '../../util';
16
-
17
17
  import { type TestItem, createTree, updateState } from './testing';
18
- import { Tree, type TreeProps } from './Tree';
18
+ import { Tree } from './Tree';
19
+ import { type TreeModel } from './TreeContext';
19
20
  import { type TreeData } from './TreeItem';
20
21
 
21
- faker.seed(1234);
22
+ random.seed(1234);
23
+
24
+ const tree = createTree() as TestItem;
25
+
26
+ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
27
+ const registry = useContext(RegistryContext);
28
+ const stateAtomsRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
29
+
30
+ const getOrCreateStateAtom = useCallback((pathKey: string) => {
31
+ let atom = stateAtomsRef.current.get(pathKey);
32
+ if (!atom) {
33
+ atom = Atom.make({ open: false, current: false }).pipe(Atom.keepAlive);
34
+ stateAtomsRef.current.set(pathKey, atom);
35
+ }
36
+ return atom;
37
+ }, []);
38
+
39
+ // Build a lookup map of all items by ID.
40
+ const itemMap = useMemo(() => {
41
+ const map = new Map<string, TestItem>();
42
+ const walk = (item: TestItem) => {
43
+ map.set(item.id, item);
44
+ item.items?.forEach(walk);
45
+ };
46
+ walk(tree);
47
+ return map;
48
+ }, []);
49
+
50
+ // Build a child IDs map keyed by parent ID.
51
+ const childIdsMap = useMemo(() => {
52
+ const map = new Map<string, string[]>();
53
+ const walk = (item: TestItem) => {
54
+ if (item.items) {
55
+ map.set(
56
+ item.id,
57
+ item.items.map((child) => child.id),
58
+ );
59
+ item.items.forEach(walk);
60
+ }
61
+ };
62
+ // Root children.
63
+ map.set(
64
+ tree.id,
65
+ (tree.items ?? []).map((child) => child.id),
66
+ );
67
+ walk(tree);
68
+ return map;
69
+ }, []);
70
+
71
+ const childIdsFamily = useMemo(
72
+ () => Atom.family((id: string) => Atom.make(() => childIdsMap.get(id) ?? []).pipe(Atom.keepAlive)),
73
+ [childIdsMap],
74
+ );
75
+
76
+ const itemFamily = useMemo(
77
+ () => Atom.family((id: string) => Atom.make(() => itemMap.get(id)).pipe(Atom.keepAlive)),
78
+ [itemMap],
79
+ );
80
+
81
+ const itemPropsFamily = useMemo(
82
+ () =>
83
+ Atom.family((pathKey: string) => {
84
+ const id = pathKey.split('~').pop()!;
85
+ return Atom.make(() => {
86
+ const parent = itemMap.get(id);
87
+ if (!parent) {
88
+ return { id, label: id };
89
+ }
90
+ return {
91
+ id: parent.id,
92
+ label: parent.name,
93
+ icon: parent.icon,
94
+ ...((parent.items?.length ?? 0) > 0 && {
95
+ parentOf: parent.items!.map(({ id }) => id),
96
+ count: parent.items!.length,
97
+ // Demonstrate the rose "new/modified" badge on a subset of branches (replaces the neutral count).
98
+ ...(parent.name.length % 3 === 0 && { modifiedCount: (parent.name.length % 5) + 1 }),
99
+ }),
100
+ };
101
+ }).pipe(Atom.keepAlive);
102
+ }),
103
+ [itemMap],
104
+ );
105
+
106
+ const itemOpenFamily = useMemo(
107
+ () =>
108
+ Atom.family((pathKey: string) => {
109
+ const stateAtom = getOrCreateStateAtom(pathKey);
110
+ return Atom.make((get) => get(stateAtom).open).pipe(Atom.keepAlive);
111
+ }),
112
+ [getOrCreateStateAtom],
113
+ );
114
+
115
+ const itemCurrentFamily = useMemo(
116
+ () =>
117
+ Atom.family((pathKey: string) => {
118
+ const stateAtom = getOrCreateStateAtom(pathKey);
119
+ return Atom.make((get) => get(stateAtom).current).pipe(Atom.keepAlive);
120
+ }),
121
+ [getOrCreateStateAtom],
122
+ );
123
+
124
+ const model: TreeModel<TestItem> = useMemo(
125
+ () => ({
126
+ childIds: (parentId?: string) => childIdsFamily(parentId ?? tree.id),
127
+ item: (id: string) => itemFamily(id),
128
+ itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
129
+ itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
130
+ itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
131
+ }),
132
+ [childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
133
+ );
134
+
135
+ const handleOpenChange = useCallback(
136
+ ({ path: pathProp, open }: { path: string[]; open: boolean }) => {
137
+ const path = Path.create(...pathProp);
138
+ const atom = getOrCreateStateAtom(path);
139
+ const prev = registry.get(atom);
140
+ registry.set(atom, { ...prev, open });
141
+ },
142
+ [getOrCreateStateAtom, registry],
143
+ );
144
+
145
+ const handleSelect = useCallback(
146
+ ({ path: pathProp, current }: { path: string[]; current: boolean }) => {
147
+ const path = Path.create(...pathProp);
148
+ const atom = getOrCreateStateAtom(path);
149
+ const prev = registry.get(atom);
150
+ registry.set(atom, { ...prev, current });
151
+ },
152
+ [getOrCreateStateAtom, registry],
153
+ );
22
154
 
23
- const DefaultStory = (props: TreeProps) => {
24
155
  useEffect(() => {
25
156
  return monitorForElements({
26
157
  canMonitor: ({ source }) => typeof source.data.id === 'string' && Array.isArray(source.data.path),
27
158
  onDrop: ({ location, source }) => {
28
- // Didn't drop on anything.
29
159
  if (!location.current.dropTargets.length) {
30
160
  return;
31
161
  }
@@ -44,72 +174,34 @@ const DefaultStory = (props: TreeProps) => {
44
174
  });
45
175
  }, []);
46
176
 
47
- return <Tree {...props} />;
177
+ return (
178
+ <Tree
179
+ model={model}
180
+ id={tree.id}
181
+ rootId={tree.id}
182
+ draggable={draggable}
183
+ renderColumns={() => (
184
+ <div className='flex items-center'>
185
+ <Icon icon='ph--circle-dashed--regular' />
186
+ </div>
187
+ )}
188
+ onOpenChange={handleOpenChange}
189
+ onSelect={handleSelect}
190
+ />
191
+ );
48
192
  };
49
193
 
50
- const tree = live<TestItem>(createTree());
51
- const state = new Map<string, Live<{ open: boolean; current: boolean }>>();
52
-
53
194
  const meta = {
54
195
  title: 'ui/react-ui-list/Tree',
55
196
 
56
- decorators: [withTheme],
197
+ decorators: [withTheme(), withRegistry],
57
198
  component: Tree,
58
199
  render: DefaultStory,
59
- args: {
60
- id: tree.id,
61
- useItems: (parent?: TestItem) => {
62
- return parent?.items ?? tree.items;
63
- },
64
- getProps: (parent: TestItem) => ({
65
- id: parent.id,
66
- label: parent.name,
67
- icon: parent.icon,
68
- ...((parent.items?.length ?? 0) > 0 && {
69
- parentOf: parent.items!.map(({ id }) => id),
70
- }),
71
- }),
72
- isOpen: (_path: string[]) => {
73
- const path = Path.create(..._path);
74
- const object = state.get(path) ?? live({ open: false, current: false });
75
- if (!state.has(path)) {
76
- state.set(path, object);
77
- }
78
-
79
- return object.open;
80
- },
81
- isCurrent: (_path: string[]) => {
82
- const path = Path.create(..._path);
83
- const object = state.get(path) ?? live({ open: false, current: false });
84
- if (!state.has(path)) {
85
- state.set(path, object);
86
- }
87
-
88
- return object.current;
89
- },
90
- renderColumns: () => {
91
- return (
92
- <div className='flex items-center'>
93
- <Icon icon='ph--placeholder--regular' size={5} />
94
- </div>
95
- );
96
- },
97
- onOpenChange: ({ path: _path, open }) => {
98
- const path = Path.create(..._path);
99
- const object = state.get(path);
100
- object!.open = open;
101
- },
102
- onSelect: ({ path: _path, current }) => {
103
- const path = Path.create(..._path);
104
- const object = state.get(path);
105
- object!.current = current;
106
- },
107
- },
108
200
  } satisfies Meta<typeof Tree<TestItem>>;
109
201
 
110
202
  export default meta;
111
203
 
112
- type Story = StoryObj<typeof meta>;
204
+ type Story = StoryObj<typeof DefaultStory>;
113
205
 
114
206
  export const Default: Story = {};
115
207
 
@@ -1,73 +1,71 @@
1
1
  //
2
2
  // Copyright 2024 DXOS.org
3
- //
4
3
 
4
+ import { useAtomValue } from '@effect-atom/atom-react';
5
5
  import React, { useMemo } from 'react';
6
6
 
7
- import { type HasId } from '@dxos/echo/internal';
8
7
  import { Treegrid, type TreegridRootProps } from '@dxos/react-ui';
9
8
 
10
- import { type TreeContextType, TreeProvider } from './TreeContext';
11
- import { TreeItem, type TreeItemProps } from './TreeItem';
9
+ import { type TreeModel, TreeProvider } from './TreeContext';
10
+ import { TreeItemById, type TreeItemByIdProps, type TreeItemProps } from './TreeItem';
12
11
 
13
- export type TreeProps<T extends HasId = any, O = any> = {
14
- root?: T;
12
+ export type TreeProps<T extends { id: string } = any> = {
13
+ model: TreeModel<T>;
14
+ rootId?: string;
15
15
  path?: string[];
16
16
  id: string;
17
- } & TreeContextType<T, O> &
18
- Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
17
+ } & Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
19
18
  Pick<
20
19
  TreeItemProps<T>,
21
- 'draggable' | 'renderColumns' | 'canDrop' | 'canSelect' | 'onOpenChange' | 'onSelect' | 'levelOffset'
20
+ | 'draggable'
21
+ | 'renderColumns'
22
+ | 'blockInstruction'
23
+ | 'canDrop'
24
+ | 'canSelect'
25
+ | 'onOpenChange'
26
+ | 'onSelect'
27
+ | 'onItemHover'
28
+ | 'levelOffset'
22
29
  >;
23
30
 
24
- export const Tree = <T extends HasId = any, O = any>({
25
- root,
31
+ export const Tree = <T extends { id: string } = any>({
32
+ classNames,
33
+ model,
34
+ rootId,
26
35
  path,
27
36
  id,
28
- useItems,
29
- getProps,
30
- isOpen,
31
- isCurrent,
32
37
  draggable = false,
33
- gridTemplateColumns = '[tree-row-start] 1fr min-content [tree-row-end]',
34
- classNames,
38
+ gridTemplateColumns = '[tree-row-start] minmax(0, 1fr) min-content [tree-row-end]',
35
39
  levelOffset,
36
40
  renderColumns,
41
+ blockInstruction,
37
42
  canDrop,
38
43
  canSelect,
39
44
  onOpenChange,
40
45
  onSelect,
41
- }: TreeProps<T, O>) => {
42
- const context = useMemo(
43
- () => ({
44
- useItems,
45
- getProps,
46
- isOpen,
47
- isCurrent,
48
- }),
49
- [useItems, getProps, isOpen, isCurrent],
50
- );
51
- const items = useItems(root);
46
+ onItemHover,
47
+ }: TreeProps<T>) => {
48
+ const childIds = useAtomValue(model.childIds(rootId));
52
49
  const treePath = useMemo(() => (path ? [...path, id] : [id]), [id, path]);
53
50
 
51
+ const childProps: Omit<TreeItemByIdProps, 'id' | 'last'> = {
52
+ path: treePath,
53
+ levelOffset,
54
+ draggable,
55
+ renderColumns,
56
+ blockInstruction,
57
+ canDrop,
58
+ canSelect,
59
+ onOpenChange,
60
+ onSelect,
61
+ onItemHover,
62
+ };
63
+
54
64
  return (
55
65
  <Treegrid.Root gridTemplateColumns={gridTemplateColumns} classNames={classNames}>
56
- <TreeProvider value={context}>
57
- {items.map((item, index) => (
58
- <TreeItem
59
- key={item.id}
60
- item={item}
61
- last={index === items.length - 1}
62
- path={treePath}
63
- levelOffset={levelOffset}
64
- draggable={draggable}
65
- renderColumns={renderColumns}
66
- canDrop={canDrop}
67
- canSelect={canSelect}
68
- onOpenChange={onOpenChange}
69
- onSelect={onSelect}
70
- />
66
+ <TreeProvider value={model}>
67
+ {childIds.map((childId, index) => (
68
+ <TreeItemById key={childId} id={childId} last={index === childIds.length - 1} {...childProps} />
71
69
  ))}
72
70
  </TreeProvider>
73
71
  </Treegrid.Root>
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Atom } from '@effect-atom/atom-react';
5
6
  import { createContext, useContext } from 'react';
6
7
 
7
8
  import { raise } from '@dxos/debug';
@@ -11,22 +12,36 @@ export type TreeItemDataProps = {
11
12
  id: string;
12
13
  label: Label;
13
14
  parentOf?: string[];
15
+ /** When `false`, the item cannot be dragged (overrides tree-level `draggable`). */
16
+ draggable?: boolean;
17
+ /** When `false`, the item does not participate as a drop target. */
18
+ droppable?: boolean;
14
19
  className?: string;
15
20
  headingClassName?: string;
16
21
  icon?: string;
17
22
  iconHue?: string;
18
23
  disabled?: boolean;
19
24
  testId?: string;
25
+ /** Optional item count rendered as a neutral badge directly after the label. */
26
+ count?: number;
27
+ /** Optional count of new/modified items; when greater than zero it shows as a rose badge in place of `count`. */
28
+ modifiedCount?: number;
20
29
  };
21
30
 
22
- export type TreeContextType<T = any, O = any> = {
23
- useItems: (parent?: T, options?: O) => T[];
24
- getProps: (item: T, parent: string[]) => TreeItemDataProps;
25
- isOpen: (path: string[], item: T) => boolean;
26
- isCurrent: (path: string[], item: T) => boolean;
27
- };
31
+ export interface TreeModel<T extends { id: string } = any> {
32
+ /** Atom family: resolve item by ID (content). */
33
+ item: (id: string) => Atom.Atom<T | undefined>;
34
+ /** Atom family: open state keyed by path. */
35
+ itemOpen: (path: string[]) => Atom.Atom<boolean>;
36
+ /** Atom family: current (selected) state keyed by path. */
37
+ itemCurrent: (path: string[]) => Atom.Atom<boolean>;
38
+ /** Atom family: display props for an item at a given path (path includes item's own ID at end). */
39
+ itemProps: (path: string[]) => Atom.Atom<TreeItemDataProps>;
40
+ /** Atom family: outbound child IDs for a parent ID (topology). Undefined = root. */
41
+ childIds: (parentId?: string) => Atom.Atom<string[]>;
42
+ }
28
43
 
29
- const TreeContext = createContext<null | TreeContextType>(null);
44
+ const TreeContext = createContext<TreeModel | null>(null);
30
45
 
31
46
  export const TreeProvider = TreeContext.Provider;
32
47