@dxos/react-ui-list 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5

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