@dxos/react-ui-list 0.8.4-main.67995b8 → 0.8.4-main.6fa680abb7

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 +679 -724
  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 +679 -724
  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 +8 -8
  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 +6 -9
  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 +25 -4
  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 -30
  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 +40 -26
  44. package/src/components/List/List.tsx +2 -5
  45. package/src/components/List/ListItem.tsx +50 -38
  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 +206 -127
  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),
@@ -42,7 +54,7 @@ export const TreeDataSchema = Schema.Struct({
42
54
  export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
43
55
  export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
44
56
 
45
- export type ColumnRenderer<T extends HasId = any> = FC<{
57
+ export type ColumnRenderer<T extends { id: string } = any> = FC<{
46
58
  item: T;
47
59
  path: string[];
48
60
  open: boolean;
@@ -50,46 +62,73 @@ export type ColumnRenderer<T extends HasId = any> = FC<{
50
62
  setMenuOpen: (open: boolean) => void;
51
63
  }>;
52
64
 
53
- export type TreeItemProps<T extends HasId = any> = {
65
+ export type TreeItemProps<T extends { id: string } = any> = {
54
66
  item: T;
55
67
  path: string[];
56
68
  levelOffset?: number;
57
69
  last: boolean;
58
70
  draggable?: boolean;
59
71
  renderColumns?: ColumnRenderer<T>;
72
+ blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
60
73
  canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
74
+ canSelect?: (params: { item: T; path: string[] }) => boolean;
61
75
  onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
62
76
  onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
77
+ onItemHover?: (params: { item: T }) => void;
63
78
  };
64
79
 
65
- const RawTreeItem = <T extends HasId = any>({
80
+ const RawTreeItem = <T extends { id: string } = any>({
66
81
  item,
67
- path: _path,
82
+ path: pathProp,
83
+ levelOffset = 2,
68
84
  last,
69
- draggable: _draggable,
85
+ draggable: draggableProp,
70
86
  renderColumns: Columns,
87
+ blockInstruction,
71
88
  canDrop,
89
+ canSelect,
72
90
  onOpenChange,
73
91
  onSelect,
74
- levelOffset = 2,
92
+ onItemHover,
75
93
  }: TreeItemProps<T>) => {
76
94
  const rowRef = useRef<HTMLDivElement | null>(null);
77
95
  const buttonRef = useRef<HTMLButtonElement | null>(null);
78
96
  const openRef = useRef(false);
79
97
  const cancelExpandRef = useRef<NodeJS.Timeout | null>(null);
80
- const [_state, setState] = useState<TreeItemState>('idle');
98
+ const [_state, setState] = useState<TreeItemDragState>('idle');
81
99
  const [instruction, setInstruction] = useState<Instruction | null>(null);
82
100
  const [menuOpen, setMenuOpen] = useState(false);
83
101
 
84
- const { useItems, getProps, isOpen, isCurrent } = useTree();
85
- const items = useItems(item);
86
- const { id, label, parentOf, icon, disabled, className, headingClassName, testId } = getProps(item, _path);
87
- const path = useMemo(() => [..._path, id], [_path, id]);
88
- const open = isOpen(path, item);
89
- 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
+
90
127
  const level = path.length - levelOffset;
91
128
  const isBranch = !!parentOf;
92
129
  const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
130
+ const canSelectItem = canSelect?.({ item, path }) ?? true;
131
+ const data = { id, path, item } satisfies TreeData;
93
132
 
94
133
  const cancelExpand = useCallback(() => {
95
134
  if (cancelExpandRef.current) {
@@ -98,19 +137,19 @@ const RawTreeItem = <T extends HasId = any>({
98
137
  }
99
138
  }, []);
100
139
 
140
+ const isItemDraggable = draggableProp && itemDraggable !== false;
141
+ const isItemDroppable = itemDroppable !== false;
142
+
101
143
  useEffect(() => {
102
- if (!_draggable) {
144
+ if (!draggableProp) {
103
145
  return;
104
146
  }
105
147
 
106
148
  invariant(buttonRef.current);
107
149
 
108
- const data = { id, path, item } satisfies TreeData;
109
-
110
- // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
111
- return combine(
150
+ const makeDraggable = () =>
112
151
  draggable({
113
- element: buttonRef.current,
152
+ element: buttonRef.current!,
114
153
  getInitialData: () => data,
115
154
  onDragStart: () => {
116
155
  setState('dragging');
@@ -125,96 +164,132 @@ const RawTreeItem = <T extends HasId = any>({
125
164
  onOpenChange?.({ item, path, open: true });
126
165
  }
127
166
  },
128
- }),
129
- // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx
130
- dropTargetForElements({
131
- element: buttonRef.current,
132
- getData: ({ input, element }) => {
133
- return attachInstruction(data, {
134
- input,
135
- element,
136
- indentPerLevel: DEFAULT_INDENTATION,
137
- currentLevel: level,
138
- mode,
139
- block: isBranch ? [] : ['make-child'],
140
- });
141
- },
142
- canDrop: ({ source }) => {
143
- const _canDrop = canDrop ?? (() => true);
144
- return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
145
- },
146
- getIsSticky: () => true,
147
- onDrag: ({ self, source }) => {
148
- const instruction = extractInstruction(self.data);
149
-
150
- if (source.data.id !== id) {
151
- if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
152
- cancelExpandRef.current = setTimeout(() => {
153
- onOpenChange?.({ item, path, open: true });
154
- }, 500);
155
- }
156
-
157
- if (instruction?.type !== 'make-child') {
158
- cancelExpand();
159
- }
160
-
161
- setInstruction(instruction);
162
- } else if (instruction?.type === 'reparent') {
163
- // TODO(wittjosiah): This is not occurring in the current implementation.
164
- setInstruction(instruction);
165
- } else {
166
- 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);
167
202
  }
168
- },
169
- onDragLeave: () => {
170
- cancelExpand();
171
- setInstruction(null);
172
- },
173
- onDrop: () => {
174
- 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 {
175
213
  setInstruction(null);
176
- },
177
- }),
178
- );
179
- }, [_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]);
180
233
 
181
234
  // Cancel expand on unmount.
182
235
  useEffect(() => () => cancelExpand(), [cancelExpand]);
183
236
 
184
- const handleOpenChange = useCallback(
237
+ const handleOpenToggle = useCallback(
185
238
  () => onOpenChange?.({ item, path, open: !open }),
186
239
  [onOpenChange, item, path, open],
187
240
  );
188
241
 
189
242
  const handleSelect = useCallback(
190
243
  (option = false) => {
191
- if (isBranch) {
192
- handleOpenChange();
193
- } 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 });
194
251
  rowRef.current?.focus();
195
252
  onSelect?.({ item, path, current: !current, option });
196
253
  }
197
254
  },
198
- [item, path, current, isBranch, handleOpenChange, onSelect],
255
+ [item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
199
256
  );
200
257
 
201
258
  const handleKeyDown = useCallback(
202
259
  (event: KeyboardEvent) => {
203
260
  switch (event.key) {
204
261
  case 'ArrowRight':
205
- isBranch && !open && handleOpenChange();
206
- break;
207
262
  case 'ArrowLeft':
208
- isBranch && open && handleOpenChange();
209
- break;
210
- case ' ':
211
- handleSelect(event.altKey);
263
+ isBranch && handleOpenToggle();
212
264
  break;
213
265
  }
214
266
  },
215
- [isBranch, open, handleOpenChange, handleSelect],
267
+ [isBranch, open, handleOpenToggle, handleSelect],
268
+ );
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],
216
280
  );
217
281
 
282
+ const childProps = {
283
+ draggable: draggableProp,
284
+ renderColumns: Columns,
285
+ blockInstruction,
286
+ canDrop,
287
+ canSelect,
288
+ onItemHover,
289
+ onOpenChange,
290
+ onSelect,
291
+ };
292
+
218
293
  return (
219
294
  <>
220
295
  <Treegrid.Row
@@ -223,64 +298,68 @@ const RawTreeItem = <T extends HasId = any>({
223
298
  id={id}
224
299
  aria-labelledby={`${id}__label`}
225
300
  parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
226
- classNames={[
227
- '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',
228
309
  hoverableControls,
229
310
  hoverableFocusedKeyboardControls,
230
311
  hoverableFocusedWithinControls,
231
312
  hoverableDescriptionIcons,
313
+ ghostFocusWithin,
232
314
  ghostHover,
233
315
  className,
234
- ]}
235
- data-itemid={id}
236
- data-testid={testId}
237
- // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
238
- // without alerting the user (except for in the correct link element). See also:
239
- // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
240
- aria-current={current ? ('' as 'page') : undefined}
316
+ )}
241
317
  onKeyDown={handleKeyDown}
242
- onContextMenu={(event) => {
243
- event.preventDefault();
244
- setMenuOpen(true);
245
- }}
318
+ onMouseEnter={handleItemHover}
319
+ onContextMenu={handleContextMenu}
246
320
  >
247
- <Treegrid.Cell
248
- indent
249
- 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]'
250
324
  style={paddingIndentation(level)}
251
325
  >
252
- <div role='none' className='flex items-center'>
253
- <TreeItemToggle isBranch={isBranch} open={open} onToggle={handleOpenChange} />
326
+ <Treegrid.Cell classNames='flex items-center'>
327
+ <TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
254
328
  <TreeItemHeading
255
- ref={buttonRef}
256
- label={label}
257
- icon={icon}
258
- className={headingClassName}
259
329
  disabled={disabled}
260
330
  current={current}
331
+ label={label}
332
+ className={headingClassName}
333
+ icon={icon}
334
+ iconHue={iconHue}
261
335
  onSelect={handleSelect}
336
+ ref={buttonRef}
262
337
  />
263
- </div>
338
+ </Treegrid.Cell>
264
339
  {Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
265
340
  {instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
266
- </Treegrid.Cell>
341
+ </div>
267
342
  </Treegrid.Row>
268
343
  {open &&
269
- items.map((item, index) => (
270
- <TreeItem
271
- key={item.id}
272
- item={item}
273
- path={path}
274
- last={index === items.length - 1}
275
- draggable={_draggable}
276
- renderColumns={Columns}
277
- canDrop={canDrop}
278
- onOpenChange={onOpenChange}
279
- onSelect={onSelect}
280
- />
344
+ childIds.map((childId, index) => (
345
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
281
346
  ))}
282
347
  </>
283
348
  );
284
349
  };
285
350
 
286
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))),