@dxos/react-ui-list 0.8.4-main.3f58842 → 0.8.4-main.40e3dcdf1b
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 +684 -726
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +684 -726
- 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 +9 -9
- 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 +9 -10
- 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 +10 -6
- 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 +24 -10
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts +32 -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/index.d.ts +2 -0
- package/dist/types/src/components/Tree/index.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 +32 -28
- package/src/components/Accordion/Accordion.stories.tsx +5 -7
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/Accordion/AccordionItem.tsx +8 -5
- package/src/components/Accordion/AccordionRoot.tsx +1 -1
- package/src/components/List/List.stories.tsx +42 -28
- package/src/components/List/List.tsx +3 -6
- package/src/components/List/ListItem.tsx +79 -49
- package/src/components/List/ListRoot.tsx +3 -3
- package/src/components/List/testing.ts +3 -3
- package/src/components/Tree/Tree.stories.tsx +172 -79
- package/src/components/Tree/Tree.tsx +43 -40
- package/src/components/Tree/TreeContext.tsx +21 -9
- package/src/components/Tree/TreeItem.tsx +214 -134
- package/src/components/Tree/TreeItemHeading.tsx +13 -8
- package/src/components/Tree/TreeItemToggle.tsx +29 -18
- package/src/components/Tree/index.ts +2 -0
- package/src/components/Tree/testing.ts +5 -4
|
@@ -5,34 +5,46 @@
|
|
|
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
|
|
13
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
14
|
+
import * as Schema from 'effect/Schema';
|
|
15
|
+
import React, {
|
|
16
|
+
type FC,
|
|
17
|
+
type KeyboardEvent,
|
|
18
|
+
type MouseEvent,
|
|
19
|
+
memo,
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect,
|
|
22
|
+
useMemo,
|
|
23
|
+
useRef,
|
|
24
|
+
useState,
|
|
25
|
+
} from 'react';
|
|
15
26
|
|
|
16
|
-
import { type HasId } from '@dxos/echo-schema';
|
|
17
27
|
import { invariant } from '@dxos/invariant';
|
|
18
|
-
import {
|
|
28
|
+
import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
|
|
19
29
|
import {
|
|
30
|
+
ghostFocusWithin,
|
|
20
31
|
ghostHover,
|
|
21
32
|
hoverableControls,
|
|
22
33
|
hoverableFocusedKeyboardControls,
|
|
23
34
|
hoverableFocusedWithinControls,
|
|
24
|
-
|
|
35
|
+
mx,
|
|
36
|
+
} from '@dxos/ui-theme';
|
|
25
37
|
|
|
38
|
+
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
26
39
|
import { useTree } from './TreeContext';
|
|
27
40
|
import { TreeItemHeading } from './TreeItemHeading';
|
|
28
41
|
import { TreeItemToggle } from './TreeItemToggle';
|
|
29
|
-
import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
|
|
30
|
-
|
|
31
|
-
type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
|
|
32
42
|
|
|
33
43
|
const hoverableDescriptionIcons =
|
|
34
44
|
'[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
|
|
35
45
|
|
|
46
|
+
type TreeItemDragState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
|
|
47
|
+
|
|
36
48
|
export const TreeDataSchema = Schema.Struct({
|
|
37
49
|
id: Schema.String,
|
|
38
50
|
path: Schema.Array(Schema.String),
|
|
@@ -40,55 +52,83 @@ export const TreeDataSchema = Schema.Struct({
|
|
|
40
52
|
});
|
|
41
53
|
|
|
42
54
|
export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
|
|
43
|
-
|
|
44
55
|
export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
|
|
45
56
|
|
|
46
|
-
export type
|
|
57
|
+
export type ColumnRenderer<T extends { id: string } = any> = FC<{
|
|
58
|
+
item: T;
|
|
59
|
+
path: string[];
|
|
60
|
+
open: boolean;
|
|
61
|
+
menuOpen: boolean;
|
|
62
|
+
setMenuOpen: (open: boolean) => void;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
export type TreeItemProps<T extends { id: string } = any> = {
|
|
47
66
|
item: T;
|
|
48
67
|
path: string[];
|
|
49
68
|
levelOffset?: number;
|
|
50
69
|
last: boolean;
|
|
51
70
|
draggable?: boolean;
|
|
52
|
-
renderColumns?:
|
|
53
|
-
|
|
54
|
-
path: string[];
|
|
55
|
-
open: boolean;
|
|
56
|
-
menuOpen: boolean;
|
|
57
|
-
setMenuOpen: (open: boolean) => void;
|
|
58
|
-
}>;
|
|
71
|
+
renderColumns?: ColumnRenderer<T>;
|
|
72
|
+
blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
|
|
59
73
|
canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
|
|
74
|
+
canSelect?: (params: { item: T; path: string[] }) => boolean;
|
|
60
75
|
onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
|
|
61
76
|
onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
|
|
77
|
+
onItemHover?: (params: { item: T }) => void;
|
|
62
78
|
};
|
|
63
79
|
|
|
64
|
-
const RawTreeItem = <T extends
|
|
80
|
+
const RawTreeItem = <T extends { id: string } = any>({
|
|
65
81
|
item,
|
|
66
|
-
path:
|
|
82
|
+
path: pathProp,
|
|
83
|
+
levelOffset = 2,
|
|
67
84
|
last,
|
|
68
|
-
draggable:
|
|
85
|
+
draggable: draggableProp,
|
|
69
86
|
renderColumns: Columns,
|
|
87
|
+
blockInstruction,
|
|
70
88
|
canDrop,
|
|
89
|
+
canSelect,
|
|
71
90
|
onOpenChange,
|
|
72
91
|
onSelect,
|
|
73
|
-
|
|
92
|
+
onItemHover,
|
|
74
93
|
}: TreeItemProps<T>) => {
|
|
75
94
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
|
76
95
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
77
96
|
const openRef = useRef(false);
|
|
78
97
|
const cancelExpandRef = useRef<NodeJS.Timeout | null>(null);
|
|
79
|
-
const [_state, setState] = useState<
|
|
98
|
+
const [_state, setState] = useState<TreeItemDragState>('idle');
|
|
80
99
|
const [instruction, setInstruction] = useState<Instruction | null>(null);
|
|
81
100
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
82
101
|
|
|
83
|
-
const {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
const {
|
|
103
|
+
itemProps: itemPropsAtom,
|
|
104
|
+
childIds: childIdsAtom,
|
|
105
|
+
itemOpen: itemOpenAtom,
|
|
106
|
+
itemCurrent: itemCurrentAtom,
|
|
107
|
+
} = useTree();
|
|
108
|
+
const path = useMemo(() => [...pathProp, item.id], [pathProp, item.id]);
|
|
109
|
+
|
|
110
|
+
const {
|
|
111
|
+
id,
|
|
112
|
+
parentOf,
|
|
113
|
+
draggable: itemDraggable,
|
|
114
|
+
droppable: itemDroppable,
|
|
115
|
+
label,
|
|
116
|
+
className,
|
|
117
|
+
headingClassName,
|
|
118
|
+
icon,
|
|
119
|
+
iconHue,
|
|
120
|
+
disabled,
|
|
121
|
+
testId,
|
|
122
|
+
} = useAtomValue(itemPropsAtom(path));
|
|
123
|
+
const childIds = useAtomValue(childIdsAtom(item.id));
|
|
124
|
+
const open = useAtomValue(itemOpenAtom(path));
|
|
125
|
+
const current = useAtomValue(itemCurrentAtom(path));
|
|
126
|
+
|
|
89
127
|
const level = path.length - levelOffset;
|
|
90
128
|
const isBranch = !!parentOf;
|
|
91
129
|
const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
|
|
130
|
+
const canSelectItem = canSelect?.({ item, path }) ?? true;
|
|
131
|
+
const data = { id, path, item } satisfies TreeData;
|
|
92
132
|
|
|
93
133
|
const cancelExpand = useCallback(() => {
|
|
94
134
|
if (cancelExpandRef.current) {
|
|
@@ -97,19 +137,19 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
97
137
|
}
|
|
98
138
|
}, []);
|
|
99
139
|
|
|
140
|
+
const isItemDraggable = draggableProp && itemDraggable !== false;
|
|
141
|
+
const isItemDroppable = itemDroppable !== false;
|
|
142
|
+
|
|
100
143
|
useEffect(() => {
|
|
101
|
-
if (!
|
|
144
|
+
if (!draggableProp) {
|
|
102
145
|
return;
|
|
103
146
|
}
|
|
104
147
|
|
|
105
148
|
invariant(buttonRef.current);
|
|
106
149
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
// https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
|
|
110
|
-
return combine(
|
|
150
|
+
const makeDraggable = () =>
|
|
111
151
|
draggable({
|
|
112
|
-
element: buttonRef.current
|
|
152
|
+
element: buttonRef.current!,
|
|
113
153
|
getInitialData: () => data,
|
|
114
154
|
onDragStart: () => {
|
|
115
155
|
setState('dragging');
|
|
@@ -124,96 +164,132 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
124
164
|
onOpenChange?.({ item, path, open: true });
|
|
125
165
|
}
|
|
126
166
|
},
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// TODO(wittjosiah): This is not occurring in the current implementation.
|
|
163
|
-
setInstruction(instruction);
|
|
164
|
-
} else {
|
|
165
|
-
setInstruction(null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!isItemDroppable) {
|
|
170
|
+
return isItemDraggable ? makeDraggable() : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const dropTarget = dropTargetForElements({
|
|
174
|
+
element: buttonRef.current,
|
|
175
|
+
getData: ({ input, element }) => {
|
|
176
|
+
return attachInstruction(data, {
|
|
177
|
+
input,
|
|
178
|
+
element,
|
|
179
|
+
indentPerLevel: DEFAULT_INDENTATION,
|
|
180
|
+
currentLevel: level,
|
|
181
|
+
mode,
|
|
182
|
+
block: isBranch ? [] : ['make-child'],
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
canDrop: ({ source }) => {
|
|
186
|
+
const _canDrop = canDrop ?? (() => true);
|
|
187
|
+
return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
|
|
188
|
+
},
|
|
189
|
+
getIsSticky: () => true,
|
|
190
|
+
onDrag: ({ self, source }) => {
|
|
191
|
+
const desired = extractInstruction(self.data);
|
|
192
|
+
const block =
|
|
193
|
+
desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
|
|
194
|
+
const instruction: Instruction | null =
|
|
195
|
+
block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
|
|
196
|
+
|
|
197
|
+
if (source.data.id !== id) {
|
|
198
|
+
if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
|
|
199
|
+
cancelExpandRef.current = setTimeout(() => {
|
|
200
|
+
onOpenChange?.({ item, path, open: true });
|
|
201
|
+
}, 500);
|
|
166
202
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
|
|
204
|
+
if (instruction?.type !== 'make-child') {
|
|
205
|
+
cancelExpand();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setInstruction(instruction);
|
|
209
|
+
} else if (instruction?.type === 'reparent') {
|
|
210
|
+
// TODO(wittjosiah): This is not occurring in the current implementation.
|
|
211
|
+
setInstruction(instruction);
|
|
212
|
+
} else {
|
|
174
213
|
setInstruction(null);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
onDragLeave: () => {
|
|
217
|
+
cancelExpand();
|
|
218
|
+
setInstruction(null);
|
|
219
|
+
},
|
|
220
|
+
onDrop: () => {
|
|
221
|
+
cancelExpand();
|
|
222
|
+
setInstruction(null);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!isItemDraggable) {
|
|
227
|
+
return dropTarget;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
|
|
231
|
+
return combine(makeDraggable(), dropTarget);
|
|
232
|
+
}, [draggableProp, isItemDraggable, isItemDroppable, item, id, mode, path, open, blockInstruction, canDrop]);
|
|
179
233
|
|
|
180
234
|
// Cancel expand on unmount.
|
|
181
235
|
useEffect(() => () => cancelExpand(), [cancelExpand]);
|
|
182
236
|
|
|
183
|
-
const
|
|
237
|
+
const handleOpenToggle = useCallback(
|
|
184
238
|
() => onOpenChange?.({ item, path, open: !open }),
|
|
185
239
|
[onOpenChange, item, path, open],
|
|
186
240
|
);
|
|
187
241
|
|
|
188
242
|
const handleSelect = useCallback(
|
|
189
243
|
(option = false) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
// If the item is a branch, toggle it if:
|
|
245
|
+
// - also holding down the option key
|
|
246
|
+
// - or the item is currently selected
|
|
247
|
+
if (isBranch && (option || current)) {
|
|
248
|
+
handleOpenToggle();
|
|
249
|
+
} else if (canSelectItem) {
|
|
250
|
+
canSelect?.({ item, path });
|
|
193
251
|
rowRef.current?.focus();
|
|
194
252
|
onSelect?.({ item, path, current: !current, option });
|
|
195
253
|
}
|
|
196
254
|
},
|
|
197
|
-
[item, path, current, isBranch,
|
|
255
|
+
[item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
|
|
198
256
|
);
|
|
199
257
|
|
|
200
258
|
const handleKeyDown = useCallback(
|
|
201
259
|
(event: KeyboardEvent) => {
|
|
202
260
|
switch (event.key) {
|
|
203
261
|
case 'ArrowRight':
|
|
204
|
-
isBranch && !open && handleOpenChange();
|
|
205
|
-
break;
|
|
206
262
|
case 'ArrowLeft':
|
|
207
|
-
isBranch &&
|
|
208
|
-
break;
|
|
209
|
-
case ' ':
|
|
210
|
-
handleSelect(event.altKey);
|
|
263
|
+
isBranch && handleOpenToggle();
|
|
211
264
|
break;
|
|
212
265
|
}
|
|
213
266
|
},
|
|
214
|
-
[isBranch, open,
|
|
267
|
+
[isBranch, open, handleOpenToggle, handleSelect],
|
|
215
268
|
);
|
|
216
269
|
|
|
270
|
+
const handleItemHover = useCallback(() => {
|
|
271
|
+
onItemHover?.({ item });
|
|
272
|
+
}, [onItemHover, item]);
|
|
273
|
+
|
|
274
|
+
const handleContextMenu = useCallback(
|
|
275
|
+
(event: MouseEvent) => {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
setMenuOpen(true);
|
|
278
|
+
},
|
|
279
|
+
[setMenuOpen],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const childProps = {
|
|
283
|
+
draggable: draggableProp,
|
|
284
|
+
renderColumns: Columns,
|
|
285
|
+
blockInstruction,
|
|
286
|
+
canDrop,
|
|
287
|
+
canSelect,
|
|
288
|
+
onItemHover,
|
|
289
|
+
onOpenChange,
|
|
290
|
+
onSelect,
|
|
291
|
+
};
|
|
292
|
+
|
|
217
293
|
return (
|
|
218
294
|
<>
|
|
219
295
|
<Treegrid.Row
|
|
@@ -222,64 +298,68 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
222
298
|
id={id}
|
|
223
299
|
aria-labelledby={`${id}__label`}
|
|
224
300
|
parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
|
|
225
|
-
|
|
226
|
-
|
|
301
|
+
data-object-id={id}
|
|
302
|
+
data-testid={testId}
|
|
303
|
+
// NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
|
|
304
|
+
// without alerting the user (except for in the correct link element). See also:
|
|
305
|
+
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
|
|
306
|
+
aria-current={current ? ('' as 'page') : undefined}
|
|
307
|
+
classNames={mx(
|
|
308
|
+
'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-active-surface',
|
|
227
309
|
hoverableControls,
|
|
228
310
|
hoverableFocusedKeyboardControls,
|
|
229
311
|
hoverableFocusedWithinControls,
|
|
230
312
|
hoverableDescriptionIcons,
|
|
313
|
+
ghostFocusWithin,
|
|
231
314
|
ghostHover,
|
|
232
315
|
className,
|
|
233
|
-
|
|
234
|
-
data-itemid={id}
|
|
235
|
-
data-testid={testId}
|
|
236
|
-
// NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
|
|
237
|
-
// without alerting the user (except for in the correct link element). See also:
|
|
238
|
-
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
|
|
239
|
-
aria-current={current ? ('' as 'page') : undefined}
|
|
316
|
+
)}
|
|
240
317
|
onKeyDown={handleKeyDown}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
setMenuOpen(true);
|
|
244
|
-
}}
|
|
318
|
+
onMouseEnter={handleItemHover}
|
|
319
|
+
onContextMenu={handleContextMenu}
|
|
245
320
|
>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
321
|
+
<div
|
|
322
|
+
role='none'
|
|
323
|
+
className='indent relative grid grid-cols-subgrid col-[tree-row]'
|
|
249
324
|
style={paddingIndentation(level)}
|
|
250
325
|
>
|
|
251
|
-
<
|
|
252
|
-
<TreeItemToggle isBranch={isBranch} open={open}
|
|
326
|
+
<Treegrid.Cell classNames='flex items-center'>
|
|
327
|
+
<TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
|
|
253
328
|
<TreeItemHeading
|
|
254
|
-
ref={buttonRef}
|
|
255
|
-
label={label}
|
|
256
|
-
icon={icon}
|
|
257
|
-
className={headingClassName}
|
|
258
329
|
disabled={disabled}
|
|
259
330
|
current={current}
|
|
331
|
+
label={label}
|
|
332
|
+
className={headingClassName}
|
|
333
|
+
icon={icon}
|
|
334
|
+
iconHue={iconHue}
|
|
260
335
|
onSelect={handleSelect}
|
|
336
|
+
ref={buttonRef}
|
|
261
337
|
/>
|
|
262
|
-
</
|
|
338
|
+
</Treegrid.Cell>
|
|
263
339
|
{Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
|
|
264
340
|
{instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
|
|
265
|
-
</
|
|
341
|
+
</div>
|
|
266
342
|
</Treegrid.Row>
|
|
267
343
|
{open &&
|
|
268
|
-
|
|
269
|
-
<
|
|
270
|
-
key={item.id}
|
|
271
|
-
item={item}
|
|
272
|
-
path={path}
|
|
273
|
-
last={index === items.length - 1}
|
|
274
|
-
draggable={_draggable}
|
|
275
|
-
renderColumns={Columns}
|
|
276
|
-
canDrop={canDrop}
|
|
277
|
-
onOpenChange={onOpenChange}
|
|
278
|
-
onSelect={onSelect}
|
|
279
|
-
/>
|
|
344
|
+
childIds.map((childId, index) => (
|
|
345
|
+
<TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
|
|
280
346
|
))}
|
|
281
347
|
</>
|
|
282
348
|
);
|
|
283
349
|
};
|
|
284
350
|
|
|
285
351
|
export const TreeItem = memo(RawTreeItem) as FC<TreeItemProps>;
|
|
352
|
+
|
|
353
|
+
/** Resolves a child ID to an item via the `item` atom and renders a TreeItem. */
|
|
354
|
+
export type TreeItemByIdProps = Omit<TreeItemProps, 'item'> & { id: string };
|
|
355
|
+
|
|
356
|
+
const RawTreeItemById = <T extends { id: string } = any>({ id, ...props }: TreeItemByIdProps) => {
|
|
357
|
+
const { item: itemAtom } = useTree();
|
|
358
|
+
const item = useAtomValue(itemAtom(id)) as T | undefined;
|
|
359
|
+
if (!item) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return <TreeItem item={item} {...props} />;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const TreeItemById = memo(RawTreeItemById) as FC<TreeItemByIdProps>;
|
|
@@ -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) => {
|
|
@@ -55,7 +58,7 @@ export const TreeItemHeading = memo(
|
|
|
55
58
|
variant='ghost'
|
|
56
59
|
density='fine'
|
|
57
60
|
classNames={[
|
|
58
|
-
'grow gap-2
|
|
61
|
+
'grow gap-2 ps-0.5 hover:bg-transparent dark:hover:bg-transparent',
|
|
59
62
|
'disabled:cursor-default disabled:opacity-100',
|
|
60
63
|
className,
|
|
61
64
|
]}
|
|
@@ -64,8 +67,10 @@ export const TreeItemHeading = memo(
|
|
|
64
67
|
onKeyDown={handleButtonKeydown}
|
|
65
68
|
{...(current && { 'aria-current': 'location' })}
|
|
66
69
|
>
|
|
67
|
-
{icon &&
|
|
68
|
-
|
|
70
|
+
{icon && (
|
|
71
|
+
<Icon icon={icon ?? 'ph--placeholder--regular'} size={5} classNames={['my-1', styles?.surfaceText]} />
|
|
72
|
+
)}
|
|
73
|
+
<span className='flex-1 w-0 truncate text-start font-normal' data-tooltip>
|
|
69
74
|
{toLocalizedString(label, t)}
|
|
70
75
|
</span>
|
|
71
76
|
</Button>
|
|
@@ -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
|
+
'h-full w-6 px-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
|
);
|
|
@@ -3,22 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
6
|
-
import
|
|
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))),
|