@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.
Files changed (55) hide show
  1. package/dist/lib/browser/index.mjs +693 -725
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +693 -725
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Accordion/Accordion.stories.d.ts +7 -4
  8. package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/Accordion/AccordionItem.d.ts +1 -1
  10. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  11. package/dist/types/src/components/List/List.d.ts +11 -9
  12. package/dist/types/src/components/List/List.d.ts.map +1 -1
  13. package/dist/types/src/components/List/List.stories.d.ts +14 -5
  14. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/List/ListItem.d.ts +9 -10
  16. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  17. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  18. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  19. package/dist/types/src/components/List/testing.d.ts +1 -1
  20. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  21. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  22. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  23. package/dist/types/src/components/Tree/Tree.stories.d.ts +18 -7
  24. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Tree/TreeContext.d.ts +24 -10
  26. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  27. package/dist/types/src/components/Tree/TreeItem.d.ts +32 -10
  28. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  29. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
  30. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  31. package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
  32. package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
  33. package/dist/types/src/components/Tree/index.d.ts +2 -0
  34. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  35. package/dist/types/src/components/Tree/testing.d.ts +3 -3
  36. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +32 -28
  39. package/src/components/Accordion/Accordion.stories.tsx +5 -7
  40. package/src/components/Accordion/Accordion.tsx +1 -1
  41. package/src/components/Accordion/AccordionItem.tsx +8 -5
  42. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  43. package/src/components/List/List.stories.tsx +44 -30
  44. package/src/components/List/List.tsx +5 -13
  45. package/src/components/List/ListItem.tsx +79 -47
  46. package/src/components/List/ListRoot.tsx +3 -3
  47. package/src/components/List/testing.ts +3 -3
  48. package/src/components/Tree/Tree.stories.tsx +173 -80
  49. package/src/components/Tree/Tree.tsx +43 -40
  50. package/src/components/Tree/TreeContext.tsx +21 -9
  51. package/src/components/Tree/TreeItem.tsx +223 -135
  52. package/src/components/Tree/TreeItemHeading.tsx +11 -9
  53. package/src/components/Tree/TreeItemToggle.tsx +29 -18
  54. package/src/components/Tree/index.ts +2 -0
  55. 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 { Schema } from 'effect';
14
- import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type FC, type KeyboardEvent } from 'react';
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 { Treegrid, TreeItem as NaturalTreeItem } from '@dxos/react-ui';
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
- } from '@dxos/react-ui-theme';
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 TreeItemProps<T extends HasId = any> = {
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?: FC<{
53
- item: T;
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 HasId = any>({
80
+ const RawTreeItem = <T extends { id: string } = any>({
65
81
  item,
66
- path: _path,
82
+ path: pathProp,
83
+ levelOffset = 2,
67
84
  last,
68
- draggable: _draggable,
85
+ draggable: draggableProp,
69
86
  renderColumns: Columns,
87
+ blockInstruction,
70
88
  canDrop,
89
+ canSelect,
71
90
  onOpenChange,
72
91
  onSelect,
73
- levelOffset = 2,
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<TreeItemState>('idle');
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 { useItems, getProps, isOpen, isCurrent } = useTree();
84
- const items = useItems(item);
85
- const { id, label, parentOf, icon, disabled, className, headingClassName, testId } = getProps(item, _path);
86
- const path = useMemo(() => [..._path, id], [_path, id]);
87
- const open = isOpen(path, item);
88
- const current = isCurrent(path, item);
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 (!_draggable) {
146
+ if (!draggableProp) {
102
147
  return;
103
148
  }
104
149
 
105
150
  invariant(buttonRef.current);
106
151
 
107
- const data = { id, path, item } satisfies TreeData;
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
- // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx
129
- dropTargetForElements({
130
- element: buttonRef.current,
131
- getData: ({ input, element }) => {
132
- return attachInstruction(data, {
133
- input,
134
- element,
135
- indentPerLevel: DEFAULT_INDENTATION,
136
- currentLevel: level,
137
- mode,
138
- block: isBranch ? [] : ['make-child'],
139
- });
140
- },
141
- canDrop: ({ source }) => {
142
- const _canDrop = canDrop ?? (() => true);
143
- return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
144
- },
145
- getIsSticky: () => true,
146
- onDrag: ({ self, source }) => {
147
- const instruction = extractInstruction(self.data);
148
-
149
- if (source.data.id !== id) {
150
- if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
151
- cancelExpandRef.current = setTimeout(() => {
152
- onOpenChange?.({ item, path, open: true });
153
- }, 500);
154
- }
155
-
156
- if (instruction?.type !== 'make-child') {
157
- cancelExpand();
158
- }
159
-
160
- setInstruction(instruction);
161
- } else if (instruction?.type === 'reparent') {
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
- onDragLeave: () => {
169
- cancelExpand();
170
- setInstruction(null);
171
- },
172
- onDrop: () => {
173
- cancelExpand();
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
- }, [_draggable, item, id, mode, path, open, canDrop]);
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 handleOpenChange = useCallback(
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
- if (isBranch) {
191
- handleOpenChange();
192
- } else {
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, handleOpenChange, onSelect],
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 && open && handleOpenChange();
208
- break;
209
- case ' ':
210
- handleSelect(event.altKey);
271
+ isBranch && handleOpenToggle();
211
272
  break;
212
273
  }
213
274
  },
214
- [isBranch, open, handleOpenChange, handleSelect],
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(Treegrid.PARENT_OF_SEPARATOR)}
225
- classNames={[
226
- 'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
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
- onContextMenu={(event) => {
242
- event.preventDefault();
243
- setMenuOpen(true);
244
- }}
326
+ onMouseEnter={handleItemHover}
327
+ onContextMenu={handleContextMenu}
245
328
  >
246
- <Treegrid.Cell
247
- indent
248
- classNames='relative grid grid-cols-subgrid col-[tree-row]'
329
+ <div
330
+ role='none'
331
+ className='indent relative grid grid-cols-subgrid col-[tree-row]'
249
332
  style={paddingIndentation(level)}
250
333
  >
251
- <div role='none' className='flex items-center'>
252
- <TreeItemToggle isBranch={isBranch} open={open} onToggle={handleOpenChange} />
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
- </div>
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
- </Treegrid.Cell>
349
+ </div>
266
350
  </Treegrid.Row>
267
351
  {open &&
268
- items.map((item, index) => (
269
- <TreeItem
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, type Label } from '@dxos/react-ui';
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 NavTreeItemHeadingProps = {
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, NavTreeItemHeadingProps>(
23
- ({ label, icon, className, disabled, current, onSelect }, forwardedRef) => {
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 pis-0.5 hover:bg-transparent dark:hover:bg-transparent',
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'} size={5} classNames='mlb-1' />}
68
- <span className='flex-1 is-0 truncate text-start text-sm font-normal' data-tooltip>
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 { Button, Icon } from '@dxos/react-ui';
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>(({ open, isBranch, hidden, onToggle }, forwardedRef) => {
18
- return (
19
- <Button
20
- ref={forwardedRef}
21
- data-testid='treeItem.toggle'
22
- aria-expanded={open}
23
- variant='ghost'
24
- density='fine'
25
- classNames={['is-6 pli-0 dx-focus-ring-inset', hidden ? 'hidden' : !isBranch && 'invisible']}
26
- onClick={onToggle}
27
- >
28
- <Icon icon='ph--caret-right--bold' size={3} classNames={['transition duration-200', open && 'rotate-90']} />
29
- </Button>
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
  );
@@ -5,3 +5,5 @@
5
5
  export * from './Tree';
6
6
  export * from './TreeContext';
7
7
  export * from './TreeItem';
8
+ export * from './TreeItemToggle';
9
+ export * from './helpers';
@@ -3,22 +3,23 @@
3
3
  //
4
4
 
5
5
  import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
6
- import { Schema } from 'effect';
6
+ import * as Schema from 'effect/Schema';
7
7
 
8
- import { type HasId, ObjectId } from '@dxos/echo-schema';
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 = HasId & {
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: ObjectId,
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))),