@dxos/react-ui-list 0.8.4-main.69d29f4 → 0.8.4-main.6fa680abb7
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.
- package/dist/lib/browser/index.mjs +192 -166
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +192 -166
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/List/List.d.ts.map +1 -1
- package/dist/types/src/components/List/ListItem.d.ts +2 -2
- package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.d.ts +6 -5
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeContext.d.ts +21 -10
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts +8 -0
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/Tree/index.d.ts +2 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/components/Accordion/Accordion.stories.tsx +3 -3
- package/src/components/Accordion/AccordionItem.tsx +1 -1
- package/src/components/List/List.stories.tsx +7 -7
- package/src/components/List/ListItem.tsx +10 -10
- package/src/components/Tree/Tree.stories.tsx +102 -26
- package/src/components/Tree/Tree.tsx +30 -40
- package/src/components/Tree/TreeContext.tsx +18 -9
- package/src/components/Tree/TreeItem.tsx +166 -99
- package/src/components/Tree/TreeItemHeading.tsx +5 -3
- package/src/components/Tree/TreeItemToggle.tsx +1 -1
- package/src/components/Tree/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-list",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.6fa680abb7",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -30,35 +30,35 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
33
|
-
"@effect-atom/atom-react": "^0.
|
|
33
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
34
34
|
"@radix-ui/react-accordion": "1.2.3",
|
|
35
35
|
"@radix-ui/react-context": "1.1.1",
|
|
36
|
-
"@dxos/debug": "0.8.4-main.
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/ui-
|
|
43
|
-
"@dxos/ui-
|
|
44
|
-
"@dxos/util": "0.8.4-main.
|
|
36
|
+
"@dxos/debug": "0.8.4-main.6fa680abb7",
|
|
37
|
+
"@dxos/invariant": "0.8.4-main.6fa680abb7",
|
|
38
|
+
"@dxos/log": "0.8.4-main.6fa680abb7",
|
|
39
|
+
"@dxos/react-ui": "0.8.4-main.6fa680abb7",
|
|
40
|
+
"@dxos/echo": "0.8.4-main.6fa680abb7",
|
|
41
|
+
"@dxos/ui-theme": "0.8.4-main.6fa680abb7",
|
|
42
|
+
"@dxos/ui-types": "0.8.4-main.6fa680abb7",
|
|
43
|
+
"@dxos/react-ui-text-tooltip": "0.8.4-main.6fa680abb7",
|
|
44
|
+
"@dxos/util": "0.8.4-main.6fa680abb7"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/react": "~19.2.7",
|
|
48
48
|
"@types/react-dom": "~19.2.3",
|
|
49
|
-
"effect": "3.19.
|
|
49
|
+
"effect": "3.19.16",
|
|
50
50
|
"react": "~19.2.3",
|
|
51
51
|
"react-dom": "~19.2.3",
|
|
52
|
-
"vite": "7.1.
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
52
|
+
"vite": "^7.1.11",
|
|
53
|
+
"@dxos/storybook-utils": "0.8.4-main.6fa680abb7",
|
|
54
|
+
"@dxos/random": "0.8.4-main.6fa680abb7"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"effect": "3.19.
|
|
57
|
+
"effect": "3.19.16",
|
|
58
58
|
"react": "~19.2.3",
|
|
59
59
|
"react-dom": "~19.2.3",
|
|
60
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
61
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
60
|
+
"@dxos/react-ui": "0.8.4-main.6fa680abb7",
|
|
61
|
+
"@dxos/ui-theme": "0.8.4-main.6fa680abb7"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|
|
@@ -22,9 +22,9 @@ const items: TestItem[] = Array.from({ length: 10 }, (_, i) => ({
|
|
|
22
22
|
|
|
23
23
|
const DefaultStory = () => {
|
|
24
24
|
return (
|
|
25
|
-
<Accordion.Root<TestItem> items={items} classNames='
|
|
25
|
+
<Accordion.Root<TestItem> items={items} classNames='w-[40rem]'>
|
|
26
26
|
{({ items }) => (
|
|
27
|
-
<div className='flex flex-col
|
|
27
|
+
<div className='flex flex-col w-full border-y border-separator divide-y divide-separator'>
|
|
28
28
|
{items.map((item) => (
|
|
29
29
|
<Accordion.Item key={item.id} item={item} classNames='border-x border-separator'>
|
|
30
30
|
<Accordion.ItemHeader>{item.name}</Accordion.ItemHeader>
|
|
@@ -42,7 +42,7 @@ const DefaultStory = () => {
|
|
|
42
42
|
const meta = {
|
|
43
43
|
title: 'ui/react-ui-list/Accordion',
|
|
44
44
|
render: DefaultStory,
|
|
45
|
-
decorators: [withTheme, withLayout({ layout: 'column' })],
|
|
45
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
46
46
|
} satisfies Meta<typeof Accordion>;
|
|
47
47
|
|
|
48
48
|
export default meta;
|
|
@@ -43,7 +43,7 @@ export type AccordionItemHeaderProps = ThemedClassName<AccordionPrimitive.Accord
|
|
|
43
43
|
export const AccordionItemHeader = ({ classNames, children, ...props }: AccordionItemHeaderProps) => {
|
|
44
44
|
return (
|
|
45
45
|
<AccordionPrimitive.Header {...props} className={mx(classNames)}>
|
|
46
|
-
<AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset
|
|
46
|
+
<AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset w-full text-start'>
|
|
47
47
|
{children}
|
|
48
48
|
<Icon
|
|
49
49
|
icon='ph--caret-right--regular'
|
|
@@ -7,7 +7,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
7
7
|
import * as Schema from 'effect/Schema';
|
|
8
8
|
import React, { useContext, useMemo } from 'react';
|
|
9
9
|
|
|
10
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
import { withRegistry } from '@dxos/storybook-utils';
|
|
12
12
|
import { ghostHover, mx } from '@dxos/ui-theme';
|
|
13
13
|
import { arrayMove } from '@dxos/util';
|
|
@@ -16,7 +16,7 @@ import { List, type ListRootProps } from './List';
|
|
|
16
16
|
import { TestItemSchema, type TestItemType, type TestList, createList } from './testing';
|
|
17
17
|
|
|
18
18
|
// TODO(burdon): var-icon-size.
|
|
19
|
-
const grid = 'grid grid-cols-[32px_1fr_32px] min-
|
|
19
|
+
const grid = 'grid grid-cols-[32px_1fr_32px] min-h-[2rem] rounded-sm';
|
|
20
20
|
|
|
21
21
|
const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
22
22
|
const registry = useContext(RegistryContext);
|
|
@@ -45,13 +45,13 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
45
45
|
<List.Root<TestItemType> dragPreview items={items} getId={(item) => item.id} onMove={handleMove} {...props}>
|
|
46
46
|
{({ items }) => (
|
|
47
47
|
<>
|
|
48
|
-
<div className='flex flex-col
|
|
48
|
+
<div className='flex flex-col w-full'>
|
|
49
49
|
<div role='none' className={grid}>
|
|
50
50
|
<div />
|
|
51
51
|
<div className='flex items-center text-sm'>Items</div>
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
|
-
<div role='list' className='
|
|
54
|
+
<div role='list' className='w-full h-full overflow-auto'>
|
|
55
55
|
{items?.map((item) => (
|
|
56
56
|
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
|
|
57
57
|
<List.ItemDragHandle />
|
|
@@ -69,7 +69,7 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
69
69
|
|
|
70
70
|
<List.ItemDragPreview<TestItemType>>
|
|
71
71
|
{({ item }) => (
|
|
72
|
-
<List.ItemWrapper classNames={mx(grid, 'bg-
|
|
72
|
+
<List.ItemWrapper classNames={mx(grid, 'bg-modal-surface border border-separator')}>
|
|
73
73
|
<List.ItemDragHandle />
|
|
74
74
|
<div className='flex items-center'>{item.name}</div>
|
|
75
75
|
</List.ItemWrapper>
|
|
@@ -89,7 +89,7 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
89
89
|
return (
|
|
90
90
|
<List.Root<TestItemType> dragPreview items={items} {...props}>
|
|
91
91
|
{({ items }) => (
|
|
92
|
-
<div role='list' className='
|
|
92
|
+
<div role='list' className='w-full h-full overflow-auto'>
|
|
93
93
|
{items?.map((item) => (
|
|
94
94
|
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
|
|
95
95
|
<List.ItemDragHandle />
|
|
@@ -106,7 +106,7 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
106
106
|
const meta = {
|
|
107
107
|
title: 'ui/react-ui-list/List',
|
|
108
108
|
component: List.Root,
|
|
109
|
-
decorators: [withTheme, withRegistry],
|
|
109
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withRegistry],
|
|
110
110
|
parameters: {
|
|
111
111
|
layout: 'fullscreen',
|
|
112
112
|
},
|
|
@@ -46,17 +46,17 @@ export type ItemDragState =
|
|
|
46
46
|
container: HTMLElement;
|
|
47
47
|
}
|
|
48
48
|
| {
|
|
49
|
-
type: '
|
|
49
|
+
type: 'w-dragging';
|
|
50
50
|
}
|
|
51
51
|
| {
|
|
52
|
-
type: '
|
|
52
|
+
type: 'w-dragging-over';
|
|
53
53
|
closestEdge: Edge | null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
export const idle: ItemDragState = { type: 'idle' };
|
|
57
57
|
|
|
58
58
|
const stateStyles: { [Key in ItemDragState['type']]?: HTMLAttributes<HTMLDivElement>['className'] } = {
|
|
59
|
-
'
|
|
59
|
+
'w-dragging': 'opacity-50',
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
type ListItemContext<T extends ListItemRecord> = {
|
|
@@ -124,8 +124,8 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
|
|
|
124
124
|
}
|
|
125
125
|
: undefined,
|
|
126
126
|
onDragStart: () => {
|
|
127
|
-
setState({ type: '
|
|
128
|
-
setRootState({ type: '
|
|
127
|
+
setState({ type: 'w-dragging' });
|
|
128
|
+
setRootState({ type: 'w-dragging', item });
|
|
129
129
|
},
|
|
130
130
|
onDrop: () => {
|
|
131
131
|
setState(idle);
|
|
@@ -147,7 +147,7 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
|
|
|
147
147
|
getIsSticky: () => true,
|
|
148
148
|
onDragEnter: ({ self }) => {
|
|
149
149
|
const closestEdge = extractClosestEdge(self.data);
|
|
150
|
-
setState({ type: '
|
|
150
|
+
setState({ type: 'w-dragging-over', closestEdge });
|
|
151
151
|
},
|
|
152
152
|
onDragLeave: () => {
|
|
153
153
|
setState(idle);
|
|
@@ -155,10 +155,10 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
|
|
|
155
155
|
onDrag: ({ self }) => {
|
|
156
156
|
const closestEdge = extractClosestEdge(self.data);
|
|
157
157
|
setState((current) => {
|
|
158
|
-
if (current.type === '
|
|
158
|
+
if (current.type === 'w-dragging-over' && current.closestEdge === closestEdge) {
|
|
159
159
|
return current;
|
|
160
160
|
}
|
|
161
|
-
return { type: '
|
|
161
|
+
return { type: 'w-dragging-over', closestEdge };
|
|
162
162
|
});
|
|
163
163
|
},
|
|
164
164
|
onDrop: () => {
|
|
@@ -172,7 +172,7 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
|
|
|
172
172
|
<ListItemProvider item={item} dragHandleRef={dragHandleRef}>
|
|
173
173
|
<div ref={ref} role='listitem' className={mx('flex relative', classNames, stateStyles[state.type])} {...props}>
|
|
174
174
|
{children}
|
|
175
|
-
{state.type === '
|
|
175
|
+
{state.type === 'w-dragging-over' && state.closestEdge && (
|
|
176
176
|
<NaturalListItem.DropIndicator edge={state.closestEdge} />
|
|
177
177
|
)}
|
|
178
178
|
</div>
|
|
@@ -255,7 +255,7 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
|
|
|
255
255
|
};
|
|
256
256
|
|
|
257
257
|
export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
|
|
258
|
-
<div className={mx('flex
|
|
258
|
+
<div className={mx('flex w-full gap-2', classNames)}>{children}</div>
|
|
259
259
|
);
|
|
260
260
|
|
|
261
261
|
export const ListItemTitle = ({
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
6
|
import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
7
|
-
import { Atom, RegistryContext
|
|
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
|
|
|
@@ -17,6 +17,7 @@ import { Path } from '../../util';
|
|
|
17
17
|
|
|
18
18
|
import { type TestItem, createTree, updateState } from './testing';
|
|
19
19
|
import { Tree } from './Tree';
|
|
20
|
+
import { type TreeModel } from './TreeContext';
|
|
20
21
|
import { type TreeData } from './TreeItem';
|
|
21
22
|
|
|
22
23
|
faker.seed(1234);
|
|
@@ -27,27 +28,111 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
27
28
|
const registry = useContext(RegistryContext);
|
|
28
29
|
const stateAtomsRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
29
30
|
|
|
30
|
-
const getOrCreateStateAtom = useCallback((
|
|
31
|
-
let atom = stateAtomsRef.current.get(
|
|
31
|
+
const getOrCreateStateAtom = useCallback((pathKey: string) => {
|
|
32
|
+
let atom = stateAtomsRef.current.get(pathKey);
|
|
32
33
|
if (!atom) {
|
|
33
34
|
atom = Atom.make({ open: false, current: false }).pipe(Atom.keepAlive);
|
|
34
|
-
stateAtomsRef.current.set(
|
|
35
|
+
stateAtomsRef.current.set(pathKey, atom);
|
|
35
36
|
}
|
|
36
37
|
return atom;
|
|
37
38
|
}, []);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
// Build a lookup map of all items by ID.
|
|
41
|
+
const itemMap = useMemo(() => {
|
|
42
|
+
const map = new Map<string, TestItem>();
|
|
43
|
+
const walk = (item: TestItem) => {
|
|
44
|
+
map.set(item.id, item);
|
|
45
|
+
item.items?.forEach(walk);
|
|
46
|
+
};
|
|
47
|
+
walk(tree);
|
|
48
|
+
return map;
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
// Build a child IDs map keyed by parent ID.
|
|
52
|
+
const childIdsMap = useMemo(() => {
|
|
53
|
+
const map = new Map<string, string[]>();
|
|
54
|
+
const walk = (item: TestItem) => {
|
|
55
|
+
if (item.items) {
|
|
56
|
+
map.set(
|
|
57
|
+
item.id,
|
|
58
|
+
item.items.map((child) => child.id),
|
|
59
|
+
);
|
|
60
|
+
item.items.forEach(walk);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// Root children.
|
|
64
|
+
map.set(
|
|
65
|
+
tree.id,
|
|
66
|
+
(tree.items ?? []).map((child) => child.id),
|
|
67
|
+
);
|
|
68
|
+
walk(tree);
|
|
69
|
+
return map;
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const childIdsFamily = useMemo(
|
|
73
|
+
() => Atom.family((id: string) => Atom.make(() => childIdsMap.get(id) ?? []).pipe(Atom.keepAlive)),
|
|
74
|
+
[childIdsMap],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const itemFamily = useMemo(
|
|
78
|
+
() => Atom.family((id: string) => Atom.make(() => itemMap.get(id)).pipe(Atom.keepAlive)),
|
|
79
|
+
[itemMap],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const itemPropsFamily = useMemo(
|
|
83
|
+
() =>
|
|
84
|
+
Atom.family((pathKey: string) => {
|
|
85
|
+
const id = pathKey.split('~').pop()!;
|
|
86
|
+
return Atom.make(() => {
|
|
87
|
+
const parent = itemMap.get(id);
|
|
88
|
+
if (!parent) {
|
|
89
|
+
return { id, label: id };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
id: parent.id,
|
|
93
|
+
label: parent.name,
|
|
94
|
+
icon: parent.icon,
|
|
95
|
+
...((parent.items?.length ?? 0) > 0 && {
|
|
96
|
+
parentOf: parent.items!.map(({ id }) => id),
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}).pipe(Atom.keepAlive);
|
|
100
|
+
}),
|
|
101
|
+
[itemMap],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const itemOpenFamily = useMemo(
|
|
105
|
+
() =>
|
|
106
|
+
Atom.family((pathKey: string) => {
|
|
107
|
+
const stateAtom = getOrCreateStateAtom(pathKey);
|
|
108
|
+
return Atom.make((get) => get(stateAtom).open).pipe(Atom.keepAlive);
|
|
109
|
+
}),
|
|
110
|
+
[getOrCreateStateAtom],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const itemCurrentFamily = useMemo(
|
|
114
|
+
() =>
|
|
115
|
+
Atom.family((pathKey: string) => {
|
|
116
|
+
const stateAtom = getOrCreateStateAtom(pathKey);
|
|
117
|
+
return Atom.make((get) => get(stateAtom).current).pipe(Atom.keepAlive);
|
|
118
|
+
}),
|
|
45
119
|
[getOrCreateStateAtom],
|
|
46
120
|
);
|
|
47
121
|
|
|
122
|
+
const model: TreeModel<TestItem> = useMemo(
|
|
123
|
+
() => ({
|
|
124
|
+
childIds: (parentId?: string) => childIdsFamily(parentId ?? tree.id),
|
|
125
|
+
item: (id: string) => itemFamily(id),
|
|
126
|
+
itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
|
|
127
|
+
itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
|
|
128
|
+
itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
|
|
129
|
+
}),
|
|
130
|
+
[childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
|
|
131
|
+
);
|
|
132
|
+
|
|
48
133
|
const handleOpenChange = useCallback(
|
|
49
|
-
({ path:
|
|
50
|
-
const path = Path.create(...
|
|
134
|
+
({ path: pathProp, open }: { path: string[]; open: boolean }) => {
|
|
135
|
+
const path = Path.create(...pathProp);
|
|
51
136
|
const atom = getOrCreateStateAtom(path);
|
|
52
137
|
const prev = registry.get(atom);
|
|
53
138
|
registry.set(atom, { ...prev, open });
|
|
@@ -56,8 +141,8 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
56
141
|
);
|
|
57
142
|
|
|
58
143
|
const handleSelect = useCallback(
|
|
59
|
-
({ path:
|
|
60
|
-
const path = Path.create(...
|
|
144
|
+
({ path: pathProp, current }: { path: string[]; current: boolean }) => {
|
|
145
|
+
const path = Path.create(...pathProp);
|
|
61
146
|
const atom = getOrCreateStateAtom(path);
|
|
62
147
|
const prev = registry.get(atom);
|
|
63
148
|
registry.set(atom, { ...prev, current });
|
|
@@ -89,19 +174,10 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
89
174
|
|
|
90
175
|
return (
|
|
91
176
|
<Tree
|
|
177
|
+
model={model}
|
|
92
178
|
id={tree.id}
|
|
179
|
+
rootId={tree.id}
|
|
93
180
|
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
181
|
renderColumns={() => (
|
|
106
182
|
<div className='flex items-center'>
|
|
107
183
|
<Icon icon='ph--placeholder--regular' size={5} />
|
|
@@ -116,7 +192,7 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
116
192
|
const meta = {
|
|
117
193
|
title: 'ui/react-ui-list/Tree',
|
|
118
194
|
|
|
119
|
-
decorators: [withTheme, withRegistry],
|
|
195
|
+
decorators: [withTheme(), withRegistry],
|
|
120
196
|
component: Tree,
|
|
121
197
|
render: DefaultStory,
|
|
122
198
|
} 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
|
|
10
|
-
import {
|
|
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
|
|
13
|
-
|
|
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
|
-
} &
|
|
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
|
|
31
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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={
|
|
64
|
-
{
|
|
65
|
-
<
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
|
|
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<
|
|
40
|
+
const TreeContext = createContext<TreeModel | null>(null);
|
|
32
41
|
|
|
33
42
|
export const TreeProvider = TreeContext.Provider;
|
|
34
43
|
|