@dxos/react-ui-list 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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 (41) hide show
  1. package/dist/lib/browser/index.mjs +233 -194
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +233 -194
  5. package/dist/lib/node-esm/index.mjs.map +3 -3
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  8. package/dist/types/src/components/List/List.d.ts +6 -4
  9. package/dist/types/src/components/List/List.d.ts.map +1 -1
  10. package/dist/types/src/components/List/ListItem.d.ts +8 -6
  11. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  12. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  13. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  14. package/dist/types/src/components/Tree/Tree.d.ts +6 -5
  15. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  16. package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
  17. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Tree/TreeContext.d.ts +21 -10
  19. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  20. package/dist/types/src/components/Tree/TreeItem.d.ts +8 -0
  21. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  22. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  23. package/dist/types/src/components/Tree/index.d.ts +2 -0
  24. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  25. package/dist/types/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +19 -18
  27. package/src/components/Accordion/Accordion.stories.tsx +6 -6
  28. package/src/components/Accordion/AccordionItem.tsx +1 -2
  29. package/src/components/List/List.stories.tsx +10 -10
  30. package/src/components/List/List.tsx +4 -9
  31. package/src/components/List/ListItem.tsx +58 -38
  32. package/src/components/List/ListRoot.tsx +3 -3
  33. package/src/components/List/testing.ts +4 -4
  34. package/src/components/Tree/Tree.stories.tsx +106 -31
  35. package/src/components/Tree/Tree.tsx +30 -40
  36. package/src/components/Tree/TreeContext.tsx +18 -9
  37. package/src/components/Tree/TreeItem.tsx +178 -103
  38. package/src/components/Tree/TreeItemHeading.tsx +3 -4
  39. package/src/components/Tree/TreeItemToggle.tsx +4 -4
  40. package/src/components/Tree/index.ts +2 -0
  41. package/src/components/Tree/testing.ts +5 -5
@@ -2,24 +2,24 @@
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';
7
- import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
7
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
8
8
  import { type Meta, type StoryObj } from '@storybook/react-vite';
9
9
  import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
10
10
 
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
14
  import { withRegistry } from '@dxos/storybook-utils';
15
15
 
16
16
  import { Path } from '../../util';
17
-
18
17
  import { type TestItem, createTree, updateState } from './testing';
19
18
  import { Tree } from './Tree';
19
+ import { type TreeModel } from './TreeContext';
20
20
  import { type TreeData } from './TreeItem';
21
21
 
22
- faker.seed(1234);
22
+ random.seed(1234);
23
23
 
24
24
  const tree = createTree() as TestItem;
25
25
 
@@ -27,27 +27,111 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
27
27
  const registry = useContext(RegistryContext);
28
28
  const stateAtomsRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
29
29
 
30
- const getOrCreateStateAtom = useCallback((path: string) => {
31
- let atom = stateAtomsRef.current.get(path);
30
+ const getOrCreateStateAtom = useCallback((pathKey: string) => {
31
+ let atom = stateAtomsRef.current.get(pathKey);
32
32
  if (!atom) {
33
33
  atom = Atom.make({ open: false, current: false }).pipe(Atom.keepAlive);
34
- stateAtomsRef.current.set(path, atom);
34
+ stateAtomsRef.current.set(pathKey, atom);
35
35
  }
36
36
  return atom;
37
37
  }, []);
38
38
 
39
- const useItemState = useCallback(
40
- (_path: string[]) => {
41
- const path = useMemo(() => Path.create(..._path), [_path.join('~')]);
42
- const atom = getOrCreateStateAtom(path);
43
- return useAtomValue(atom);
44
- },
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
+ }),
97
+ };
98
+ }).pipe(Atom.keepAlive);
99
+ }),
100
+ [itemMap],
101
+ );
102
+
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
+ }),
45
109
  [getOrCreateStateAtom],
46
110
  );
47
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);
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)),
128
+ }),
129
+ [childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
130
+ );
131
+
48
132
  const handleOpenChange = useCallback(
49
- ({ path: _path, open }: { path: string[]; open: boolean }) => {
50
- const path = Path.create(..._path);
133
+ ({ path: pathProp, open }: { path: string[]; open: boolean }) => {
134
+ const path = Path.create(...pathProp);
51
135
  const atom = getOrCreateStateAtom(path);
52
136
  const prev = registry.get(atom);
53
137
  registry.set(atom, { ...prev, open });
@@ -56,8 +140,8 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
56
140
  );
57
141
 
58
142
  const handleSelect = useCallback(
59
- ({ path: _path, current }: { path: string[]; current: boolean }) => {
60
- const path = Path.create(..._path);
143
+ ({ path: pathProp, current }: { path: string[]; current: boolean }) => {
144
+ const path = Path.create(...pathProp);
61
145
  const atom = getOrCreateStateAtom(path);
62
146
  const prev = registry.get(atom);
63
147
  registry.set(atom, { ...prev, current });
@@ -89,22 +173,13 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
89
173
 
90
174
  return (
91
175
  <Tree
176
+ model={model}
92
177
  id={tree.id}
178
+ rootId={tree.id}
93
179
  draggable={draggable}
94
- useItems={(parent?: TestItem) => parent?.items ?? tree.items}
95
- getProps={(parent: TestItem) => ({
96
- id: parent.id,
97
- label: parent.name,
98
- icon: parent.icon,
99
- ...((parent.items?.length ?? 0) > 0 && {
100
- parentOf: parent.items!.map(({ id }) => id),
101
- }),
102
- })}
103
- useIsOpen={(_path: string[]) => useItemState(_path).open}
104
- useIsCurrent={(_path: string[]) => useItemState(_path).current}
105
180
  renderColumns={() => (
106
181
  <div className='flex items-center'>
107
- <Icon icon='ph--placeholder--regular' size={5} />
182
+ <Icon icon='ph--placeholder--regular' />
108
183
  </div>
109
184
  )}
110
185
  onOpenChange={handleOpenChange}
@@ -116,7 +191,7 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
116
191
  const meta = {
117
192
  title: 'ui/react-ui-list/Tree',
118
193
 
119
- decorators: [withTheme, withRegistry],
194
+ decorators: [withTheme(), withRegistry],
120
195
  component: Tree,
121
196
  render: DefaultStory,
122
197
  } satisfies Meta<typeof Tree<TestItem>>;
@@ -1,20 +1,20 @@
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
7
  import { Treegrid, type TreegridRootProps } from '@dxos/react-ui';
8
8
 
9
- import { type TreeContextType, TreeProvider } from './TreeContext';
10
- import { TreeItem, type TreeItemProps } from './TreeItem';
9
+ import { type TreeModel, TreeProvider } from './TreeContext';
10
+ import { TreeItemById, type TreeItemByIdProps, type TreeItemProps } from './TreeItem';
11
11
 
12
- export type TreeProps<T extends { id: string } = any, O = any> = {
13
- root?: T;
12
+ export type TreeProps<T extends { id: string } = any> = {
13
+ model: TreeModel<T>;
14
+ rootId?: string;
14
15
  path?: string[];
15
16
  id: string;
16
- } & TreeContextType<T, O> &
17
- Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
17
+ } & Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
18
18
  Pick<
19
19
  TreeItemProps<T>,
20
20
  | 'draggable'
@@ -24,17 +24,15 @@ export type TreeProps<T extends { id: string } = any, O = any> = {
24
24
  | 'canSelect'
25
25
  | 'onOpenChange'
26
26
  | 'onSelect'
27
+ | 'onItemHover'
27
28
  | 'levelOffset'
28
29
  >;
29
30
 
30
- export const Tree = <T extends { id: string } = any, O = any>({
31
- root,
31
+ export const Tree = <T extends { id: string } = any>({
32
+ model,
33
+ rootId,
32
34
  path,
33
35
  id,
34
- useItems,
35
- getProps,
36
- useIsOpen,
37
- useIsCurrent,
38
36
  draggable = false,
39
37
  gridTemplateColumns = '[tree-row-start] 1fr min-content [tree-row-end]',
40
38
  classNames,
@@ -45,37 +43,29 @@ export const Tree = <T extends { id: string } = any, O = any>({
45
43
  canSelect,
46
44
  onOpenChange,
47
45
  onSelect,
48
- }: TreeProps<T, O>) => {
49
- const context = useMemo(
50
- () => ({
51
- useItems,
52
- getProps,
53
- useIsOpen,
54
- useIsCurrent,
55
- }),
56
- [useItems, getProps, useIsOpen, useIsCurrent],
57
- );
58
- const items = useItems(root);
46
+ onItemHover,
47
+ }: TreeProps<T>) => {
48
+ const childIds = useAtomValue(model.childIds(rootId));
59
49
  const treePath = useMemo(() => (path ? [...path, id] : [id]), [id, path]);
60
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
+
61
64
  return (
62
65
  <Treegrid.Root gridTemplateColumns={gridTemplateColumns} classNames={classNames}>
63
- <TreeProvider value={context}>
64
- {items.map((item, index) => (
65
- <TreeItem
66
- key={item.id}
67
- item={item}
68
- last={index === items.length - 1}
69
- path={treePath}
70
- levelOffset={levelOffset}
71
- draggable={draggable}
72
- renderColumns={renderColumns}
73
- blockInstruction={blockInstruction}
74
- canDrop={canDrop}
75
- canSelect={canSelect}
76
- onOpenChange={onOpenChange}
77
- onSelect={onSelect}
78
- />
66
+ <TreeProvider value={model}>
67
+ {childIds.map((childId, index) => (
68
+ <TreeItemById key={childId} id={childId} last={index === childIds.length - 1} {...childProps} />
79
69
  ))}
80
70
  </TreeProvider>
81
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,6 +12,10 @@ 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;
@@ -19,16 +24,20 @@ export type TreeItemDataProps = {
19
24
  testId?: string;
20
25
  };
21
26
 
22
- export type TreeContextType<T = any, O = any> = {
23
- useItems: (parent?: T, options?: O) => T[];
24
- getProps: (item: T, parent: string[]) => TreeItemDataProps;
25
- /** Hook that subscribes to and returns the open state for a tree item. */
26
- useIsOpen: (path: string[], item: T) => boolean;
27
- /** Hook that subscribes to and returns the current state for a tree item. */
28
- useIsCurrent: (path: string[], item: T) => boolean;
29
- };
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
+ }
30
39
 
31
- const TreeContext = createContext<null | TreeContextType>(null);
40
+ const TreeContext = createContext<TreeModel | null>(null);
32
41
 
33
42
  export const TreeProvider = TreeContext.Provider;
34
43