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