@dxos/react-ui-list 0.6.13-main.548ca8d → 0.6.14-main.1366248
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 +413 -31
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/List/List.d.ts +1 -1
- package/dist/types/src/components/List/List.d.ts.map +1 -1
- package/dist/types/src/components/List/List.stories.d.ts +6 -10
- package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
- package/dist/types/src/components/List/ListItem.d.ts +1 -3
- package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
- package/dist/types/src/components/List/ListRoot.d.ts +5 -3
- package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
- package/dist/types/src/components/List/testing.d.ts.map +1 -0
- package/dist/types/src/components/Tree/DropIndicator.d.ts +7 -0
- package/dist/types/src/components/Tree/DropIndicator.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.d.ts +24 -0
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.stories.d.ts +8 -0
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -0
- package/dist/types/src/components/Tree/TreeItem.d.ts +34 -0
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -0
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts +12 -0
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -0
- package/dist/types/src/components/Tree/TreeItemToggle.d.ts +8 -0
- package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -0
- package/dist/types/src/components/Tree/helpers.d.ts +8 -0
- package/dist/types/src/components/Tree/helpers.d.ts.map +1 -0
- package/dist/types/src/components/Tree/index.d.ts +4 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -0
- package/dist/types/src/components/Tree/testing.d.ts +26 -0
- package/dist/types/src/components/Tree/testing.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types.d.ts +18 -0
- package/dist/types/src/components/Tree/types.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/package.json +20 -15
- package/src/components/List/List.stories.tsx +25 -19
- package/src/components/List/ListItem.tsx +11 -9
- package/src/components/List/ListRoot.tsx +47 -23
- package/src/components/Tree/DropIndicator.tsx +79 -0
- package/src/components/Tree/Tree.stories.tsx +116 -0
- package/src/components/Tree/Tree.tsx +56 -0
- package/src/components/Tree/TreeItem.tsx +237 -0
- package/src/components/Tree/TreeItemHeading.tsx +62 -0
- package/src/components/Tree/TreeItemToggle.tsx +35 -0
- package/src/components/Tree/helpers.ts +25 -0
- package/src/components/Tree/index.ts +7 -0
- package/src/components/Tree/testing.ts +170 -0
- package/src/components/Tree/types.ts +34 -0
- package/src/components/index.ts +1 -0
- package/dist/types/src/testing.d.ts.map +0 -1
- /package/dist/types/src/{testing.d.ts → components/List/testing.d.ts} +0 -0
- /package/src/{testing.ts → components/List/testing.ts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAIA,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAIA,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-list",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14-main.1366248",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"author": "DXOS.org",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"types": "./dist/types/src/index.d.ts",
|
|
12
|
+
"browser": "./dist/lib/browser/index.mjs"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -25,16 +25,18 @@
|
|
|
25
25
|
"@atlaskit/pragmatic-drag-and-drop-flourish": "^1.1.2",
|
|
26
26
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
|
27
27
|
"@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^1.1.3",
|
|
28
|
+
"@preact/signals-core": "^1.6.0",
|
|
28
29
|
"@radix-ui/react-context": "^1.0.0",
|
|
29
30
|
"effect": "^3.9.1",
|
|
30
|
-
"@dxos/debug": "0.6.
|
|
31
|
-
"@dxos/invariant": "0.6.
|
|
32
|
-
"@dxos/echo-schema": "0.6.
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/react-ui-
|
|
35
|
-
"@dxos/react-ui-
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
31
|
+
"@dxos/debug": "0.6.14-main.1366248",
|
|
32
|
+
"@dxos/invariant": "0.6.14-main.1366248",
|
|
33
|
+
"@dxos/echo-schema": "0.6.14-main.1366248",
|
|
34
|
+
"@dxos/log": "0.6.14-main.1366248",
|
|
35
|
+
"@dxos/react-ui-mosaic": "0.6.14-main.1366248",
|
|
36
|
+
"@dxos/react-ui-attention": "0.6.14-main.1366248",
|
|
37
|
+
"@dxos/react-ui-text-tooltip": "0.6.14-main.1366248",
|
|
38
|
+
"@dxos/react-ui-types": "0.6.14-main.1366248",
|
|
39
|
+
"@dxos/util": "0.6.14-main.1366248"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@phosphor-icons/react": "^2.1.5",
|
|
@@ -43,15 +45,18 @@
|
|
|
43
45
|
"react": "~18.2.0",
|
|
44
46
|
"react-dom": "~18.2.0",
|
|
45
47
|
"vite": "5.4.7",
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
48
|
+
"@dxos/random": "0.6.14-main.1366248",
|
|
49
|
+
"@dxos/react-ui": "0.6.14-main.1366248",
|
|
50
|
+
"@dxos/react-ui-theme": "0.6.14-main.1366248",
|
|
51
|
+
"@dxos/storybook-utils": "0.6.14-main.1366248"
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
54
|
"@phosphor-icons/react": "^2.1.5",
|
|
52
55
|
"effect": "^3.9.1",
|
|
53
56
|
"react": "~18.2.0",
|
|
54
|
-
"react-dom": "~18.2.0"
|
|
57
|
+
"react-dom": "~18.2.0",
|
|
58
|
+
"@dxos/react-ui-theme": "0.6.14-main.1366248",
|
|
59
|
+
"@dxos/react-ui": "0.6.14-main.1366248"
|
|
55
60
|
},
|
|
56
61
|
"publishConfig": {
|
|
57
62
|
"access": "public"
|
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
|
+
import { type Meta, type StoryObj } from '@storybook/react';
|
|
7
8
|
import React from 'react';
|
|
8
9
|
|
|
9
10
|
import { create, S } from '@dxos/echo-schema';
|
|
10
11
|
import { ghostHover, mx } from '@dxos/react-ui-theme';
|
|
11
12
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
13
|
+
import { arrayMove } from '@dxos/util';
|
|
12
14
|
|
|
13
15
|
import { List, type ListRootProps } from './List';
|
|
14
|
-
import { createList, TestItemSchema, type TestItemType } from '
|
|
16
|
+
import { createList, TestItemSchema, type TestItemType } from './testing';
|
|
15
17
|
|
|
16
18
|
// TODO(burdon): var-icon-size.
|
|
17
19
|
const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2rem] rounded';
|
|
18
20
|
|
|
19
|
-
const
|
|
21
|
+
const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
20
22
|
const handleSelect = (item: TestItemType) => {
|
|
21
23
|
console.log('select', item);
|
|
22
24
|
};
|
|
@@ -24,9 +26,12 @@ const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
|
24
26
|
const idx = items.findIndex((i) => i.id === item.id);
|
|
25
27
|
items.splice(idx, 1);
|
|
26
28
|
};
|
|
29
|
+
const handleMove = (from: number, to: number) => {
|
|
30
|
+
arrayMove(items, from, to);
|
|
31
|
+
};
|
|
27
32
|
|
|
28
33
|
return (
|
|
29
|
-
<List.Root<TestItemType> dragPreview items={items} {...props}>
|
|
34
|
+
<List.Root<TestItemType> dragPreview items={items} getId={(item) => item.id} onMove={handleMove} {...props}>
|
|
30
35
|
{({ items }) => (
|
|
31
36
|
<>
|
|
32
37
|
<div className='flex flex-col w-full'>
|
|
@@ -36,7 +41,7 @@ const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
|
36
41
|
</div>
|
|
37
42
|
|
|
38
43
|
<div role='list' className='w-full h-full overflow-auto'>
|
|
39
|
-
{items
|
|
44
|
+
{items?.map((item) => (
|
|
40
45
|
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
|
|
41
46
|
<List.ItemDragHandle />
|
|
42
47
|
<List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
|
|
@@ -47,7 +52,7 @@ const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
|
47
52
|
|
|
48
53
|
<div role='none' className={grid}>
|
|
49
54
|
<div />
|
|
50
|
-
<div className='flex items-center text-sm'>{items
|
|
55
|
+
<div className='flex items-center text-sm'>{items?.length} Items</div>
|
|
51
56
|
</div>
|
|
52
57
|
</div>
|
|
53
58
|
|
|
@@ -70,7 +75,7 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
|
70
75
|
<List.Root<TestItemType> dragPreview items={items} {...props}>
|
|
71
76
|
{({ items }) => (
|
|
72
77
|
<div role='list' className='w-full h-full overflow-auto'>
|
|
73
|
-
{items
|
|
78
|
+
{items?.map((item) => (
|
|
74
79
|
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
|
|
75
80
|
<List.ItemDragHandle />
|
|
76
81
|
<List.ItemTitle>{item.name}</List.ItemTitle>
|
|
@@ -83,26 +88,27 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
|
|
|
83
88
|
);
|
|
84
89
|
};
|
|
85
90
|
|
|
86
|
-
export default {
|
|
87
|
-
// TODO(burdon): Storybook collides with react-ui/List.
|
|
88
|
-
title: 'react-ui-list/List',
|
|
89
|
-
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
90
|
-
render: Story,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
91
|
const list = create(createList(100));
|
|
94
92
|
|
|
95
|
-
export const Default = {
|
|
93
|
+
export const Default: StoryObj<ListRootProps<TestItemType>> = {
|
|
94
|
+
render: DefaultStory,
|
|
96
95
|
args: {
|
|
97
96
|
items: list.items,
|
|
98
97
|
isItem: S.is(TestItemSchema),
|
|
99
|
-
}
|
|
100
|
-
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
101
100
|
|
|
102
|
-
export const Simple = {
|
|
101
|
+
export const Simple: StoryObj<ListRootProps<TestItemType>> = {
|
|
103
102
|
render: SimpleStory,
|
|
104
103
|
args: {
|
|
105
104
|
items: list.items,
|
|
106
105
|
isItem: S.is(TestItemSchema),
|
|
107
|
-
}
|
|
108
|
-
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const meta: Meta = {
|
|
110
|
+
title: 'ui/react-ui-list/List',
|
|
111
|
+
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default meta;
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
6
6
|
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
7
7
|
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
type Edge,
|
|
10
|
+
attachClosestEdge,
|
|
11
|
+
extractClosestEdge,
|
|
12
|
+
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
10
13
|
import { createContext } from '@radix-ui/react-context';
|
|
11
14
|
import React, {
|
|
12
15
|
type ComponentProps,
|
|
@@ -22,14 +25,13 @@ import React, {
|
|
|
22
25
|
import { createPortal } from 'react-dom';
|
|
23
26
|
|
|
24
27
|
import { invariant } from '@dxos/invariant';
|
|
25
|
-
import { type ThemedClassName } from '@dxos/react-ui';
|
|
26
|
-
import { Icon } from '@dxos/react-ui';
|
|
28
|
+
import { Icon, type ThemedClassName } from '@dxos/react-ui';
|
|
27
29
|
import { mx } from '@dxos/react-ui-theme';
|
|
28
30
|
|
|
29
31
|
import { DropIndicator } from './DropIndicator';
|
|
30
32
|
import { useListContext } from './ListRoot';
|
|
31
33
|
|
|
32
|
-
export type ListItemRecord = {
|
|
34
|
+
export type ListItemRecord = {};
|
|
33
35
|
|
|
34
36
|
export type ItemState =
|
|
35
37
|
| {
|
|
@@ -163,7 +165,7 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item
|
|
|
163
165
|
return (
|
|
164
166
|
<ListItemProvider item={item} dragHandleRef={dragHandleRef}>
|
|
165
167
|
<div className='relative'>
|
|
166
|
-
<div ref={ref} role='listitem' className={mx('flex', classNames, stateStyles[state.type])}>
|
|
168
|
+
<div ref={ref} role='listitem' className={mx('flex overflow-hidden', classNames, stateStyles[state.type])}>
|
|
167
169
|
{children}
|
|
168
170
|
</div>
|
|
169
171
|
{state.type === 'is-dragging-over' && state.closestEdge && <DropIndicator edge={state.closestEdge} />}
|
|
@@ -207,7 +209,7 @@ export const ListItemDeleteButton = ({
|
|
|
207
209
|
|
|
208
210
|
export const ListItemDragHandle = () => {
|
|
209
211
|
const { dragHandleRef } = useListItemContext('DRAG_HANDLE');
|
|
210
|
-
return <IconButton ref={dragHandleRef as any} icon='ph--dots-six--regular' />;
|
|
212
|
+
return <IconButton ref={dragHandleRef as any} icon='ph--dots-six-vertical--regular' />;
|
|
211
213
|
};
|
|
212
214
|
|
|
213
215
|
export const ListItemDragPreview = <T extends ListItemRecord>({
|
|
@@ -220,7 +222,7 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
|
|
|
220
222
|
};
|
|
221
223
|
|
|
222
224
|
export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
|
|
223
|
-
<div className={mx('flex
|
|
225
|
+
<div className={mx('flex is-full gap-2', classNames)}>{children}</div>
|
|
224
226
|
);
|
|
225
227
|
|
|
226
228
|
export const ListItemTitle = ({
|
|
@@ -228,7 +230,7 @@ export const ListItemTitle = ({
|
|
|
228
230
|
children,
|
|
229
231
|
...props
|
|
230
232
|
}: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
|
|
231
|
-
<div className={mx('flex
|
|
233
|
+
<div className={mx('flex grow items-center truncate', classNames)} {...props}>
|
|
232
234
|
{children}
|
|
233
235
|
</div>
|
|
234
236
|
);
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
6
|
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
7
|
-
import {
|
|
7
|
+
import { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index';
|
|
8
8
|
import { createContext } from '@radix-ui/react-context';
|
|
9
|
-
import React, { type ReactNode, useEffect, useState } from 'react';
|
|
10
|
-
import { flushSync } from 'react-dom';
|
|
9
|
+
import React, { type ReactNode, useCallback, useEffect, useState } from 'react';
|
|
11
10
|
|
|
12
|
-
import { type ThemedClassName
|
|
11
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { idle, type ItemState, type ListItemRecord } from './ListItem';
|
|
15
14
|
|
|
16
15
|
type ListContext<T extends ListItemRecord> = {
|
|
17
16
|
isItem: (item: any) => boolean;
|
|
17
|
+
getId?: (item: T) => string; // TODO(burdon): Require if T doesn't conform to type.
|
|
18
18
|
dragPreview?: boolean;
|
|
19
19
|
state: ItemState & { item?: T };
|
|
20
20
|
setState: (state: ItemState & { item?: T }) => void;
|
|
@@ -32,19 +32,44 @@ export type ListRendererProps<T extends ListItemRecord> = {
|
|
|
32
32
|
export type ListRootProps<T extends ListItemRecord> = ThemedClassName<{
|
|
33
33
|
children?: (props: ListRendererProps<T>) => ReactNode;
|
|
34
34
|
items?: T[];
|
|
35
|
+
onMove?: (fromIndex: number, toIndex: number) => void;
|
|
35
36
|
}> &
|
|
36
|
-
Pick<ListContext<T>, 'isItem' | 'dragPreview'>;
|
|
37
|
+
Pick<ListContext<T>, 'isItem' | 'getId' | 'dragPreview'>;
|
|
38
|
+
|
|
39
|
+
const defaultGetId = <T extends ListItemRecord>(item: T) => (item as any)?.id;
|
|
37
40
|
|
|
38
41
|
export const ListRoot = <T extends ListItemRecord>({
|
|
39
42
|
classNames,
|
|
40
43
|
children,
|
|
41
|
-
items
|
|
44
|
+
items,
|
|
42
45
|
isItem,
|
|
46
|
+
getId = defaultGetId,
|
|
47
|
+
onMove,
|
|
43
48
|
...props
|
|
44
49
|
}: ListRootProps<T>) => {
|
|
45
|
-
const
|
|
50
|
+
const isEqual = useCallback(
|
|
51
|
+
(a: T, b: T) => {
|
|
52
|
+
const idA = getId?.(a);
|
|
53
|
+
const idB = getId?.(b);
|
|
54
|
+
|
|
55
|
+
if (idA !== undefined && idB !== undefined) {
|
|
56
|
+
return idA === idB;
|
|
57
|
+
} else {
|
|
58
|
+
// Fallback for primitive values or when getId fails.
|
|
59
|
+
// NOTE(ZaymonFC): After drag and drop, pragmatic internally serializes drop targets which breaks reference equality.
|
|
60
|
+
// You must provide an `getId` function that returns a stable identifier for your items.
|
|
61
|
+
return a === b;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[getId],
|
|
65
|
+
);
|
|
66
|
+
|
|
46
67
|
const [state, setState] = useState<ListContext<T>['state']>(idle);
|
|
47
68
|
useEffect(() => {
|
|
69
|
+
if (!items) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
return monitorForElements({
|
|
49
74
|
canMonitor: ({ source }) => isItem(source.data),
|
|
50
75
|
onDrop: ({ location, source }) => {
|
|
@@ -55,31 +80,30 @@ export const ListRoot = <T extends ListItemRecord>({
|
|
|
55
80
|
|
|
56
81
|
const sourceData = source.data;
|
|
57
82
|
const targetData = target.data;
|
|
83
|
+
|
|
58
84
|
if (!isItem(sourceData) || !isItem(targetData)) {
|
|
59
85
|
return;
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
const sourceIdx = items.findIndex((item) => item
|
|
63
|
-
const targetIdx = items.findIndex((item) => item
|
|
88
|
+
const sourceIdx = items.findIndex((item) => isEqual(item, sourceData as T));
|
|
89
|
+
const targetIdx = items.findIndex((item) => isEqual(item, targetData as T));
|
|
64
90
|
if (targetIdx < 0 || sourceIdx < 0) {
|
|
65
91
|
return;
|
|
66
92
|
}
|
|
67
|
-
|
|
68
93
|
const closestEdgeOfTarget = extractClosestEdge(targetData);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
indexOfTarget: targetIdx,
|
|
75
|
-
axis: 'vertical',
|
|
76
|
-
closestEdgeOfTarget,
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
94
|
+
const destinationIndex = getReorderDestinationIndex({
|
|
95
|
+
closestEdgeOfTarget,
|
|
96
|
+
startIndex: sourceIdx,
|
|
97
|
+
indexOfTarget: targetIdx,
|
|
98
|
+
axis: 'vertical',
|
|
79
99
|
});
|
|
100
|
+
|
|
101
|
+
onMove?.(sourceIdx, destinationIndex);
|
|
80
102
|
},
|
|
81
103
|
});
|
|
82
|
-
}, [items]);
|
|
104
|
+
}, [items, isEqual, onMove]);
|
|
83
105
|
|
|
84
|
-
return
|
|
106
|
+
return (
|
|
107
|
+
<ListProvider {...{ isItem, state, setState, ...props }}>{children?.({ state, items: items ?? [] })}</ListProvider>
|
|
108
|
+
);
|
|
85
109
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
6
|
+
import React, { type HTMLAttributes, type CSSProperties } from 'react';
|
|
7
|
+
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
// Tree item hitbox
|
|
11
|
+
// https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx#tree-item
|
|
12
|
+
|
|
13
|
+
export type DropIndicatorProps = {
|
|
14
|
+
instruction: Instruction;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type InstructionType = Exclude<Instruction, { type: 'instruction-blocked' }>['type'];
|
|
18
|
+
type Orientation = 'sibling' | 'child';
|
|
19
|
+
|
|
20
|
+
const edgeToOrientationMap: Record<InstructionType, Orientation> = {
|
|
21
|
+
'reorder-above': 'sibling',
|
|
22
|
+
'reorder-below': 'sibling',
|
|
23
|
+
'make-child': 'child',
|
|
24
|
+
reparent: 'child',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const orientationStyles: Record<Orientation, HTMLAttributes<HTMLElement>['className']> = {
|
|
28
|
+
// TODO(wittjosiah): Stop using left/right here.
|
|
29
|
+
sibling:
|
|
30
|
+
'bs-[--line-thickness] left-[--horizontal-indent] right-0 bg-accentSurface before:left-[--negative-terminal-size]',
|
|
31
|
+
child: 'is-full block-start-0 block-end-0 border-[length:--line-thickness] before:invisible',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const instructionStyles: Record<InstructionType, HTMLAttributes<HTMLElement>['className']> = {
|
|
35
|
+
'reorder-above': 'block-start-[--line-offset] before:block-start-[--offset-terminal]',
|
|
36
|
+
'reorder-below': 'block-end-[--line-offset] before:block-end-[--offset-terminal]',
|
|
37
|
+
'make-child': 'border-accentSurface',
|
|
38
|
+
// TODO(wittjosiah): This is not occurring in the current implementation.
|
|
39
|
+
reparent: '',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const strokeSize = 2;
|
|
43
|
+
const terminalSize = 8;
|
|
44
|
+
const offsetToAlignTerminalWithLine = (strokeSize - terminalSize) / 2;
|
|
45
|
+
const gap = '0px';
|
|
46
|
+
|
|
47
|
+
export const DropIndicator = ({ instruction }: DropIndicatorProps) => {
|
|
48
|
+
const lineOffset = `calc(-0.5 * (${gap} + ${strokeSize}px))`;
|
|
49
|
+
const isBlocked = instruction.type === 'instruction-blocked';
|
|
50
|
+
const desiredInstruction = isBlocked ? instruction.desired : instruction;
|
|
51
|
+
const orientation = edgeToOrientationMap[desiredInstruction.type];
|
|
52
|
+
|
|
53
|
+
if (isBlocked) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
style={
|
|
60
|
+
{
|
|
61
|
+
'--line-thickness': `${strokeSize}px`,
|
|
62
|
+
'--line-offset': `${lineOffset}`,
|
|
63
|
+
'--terminal-size': `${terminalSize}px`,
|
|
64
|
+
'--terminal-radius': `${terminalSize / 2}px`,
|
|
65
|
+
'--negative-terminal-size': `-${terminalSize}px`,
|
|
66
|
+
'--offset-terminal': `${offsetToAlignTerminalWithLine}px`,
|
|
67
|
+
'--horizontal-indent': `${desiredInstruction.currentLevel * desiredInstruction.indentPerLevel + 4}px`,
|
|
68
|
+
} as CSSProperties
|
|
69
|
+
}
|
|
70
|
+
className={mx(
|
|
71
|
+
'absolute z-10 pointer-events-none',
|
|
72
|
+
'before:is-[--terminal-size] before:bs-[--terminal-size] box-border before:absolute',
|
|
73
|
+
'before:border-[length:--line-thickness] before:border-solid before:border-accentSurface before:rounded-full',
|
|
74
|
+
orientationStyles[orientation],
|
|
75
|
+
instructionStyles[desiredInstruction.type],
|
|
76
|
+
)}
|
|
77
|
+
></div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
8
|
+
import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
9
|
+
import { type StoryObj, type Meta } from '@storybook/react';
|
|
10
|
+
import React, { useEffect } from 'react';
|
|
11
|
+
|
|
12
|
+
import { create } from '@dxos/echo-schema';
|
|
13
|
+
import { faker } from '@dxos/random';
|
|
14
|
+
import { Icon } from '@dxos/react-ui';
|
|
15
|
+
import { Path } from '@dxos/react-ui-mosaic';
|
|
16
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
17
|
+
|
|
18
|
+
import { Tree, type TreeProps } from './Tree';
|
|
19
|
+
import { createTree, flattenTree, getItem, updateState, type TestItem } from './testing';
|
|
20
|
+
import { isItem, type ItemType } from './types';
|
|
21
|
+
|
|
22
|
+
faker.seed(1234);
|
|
23
|
+
|
|
24
|
+
type State = {
|
|
25
|
+
tree: TestItem;
|
|
26
|
+
open: string[];
|
|
27
|
+
current: string[];
|
|
28
|
+
flatTree: ItemType[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const state = create<State>({
|
|
32
|
+
tree: createTree(),
|
|
33
|
+
open: [],
|
|
34
|
+
current: [],
|
|
35
|
+
get flatTree() {
|
|
36
|
+
return flattenTree(this.tree, this.open, getItem);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const Story = (args: Partial<TreeProps>) => {
|
|
41
|
+
// NOTE: If passed directly to args, this won't be reactive.
|
|
42
|
+
const items = state.flatTree;
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
return monitorForElements({
|
|
46
|
+
canMonitor: ({ source }) => isItem(source.data),
|
|
47
|
+
onDrop: ({ location, source }) => {
|
|
48
|
+
// Didn't drop on anything.
|
|
49
|
+
if (!location.current.dropTargets.length) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const target = location.current.dropTargets[0];
|
|
54
|
+
|
|
55
|
+
const instruction: Instruction | null = extractInstruction(target.data);
|
|
56
|
+
if (instruction !== null) {
|
|
57
|
+
updateState({
|
|
58
|
+
state: state.tree,
|
|
59
|
+
instruction,
|
|
60
|
+
source: source.data as ItemType,
|
|
61
|
+
target: target.data as ItemType,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return <Tree items={items} open={state.open} current={state.current} {...args} />;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const meta: Meta<typeof Tree> = {
|
|
72
|
+
title: 'ui/react-ui-list/Tree',
|
|
73
|
+
component: Tree,
|
|
74
|
+
render: Story,
|
|
75
|
+
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
76
|
+
args: {
|
|
77
|
+
renderColumns: () => {
|
|
78
|
+
return (
|
|
79
|
+
<div className='flex items-center'>
|
|
80
|
+
<Icon icon='ph--placeholder--regular' size={5} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
onOpenChange: (item: ItemType, open: boolean) => {
|
|
85
|
+
const path = Path.create(...item.path);
|
|
86
|
+
if (open) {
|
|
87
|
+
state.open.push(path);
|
|
88
|
+
} else {
|
|
89
|
+
const index = state.open.indexOf(path);
|
|
90
|
+
if (index > -1) {
|
|
91
|
+
state.open.splice(index, 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onSelect: (item: ItemType, current: boolean) => {
|
|
96
|
+
if (current) {
|
|
97
|
+
state.current.push(item.id);
|
|
98
|
+
} else {
|
|
99
|
+
const index = state.current.indexOf(item.id);
|
|
100
|
+
if (index > -1) {
|
|
101
|
+
state.current.splice(index, 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default meta;
|
|
109
|
+
|
|
110
|
+
export const Default = {};
|
|
111
|
+
|
|
112
|
+
export const Draggable: StoryObj<typeof Tree> = {
|
|
113
|
+
args: {
|
|
114
|
+
draggable: true,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Treegrid, type TreegridRootProps } from '@dxos/react-ui';
|
|
8
|
+
import { Path } from '@dxos/react-ui-mosaic';
|
|
9
|
+
|
|
10
|
+
import { TreeItem, type TreeItemProps } from './TreeItem';
|
|
11
|
+
import { getMode } from './helpers';
|
|
12
|
+
import { type ItemType } from './types';
|
|
13
|
+
|
|
14
|
+
export type TreeProps<T extends ItemType = ItemType> = {
|
|
15
|
+
items: T[];
|
|
16
|
+
open: string[];
|
|
17
|
+
current: string[];
|
|
18
|
+
} & Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
|
|
19
|
+
Pick<TreeItemProps<T>, 'draggable' | 'renderColumns' | 'canDrop' | 'onOpenChange' | 'onSelect'>;
|
|
20
|
+
|
|
21
|
+
export const Tree = <T extends ItemType = ItemType>({
|
|
22
|
+
items,
|
|
23
|
+
open,
|
|
24
|
+
current,
|
|
25
|
+
draggable = false,
|
|
26
|
+
gridTemplateColumns = '[tree-row-start] 1fr min-content [tree-row-end]',
|
|
27
|
+
classNames,
|
|
28
|
+
renderColumns,
|
|
29
|
+
canDrop,
|
|
30
|
+
onOpenChange,
|
|
31
|
+
onSelect,
|
|
32
|
+
}: TreeProps<T>) => {
|
|
33
|
+
return (
|
|
34
|
+
<Treegrid.Root gridTemplateColumns={gridTemplateColumns} classNames={classNames}>
|
|
35
|
+
{items.map((item, i) => {
|
|
36
|
+
const path = Path.create(...item.path);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<TreeItem<T>
|
|
40
|
+
key={item.id}
|
|
41
|
+
item={item}
|
|
42
|
+
mode={getMode(items, i)}
|
|
43
|
+
open={open.includes(path)}
|
|
44
|
+
// TODO(wittjosiah): This should also be path-based.
|
|
45
|
+
current={current.includes(item.id)}
|
|
46
|
+
draggable={draggable}
|
|
47
|
+
renderColumns={renderColumns}
|
|
48
|
+
canDrop={canDrop}
|
|
49
|
+
onOpenChange={onOpenChange}
|
|
50
|
+
onSelect={onSelect}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</Treegrid.Root>
|
|
55
|
+
);
|
|
56
|
+
};
|