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