@dxos/react-ui-list 0.8.4-main.e8ec1fe → 0.8.4-main.ef1bc66f44
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 +637 -712
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +637 -712
- 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 +2 -2
- package/dist/types/src/components/List/List.d.ts.map +1 -1
- package/dist/types/src/components/List/List.stories.d.ts +2 -2
- package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
- package/dist/types/src/components/List/ListRoot.d.ts +2 -2
- package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.d.ts +7 -4
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +9 -28
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeContext.d.ts +4 -2
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts +12 -3
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing.d.ts +2 -2
- package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -27
- package/src/components/Accordion/Accordion.stories.tsx +1 -1
- package/src/components/Accordion/AccordionItem.tsx +2 -2
- package/src/components/Accordion/AccordionRoot.tsx +1 -1
- package/src/components/List/List.stories.tsx +26 -14
- package/src/components/List/ListItem.tsx +3 -3
- package/src/components/List/ListRoot.tsx +2 -2
- package/src/components/List/testing.ts +2 -2
- package/src/components/Tree/Tree.stories.tsx +74 -60
- package/src/components/Tree/Tree.tsx +17 -9
- package/src/components/Tree/TreeContext.tsx +4 -2
- package/src/components/Tree/TreeItem.tsx +16 -10
- package/src/components/Tree/TreeItemHeading.tsx +1 -1
- package/src/components/Tree/testing.ts +4 -3
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-list",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.ef1bc66f44",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"type": "module",
|
|
@@ -24,39 +28,37 @@
|
|
|
24
28
|
"src"
|
|
25
29
|
],
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"@atlaskit/pragmatic-drag-and-drop": "
|
|
28
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "
|
|
29
|
-
"@
|
|
30
|
-
"@preact/signals-core": "^1.12.1",
|
|
31
|
+
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
32
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
33
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
31
34
|
"@radix-ui/react-accordion": "1.2.3",
|
|
32
35
|
"@radix-ui/react-context": "1.1.1",
|
|
33
|
-
"@dxos/debug": "0.8.4-main.
|
|
34
|
-
"@dxos/echo": "0.8.4-main.
|
|
35
|
-
"@dxos/invariant": "0.8.4-main.
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/log": "0.8.4-main.
|
|
38
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/util": "0.8.4-main.
|
|
42
|
-
"@dxos/react-ui-theme": "0.8.4-main.e8ec1fe"
|
|
36
|
+
"@dxos/debug": "0.8.4-main.ef1bc66f44",
|
|
37
|
+
"@dxos/echo": "0.8.4-main.ef1bc66f44",
|
|
38
|
+
"@dxos/invariant": "0.8.4-main.ef1bc66f44",
|
|
39
|
+
"@dxos/react-ui": "0.8.4-main.ef1bc66f44",
|
|
40
|
+
"@dxos/log": "0.8.4-main.ef1bc66f44",
|
|
41
|
+
"@dxos/react-ui-text-tooltip": "0.8.4-main.ef1bc66f44",
|
|
42
|
+
"@dxos/ui-types": "0.8.4-main.ef1bc66f44",
|
|
43
|
+
"@dxos/ui-theme": "0.8.4-main.ef1bc66f44",
|
|
44
|
+
"@dxos/util": "0.8.4-main.ef1bc66f44"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
|
-
"@types/react": "~19.2.
|
|
46
|
-
"@types/react-dom": "~19.2.
|
|
47
|
-
"effect": "3.
|
|
48
|
-
"react": "~19.2.
|
|
49
|
-
"react-dom": "~19.2.
|
|
47
|
+
"@types/react": "~19.2.7",
|
|
48
|
+
"@types/react-dom": "~19.2.3",
|
|
49
|
+
"effect": "3.19.16",
|
|
50
|
+
"react": "~19.2.3",
|
|
51
|
+
"react-dom": "~19.2.3",
|
|
50
52
|
"vite": "7.1.9",
|
|
51
|
-
"@dxos/random": "0.8.4-main.
|
|
52
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
53
|
+
"@dxos/random": "0.8.4-main.ef1bc66f44",
|
|
54
|
+
"@dxos/storybook-utils": "0.8.4-main.ef1bc66f44"
|
|
53
55
|
},
|
|
54
56
|
"peerDependencies": {
|
|
55
|
-
"effect": "
|
|
56
|
-
"react": "
|
|
57
|
-
"react-dom": "
|
|
58
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
59
|
-
"@dxos/
|
|
57
|
+
"effect": "3.19.16",
|
|
58
|
+
"react": "~19.2.3",
|
|
59
|
+
"react-dom": "~19.2.3",
|
|
60
|
+
"@dxos/react-ui": "0.8.4-main.ef1bc66f44",
|
|
61
|
+
"@dxos/ui-theme": "0.8.4-main.ef1bc66f44"
|
|
60
62
|
},
|
|
61
63
|
"publishConfig": {
|
|
62
64
|
"access": "public"
|
|
@@ -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({
|
|
45
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
46
46
|
} satisfies Meta<typeof Accordion>;
|
|
47
47
|
|
|
48
48
|
export default meta;
|
|
@@ -7,7 +7,7 @@ import { createContext } from '@radix-ui/react-context';
|
|
|
7
7
|
import React, { type PropsWithChildren } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Icon, type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
-
import { mx } from '@dxos/
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
12
|
import { type ListItemRecord } from '../List';
|
|
13
13
|
|
|
@@ -59,7 +59,7 @@ export type AccordionItemBodyProps = ThemedClassName<PropsWithChildren>;
|
|
|
59
59
|
|
|
60
60
|
export const AccordionItemBody = ({ children, classNames }: AccordionItemBodyProps) => {
|
|
61
61
|
return (
|
|
62
|
-
<AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-
|
|
62
|
+
<AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down'>
|
|
63
63
|
<div role='none' className={mx('p-2', classNames)}>
|
|
64
64
|
{children}
|
|
65
65
|
</div>
|
|
@@ -7,7 +7,7 @@ import { createContext } from '@radix-ui/react-context';
|
|
|
7
7
|
import React, { type ReactNode } from 'react';
|
|
8
8
|
|
|
9
9
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
-
import { mx } from '@dxos/
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
12
|
import { type ListItemRecord } from '../List';
|
|
13
13
|
|
|
@@ -2,31 +2,43 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
5
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
7
|
import * as Schema from 'effect/Schema';
|
|
7
|
-
import React from 'react';
|
|
8
|
+
import React, { useContext, useMemo } from 'react';
|
|
8
9
|
|
|
9
|
-
import { live } from '@dxos/live-object';
|
|
10
10
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
|
-
import {
|
|
11
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
12
|
+
import { ghostHover, mx } from '@dxos/ui-theme';
|
|
12
13
|
import { arrayMove } from '@dxos/util';
|
|
13
14
|
|
|
14
15
|
import { List, type ListRootProps } from './List';
|
|
15
|
-
import { TestItemSchema, type TestItemType, createList } from './testing';
|
|
16
|
+
import { TestItemSchema, type TestItemType, type TestList, createList } from './testing';
|
|
16
17
|
|
|
17
18
|
// TODO(burdon): var-icon-size.
|
|
18
19
|
const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2rem] rounded';
|
|
19
20
|
|
|
20
|
-
const DefaultStory = (
|
|
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;
|
|
26
|
+
|
|
21
27
|
const handleSelect = (item: TestItemType) => {
|
|
22
28
|
console.log('select', item);
|
|
23
29
|
};
|
|
24
30
|
const handleDelete = (item: TestItemType) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
31
|
+
const prev = registry.get(listAtom);
|
|
32
|
+
registry.set(listAtom, {
|
|
33
|
+
...prev,
|
|
34
|
+
items: prev.items.filter((i) => i.id !== item.id),
|
|
35
|
+
});
|
|
27
36
|
};
|
|
28
37
|
const handleMove = (from: number, to: number) => {
|
|
29
|
-
|
|
38
|
+
const prev = registry.get(listAtom);
|
|
39
|
+
const newItems = [...prev.items];
|
|
40
|
+
arrayMove(newItems, from, to);
|
|
41
|
+
registry.set(listAtom, { ...prev, items: newItems });
|
|
30
42
|
};
|
|
31
43
|
|
|
32
44
|
return (
|
|
@@ -69,7 +81,11 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
|
|
|
69
81
|
);
|
|
70
82
|
};
|
|
71
83
|
|
|
72
|
-
const SimpleStory = (
|
|
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
|
+
|
|
73
89
|
return (
|
|
74
90
|
<List.Root<TestItemType> dragPreview items={items} {...props}>
|
|
75
91
|
{({ items }) => (
|
|
@@ -87,12 +103,10 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
|
87
103
|
);
|
|
88
104
|
};
|
|
89
105
|
|
|
90
|
-
const list = live(createList(100));
|
|
91
|
-
|
|
92
106
|
const meta = {
|
|
93
107
|
title: 'ui/react-ui-list/List',
|
|
94
108
|
component: List.Root,
|
|
95
|
-
decorators: [withTheme],
|
|
109
|
+
decorators: [withTheme(), withRegistry],
|
|
96
110
|
parameters: {
|
|
97
111
|
layout: 'fullscreen',
|
|
98
112
|
},
|
|
@@ -103,7 +117,6 @@ export default meta;
|
|
|
103
117
|
export const Default: StoryObj<typeof DefaultStory> = {
|
|
104
118
|
render: DefaultStory,
|
|
105
119
|
args: {
|
|
106
|
-
items: list.items,
|
|
107
120
|
isItem: Schema.is(TestItemSchema),
|
|
108
121
|
},
|
|
109
122
|
};
|
|
@@ -111,7 +124,6 @@ export const Default: StoryObj<typeof DefaultStory> = {
|
|
|
111
124
|
export const Simple: StoryObj<typeof SimpleStory> = {
|
|
112
125
|
render: SimpleStory,
|
|
113
126
|
args: {
|
|
114
|
-
items: list.items,
|
|
115
127
|
isItem: Schema.is(TestItemSchema),
|
|
116
128
|
},
|
|
117
129
|
};
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
type ThemedClassName,
|
|
32
32
|
useTranslation,
|
|
33
33
|
} from '@dxos/react-ui';
|
|
34
|
-
import { mx } from '@dxos/
|
|
34
|
+
import { mx, osTranslations } from '@dxos/ui-theme';
|
|
35
35
|
|
|
36
36
|
import { useListContext } from './ListRoot';
|
|
37
37
|
|
|
@@ -195,7 +195,7 @@ export const ListItemDeleteButton = ({
|
|
|
195
195
|
Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
|
|
196
196
|
const { state } = useListContext('DELETE_BUTTON');
|
|
197
197
|
const isDisabled = state.type !== 'idle' || disabled;
|
|
198
|
-
const { t } = useTranslation(
|
|
198
|
+
const { t } = useTranslation(osTranslations);
|
|
199
199
|
return (
|
|
200
200
|
<IconButton
|
|
201
201
|
iconOnly
|
|
@@ -232,7 +232,7 @@ export const ListItemButton = ({
|
|
|
232
232
|
|
|
233
233
|
export const ListItemDragHandle = ({ disabled }: Pick<IconButtonProps, 'disabled'>) => {
|
|
234
234
|
const { dragHandleRef } = useListItemContext('DRAG_HANDLE');
|
|
235
|
-
const { t } = useTranslation(
|
|
235
|
+
const { t } = useTranslation(osTranslations);
|
|
236
236
|
return (
|
|
237
237
|
<IconButton
|
|
238
238
|
iconOnly
|
|
@@ -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
|
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Schema from 'effect/Schema';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Obj } from '@dxos/echo';
|
|
8
8
|
import { faker } from '@dxos/random';
|
|
9
9
|
|
|
10
10
|
export const TestItemSchema = Schema.Struct({
|
|
11
|
-
id:
|
|
11
|
+
id: Obj.ID,
|
|
12
12
|
name: Schema.String,
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -4,28 +4,71 @@
|
|
|
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, useAtomValue } 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
11
|
import { faker } 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
18
|
import { type TestItem, createTree, updateState } from './testing';
|
|
18
|
-
import { Tree
|
|
19
|
+
import { Tree } from './Tree';
|
|
19
20
|
import { type TreeData } from './TreeItem';
|
|
20
21
|
|
|
21
22
|
faker.seed(1234);
|
|
22
23
|
|
|
23
|
-
const
|
|
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((path: string) => {
|
|
31
|
+
let atom = stateAtomsRef.current.get(path);
|
|
32
|
+
if (!atom) {
|
|
33
|
+
atom = Atom.make({ open: false, current: false }).pipe(Atom.keepAlive);
|
|
34
|
+
stateAtomsRef.current.set(path, atom);
|
|
35
|
+
}
|
|
36
|
+
return atom;
|
|
37
|
+
}, []);
|
|
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
|
+
},
|
|
45
|
+
[getOrCreateStateAtom],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const handleOpenChange = useCallback(
|
|
49
|
+
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
50
|
+
const path = Path.create(..._path);
|
|
51
|
+
const atom = getOrCreateStateAtom(path);
|
|
52
|
+
const prev = registry.get(atom);
|
|
53
|
+
registry.set(atom, { ...prev, open });
|
|
54
|
+
},
|
|
55
|
+
[getOrCreateStateAtom, registry],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const handleSelect = useCallback(
|
|
59
|
+
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
60
|
+
const path = Path.create(..._path);
|
|
61
|
+
const atom = getOrCreateStateAtom(path);
|
|
62
|
+
const prev = registry.get(atom);
|
|
63
|
+
registry.set(atom, { ...prev, current });
|
|
64
|
+
},
|
|
65
|
+
[getOrCreateStateAtom, registry],
|
|
66
|
+
);
|
|
67
|
+
|
|
24
68
|
useEffect(() => {
|
|
25
69
|
return monitorForElements({
|
|
26
70
|
canMonitor: ({ source }) => typeof source.data.id === 'string' && Array.isArray(source.data.path),
|
|
27
71
|
onDrop: ({ location, source }) => {
|
|
28
|
-
// Didn't drop on anything.
|
|
29
72
|
if (!location.current.dropTargets.length) {
|
|
30
73
|
return;
|
|
31
74
|
}
|
|
@@ -44,72 +87,43 @@ const DefaultStory = (props: TreeProps) => {
|
|
|
44
87
|
});
|
|
45
88
|
}, []);
|
|
46
89
|
|
|
47
|
-
return
|
|
90
|
+
return (
|
|
91
|
+
<Tree
|
|
92
|
+
id={tree.id}
|
|
93
|
+
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
|
+
renderColumns={() => (
|
|
106
|
+
<div className='flex items-center'>
|
|
107
|
+
<Icon icon='ph--placeholder--regular' size={5} />
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
onOpenChange={handleOpenChange}
|
|
111
|
+
onSelect={handleSelect}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
48
114
|
};
|
|
49
115
|
|
|
50
|
-
const tree = live<TestItem>(createTree());
|
|
51
|
-
const state = new Map<string, Live<{ open: boolean; current: boolean }>>();
|
|
52
|
-
|
|
53
116
|
const meta = {
|
|
54
117
|
title: 'ui/react-ui-list/Tree',
|
|
55
118
|
|
|
56
|
-
decorators: [withTheme],
|
|
119
|
+
decorators: [withTheme(), withRegistry],
|
|
57
120
|
component: Tree,
|
|
58
121
|
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
122
|
} satisfies Meta<typeof Tree<TestItem>>;
|
|
109
123
|
|
|
110
124
|
export default meta;
|
|
111
125
|
|
|
112
|
-
type Story = StoryObj<typeof
|
|
126
|
+
type Story = StoryObj<typeof DefaultStory>;
|
|
113
127
|
|
|
114
128
|
export const Default: Story = {};
|
|
115
129
|
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
|
|
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
9
|
import { type TreeContextType, TreeProvider } from './TreeContext';
|
|
11
10
|
import { TreeItem, type TreeItemProps } from './TreeItem';
|
|
12
11
|
|
|
13
|
-
export type TreeProps<T extends
|
|
12
|
+
export type TreeProps<T extends { id: string } = any, O = any> = {
|
|
14
13
|
root?: T;
|
|
15
14
|
path?: string[];
|
|
16
15
|
id: string;
|
|
@@ -18,22 +17,30 @@ export type TreeProps<T extends HasId = any, O = any> = {
|
|
|
18
17
|
Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
|
|
19
18
|
Pick<
|
|
20
19
|
TreeItemProps<T>,
|
|
21
|
-
|
|
20
|
+
| 'draggable'
|
|
21
|
+
| 'renderColumns'
|
|
22
|
+
| 'blockInstruction'
|
|
23
|
+
| 'canDrop'
|
|
24
|
+
| 'canSelect'
|
|
25
|
+
| 'onOpenChange'
|
|
26
|
+
| 'onSelect'
|
|
27
|
+
| 'levelOffset'
|
|
22
28
|
>;
|
|
23
29
|
|
|
24
|
-
export const Tree = <T extends
|
|
30
|
+
export const Tree = <T extends { id: string } = any, O = any>({
|
|
25
31
|
root,
|
|
26
32
|
path,
|
|
27
33
|
id,
|
|
28
34
|
useItems,
|
|
29
35
|
getProps,
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
useIsOpen,
|
|
37
|
+
useIsCurrent,
|
|
32
38
|
draggable = false,
|
|
33
39
|
gridTemplateColumns = '[tree-row-start] 1fr min-content [tree-row-end]',
|
|
34
40
|
classNames,
|
|
35
41
|
levelOffset,
|
|
36
42
|
renderColumns,
|
|
43
|
+
blockInstruction,
|
|
37
44
|
canDrop,
|
|
38
45
|
canSelect,
|
|
39
46
|
onOpenChange,
|
|
@@ -43,10 +50,10 @@ export const Tree = <T extends HasId = any, O = any>({
|
|
|
43
50
|
() => ({
|
|
44
51
|
useItems,
|
|
45
52
|
getProps,
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
useIsOpen,
|
|
54
|
+
useIsCurrent,
|
|
48
55
|
}),
|
|
49
|
-
[useItems, getProps,
|
|
56
|
+
[useItems, getProps, useIsOpen, useIsCurrent],
|
|
50
57
|
);
|
|
51
58
|
const items = useItems(root);
|
|
52
59
|
const treePath = useMemo(() => (path ? [...path, id] : [id]), [id, path]);
|
|
@@ -63,6 +70,7 @@ export const Tree = <T extends HasId = any, O = any>({
|
|
|
63
70
|
levelOffset={levelOffset}
|
|
64
71
|
draggable={draggable}
|
|
65
72
|
renderColumns={renderColumns}
|
|
73
|
+
blockInstruction={blockInstruction}
|
|
66
74
|
canDrop={canDrop}
|
|
67
75
|
canSelect={canSelect}
|
|
68
76
|
onOpenChange={onOpenChange}
|
|
@@ -22,8 +22,10 @@ export type TreeItemDataProps = {
|
|
|
22
22
|
export type TreeContextType<T = any, O = any> = {
|
|
23
23
|
useItems: (parent?: T, options?: O) => T[];
|
|
24
24
|
getProps: (item: T, parent: string[]) => TreeItemDataProps;
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const TreeContext = createContext<null | TreeContextType>(null);
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import * as Schema from 'effect/Schema';
|
|
14
14
|
import React, { type FC, type KeyboardEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
|
|
16
|
-
import { type HasId } from '@dxos/echo/internal';
|
|
17
16
|
import { invariant } from '@dxos/invariant';
|
|
18
17
|
import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
|
|
19
18
|
import {
|
|
@@ -22,7 +21,7 @@ import {
|
|
|
22
21
|
hoverableControls,
|
|
23
22
|
hoverableFocusedKeyboardControls,
|
|
24
23
|
hoverableFocusedWithinControls,
|
|
25
|
-
} from '@dxos/
|
|
24
|
+
} from '@dxos/ui-theme';
|
|
26
25
|
|
|
27
26
|
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
28
27
|
import { useTree } from './TreeContext';
|
|
@@ -43,7 +42,7 @@ export const TreeDataSchema = Schema.Struct({
|
|
|
43
42
|
export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
|
|
44
43
|
export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
|
|
45
44
|
|
|
46
|
-
export type ColumnRenderer<T extends
|
|
45
|
+
export type ColumnRenderer<T extends { id: string } = any> = FC<{
|
|
47
46
|
item: T;
|
|
48
47
|
path: string[];
|
|
49
48
|
open: boolean;
|
|
@@ -51,26 +50,28 @@ export type ColumnRenderer<T extends HasId = any> = FC<{
|
|
|
51
50
|
setMenuOpen: (open: boolean) => void;
|
|
52
51
|
}>;
|
|
53
52
|
|
|
54
|
-
export type TreeItemProps<T extends
|
|
53
|
+
export type TreeItemProps<T extends { id: string } = any> = {
|
|
55
54
|
item: T;
|
|
56
55
|
path: string[];
|
|
57
56
|
levelOffset?: number;
|
|
58
57
|
last: boolean;
|
|
59
58
|
draggable?: boolean;
|
|
60
59
|
renderColumns?: ColumnRenderer<T>;
|
|
60
|
+
blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
|
|
61
61
|
canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
|
|
62
62
|
canSelect?: (params: { item: T; path: string[] }) => boolean;
|
|
63
63
|
onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
|
|
64
64
|
onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const RawTreeItem = <T extends
|
|
67
|
+
const RawTreeItem = <T extends { id: string } = any>({
|
|
68
68
|
item,
|
|
69
69
|
path: _path,
|
|
70
70
|
levelOffset = 2,
|
|
71
71
|
last,
|
|
72
72
|
draggable: _draggable,
|
|
73
73
|
renderColumns: Columns,
|
|
74
|
+
blockInstruction,
|
|
74
75
|
canDrop,
|
|
75
76
|
canSelect,
|
|
76
77
|
onOpenChange,
|
|
@@ -84,12 +85,12 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
84
85
|
const [instruction, setInstruction] = useState<Instruction | null>(null);
|
|
85
86
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
86
87
|
|
|
87
|
-
const { useItems, getProps,
|
|
88
|
+
const { useItems, getProps, useIsOpen, useIsCurrent } = useTree();
|
|
88
89
|
const items = useItems(item);
|
|
89
90
|
const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = getProps(item, _path);
|
|
90
91
|
const path = useMemo(() => [..._path, id], [_path, id]);
|
|
91
|
-
const open =
|
|
92
|
-
const current =
|
|
92
|
+
const open = useIsOpen(path, item);
|
|
93
|
+
const current = useIsCurrent(path, item);
|
|
93
94
|
const level = path.length - levelOffset;
|
|
94
95
|
const isBranch = !!parentOf;
|
|
95
96
|
const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
|
|
@@ -149,7 +150,11 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
149
150
|
},
|
|
150
151
|
getIsSticky: () => true,
|
|
151
152
|
onDrag: ({ self, source }) => {
|
|
152
|
-
const
|
|
153
|
+
const desired = extractInstruction(self.data);
|
|
154
|
+
const block =
|
|
155
|
+
desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
|
|
156
|
+
const instruction: Instruction | null =
|
|
157
|
+
block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
|
|
153
158
|
|
|
154
159
|
if (source.data.id !== id) {
|
|
155
160
|
if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
|
|
@@ -180,7 +185,7 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
180
185
|
},
|
|
181
186
|
}),
|
|
182
187
|
);
|
|
183
|
-
}, [_draggable, item, id, mode, path, open, canDrop]);
|
|
188
|
+
}, [_draggable, item, id, mode, path, open, blockInstruction, canDrop]);
|
|
184
189
|
|
|
185
190
|
// Cancel expand on unmount.
|
|
186
191
|
useEffect(() => () => cancelExpand(), [cancelExpand]);
|
|
@@ -279,6 +284,7 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
279
284
|
last={index === items.length - 1}
|
|
280
285
|
draggable={_draggable}
|
|
281
286
|
renderColumns={Columns}
|
|
287
|
+
blockInstruction={blockInstruction}
|
|
282
288
|
canDrop={canDrop}
|
|
283
289
|
canSelect={canSelect}
|
|
284
290
|
onOpenChange={onOpenChange}
|
|
@@ -6,7 +6,7 @@ import React, { type KeyboardEvent, type MouseEvent, forwardRef, memo, useCallba
|
|
|
6
6
|
|
|
7
7
|
import { Button, Icon, type Label, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { TextTooltip } from '@dxos/react-ui-text-tooltip';
|
|
9
|
-
import { getStyles } from '@dxos/
|
|
9
|
+
import { getStyles } from '@dxos/ui-theme';
|
|
10
10
|
|
|
11
11
|
// TODO(wittjosiah): Consider whether there should be a separate disabled prop which was visually distinct
|
|
12
12
|
// rather than just making the item unselectable.
|
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Obj } from '@dxos/echo';
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
import { faker } from '@dxos/random';
|
|
11
11
|
|
|
12
12
|
import { type TreeData } from './TreeItem';
|
|
13
13
|
|
|
14
|
-
export type TestItem =
|
|
14
|
+
export type TestItem = {
|
|
15
|
+
id: string;
|
|
15
16
|
name: string;
|
|
16
17
|
icon?: string;
|
|
17
18
|
items: TestItem[];
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export const TestItemSchema = Schema.Struct({
|
|
21
|
-
id:
|
|
22
|
+
id: Obj.ID,
|
|
22
23
|
name: Schema.String,
|
|
23
24
|
icon: Schema.optional(Schema.String),
|
|
24
25
|
items: Schema.mutable(Schema.Array(Schema.suspend((): Schema.Schema<TestItem> => TestItemSchema))),
|