@dxos/react-ui-list 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5

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 +696 -712
  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 +696 -712
  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 +8 -10
  40. package/src/components/Accordion/Accordion.tsx +1 -1
  41. package/src/components/Accordion/AccordionItem.tsx +7 -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 +82 -50
  46. package/src/components/List/ListRoot.tsx +4 -4
  47. package/src/components/List/testing.ts +7 -7
  48. package/src/components/Tree/Tree.stories.tsx +174 -82
  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 +222 -135
  52. package/src/components/Tree/TreeItemHeading.tsx +13 -12
  53. package/src/components/Tree/TreeItemToggle.tsx +29 -19
  54. package/src/components/Tree/index.ts +2 -0
  55. package/src/components/Tree/testing.ts +10 -9
@@ -2,38 +2,49 @@
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
- attachInstruction,
9
- extractInstruction,
10
6
  type Instruction,
11
7
  type ItemMode,
8
+ attachInstruction,
9
+ extractInstruction,
12
10
  } 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';
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';
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
35
  mx,
25
- } from '@dxos/react-ui-theme';
36
+ } from '@dxos/ui-theme';
26
37
 
38
+ import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
27
39
  import { useTree } from './TreeContext';
28
40
  import { TreeItemHeading } from './TreeItemHeading';
29
41
  import { TreeItemToggle } from './TreeItemToggle';
30
- import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
31
-
32
- type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
33
42
 
34
43
  const hoverableDescriptionIcons =
35
44
  '[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
36
45
 
46
+ type TreeItemDragState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
47
+
37
48
  export const TreeDataSchema = Schema.Struct({
38
49
  id: Schema.String,
39
50
  path: Schema.Array(Schema.String),
@@ -41,55 +52,84 @@ export const TreeDataSchema = Schema.Struct({
41
52
  });
42
53
 
43
54
  export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
44
-
45
55
  export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
46
56
 
47
- 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> = {
48
66
  item: T;
49
67
  path: string[];
50
68
  levelOffset?: number;
51
69
  last: boolean;
52
70
  draggable?: boolean;
53
- renderColumns?: FC<{
54
- item: T;
55
- path: string[];
56
- open: boolean;
57
- menuOpen: boolean;
58
- setMenuOpen: (open: boolean) => void;
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;
132
+ const shouldSeedNativeDragData = typeof document !== 'undefined' && document.body.hasAttribute('data-platform');
93
133
 
94
134
  const cancelExpand = useCallback(() => {
95
135
  if (cancelExpandRef.current) {
@@ -98,20 +138,27 @@ const RawTreeItem = <T extends HasId = any>({
98
138
  }
99
139
  }, []);
100
140
 
141
+ const isItemDraggable = draggableProp && itemDraggable !== false;
142
+ const isItemDroppable = itemDroppable !== false;
143
+ const nativeDragText = id;
144
+
101
145
  useEffect(() => {
102
- if (!_draggable) {
146
+ if (!draggableProp) {
103
147
  return;
104
148
  }
105
149
 
106
150
  invariant(buttonRef.current);
107
151
 
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(
152
+ const makeDraggable = () =>
112
153
  draggable({
113
- element: buttonRef.current,
154
+ element: buttonRef.current!,
114
155
  getInitialData: () => data,
156
+ getInitialDataForExternal: () => {
157
+ if (!shouldSeedNativeDragData) {
158
+ return {};
159
+ }
160
+ return { 'text/plain': nativeDragText };
161
+ },
115
162
  onDragStart: () => {
116
163
  setState('dragging');
117
164
  if (open) {
@@ -125,96 +172,132 @@ const RawTreeItem = <T extends HasId = any>({
125
172
  onOpenChange?.({ item, path, open: true });
126
173
  }
127
174
  },
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);
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);
167
210
  }
168
- },
169
- onDragLeave: () => {
170
- cancelExpand();
171
- setInstruction(null);
172
- },
173
- onDrop: () => {
174
- 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 {
175
221
  setInstruction(null);
176
- },
177
- }),
178
- );
179
- }, [_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]);
180
241
 
181
242
  // Cancel expand on unmount.
182
243
  useEffect(() => () => cancelExpand(), [cancelExpand]);
183
244
 
184
- const handleOpenChange = useCallback(
245
+ const handleOpenToggle = useCallback(
185
246
  () => onOpenChange?.({ item, path, open: !open }),
186
247
  [onOpenChange, item, path, open],
187
248
  );
188
249
 
189
250
  const handleSelect = useCallback(
190
251
  (option = false) => {
191
- if (isBranch) {
192
- handleOpenChange();
193
- } 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 });
194
259
  rowRef.current?.focus();
195
260
  onSelect?.({ item, path, current: !current, option });
196
261
  }
197
262
  },
198
- [item, path, current, isBranch, handleOpenChange, onSelect],
263
+ [item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
199
264
  );
200
265
 
201
266
  const handleKeyDown = useCallback(
202
267
  (event: KeyboardEvent) => {
203
268
  switch (event.key) {
204
269
  case 'ArrowRight':
205
- isBranch && !open && handleOpenChange();
206
- break;
207
270
  case 'ArrowLeft':
208
- isBranch && open && handleOpenChange();
209
- break;
210
- case ' ':
211
- handleSelect(event.altKey);
271
+ isBranch && handleOpenToggle();
212
272
  break;
213
273
  }
214
274
  },
215
- [isBranch, open, handleOpenChange, handleSelect],
275
+ [isBranch, open, handleOpenToggle, handleSelect],
216
276
  );
217
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
+
218
301
  return (
219
302
  <>
220
303
  <Treegrid.Row
@@ -222,65 +305,69 @@ const RawTreeItem = <T extends HasId = any>({
222
305
  key={id}
223
306
  id={id}
224
307
  aria-labelledby={`${id}__label`}
225
- parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
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}
226
315
  classNames={mx(
227
- 'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
316
+ 'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-active-surface',
228
317
  hoverableControls,
229
318
  hoverableFocusedKeyboardControls,
230
319
  hoverableFocusedWithinControls,
231
320
  hoverableDescriptionIcons,
321
+ ghostFocusWithin,
232
322
  ghostHover,
233
323
  className,
234
324
  )}
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}
241
325
  onKeyDown={handleKeyDown}
242
- onContextMenu={(event) => {
243
- event.preventDefault();
244
- setMenuOpen(true);
245
- }}
326
+ onMouseEnter={handleItemHover}
327
+ onContextMenu={handleContextMenu}
246
328
  >
247
- <Treegrid.Cell
248
- indent
249
- 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]'
250
332
  style={paddingIndentation(level)}
251
333
  >
252
- <div role='none' className='flex items-center'>
253
- <TreeItemToggle isBranch={isBranch} open={open} onToggle={handleOpenChange} />
334
+ <Treegrid.Cell classNames='flex items-center'>
335
+ <TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
254
336
  <TreeItemHeading
255
- ref={buttonRef}
256
- label={label}
257
- icon={icon}
258
- className={headingClassName}
259
337
  disabled={disabled}
260
338
  current={current}
339
+ label={label}
340
+ className={headingClassName}
341
+ icon={icon}
342
+ iconHue={iconHue}
261
343
  onSelect={handleSelect}
344
+ ref={buttonRef}
262
345
  />
263
- </div>
346
+ </Treegrid.Cell>
264
347
  {Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
265
348
  {instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
266
- </Treegrid.Cell>
349
+ </div>
267
350
  </Treegrid.Row>
268
351
  {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
- />
352
+ childIds.map((childId, index) => (
353
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
281
354
  ))}
282
355
  </>
283
356
  );
284
357
  };
285
358
 
286
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,25 +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 { mx } from '@dxos/react-ui-theme';
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.
13
- export type NavTreeItemHeadingProps = {
13
+ export type TreeItemHeadingProps = {
14
14
  label: Label;
15
- icon?: string;
16
15
  className?: string;
16
+ icon?: string;
17
+ iconHue?: string;
17
18
  disabled?: boolean;
18
19
  current?: boolean;
19
20
  onSelect?: (option: boolean) => void;
20
21
  };
21
22
 
22
23
  export const TreeItemHeading = memo(
23
- forwardRef<HTMLButtonElement, NavTreeItemHeadingProps>(
24
- ({ label, icon, className, disabled, current, onSelect }, forwardedRef) => {
24
+ forwardRef<HTMLButtonElement, TreeItemHeadingProps>(
25
+ ({ label, className, icon, iconHue, disabled, current, onSelect }, forwardedRef) => {
25
26
  const { t } = useTranslation();
27
+ const styles = iconHue ? getStyles(iconHue) : undefined;
26
28
 
27
29
  const handleSelect = useCallback(
28
30
  (event: MouseEvent) => {
@@ -54,19 +56,18 @@ export const TreeItemHeading = memo(
54
56
  <Button
55
57
  data-testid='treeItem.heading'
56
58
  variant='ghost'
57
- density='fine'
58
- classNames={mx(
59
- 'grow gap-2 pis-0.5 hover:bg-transparent dark:hover:bg-transparent',
59
+ classNames={[
60
+ 'grow gap-2 ps-0.5 hover:bg-transparent dark:hover:bg-transparent',
60
61
  'disabled:cursor-default disabled:opacity-100',
61
62
  className,
62
- )}
63
+ ]}
63
64
  disabled={disabled}
64
65
  onClick={handleSelect}
65
66
  onKeyDown={handleButtonKeydown}
66
67
  {...(current && { 'aria-current': 'location' })}
67
68
  >
68
- {icon && <Icon icon={icon ?? 'ph--placeholder--regular'} size={5} classNames='mlb-1' />}
69
- <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>
70
71
  {toLocalizedString(label, t)}
71
72
  </span>
72
73
  </Button>
@@ -4,30 +4,40 @@
4
4
 
5
5
  import React, { forwardRef, memo } from 'react';
6
6
 
7
- import { Button, Icon } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
7
+ import { IconButton, type IconButtonProps } from '@dxos/react-ui';
9
8
 
10
- export type TreeItemToggleProps = {
9
+ export type TreeItemToggleProps = Omit<IconButtonProps, 'icon' | 'size' | 'label'> & {
11
10
  open?: boolean;
12
11
  isBranch?: boolean;
13
- onToggle?: () => void;
14
12
  hidden?: boolean;
15
13
  };
16
14
 
17
15
  export const TreeItemToggle = memo(
18
- forwardRef<HTMLButtonElement, TreeItemToggleProps>(({ open, isBranch, hidden, onToggle }, forwardedRef) => {
19
- return (
20
- <Button
21
- ref={forwardedRef}
22
- data-testid='treeItem.toggle'
23
- aria-expanded={open}
24
- variant='ghost'
25
- density='fine'
26
- classNames={mx('is-6 pli-0 dx-focus-ring-inset', hidden ? 'hidden' : !isBranch && 'invisible')}
27
- onClick={onToggle}
28
- >
29
- <Icon icon='ph--caret-right--bold' size={3} classNames={mx('transition duration-200', open && 'rotate-90')} />
30
- </Button>
31
- );
32
- }),
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
+ ),
33
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';