@dxos/react-ui-list 0.8.4-main.b97322e → 0.8.4-main.bcb3aa67d6
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 +693 -725
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +693 -725
- 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 +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 +44 -30
- package/src/components/List/List.tsx +5 -13
- package/src/components/List/ListItem.tsx +79 -47
- package/src/components/List/ListRoot.tsx +3 -3
- package/src/components/List/testing.ts +3 -3
- package/src/components/Tree/Tree.stories.tsx +173 -80
- package/src/components/Tree/Tree.tsx +43 -40
- package/src/components/Tree/TreeContext.tsx +21 -9
- package/src/components/Tree/TreeItem.tsx +223 -135
- package/src/components/Tree/TreeItemHeading.tsx +11 -9
- 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, 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,
|
|
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,84 @@ 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;
|
|
132
|
+
const shouldSeedNativeDragData = typeof document !== 'undefined' && document.body.hasAttribute('data-platform');
|
|
92
133
|
|
|
93
134
|
const cancelExpand = useCallback(() => {
|
|
94
135
|
if (cancelExpandRef.current) {
|
|
@@ -97,20 +138,27 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
97
138
|
}
|
|
98
139
|
}, []);
|
|
99
140
|
|
|
141
|
+
const isItemDraggable = draggableProp && itemDraggable !== false;
|
|
142
|
+
const isItemDroppable = itemDroppable !== false;
|
|
143
|
+
const nativeDragText = id;
|
|
144
|
+
|
|
100
145
|
useEffect(() => {
|
|
101
|
-
if (!
|
|
146
|
+
if (!draggableProp) {
|
|
102
147
|
return;
|
|
103
148
|
}
|
|
104
149
|
|
|
105
150
|
invariant(buttonRef.current);
|
|
106
151
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
// https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
|
|
110
|
-
return combine(
|
|
152
|
+
const makeDraggable = () =>
|
|
111
153
|
draggable({
|
|
112
|
-
element: buttonRef.current
|
|
154
|
+
element: buttonRef.current!,
|
|
113
155
|
getInitialData: () => data,
|
|
156
|
+
getInitialDataForExternal: () => {
|
|
157
|
+
if (!shouldSeedNativeDragData) {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
return { 'text/plain': nativeDragText };
|
|
161
|
+
},
|
|
114
162
|
onDragStart: () => {
|
|
115
163
|
setState('dragging');
|
|
116
164
|
if (open) {
|
|
@@ -124,96 +172,132 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
124
172
|
onOpenChange?.({ item, path, open: true });
|
|
125
173
|
}
|
|
126
174
|
},
|
|
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);
|
|
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);
|
|
166
210
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 {
|
|
174
221
|
setInstruction(null);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
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]);
|
|
179
241
|
|
|
180
242
|
// Cancel expand on unmount.
|
|
181
243
|
useEffect(() => () => cancelExpand(), [cancelExpand]);
|
|
182
244
|
|
|
183
|
-
const
|
|
245
|
+
const handleOpenToggle = useCallback(
|
|
184
246
|
() => onOpenChange?.({ item, path, open: !open }),
|
|
185
247
|
[onOpenChange, item, path, open],
|
|
186
248
|
);
|
|
187
249
|
|
|
188
250
|
const handleSelect = useCallback(
|
|
189
251
|
(option = false) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 });
|
|
193
259
|
rowRef.current?.focus();
|
|
194
260
|
onSelect?.({ item, path, current: !current, option });
|
|
195
261
|
}
|
|
196
262
|
},
|
|
197
|
-
[item, path, current, isBranch,
|
|
263
|
+
[item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
|
|
198
264
|
);
|
|
199
265
|
|
|
200
266
|
const handleKeyDown = useCallback(
|
|
201
267
|
(event: KeyboardEvent) => {
|
|
202
268
|
switch (event.key) {
|
|
203
269
|
case 'ArrowRight':
|
|
204
|
-
isBranch && !open && handleOpenChange();
|
|
205
|
-
break;
|
|
206
270
|
case 'ArrowLeft':
|
|
207
|
-
isBranch &&
|
|
208
|
-
break;
|
|
209
|
-
case ' ':
|
|
210
|
-
handleSelect(event.altKey);
|
|
271
|
+
isBranch && handleOpenToggle();
|
|
211
272
|
break;
|
|
212
273
|
}
|
|
213
274
|
},
|
|
214
|
-
[isBranch, open,
|
|
275
|
+
[isBranch, open, handleOpenToggle, handleSelect],
|
|
215
276
|
);
|
|
216
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
|
+
|
|
217
301
|
return (
|
|
218
302
|
<>
|
|
219
303
|
<Treegrid.Row
|
|
@@ -221,65 +305,69 @@ const RawTreeItem = <T extends HasId = any>({
|
|
|
221
305
|
key={id}
|
|
222
306
|
id={id}
|
|
223
307
|
aria-labelledby={`${id}__label`}
|
|
224
|
-
parentOf={parentOf?.join(
|
|
225
|
-
|
|
226
|
-
|
|
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}
|
|
315
|
+
classNames={mx(
|
|
316
|
+
'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-active-surface',
|
|
227
317
|
hoverableControls,
|
|
228
318
|
hoverableFocusedKeyboardControls,
|
|
229
319
|
hoverableFocusedWithinControls,
|
|
230
320
|
hoverableDescriptionIcons,
|
|
321
|
+
ghostFocusWithin,
|
|
231
322
|
ghostHover,
|
|
232
323
|
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}
|
|
324
|
+
)}
|
|
240
325
|
onKeyDown={handleKeyDown}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
setMenuOpen(true);
|
|
244
|
-
}}
|
|
326
|
+
onMouseEnter={handleItemHover}
|
|
327
|
+
onContextMenu={handleContextMenu}
|
|
245
328
|
>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
329
|
+
<div
|
|
330
|
+
role='none'
|
|
331
|
+
className='indent relative grid grid-cols-subgrid col-[tree-row]'
|
|
249
332
|
style={paddingIndentation(level)}
|
|
250
333
|
>
|
|
251
|
-
<
|
|
252
|
-
<TreeItemToggle isBranch={isBranch} open={open}
|
|
334
|
+
<Treegrid.Cell classNames='flex items-center'>
|
|
335
|
+
<TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
|
|
253
336
|
<TreeItemHeading
|
|
254
|
-
ref={buttonRef}
|
|
255
|
-
label={label}
|
|
256
|
-
icon={icon}
|
|
257
|
-
className={headingClassName}
|
|
258
337
|
disabled={disabled}
|
|
259
338
|
current={current}
|
|
339
|
+
label={label}
|
|
340
|
+
className={headingClassName}
|
|
341
|
+
icon={icon}
|
|
342
|
+
iconHue={iconHue}
|
|
260
343
|
onSelect={handleSelect}
|
|
344
|
+
ref={buttonRef}
|
|
261
345
|
/>
|
|
262
|
-
</
|
|
346
|
+
</Treegrid.Cell>
|
|
263
347
|
{Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
|
|
264
348
|
{instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
|
|
265
|
-
</
|
|
349
|
+
</div>
|
|
266
350
|
</Treegrid.Row>
|
|
267
351
|
{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
|
-
/>
|
|
352
|
+
childIds.map((childId, index) => (
|
|
353
|
+
<TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
|
|
280
354
|
))}
|
|
281
355
|
</>
|
|
282
356
|
);
|
|
283
357
|
};
|
|
284
358
|
|
|
285
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,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) => {
|
|
@@ -53,9 +56,8 @@ export const TreeItemHeading = memo(
|
|
|
53
56
|
<Button
|
|
54
57
|
data-testid='treeItem.heading'
|
|
55
58
|
variant='ghost'
|
|
56
|
-
density='fine'
|
|
57
59
|
classNames={[
|
|
58
|
-
'grow gap-2
|
|
60
|
+
'grow gap-2 ps-0.5 hover:bg-transparent dark:hover:bg-transparent',
|
|
59
61
|
'disabled:cursor-default disabled:opacity-100',
|
|
60
62
|
className,
|
|
61
63
|
]}
|
|
@@ -64,8 +66,8 @@ export const TreeItemHeading = memo(
|
|
|
64
66
|
onKeyDown={handleButtonKeydown}
|
|
65
67
|
{...(current && { 'aria-current': 'location' })}
|
|
66
68
|
>
|
|
67
|
-
{icon && <Icon icon={icon ?? 'ph--placeholder--regular'}
|
|
68
|
-
<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>
|
|
69
71
|
{toLocalizedString(label, t)}
|
|
70
72
|
</span>
|
|
71
73
|
</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
|
+
({ 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
|
+
),
|
|
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))),
|