@dxos/react-ui-list 0.8.4-main.3f58842 → 0.8.4-main.40e3dcdf1b

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