@dxos/react-ui-list 0.8.4-main.b97322e → 0.8.4-main.bc674ce
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 -728
- 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 -728
- 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 +2 -2
- 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 +3 -3
- 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 +64 -51
- package/src/components/Tree/TreeItemHeading.tsx +9 -6
- package/src/components/Tree/TreeItemToggle.tsx +29 -18
- 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,34 +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
|
-
} from '@dxos/
|
|
24
|
+
} from '@dxos/ui-theme';
|
|
25
25
|
|
|
26
|
+
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
26
27
|
import { useTree } from './TreeContext';
|
|
27
28
|
import { TreeItemHeading } from './TreeItemHeading';
|
|
28
29
|
import { TreeItemToggle } from './TreeItemToggle';
|
|
29
|
-
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
30
|
-
|
|
31
|
-
type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
|
|
32
30
|
|
|
33
31
|
const hoverableDescriptionIcons =
|
|
34
32
|
'[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
|
|
35
33
|
|
|
34
|
+
type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
|
|
35
|
+
|
|
36
36
|
export const TreeDataSchema = Schema.Struct({
|
|
37
37
|
id: Schema.String,
|
|
38
38
|
path: Schema.Array(Schema.String),
|
|
@@ -40,37 +40,42 @@ export const TreeDataSchema = Schema.Struct({
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
|
|
43
|
-
|
|
44
43
|
export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
|
|
45
44
|
|
|
46
|
-
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> = {
|
|
47
54
|
item: T;
|
|
48
55
|
path: string[];
|
|
49
56
|
levelOffset?: number;
|
|
50
57
|
last: boolean;
|
|
51
58
|
draggable?: boolean;
|
|
52
|
-
renderColumns?:
|
|
53
|
-
|
|
54
|
-
path: string[];
|
|
55
|
-
open: boolean;
|
|
56
|
-
menuOpen: boolean;
|
|
57
|
-
setMenuOpen: (open: boolean) => void;
|
|
58
|
-
}>;
|
|
59
|
+
renderColumns?: ColumnRenderer<T>;
|
|
60
|
+
blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
|
|
59
61
|
canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
|
|
62
|
+
canSelect?: (params: { item: T; path: string[] }) => boolean;
|
|
60
63
|
onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
|
|
61
64
|
onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
|
|
62
65
|
};
|
|
63
66
|
|
|
64
|
-
const RawTreeItem = <T extends
|
|
67
|
+
const RawTreeItem = <T extends { id: string } = any>({
|
|
65
68
|
item,
|
|
66
69
|
path: _path,
|
|
70
|
+
levelOffset = 2,
|
|
67
71
|
last,
|
|
68
72
|
draggable: _draggable,
|
|
69
73
|
renderColumns: Columns,
|
|
74
|
+
blockInstruction,
|
|
70
75
|
canDrop,
|
|
76
|
+
canSelect,
|
|
71
77
|
onOpenChange,
|
|
72
78
|
onSelect,
|
|
73
|
-
levelOffset = 2,
|
|
74
79
|
}: TreeItemProps<T>) => {
|
|
75
80
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
|
76
81
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
@@ -80,15 +85,16 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
80
85
|
const [instruction, setInstruction] = useState<Instruction | null>(null);
|
|
81
86
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
82
87
|
|
|
83
|
-
const { useItems, getProps,
|
|
88
|
+
const { useItems, getProps, useIsOpen, useIsCurrent } = useTree();
|
|
84
89
|
const items = useItems(item);
|
|
85
|
-
const { id, label,
|
|
90
|
+
const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = getProps(item, _path);
|
|
86
91
|
const path = useMemo(() => [..._path, id], [_path, id]);
|
|
87
|
-
const open =
|
|
88
|
-
const current =
|
|
92
|
+
const open = useIsOpen(path, item);
|
|
93
|
+
const current = useIsCurrent(path, item);
|
|
89
94
|
const level = path.length - levelOffset;
|
|
90
95
|
const isBranch = !!parentOf;
|
|
91
96
|
const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
|
|
97
|
+
const canSelectItem = canSelect?.({ item, path }) ?? true;
|
|
92
98
|
|
|
93
99
|
const cancelExpand = useCallback(() => {
|
|
94
100
|
if (cancelExpandRef.current) {
|
|
@@ -144,7 +150,11 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
144
150
|
},
|
|
145
151
|
getIsSticky: () => true,
|
|
146
152
|
onDrag: ({ self, source }) => {
|
|
147
|
-
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;
|
|
148
158
|
|
|
149
159
|
if (source.data.id !== id) {
|
|
150
160
|
if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
|
|
@@ -175,43 +185,42 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
175
185
|
},
|
|
176
186
|
}),
|
|
177
187
|
);
|
|
178
|
-
}, [_draggable, item, id, mode, path, open, canDrop]);
|
|
188
|
+
}, [_draggable, item, id, mode, path, open, blockInstruction, canDrop]);
|
|
179
189
|
|
|
180
190
|
// Cancel expand on unmount.
|
|
181
191
|
useEffect(() => () => cancelExpand(), [cancelExpand]);
|
|
182
192
|
|
|
183
|
-
const
|
|
193
|
+
const handleOpenToggle = useCallback(
|
|
184
194
|
() => onOpenChange?.({ item, path, open: !open }),
|
|
185
195
|
[onOpenChange, item, path, open],
|
|
186
196
|
);
|
|
187
197
|
|
|
188
198
|
const handleSelect = useCallback(
|
|
189
199
|
(option = false) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 });
|
|
193
207
|
rowRef.current?.focus();
|
|
194
208
|
onSelect?.({ item, path, current: !current, option });
|
|
195
209
|
}
|
|
196
210
|
},
|
|
197
|
-
[item, path, current, isBranch,
|
|
211
|
+
[item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
|
|
198
212
|
);
|
|
199
213
|
|
|
200
214
|
const handleKeyDown = useCallback(
|
|
201
215
|
(event: KeyboardEvent) => {
|
|
202
216
|
switch (event.key) {
|
|
203
217
|
case 'ArrowRight':
|
|
204
|
-
isBranch && !open && handleOpenChange();
|
|
205
|
-
break;
|
|
206
218
|
case 'ArrowLeft':
|
|
207
|
-
isBranch &&
|
|
208
|
-
break;
|
|
209
|
-
case ' ':
|
|
210
|
-
handleSelect(event.altKey);
|
|
219
|
+
isBranch && handleOpenToggle();
|
|
211
220
|
break;
|
|
212
221
|
}
|
|
213
222
|
},
|
|
214
|
-
[isBranch, open,
|
|
223
|
+
[isBranch, open, handleOpenToggle, handleSelect],
|
|
215
224
|
);
|
|
216
225
|
|
|
217
226
|
return (
|
|
@@ -229,9 +238,10 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
229
238
|
hoverableFocusedWithinControls,
|
|
230
239
|
hoverableDescriptionIcons,
|
|
231
240
|
ghostHover,
|
|
241
|
+
ghostFocusWithin,
|
|
232
242
|
className,
|
|
233
243
|
]}
|
|
234
|
-
data-
|
|
244
|
+
data-object-id={id}
|
|
235
245
|
data-testid={testId}
|
|
236
246
|
// NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
|
|
237
247
|
// without alerting the user (except for in the correct link element). See also:
|
|
@@ -243,26 +253,27 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
243
253
|
setMenuOpen(true);
|
|
244
254
|
}}
|
|
245
255
|
>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
<div
|
|
257
|
+
role='none'
|
|
258
|
+
className='indent relative grid grid-cols-subgrid col-[tree-row]'
|
|
249
259
|
style={paddingIndentation(level)}
|
|
250
260
|
>
|
|
251
|
-
<
|
|
252
|
-
<TreeItemToggle isBranch={isBranch} open={open}
|
|
261
|
+
<Treegrid.Cell classNames='flex items-center'>
|
|
262
|
+
<TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
|
|
253
263
|
<TreeItemHeading
|
|
254
|
-
ref={buttonRef}
|
|
255
|
-
label={label}
|
|
256
|
-
icon={icon}
|
|
257
|
-
className={headingClassName}
|
|
258
264
|
disabled={disabled}
|
|
259
265
|
current={current}
|
|
266
|
+
label={label}
|
|
267
|
+
className={headingClassName}
|
|
268
|
+
icon={icon}
|
|
269
|
+
iconHue={iconHue}
|
|
260
270
|
onSelect={handleSelect}
|
|
271
|
+
ref={buttonRef}
|
|
261
272
|
/>
|
|
262
|
-
</
|
|
273
|
+
</Treegrid.Cell>
|
|
263
274
|
{Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
|
|
264
275
|
{instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
|
|
265
|
-
</
|
|
276
|
+
</div>
|
|
266
277
|
</Treegrid.Row>
|
|
267
278
|
{open &&
|
|
268
279
|
items.map((item, index) => (
|
|
@@ -273,7 +284,9 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
273
284
|
last={index === items.length - 1}
|
|
274
285
|
draggable={_draggable}
|
|
275
286
|
renderColumns={Columns}
|
|
287
|
+
blockInstruction={blockInstruction}
|
|
276
288
|
canDrop={canDrop}
|
|
289
|
+
canSelect={canSelect}
|
|
277
290
|
onOpenChange={onOpenChange}
|
|
278
291
|
onSelect={onSelect}
|
|
279
292
|
/>
|
|
@@ -4,24 +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 { getStyles } from '@dxos/ui-theme';
|
|
9
10
|
|
|
10
11
|
// TODO(wittjosiah): Consider whether there should be a separate disabled prop which was visually distinct
|
|
11
12
|
// rather than just making the item unselectable.
|
|
12
|
-
export type
|
|
13
|
+
export type TreeItemHeadingProps = {
|
|
13
14
|
label: Label;
|
|
14
|
-
icon?: string;
|
|
15
15
|
className?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconHue?: string;
|
|
16
18
|
disabled?: boolean;
|
|
17
19
|
current?: boolean;
|
|
18
20
|
onSelect?: (option: boolean) => void;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export const TreeItemHeading = memo(
|
|
22
|
-
forwardRef<HTMLButtonElement,
|
|
23
|
-
({ label, icon,
|
|
24
|
+
forwardRef<HTMLButtonElement, TreeItemHeadingProps>(
|
|
25
|
+
({ label, className, icon, iconHue, disabled, current, onSelect }, forwardedRef) => {
|
|
24
26
|
const { t } = useTranslation();
|
|
27
|
+
const styles = iconHue ? getStyles(iconHue) : undefined;
|
|
25
28
|
|
|
26
29
|
const handleSelect = useCallback(
|
|
27
30
|
(event: MouseEvent) => {
|
|
@@ -64,7 +67,7 @@ export const TreeItemHeading = memo(
|
|
|
64
67
|
onKeyDown={handleButtonKeydown}
|
|
65
68
|
{...(current && { 'aria-current': 'location' })}
|
|
66
69
|
>
|
|
67
|
-
{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]} />}
|
|
68
71
|
<span className='flex-1 is-0 truncate text-start text-sm font-normal' data-tooltip>
|
|
69
72
|
{toLocalizedString(label, t)}
|
|
70
73
|
</span>
|
|
@@ -4,29 +4,40 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { forwardRef, memo } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { IconButton, type IconButtonProps } from '@dxos/react-ui';
|
|
8
8
|
|
|
9
|
-
export type TreeItemToggleProps = {
|
|
9
|
+
export type TreeItemToggleProps = Omit<IconButtonProps, 'icon' | 'size' | 'label'> & {
|
|
10
10
|
open?: boolean;
|
|
11
11
|
isBranch?: boolean;
|
|
12
|
-
onToggle?: () => void;
|
|
13
12
|
hidden?: boolean;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
export const TreeItemToggle = memo(
|
|
17
|
-
forwardRef<HTMLButtonElement, TreeItemToggleProps>(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
forwardRef<HTMLButtonElement, TreeItemToggleProps>(
|
|
17
|
+
({ open, isBranch, hidden, classNames, ...props }, forwardedRef) => {
|
|
18
|
+
return (
|
|
19
|
+
<IconButton
|
|
20
|
+
ref={forwardedRef}
|
|
21
|
+
data-testid='treeItem.toggle'
|
|
22
|
+
aria-expanded={open}
|
|
23
|
+
variant='ghost'
|
|
24
|
+
density='fine'
|
|
25
|
+
classNames={[
|
|
26
|
+
'bs-full is-6 pli-0',
|
|
27
|
+
'[&_svg]:transition-[transform] [&_svg]:duration-200',
|
|
28
|
+
open && '[&_svg]:rotate-90',
|
|
29
|
+
hidden ? 'hidden' : !isBranch && 'invisible',
|
|
30
|
+
classNames,
|
|
31
|
+
]}
|
|
32
|
+
size={3}
|
|
33
|
+
icon='ph--caret-right--bold'
|
|
34
|
+
iconOnly
|
|
35
|
+
noTooltip
|
|
36
|
+
label={open ? 'Click to close' : 'Click to open'}
|
|
37
|
+
tabIndex={-1}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
),
|
|
32
43
|
);
|