@dxos/react-ui-list 0.8.4-main.84f28bd → 0.8.4-main.937b3ca
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 +657 -712
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +657 -712
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Accordion/Accordion.stories.d.ts +7 -4
- package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
- package/dist/types/src/components/Accordion/AccordionItem.d.ts +1 -1
- package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/types/src/components/List/List.d.ts +8 -8
- package/dist/types/src/components/List/List.d.ts.map +1 -1
- package/dist/types/src/components/List/List.stories.d.ts +14 -5
- package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
- package/dist/types/src/components/List/ListItem.d.ts +4 -7
- package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
- package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
- package/dist/types/src/components/List/testing.d.ts +1 -1
- package/dist/types/src/components/List/testing.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 +18 -7
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeContext.d.ts +7 -4
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts +24 -10
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
- package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing.d.ts +3 -3
- package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +31 -28
- package/src/components/Accordion/Accordion.stories.tsx +7 -9
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/Accordion/AccordionItem.tsx +7 -4
- package/src/components/Accordion/AccordionRoot.tsx +1 -1
- package/src/components/List/List.stories.tsx +41 -27
- package/src/components/List/List.tsx +2 -5
- package/src/components/List/ListItem.tsx +40 -28
- package/src/components/List/ListRoot.tsx +1 -1
- package/src/components/List/testing.ts +3 -3
- package/src/components/Tree/Tree.stories.tsx +101 -84
- package/src/components/Tree/Tree.tsx +22 -9
- package/src/components/Tree/TreeContext.tsx +7 -4
- package/src/components/Tree/TreeItem.tsx +66 -54
- package/src/components/Tree/TreeItemHeading.tsx +11 -9
- package/src/components/Tree/TreeItemToggle.tsx +29 -19
- package/src/components/Tree/testing.ts +5 -4
|
@@ -2,115 +2,132 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
8
|
-
import {
|
|
6
|
+
import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
7
|
+
import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
9
8
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
10
|
-
import React, { useEffect } from 'react';
|
|
9
|
+
import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
11
10
|
|
|
12
|
-
import { live, type Live } from '@dxos/live-object';
|
|
13
11
|
import { faker } from '@dxos/random';
|
|
14
12
|
import { Icon } from '@dxos/react-ui';
|
|
15
|
-
import {
|
|
13
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
14
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
15
|
+
|
|
16
|
+
import { Path } from '../../util';
|
|
16
17
|
|
|
18
|
+
import { type TestItem, createTree, updateState } from './testing';
|
|
17
19
|
import { Tree } from './Tree';
|
|
18
20
|
import { type TreeData } from './TreeItem';
|
|
19
|
-
import { createTree, updateState, type TestItem } from './testing';
|
|
20
|
-
import { Path } from '../../util';
|
|
21
21
|
|
|
22
22
|
faker.seed(1234);
|
|
23
23
|
|
|
24
|
-
const tree =
|
|
25
|
-
const state = new Map<string, Live<{ open: boolean; current: boolean }>>();
|
|
24
|
+
const tree = createTree() as TestItem;
|
|
26
25
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const target = location.current.dropTargets[0];
|
|
42
|
-
|
|
43
|
-
const instruction: Instruction | null = extractInstruction(target.data);
|
|
44
|
-
if (instruction !== null) {
|
|
45
|
-
updateState({
|
|
46
|
-
state: tree,
|
|
47
|
-
instruction,
|
|
48
|
-
source: source.data as TreeData,
|
|
49
|
-
target: target.data as TreeData,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
}, []);
|
|
55
|
-
|
|
56
|
-
return <Tree {...args} />;
|
|
57
|
-
},
|
|
58
|
-
args: {
|
|
59
|
-
id: tree.id,
|
|
60
|
-
useItems: (testItem?: TestItem) => {
|
|
61
|
-
return testItem?.items ?? tree.items;
|
|
62
|
-
},
|
|
63
|
-
getProps: (testItem: TestItem) => ({
|
|
64
|
-
id: testItem.id,
|
|
65
|
-
label: testItem.name,
|
|
66
|
-
icon: testItem.icon,
|
|
67
|
-
...((testItem.items?.length ?? 0) > 0 && {
|
|
68
|
-
parentOf: testItem.items!.map(({ id }) => id),
|
|
69
|
-
}),
|
|
70
|
-
}),
|
|
71
|
-
isOpen: (_path: string[]) => {
|
|
72
|
-
const path = Path.create(..._path);
|
|
73
|
-
const object = state.get(path) ?? live({ open: false, current: false });
|
|
74
|
-
if (!state.has(path)) {
|
|
75
|
-
state.set(path, object);
|
|
76
|
-
}
|
|
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
|
+
}, []);
|
|
77
38
|
|
|
78
|
-
|
|
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);
|
|
79
44
|
},
|
|
80
|
-
|
|
45
|
+
[getOrCreateStateAtom],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const handleOpenChange = useCallback(
|
|
49
|
+
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
81
50
|
const path = Path.create(..._path);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
51
|
+
const atom = getOrCreateStateAtom(path);
|
|
52
|
+
const prev = registry.get(atom);
|
|
53
|
+
registry.set(atom, { ...prev, open });
|
|
54
|
+
},
|
|
55
|
+
[getOrCreateStateAtom, registry],
|
|
56
|
+
);
|
|
86
57
|
|
|
87
|
-
|
|
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 });
|
|
88
64
|
},
|
|
89
|
-
|
|
90
|
-
|
|
65
|
+
[getOrCreateStateAtom, registry],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return monitorForElements({
|
|
70
|
+
canMonitor: ({ source }) => typeof source.data.id === 'string' && Array.isArray(source.data.path),
|
|
71
|
+
onDrop: ({ location, source }) => {
|
|
72
|
+
if (!location.current.dropTargets.length) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const target = location.current.dropTargets[0];
|
|
77
|
+
const instruction: Instruction | null = extractInstruction(target.data);
|
|
78
|
+
if (instruction !== null) {
|
|
79
|
+
updateState({
|
|
80
|
+
state: tree,
|
|
81
|
+
instruction,
|
|
82
|
+
source: source.data as TreeData,
|
|
83
|
+
target: target.data as TreeData,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
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={() => (
|
|
91
106
|
<div className='flex items-center'>
|
|
92
107
|
<Icon icon='ph--placeholder--regular' size={5} />
|
|
93
108
|
</div>
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
object!.open = open;
|
|
100
|
-
},
|
|
101
|
-
onSelect: ({ path: _path, current }) => {
|
|
102
|
-
const path = Path.create(..._path);
|
|
103
|
-
const object = state.get(path);
|
|
104
|
-
object!.current = current;
|
|
105
|
-
},
|
|
106
|
-
},
|
|
109
|
+
)}
|
|
110
|
+
onOpenChange={handleOpenChange}
|
|
111
|
+
onSelect={handleSelect}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
107
114
|
};
|
|
108
115
|
|
|
116
|
+
const meta = {
|
|
117
|
+
title: 'ui/react-ui-list/Tree',
|
|
118
|
+
|
|
119
|
+
decorators: [withTheme, withRegistry],
|
|
120
|
+
component: Tree,
|
|
121
|
+
render: DefaultStory,
|
|
122
|
+
} satisfies Meta<typeof Tree<TestItem>>;
|
|
123
|
+
|
|
109
124
|
export default meta;
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
type Story = StoryObj<typeof DefaultStory>;
|
|
127
|
+
|
|
128
|
+
export const Default: Story = {};
|
|
112
129
|
|
|
113
|
-
export const Draggable:
|
|
130
|
+
export const Draggable: Story = {
|
|
114
131
|
args: {
|
|
115
132
|
draggable: true,
|
|
116
133
|
},
|
|
@@ -4,34 +4,45 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useMemo } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type HasId } from '@dxos/echo-schema';
|
|
8
7
|
import { Treegrid, type TreegridRootProps } from '@dxos/react-ui';
|
|
9
8
|
|
|
10
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;
|
|
17
16
|
} & TreeContextType<T, O> &
|
|
18
17
|
Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
|
|
19
|
-
Pick<
|
|
18
|
+
Pick<
|
|
19
|
+
TreeItemProps<T>,
|
|
20
|
+
| 'draggable'
|
|
21
|
+
| 'renderColumns'
|
|
22
|
+
| 'blockInstruction'
|
|
23
|
+
| 'canDrop'
|
|
24
|
+
| 'canSelect'
|
|
25
|
+
| 'onOpenChange'
|
|
26
|
+
| 'onSelect'
|
|
27
|
+
| 'levelOffset'
|
|
28
|
+
>;
|
|
20
29
|
|
|
21
|
-
export const Tree = <T extends
|
|
30
|
+
export const Tree = <T extends { id: string } = any, O = any>({
|
|
22
31
|
root,
|
|
23
32
|
path,
|
|
24
33
|
id,
|
|
25
34
|
useItems,
|
|
26
35
|
getProps,
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
useIsOpen,
|
|
37
|
+
useIsCurrent,
|
|
29
38
|
draggable = false,
|
|
30
39
|
gridTemplateColumns = '[tree-row-start] 1fr min-content [tree-row-end]',
|
|
31
40
|
classNames,
|
|
32
41
|
levelOffset,
|
|
33
42
|
renderColumns,
|
|
43
|
+
blockInstruction,
|
|
34
44
|
canDrop,
|
|
45
|
+
canSelect,
|
|
35
46
|
onOpenChange,
|
|
36
47
|
onSelect,
|
|
37
48
|
}: TreeProps<T, O>) => {
|
|
@@ -39,10 +50,10 @@ export const Tree = <T extends HasId = any, O = any>({
|
|
|
39
50
|
() => ({
|
|
40
51
|
useItems,
|
|
41
52
|
getProps,
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
useIsOpen,
|
|
54
|
+
useIsCurrent,
|
|
44
55
|
}),
|
|
45
|
-
[useItems, getProps,
|
|
56
|
+
[useItems, getProps, useIsOpen, useIsCurrent],
|
|
46
57
|
);
|
|
47
58
|
const items = useItems(root);
|
|
48
59
|
const treePath = useMemo(() => (path ? [...path, id] : [id]), [id, path]);
|
|
@@ -59,7 +70,9 @@ export const Tree = <T extends HasId = any, O = any>({
|
|
|
59
70
|
levelOffset={levelOffset}
|
|
60
71
|
draggable={draggable}
|
|
61
72
|
renderColumns={renderColumns}
|
|
73
|
+
blockInstruction={blockInstruction}
|
|
62
74
|
canDrop={canDrop}
|
|
75
|
+
canSelect={canSelect}
|
|
63
76
|
onOpenChange={onOpenChange}
|
|
64
77
|
onSelect={onSelect}
|
|
65
78
|
/>
|
|
@@ -11,18 +11,21 @@ export type TreeItemDataProps = {
|
|
|
11
11
|
id: string;
|
|
12
12
|
label: Label;
|
|
13
13
|
parentOf?: string[];
|
|
14
|
-
icon?: string;
|
|
15
|
-
disabled?: boolean;
|
|
16
14
|
className?: string;
|
|
17
15
|
headingClassName?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconHue?: string;
|
|
18
|
+
disabled?: boolean;
|
|
18
19
|
testId?: string;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export type TreeContextType<T = any, O = any> = {
|
|
22
23
|
useItems: (parent?: T, options?: O) => T[];
|
|
23
24
|
getProps: (item: T, parent: string[]) => TreeItemDataProps;
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
const TreeContext = createContext<null | TreeContextType>(null);
|
|
@@ -5,35 +5,34 @@
|
|
|
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 {
|
|
8
|
-
attachInstruction,
|
|
9
|
-
extractInstruction,
|
|
10
8
|
type Instruction,
|
|
11
9
|
type ItemMode,
|
|
10
|
+
attachInstruction,
|
|
11
|
+
extractInstruction,
|
|
12
12
|
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
13
|
-
import
|
|
14
|
-
import React, { memo, useCallback, useEffect, useMemo, useRef, useState
|
|
13
|
+
import * as Schema from 'effect/Schema';
|
|
14
|
+
import React, { type FC, type KeyboardEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
|
|
16
|
-
import { type HasId } from '@dxos/echo-schema';
|
|
17
16
|
import { invariant } from '@dxos/invariant';
|
|
18
|
-
import {
|
|
17
|
+
import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
|
|
19
18
|
import {
|
|
19
|
+
ghostFocusWithin,
|
|
20
20
|
ghostHover,
|
|
21
21
|
hoverableControls,
|
|
22
22
|
hoverableFocusedKeyboardControls,
|
|
23
23
|
hoverableFocusedWithinControls,
|
|
24
|
-
|
|
25
|
-
} from '@dxos/react-ui-theme';
|
|
24
|
+
} from '@dxos/ui-theme';
|
|
26
25
|
|
|
26
|
+
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
27
27
|
import { useTree } from './TreeContext';
|
|
28
28
|
import { TreeItemHeading } from './TreeItemHeading';
|
|
29
29
|
import { TreeItemToggle } from './TreeItemToggle';
|
|
30
|
-
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
31
|
-
|
|
32
|
-
type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
|
|
33
30
|
|
|
34
31
|
const hoverableDescriptionIcons =
|
|
35
32
|
'[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
|
|
36
33
|
|
|
34
|
+
type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
|
|
35
|
+
|
|
37
36
|
export const TreeDataSchema = Schema.Struct({
|
|
38
37
|
id: Schema.String,
|
|
39
38
|
path: Schema.Array(Schema.String),
|
|
@@ -41,37 +40,42 @@ export const TreeDataSchema = Schema.Struct({
|
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
|
|
44
|
-
|
|
45
43
|
export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
|
|
46
44
|
|
|
47
|
-
export type
|
|
45
|
+
export type ColumnRenderer<T extends { id: string } = any> = FC<{
|
|
46
|
+
item: T;
|
|
47
|
+
path: string[];
|
|
48
|
+
open: boolean;
|
|
49
|
+
menuOpen: boolean;
|
|
50
|
+
setMenuOpen: (open: boolean) => void;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
export type TreeItemProps<T extends { id: string } = any> = {
|
|
48
54
|
item: T;
|
|
49
55
|
path: string[];
|
|
50
56
|
levelOffset?: number;
|
|
51
57
|
last: boolean;
|
|
52
58
|
draggable?: boolean;
|
|
53
|
-
renderColumns?:
|
|
54
|
-
|
|
55
|
-
path: string[];
|
|
56
|
-
open: boolean;
|
|
57
|
-
menuOpen: boolean;
|
|
58
|
-
setMenuOpen: (open: boolean) => void;
|
|
59
|
-
}>;
|
|
59
|
+
renderColumns?: ColumnRenderer<T>;
|
|
60
|
+
blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
|
|
60
61
|
canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
|
|
62
|
+
canSelect?: (params: { item: T; path: string[] }) => boolean;
|
|
61
63
|
onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
|
|
62
64
|
onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
|
|
63
65
|
};
|
|
64
66
|
|
|
65
|
-
const RawTreeItem = <T extends
|
|
67
|
+
const RawTreeItem = <T extends { id: string } = any>({
|
|
66
68
|
item,
|
|
67
69
|
path: _path,
|
|
70
|
+
levelOffset = 2,
|
|
68
71
|
last,
|
|
69
72
|
draggable: _draggable,
|
|
70
73
|
renderColumns: Columns,
|
|
74
|
+
blockInstruction,
|
|
71
75
|
canDrop,
|
|
76
|
+
canSelect,
|
|
72
77
|
onOpenChange,
|
|
73
78
|
onSelect,
|
|
74
|
-
levelOffset = 2,
|
|
75
79
|
}: TreeItemProps<T>) => {
|
|
76
80
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
|
77
81
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
@@ -81,15 +85,16 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
81
85
|
const [instruction, setInstruction] = useState<Instruction | null>(null);
|
|
82
86
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
83
87
|
|
|
84
|
-
const { useItems, getProps,
|
|
88
|
+
const { useItems, getProps, useIsOpen, useIsCurrent } = useTree();
|
|
85
89
|
const items = useItems(item);
|
|
86
|
-
const { id, label,
|
|
90
|
+
const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = getProps(item, _path);
|
|
87
91
|
const path = useMemo(() => [..._path, id], [_path, id]);
|
|
88
|
-
const open =
|
|
89
|
-
const current =
|
|
92
|
+
const open = useIsOpen(path, item);
|
|
93
|
+
const current = useIsCurrent(path, item);
|
|
90
94
|
const level = path.length - levelOffset;
|
|
91
95
|
const isBranch = !!parentOf;
|
|
92
96
|
const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
|
|
97
|
+
const canSelectItem = canSelect?.({ item, path }) ?? true;
|
|
93
98
|
|
|
94
99
|
const cancelExpand = useCallback(() => {
|
|
95
100
|
if (cancelExpandRef.current) {
|
|
@@ -145,7 +150,11 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
145
150
|
},
|
|
146
151
|
getIsSticky: () => true,
|
|
147
152
|
onDrag: ({ self, source }) => {
|
|
148
|
-
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;
|
|
149
158
|
|
|
150
159
|
if (source.data.id !== id) {
|
|
151
160
|
if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
|
|
@@ -176,43 +185,42 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
176
185
|
},
|
|
177
186
|
}),
|
|
178
187
|
);
|
|
179
|
-
}, [_draggable, item, id, mode, path, open, canDrop]);
|
|
188
|
+
}, [_draggable, item, id, mode, path, open, blockInstruction, canDrop]);
|
|
180
189
|
|
|
181
190
|
// Cancel expand on unmount.
|
|
182
191
|
useEffect(() => () => cancelExpand(), [cancelExpand]);
|
|
183
192
|
|
|
184
|
-
const
|
|
193
|
+
const handleOpenToggle = useCallback(
|
|
185
194
|
() => onOpenChange?.({ item, path, open: !open }),
|
|
186
195
|
[onOpenChange, item, path, open],
|
|
187
196
|
);
|
|
188
197
|
|
|
189
198
|
const handleSelect = useCallback(
|
|
190
199
|
(option = false) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
// If the item is a branch, toggle it if:
|
|
201
|
+
// - also holding down the option key
|
|
202
|
+
// - or the item is currently selected
|
|
203
|
+
if (isBranch && (option || current)) {
|
|
204
|
+
handleOpenToggle();
|
|
205
|
+
} else if (canSelectItem) {
|
|
206
|
+
canSelect?.({ item, path });
|
|
194
207
|
rowRef.current?.focus();
|
|
195
208
|
onSelect?.({ item, path, current: !current, option });
|
|
196
209
|
}
|
|
197
210
|
},
|
|
198
|
-
[item, path, current, isBranch,
|
|
211
|
+
[item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
|
|
199
212
|
);
|
|
200
213
|
|
|
201
214
|
const handleKeyDown = useCallback(
|
|
202
215
|
(event: KeyboardEvent) => {
|
|
203
216
|
switch (event.key) {
|
|
204
217
|
case 'ArrowRight':
|
|
205
|
-
isBranch && !open && handleOpenChange();
|
|
206
|
-
break;
|
|
207
218
|
case 'ArrowLeft':
|
|
208
|
-
isBranch &&
|
|
209
|
-
break;
|
|
210
|
-
case ' ':
|
|
211
|
-
handleSelect(event.altKey);
|
|
219
|
+
isBranch && handleOpenToggle();
|
|
212
220
|
break;
|
|
213
221
|
}
|
|
214
222
|
},
|
|
215
|
-
[isBranch, open,
|
|
223
|
+
[isBranch, open, handleOpenToggle, handleSelect],
|
|
216
224
|
);
|
|
217
225
|
|
|
218
226
|
return (
|
|
@@ -223,16 +231,17 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
223
231
|
id={id}
|
|
224
232
|
aria-labelledby={`${id}__label`}
|
|
225
233
|
parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
|
|
226
|
-
classNames={
|
|
234
|
+
classNames={[
|
|
227
235
|
'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
|
|
228
236
|
hoverableControls,
|
|
229
237
|
hoverableFocusedKeyboardControls,
|
|
230
238
|
hoverableFocusedWithinControls,
|
|
231
239
|
hoverableDescriptionIcons,
|
|
232
240
|
ghostHover,
|
|
241
|
+
ghostFocusWithin,
|
|
233
242
|
className,
|
|
234
|
-
|
|
235
|
-
data-
|
|
243
|
+
]}
|
|
244
|
+
data-object-id={id}
|
|
236
245
|
data-testid={testId}
|
|
237
246
|
// NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
|
|
238
247
|
// without alerting the user (except for in the correct link element). See also:
|
|
@@ -244,26 +253,27 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
244
253
|
setMenuOpen(true);
|
|
245
254
|
}}
|
|
246
255
|
>
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
<div
|
|
257
|
+
role='none'
|
|
258
|
+
className='indent relative grid grid-cols-subgrid col-[tree-row]'
|
|
250
259
|
style={paddingIndentation(level)}
|
|
251
260
|
>
|
|
252
|
-
<
|
|
253
|
-
<TreeItemToggle isBranch={isBranch} open={open}
|
|
261
|
+
<Treegrid.Cell classNames='flex items-center'>
|
|
262
|
+
<TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
|
|
254
263
|
<TreeItemHeading
|
|
255
|
-
ref={buttonRef}
|
|
256
|
-
label={label}
|
|
257
|
-
icon={icon}
|
|
258
|
-
className={headingClassName}
|
|
259
264
|
disabled={disabled}
|
|
260
265
|
current={current}
|
|
266
|
+
label={label}
|
|
267
|
+
className={headingClassName}
|
|
268
|
+
icon={icon}
|
|
269
|
+
iconHue={iconHue}
|
|
261
270
|
onSelect={handleSelect}
|
|
271
|
+
ref={buttonRef}
|
|
262
272
|
/>
|
|
263
|
-
</
|
|
273
|
+
</Treegrid.Cell>
|
|
264
274
|
{Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
|
|
265
275
|
{instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
|
|
266
|
-
</
|
|
276
|
+
</div>
|
|
267
277
|
</Treegrid.Row>
|
|
268
278
|
{open &&
|
|
269
279
|
items.map((item, index) => (
|
|
@@ -274,7 +284,9 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
274
284
|
last={index === items.length - 1}
|
|
275
285
|
draggable={_draggable}
|
|
276
286
|
renderColumns={Columns}
|
|
287
|
+
blockInstruction={blockInstruction}
|
|
277
288
|
canDrop={canDrop}
|
|
289
|
+
canSelect={canSelect}
|
|
278
290
|
onOpenChange={onOpenChange}
|
|
279
291
|
onSelect={onSelect}
|
|
280
292
|
/>
|
|
@@ -4,25 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type KeyboardEvent, type MouseEvent, forwardRef, memo, useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import { Button, Icon, toLocalizedString, useTranslation
|
|
7
|
+
import { Button, Icon, type Label, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { TextTooltip } from '@dxos/react-ui-text-tooltip';
|
|
9
|
-
import {
|
|
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.
|
|
13
|
-
export type
|
|
13
|
+
export type TreeItemHeadingProps = {
|
|
14
14
|
label: Label;
|
|
15
|
-
icon?: string;
|
|
16
15
|
className?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconHue?: string;
|
|
17
18
|
disabled?: boolean;
|
|
18
19
|
current?: boolean;
|
|
19
20
|
onSelect?: (option: boolean) => void;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const TreeItemHeading = memo(
|
|
23
|
-
forwardRef<HTMLButtonElement,
|
|
24
|
-
({ label, icon,
|
|
24
|
+
forwardRef<HTMLButtonElement, TreeItemHeadingProps>(
|
|
25
|
+
({ label, className, icon, iconHue, disabled, current, onSelect }, forwardedRef) => {
|
|
25
26
|
const { t } = useTranslation();
|
|
27
|
+
const styles = iconHue ? getStyles(iconHue) : undefined;
|
|
26
28
|
|
|
27
29
|
const handleSelect = useCallback(
|
|
28
30
|
(event: MouseEvent) => {
|
|
@@ -55,17 +57,17 @@ export const TreeItemHeading = memo(
|
|
|
55
57
|
data-testid='treeItem.heading'
|
|
56
58
|
variant='ghost'
|
|
57
59
|
density='fine'
|
|
58
|
-
classNames={
|
|
60
|
+
classNames={[
|
|
59
61
|
'grow gap-2 pis-0.5 hover:bg-transparent dark:hover:bg-transparent',
|
|
60
62
|
'disabled:cursor-default disabled:opacity-100',
|
|
61
63
|
className,
|
|
62
|
-
|
|
64
|
+
]}
|
|
63
65
|
disabled={disabled}
|
|
64
66
|
onClick={handleSelect}
|
|
65
67
|
onKeyDown={handleButtonKeydown}
|
|
66
68
|
{...(current && { 'aria-current': 'location' })}
|
|
67
69
|
>
|
|
68
|
-
{icon && <Icon icon={icon ?? 'ph--placeholder--regular'} size={5} classNames='mlb-1' />}
|
|
70
|
+
{icon && <Icon icon={icon ?? 'ph--placeholder--regular'} size={5} classNames={['mlb-1', styles?.icon]} />}
|
|
69
71
|
<span className='flex-1 is-0 truncate text-start text-sm font-normal' data-tooltip>
|
|
70
72
|
{toLocalizedString(label, t)}
|
|
71
73
|
</span>
|