@dxos/react-ui-list 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae

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 +676 -729
  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 +676 -729
  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 +20 -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 +31 -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 +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 +17 -9
  51. package/src/components/Tree/TreeItem.tsx +141 -83
  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,73 @@ 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 { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = useAtomValue(
111
+ itemPropsAtom(path),
112
+ );
113
+ const childIds = useAtomValue(childIdsAtom(item.id));
114
+ const open = useAtomValue(itemOpenAtom(path));
115
+ const current = useAtomValue(itemCurrentAtom(path));
116
+
89
117
  const level = path.length - levelOffset;
90
118
  const isBranch = !!parentOf;
91
119
  const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
120
+ const canSelectItem = canSelect?.({ item, path }) ?? true;
121
+ const data = { id, path, item } satisfies TreeData;
92
122
 
93
123
  const cancelExpand = useCallback(() => {
94
124
  if (cancelExpandRef.current) {
@@ -98,14 +128,12 @@ const RawTreeItem = <T extends HasId = any>({
98
128
  }, []);
99
129
 
100
130
  useEffect(() => {
101
- if (!_draggable) {
131
+ if (!draggableProp) {
102
132
  return;
103
133
  }
104
134
 
105
135
  invariant(buttonRef.current);
106
136
 
107
- const data = { id, path, item } satisfies TreeData;
108
-
109
137
  // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
110
138
  return combine(
111
139
  draggable({
@@ -144,7 +172,11 @@ const RawTreeItem = <T extends HasId = any>({
144
172
  },
145
173
  getIsSticky: () => true,
146
174
  onDrag: ({ self, source }) => {
147
- const instruction = extractInstruction(self.data);
175
+ const desired = extractInstruction(self.data);
176
+ const block =
177
+ desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
178
+ const instruction: Instruction | null =
179
+ block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
148
180
 
149
181
  if (source.data.id !== id) {
150
182
  if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
@@ -175,45 +207,67 @@ const RawTreeItem = <T extends HasId = any>({
175
207
  },
176
208
  }),
177
209
  );
178
- }, [_draggable, item, id, mode, path, open, canDrop]);
210
+ }, [draggableProp, item, id, mode, path, open, blockInstruction, canDrop]);
179
211
 
180
212
  // Cancel expand on unmount.
181
213
  useEffect(() => () => cancelExpand(), [cancelExpand]);
182
214
 
183
- const handleOpenChange = useCallback(
215
+ const handleOpenToggle = useCallback(
184
216
  () => onOpenChange?.({ item, path, open: !open }),
185
217
  [onOpenChange, item, path, open],
186
218
  );
187
219
 
188
220
  const handleSelect = useCallback(
189
221
  (option = false) => {
190
- if (isBranch) {
191
- handleOpenChange();
192
- } else {
222
+ // If the item is a branch, toggle it if:
223
+ // - also holding down the option key
224
+ // - or the item is currently selected
225
+ if (isBranch && (option || current)) {
226
+ handleOpenToggle();
227
+ } else if (canSelectItem) {
228
+ canSelect?.({ item, path });
193
229
  rowRef.current?.focus();
194
230
  onSelect?.({ item, path, current: !current, option });
195
231
  }
196
232
  },
197
- [item, path, current, isBranch, handleOpenChange, onSelect],
233
+ [item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
198
234
  );
199
235
 
200
236
  const handleKeyDown = useCallback(
201
237
  (event: KeyboardEvent) => {
202
238
  switch (event.key) {
203
239
  case 'ArrowRight':
204
- isBranch && !open && handleOpenChange();
205
- break;
206
240
  case 'ArrowLeft':
207
- isBranch && open && handleOpenChange();
208
- break;
209
- case ' ':
210
- handleSelect(event.altKey);
241
+ isBranch && handleOpenToggle();
211
242
  break;
212
243
  }
213
244
  },
214
- [isBranch, open, handleOpenChange, handleSelect],
245
+ [isBranch, open, handleOpenToggle, handleSelect],
246
+ );
247
+
248
+ const handleItemHover = useCallback(() => {
249
+ onItemHover?.({ item });
250
+ }, [onItemHover, item]);
251
+
252
+ const handleContextMenu = useCallback(
253
+ (event: MouseEvent) => {
254
+ event.preventDefault();
255
+ setMenuOpen(true);
256
+ },
257
+ [setMenuOpen],
215
258
  );
216
259
 
260
+ const childProps = {
261
+ draggable: draggableProp,
262
+ renderColumns: Columns,
263
+ blockInstruction,
264
+ canDrop,
265
+ canSelect,
266
+ onItemHover,
267
+ onOpenChange,
268
+ onSelect,
269
+ };
270
+
217
271
  return (
218
272
  <>
219
273
  <Treegrid.Row
@@ -222,64 +276,68 @@ const RawTreeItem = <T extends HasId = any>({
222
276
  id={id}
223
277
  aria-labelledby={`${id}__label`}
224
278
  parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
225
- classNames={[
226
- 'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
279
+ data-object-id={id}
280
+ data-testid={testId}
281
+ // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
282
+ // without alerting the user (except for in the correct link element). See also:
283
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
284
+ aria-current={current ? ('' as 'page') : undefined}
285
+ classNames={mx(
286
+ 'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-active-surface',
227
287
  hoverableControls,
228
288
  hoverableFocusedKeyboardControls,
229
289
  hoverableFocusedWithinControls,
230
290
  hoverableDescriptionIcons,
291
+ ghostFocusWithin,
231
292
  ghostHover,
232
293
  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}
294
+ )}
240
295
  onKeyDown={handleKeyDown}
241
- onContextMenu={(event) => {
242
- event.preventDefault();
243
- setMenuOpen(true);
244
- }}
296
+ onMouseEnter={handleItemHover}
297
+ onContextMenu={handleContextMenu}
245
298
  >
246
- <Treegrid.Cell
247
- indent
248
- classNames='relative grid grid-cols-subgrid col-[tree-row]'
299
+ <div
300
+ role='none'
301
+ className='indent relative grid grid-cols-subgrid col-[tree-row]'
249
302
  style={paddingIndentation(level)}
250
303
  >
251
- <div role='none' className='flex items-center'>
252
- <TreeItemToggle isBranch={isBranch} open={open} onToggle={handleOpenChange} />
304
+ <Treegrid.Cell classNames='flex items-center'>
305
+ <TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
253
306
  <TreeItemHeading
254
- ref={buttonRef}
255
- label={label}
256
- icon={icon}
257
- className={headingClassName}
258
307
  disabled={disabled}
259
308
  current={current}
309
+ label={label}
310
+ className={headingClassName}
311
+ icon={icon}
312
+ iconHue={iconHue}
260
313
  onSelect={handleSelect}
314
+ ref={buttonRef}
261
315
  />
262
- </div>
316
+ </Treegrid.Cell>
263
317
  {Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
264
318
  {instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
265
- </Treegrid.Cell>
319
+ </div>
266
320
  </Treegrid.Row>
267
321
  {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
- />
322
+ childIds.map((childId, index) => (
323
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
280
324
  ))}
281
325
  </>
282
326
  );
283
327
  };
284
328
 
285
329
  export const TreeItem = memo(RawTreeItem) as FC<TreeItemProps>;
330
+
331
+ /** Resolves a child ID to an item via the `item` atom and renders a TreeItem. */
332
+ export type TreeItemByIdProps = Omit<TreeItemProps, 'item'> & { id: string };
333
+
334
+ const RawTreeItemById = <T extends { id: string } = any>({ id, ...props }: TreeItemByIdProps) => {
335
+ const { item: itemAtom } = useTree();
336
+ const item = useAtomValue(itemAtom(id)) as T | undefined;
337
+ if (!item) {
338
+ return null;
339
+ }
340
+ return <TreeItem item={item} {...props} />;
341
+ };
342
+
343
+ 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))),