@dxos/react-ui-list 0.8.4-main.67995b8 → 0.8.4-main.6fa680abb7
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 +679 -724
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +679 -724
- 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 +6 -9
- 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 +25 -4
- 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 -30
- 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 +40 -26
- package/src/components/List/List.tsx +2 -5
- package/src/components/List/ListItem.tsx +50 -38
- 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 +206 -127
- 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),
|
|
@@ -42,7 +54,7 @@ export const TreeDataSchema = Schema.Struct({
|
|
|
42
54
|
export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
|
|
43
55
|
export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
|
|
44
56
|
|
|
45
|
-
export type ColumnRenderer<T extends
|
|
57
|
+
export type ColumnRenderer<T extends { id: string } = any> = FC<{
|
|
46
58
|
item: T;
|
|
47
59
|
path: string[];
|
|
48
60
|
open: boolean;
|
|
@@ -50,46 +62,73 @@ export type ColumnRenderer<T extends HasId = any> = FC<{
|
|
|
50
62
|
setMenuOpen: (open: boolean) => void;
|
|
51
63
|
}>;
|
|
52
64
|
|
|
53
|
-
export type TreeItemProps<T extends
|
|
65
|
+
export type TreeItemProps<T extends { id: string } = any> = {
|
|
54
66
|
item: T;
|
|
55
67
|
path: string[];
|
|
56
68
|
levelOffset?: number;
|
|
57
69
|
last: boolean;
|
|
58
70
|
draggable?: boolean;
|
|
59
71
|
renderColumns?: ColumnRenderer<T>;
|
|
72
|
+
blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
|
|
60
73
|
canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
|
|
74
|
+
canSelect?: (params: { item: T; path: string[] }) => boolean;
|
|
61
75
|
onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
|
|
62
76
|
onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
|
|
77
|
+
onItemHover?: (params: { item: T }) => void;
|
|
63
78
|
};
|
|
64
79
|
|
|
65
|
-
const RawTreeItem = <T extends
|
|
80
|
+
const RawTreeItem = <T extends { id: string } = any>({
|
|
66
81
|
item,
|
|
67
|
-
path:
|
|
82
|
+
path: pathProp,
|
|
83
|
+
levelOffset = 2,
|
|
68
84
|
last,
|
|
69
|
-
draggable:
|
|
85
|
+
draggable: draggableProp,
|
|
70
86
|
renderColumns: Columns,
|
|
87
|
+
blockInstruction,
|
|
71
88
|
canDrop,
|
|
89
|
+
canSelect,
|
|
72
90
|
onOpenChange,
|
|
73
91
|
onSelect,
|
|
74
|
-
|
|
92
|
+
onItemHover,
|
|
75
93
|
}: TreeItemProps<T>) => {
|
|
76
94
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
|
77
95
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
78
96
|
const openRef = useRef(false);
|
|
79
97
|
const cancelExpandRef = useRef<NodeJS.Timeout | null>(null);
|
|
80
|
-
const [_state, setState] = useState<
|
|
98
|
+
const [_state, setState] = useState<TreeItemDragState>('idle');
|
|
81
99
|
const [instruction, setInstruction] = useState<Instruction | null>(null);
|
|
82
100
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
83
101
|
|
|
84
|
-
const {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
|
|
90
127
|
const level = path.length - levelOffset;
|
|
91
128
|
const isBranch = !!parentOf;
|
|
92
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;
|
|
93
132
|
|
|
94
133
|
const cancelExpand = useCallback(() => {
|
|
95
134
|
if (cancelExpandRef.current) {
|
|
@@ -98,19 +137,19 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
98
137
|
}
|
|
99
138
|
}, []);
|
|
100
139
|
|
|
140
|
+
const isItemDraggable = draggableProp && itemDraggable !== false;
|
|
141
|
+
const isItemDroppable = itemDroppable !== false;
|
|
142
|
+
|
|
101
143
|
useEffect(() => {
|
|
102
|
-
if (!
|
|
144
|
+
if (!draggableProp) {
|
|
103
145
|
return;
|
|
104
146
|
}
|
|
105
147
|
|
|
106
148
|
invariant(buttonRef.current);
|
|
107
149
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
// https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
|
|
111
|
-
return combine(
|
|
150
|
+
const makeDraggable = () =>
|
|
112
151
|
draggable({
|
|
113
|
-
element: buttonRef.current
|
|
152
|
+
element: buttonRef.current!,
|
|
114
153
|
getInitialData: () => data,
|
|
115
154
|
onDragStart: () => {
|
|
116
155
|
setState('dragging');
|
|
@@ -125,96 +164,132 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
125
164
|
onOpenChange?.({ item, path, open: true });
|
|
126
165
|
}
|
|
127
166
|
},
|
|
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
|
-
|
|
163
|
-
// TODO(wittjosiah): This is not occurring in the current implementation.
|
|
164
|
-
setInstruction(instruction);
|
|
165
|
-
} else {
|
|
166
|
-
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);
|
|
167
202
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 {
|
|
175
213
|
setInstruction(null);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
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]);
|
|
180
233
|
|
|
181
234
|
// Cancel expand on unmount.
|
|
182
235
|
useEffect(() => () => cancelExpand(), [cancelExpand]);
|
|
183
236
|
|
|
184
|
-
const
|
|
237
|
+
const handleOpenToggle = useCallback(
|
|
185
238
|
() => onOpenChange?.({ item, path, open: !open }),
|
|
186
239
|
[onOpenChange, item, path, open],
|
|
187
240
|
);
|
|
188
241
|
|
|
189
242
|
const handleSelect = useCallback(
|
|
190
243
|
(option = false) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 });
|
|
194
251
|
rowRef.current?.focus();
|
|
195
252
|
onSelect?.({ item, path, current: !current, option });
|
|
196
253
|
}
|
|
197
254
|
},
|
|
198
|
-
[item, path, current, isBranch,
|
|
255
|
+
[item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
|
|
199
256
|
);
|
|
200
257
|
|
|
201
258
|
const handleKeyDown = useCallback(
|
|
202
259
|
(event: KeyboardEvent) => {
|
|
203
260
|
switch (event.key) {
|
|
204
261
|
case 'ArrowRight':
|
|
205
|
-
isBranch && !open && handleOpenChange();
|
|
206
|
-
break;
|
|
207
262
|
case 'ArrowLeft':
|
|
208
|
-
isBranch &&
|
|
209
|
-
break;
|
|
210
|
-
case ' ':
|
|
211
|
-
handleSelect(event.altKey);
|
|
263
|
+
isBranch && handleOpenToggle();
|
|
212
264
|
break;
|
|
213
265
|
}
|
|
214
266
|
},
|
|
215
|
-
[isBranch, open,
|
|
267
|
+
[isBranch, open, handleOpenToggle, handleSelect],
|
|
268
|
+
);
|
|
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],
|
|
216
280
|
);
|
|
217
281
|
|
|
282
|
+
const childProps = {
|
|
283
|
+
draggable: draggableProp,
|
|
284
|
+
renderColumns: Columns,
|
|
285
|
+
blockInstruction,
|
|
286
|
+
canDrop,
|
|
287
|
+
canSelect,
|
|
288
|
+
onItemHover,
|
|
289
|
+
onOpenChange,
|
|
290
|
+
onSelect,
|
|
291
|
+
};
|
|
292
|
+
|
|
218
293
|
return (
|
|
219
294
|
<>
|
|
220
295
|
<Treegrid.Row
|
|
@@ -223,64 +298,68 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
223
298
|
id={id}
|
|
224
299
|
aria-labelledby={`${id}__label`}
|
|
225
300
|
parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
|
|
226
|
-
|
|
227
|
-
|
|
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',
|
|
228
309
|
hoverableControls,
|
|
229
310
|
hoverableFocusedKeyboardControls,
|
|
230
311
|
hoverableFocusedWithinControls,
|
|
231
312
|
hoverableDescriptionIcons,
|
|
313
|
+
ghostFocusWithin,
|
|
232
314
|
ghostHover,
|
|
233
315
|
className,
|
|
234
|
-
|
|
235
|
-
data-itemid={id}
|
|
236
|
-
data-testid={testId}
|
|
237
|
-
// NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
|
|
238
|
-
// without alerting the user (except for in the correct link element). See also:
|
|
239
|
-
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
|
|
240
|
-
aria-current={current ? ('' as 'page') : undefined}
|
|
316
|
+
)}
|
|
241
317
|
onKeyDown={handleKeyDown}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
setMenuOpen(true);
|
|
245
|
-
}}
|
|
318
|
+
onMouseEnter={handleItemHover}
|
|
319
|
+
onContextMenu={handleContextMenu}
|
|
246
320
|
>
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
321
|
+
<div
|
|
322
|
+
role='none'
|
|
323
|
+
className='indent relative grid grid-cols-subgrid col-[tree-row]'
|
|
250
324
|
style={paddingIndentation(level)}
|
|
251
325
|
>
|
|
252
|
-
<
|
|
253
|
-
<TreeItemToggle isBranch={isBranch} open={open}
|
|
326
|
+
<Treegrid.Cell classNames='flex items-center'>
|
|
327
|
+
<TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
|
|
254
328
|
<TreeItemHeading
|
|
255
|
-
ref={buttonRef}
|
|
256
|
-
label={label}
|
|
257
|
-
icon={icon}
|
|
258
|
-
className={headingClassName}
|
|
259
329
|
disabled={disabled}
|
|
260
330
|
current={current}
|
|
331
|
+
label={label}
|
|
332
|
+
className={headingClassName}
|
|
333
|
+
icon={icon}
|
|
334
|
+
iconHue={iconHue}
|
|
261
335
|
onSelect={handleSelect}
|
|
336
|
+
ref={buttonRef}
|
|
262
337
|
/>
|
|
263
|
-
</
|
|
338
|
+
</Treegrid.Cell>
|
|
264
339
|
{Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
|
|
265
340
|
{instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
|
|
266
|
-
</
|
|
341
|
+
</div>
|
|
267
342
|
</Treegrid.Row>
|
|
268
343
|
{open &&
|
|
269
|
-
|
|
270
|
-
<
|
|
271
|
-
key={item.id}
|
|
272
|
-
item={item}
|
|
273
|
-
path={path}
|
|
274
|
-
last={index === items.length - 1}
|
|
275
|
-
draggable={_draggable}
|
|
276
|
-
renderColumns={Columns}
|
|
277
|
-
canDrop={canDrop}
|
|
278
|
-
onOpenChange={onOpenChange}
|
|
279
|
-
onSelect={onSelect}
|
|
280
|
-
/>
|
|
344
|
+
childIds.map((childId, index) => (
|
|
345
|
+
<TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
|
|
281
346
|
))}
|
|
282
347
|
</>
|
|
283
348
|
);
|
|
284
349
|
};
|
|
285
350
|
|
|
286
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))),
|