@dxos/react-ui-list 0.8.4-main.ead640a → 0.8.4-main.f466a3d56e

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 (98) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +1340 -728
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +1340 -728
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
  9. package/dist/types/src/components/Accordion/Accordion.d.ts.map +1 -1
  10. package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  12. package/dist/types/src/components/Accordion/AccordionRoot.d.ts +1 -1
  13. package/dist/types/src/components/Accordion/AccordionRoot.d.ts.map +1 -1
  14. package/dist/types/src/components/Combobox/Combobox.d.ts +105 -0
  15. package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -0
  16. package/dist/types/src/components/Combobox/Combobox.stories.d.ts +12 -0
  17. package/dist/types/src/components/Combobox/Combobox.stories.d.ts.map +1 -0
  18. package/dist/types/src/components/Combobox/index.d.ts +2 -0
  19. package/dist/types/src/components/Combobox/index.d.ts.map +1 -0
  20. package/dist/types/src/components/List/List.d.ts +19 -8
  21. package/dist/types/src/components/List/List.d.ts.map +1 -1
  22. package/dist/types/src/components/List/List.stories.d.ts +2 -2
  23. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/List/ListItem.d.ts +10 -8
  25. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  26. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  27. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  28. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  29. package/dist/types/src/components/Listbox/Listbox.d.ts +27 -0
  30. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -0
  31. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +12 -0
  32. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Listbox/index.d.ts +2 -0
  34. package/dist/types/src/components/Listbox/index.d.ts.map +1 -0
  35. package/dist/types/src/components/Picker/Picker.d.ts +49 -0
  36. package/dist/types/src/components/Picker/Picker.d.ts.map +1 -0
  37. package/dist/types/src/components/Picker/Picker.stories.d.ts +28 -0
  38. package/dist/types/src/components/Picker/Picker.stories.d.ts.map +1 -0
  39. package/dist/types/src/components/Picker/context.d.ts +29 -0
  40. package/dist/types/src/components/Picker/context.d.ts.map +1 -0
  41. package/dist/types/src/components/Picker/index.d.ts +3 -0
  42. package/dist/types/src/components/Picker/index.d.ts.map +1 -0
  43. package/dist/types/src/components/RowList/RowList.d.ts +61 -0
  44. package/dist/types/src/components/RowList/RowList.d.ts.map +1 -0
  45. package/dist/types/src/components/RowList/RowList.stories.d.ts +35 -0
  46. package/dist/types/src/components/RowList/RowList.stories.d.ts.map +1 -0
  47. package/dist/types/src/components/RowList/index.d.ts +3 -0
  48. package/dist/types/src/components/RowList/index.d.ts.map +1 -0
  49. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  50. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  51. package/dist/types/src/components/Tree/Tree.stories.d.ts +9 -28
  52. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  53. package/dist/types/src/components/Tree/TreeContext.d.ts +22 -9
  54. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  55. package/dist/types/src/components/Tree/TreeItem.d.ts +20 -3
  56. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  57. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +1 -1
  58. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  59. package/dist/types/src/components/Tree/helpers.d.ts.map +1 -1
  60. package/dist/types/src/components/Tree/index.d.ts +2 -0
  61. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  62. package/dist/types/src/components/Tree/testing.d.ts +2 -2
  63. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  64. package/dist/types/src/components/index.d.ts +4 -0
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/util/path.d.ts.map +1 -1
  67. package/dist/types/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +35 -32
  69. package/src/components/Accordion/Accordion.stories.tsx +4 -4
  70. package/src/components/Accordion/AccordionItem.tsx +4 -7
  71. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  72. package/src/components/Combobox/Combobox.stories.tsx +60 -0
  73. package/src/components/Combobox/Combobox.tsx +387 -0
  74. package/src/components/Combobox/index.ts +5 -0
  75. package/src/components/List/List.stories.tsx +35 -23
  76. package/src/components/List/List.tsx +14 -10
  77. package/src/components/List/ListItem.tsx +57 -39
  78. package/src/components/List/ListRoot.tsx +3 -3
  79. package/src/components/List/testing.ts +6 -6
  80. package/src/components/Listbox/Listbox.stories.tsx +48 -0
  81. package/src/components/Listbox/Listbox.tsx +201 -0
  82. package/src/components/Listbox/index.ts +5 -0
  83. package/src/components/Picker/Picker.stories.tsx +131 -0
  84. package/src/components/Picker/Picker.tsx +368 -0
  85. package/src/components/Picker/context.ts +43 -0
  86. package/src/components/Picker/index.ts +6 -0
  87. package/src/components/RowList/RowList.stories.tsx +163 -0
  88. package/src/components/RowList/RowList.tsx +350 -0
  89. package/src/components/RowList/index.ts +6 -0
  90. package/src/components/Tree/Tree.stories.tsx +153 -64
  91. package/src/components/Tree/Tree.tsx +40 -42
  92. package/src/components/Tree/TreeContext.tsx +19 -8
  93. package/src/components/Tree/TreeItem.tsx +186 -112
  94. package/src/components/Tree/TreeItemHeading.tsx +9 -6
  95. package/src/components/Tree/TreeItemToggle.tsx +4 -4
  96. package/src/components/Tree/index.ts +2 -0
  97. package/src/components/Tree/testing.ts +9 -8
  98. package/src/components/index.ts +4 -0
@@ -2,27 +2,38 @@
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
6
  type Instruction,
9
7
  type ItemMode,
10
8
  attachInstruction,
11
9
  extractInstruction,
12
10
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
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';
13
14
  import * as Schema from 'effect/Schema';
14
- import React, { type FC, type KeyboardEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
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/internal';
17
27
  import { invariant } from '@dxos/invariant';
18
- import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
28
+ import { TreeItem as NaturalTreeItem, Treegrid, TREEGRID_PARENT_OF_SEPARATOR } from '@dxos/react-ui';
19
29
  import {
20
30
  ghostFocusWithin,
21
31
  ghostHover,
22
32
  hoverableControls,
23
33
  hoverableFocusedKeyboardControls,
24
34
  hoverableFocusedWithinControls,
25
- } from '@dxos/react-ui-theme';
35
+ mx,
36
+ } from '@dxos/ui-theme';
26
37
 
27
38
  import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
28
39
  import { useTree } from './TreeContext';
@@ -32,7 +43,7 @@ import { TreeItemToggle } from './TreeItemToggle';
32
43
  const hoverableDescriptionIcons =
33
44
  '[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
34
45
 
35
- type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
46
+ type TreeItemDragState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
36
47
 
37
48
  export const TreeDataSchema = Schema.Struct({
38
49
  id: Schema.String,
@@ -43,7 +54,7 @@ export const TreeDataSchema = Schema.Struct({
43
54
  export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
44
55
  export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
45
56
 
46
- export type ColumnRenderer<T extends HasId = any> = FC<{
57
+ export type ColumnRenderer<T extends { id: string } = any> = FC<{
47
58
  item: T;
48
59
  path: string[];
49
60
  open: boolean;
@@ -51,52 +62,74 @@ export type ColumnRenderer<T extends HasId = any> = FC<{
51
62
  setMenuOpen: (open: boolean) => void;
52
63
  }>;
53
64
 
54
- export type TreeItemProps<T extends HasId = any> = {
65
+ export type TreeItemProps<T extends { id: string } = any> = {
55
66
  item: T;
56
67
  path: string[];
57
68
  levelOffset?: number;
58
69
  last: boolean;
59
70
  draggable?: boolean;
60
71
  renderColumns?: ColumnRenderer<T>;
72
+ blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
61
73
  canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
62
74
  canSelect?: (params: { item: T; path: string[] }) => boolean;
63
75
  onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
64
76
  onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
77
+ onItemHover?: (params: { item: T }) => void;
65
78
  };
66
79
 
67
- const RawTreeItem = <T extends HasId = any>({
80
+ const RawTreeItem = <T extends { id: string } = any>({
68
81
  item,
69
- path: _path,
82
+ path: pathProp,
70
83
  levelOffset = 2,
71
84
  last,
72
- draggable: _draggable,
85
+ draggable: draggableProp,
73
86
  renderColumns: Columns,
87
+ blockInstruction,
74
88
  canDrop,
75
89
  canSelect,
76
90
  onOpenChange,
77
91
  onSelect,
92
+ onItemHover,
78
93
  }: TreeItemProps<T>) => {
79
94
  const rowRef = useRef<HTMLDivElement | null>(null);
80
95
  const buttonRef = useRef<HTMLButtonElement | null>(null);
81
96
  const openRef = useRef(false);
82
97
  const cancelExpandRef = useRef<NodeJS.Timeout | null>(null);
83
- const [_state, setState] = useState<TreeItemState>('idle');
98
+ const [_state, setState] = useState<TreeItemDragState>('idle');
84
99
  const [instruction, setInstruction] = useState<Instruction | null>(null);
85
100
  const [menuOpen, setMenuOpen] = useState(false);
86
101
 
87
- const { useItems, getProps, isOpen, isCurrent } = useTree();
88
- const items = useItems(item);
89
- const { id, parentOf, label, className, headingClassName, icon, iconClassName, disabled, testId } = getProps(
90
- item,
91
- _path,
92
- );
93
- const path = useMemo(() => [..._path, id], [_path, id]);
94
- const open = isOpen(path, item);
95
- 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
+
96
127
  const level = path.length - levelOffset;
97
128
  const isBranch = !!parentOf;
98
129
  const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
99
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');
100
133
 
101
134
  const cancelExpand = useCallback(() => {
102
135
  if (cancelExpandRef.current) {
@@ -105,20 +138,27 @@ const RawTreeItem = <T extends HasId = any>({
105
138
  }
106
139
  }, []);
107
140
 
141
+ const isItemDraggable = draggableProp && itemDraggable !== false;
142
+ const isItemDroppable = itemDroppable !== false;
143
+ const nativeDragText = id;
144
+
108
145
  useEffect(() => {
109
- if (!_draggable) {
146
+ if (!draggableProp) {
110
147
  return;
111
148
  }
112
149
 
113
150
  invariant(buttonRef.current);
114
151
 
115
- const data = { id, path, item } satisfies TreeData;
116
-
117
- // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
118
- return combine(
152
+ const makeDraggable = () =>
119
153
  draggable({
120
- element: buttonRef.current,
154
+ element: buttonRef.current!,
121
155
  getInitialData: () => data,
156
+ getInitialDataForExternal: () => {
157
+ if (!shouldSeedNativeDragData) {
158
+ return {};
159
+ }
160
+ return { 'text/plain': nativeDragText };
161
+ },
122
162
  onDragStart: () => {
123
163
  setState('dragging');
124
164
  if (open) {
@@ -132,58 +172,72 @@ const RawTreeItem = <T extends HasId = any>({
132
172
  onOpenChange?.({ item, path, open: true });
133
173
  }
134
174
  },
135
- }),
136
- // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx
137
- dropTargetForElements({
138
- element: buttonRef.current,
139
- getData: ({ input, element }) => {
140
- return attachInstruction(data, {
141
- input,
142
- element,
143
- indentPerLevel: DEFAULT_INDENTATION,
144
- currentLevel: level,
145
- mode,
146
- block: isBranch ? [] : ['make-child'],
147
- });
148
- },
149
- canDrop: ({ source }) => {
150
- const _canDrop = canDrop ?? (() => true);
151
- return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
152
- },
153
- getIsSticky: () => true,
154
- onDrag: ({ self, source }) => {
155
- const instruction = extractInstruction(self.data);
156
-
157
- if (source.data.id !== id) {
158
- if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
159
- cancelExpandRef.current = setTimeout(() => {
160
- onOpenChange?.({ item, path, open: true });
161
- }, 500);
162
- }
163
-
164
- if (instruction?.type !== 'make-child') {
165
- cancelExpand();
166
- }
167
-
168
- setInstruction(instruction);
169
- } else if (instruction?.type === 'reparent') {
170
- // TODO(wittjosiah): This is not occurring in the current implementation.
171
- setInstruction(instruction);
172
- } else {
173
- 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);
174
210
  }
175
- },
176
- onDragLeave: () => {
177
- cancelExpand();
178
- setInstruction(null);
179
- },
180
- onDrop: () => {
181
- 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 {
182
221
  setInstruction(null);
183
- },
184
- }),
185
- );
186
- }, [_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]);
187
241
 
188
242
  // Cancel expand on unmount.
189
243
  useEffect(() => () => cancelExpand(), [cancelExpand]);
@@ -221,6 +275,29 @@ const RawTreeItem = <T extends HasId = any>({
221
275
  [isBranch, open, handleOpenToggle, handleSelect],
222
276
  );
223
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
+
224
301
  return (
225
302
  <>
226
303
  <Treegrid.Row
@@ -228,34 +305,28 @@ const RawTreeItem = <T extends HasId = any>({
228
305
  key={id}
229
306
  id={id}
230
307
  aria-labelledby={`${id}__label`}
231
- parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
232
- classNames={[
233
- 'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
308
+ parentOf={parentOf?.join(TREEGRID_PARENT_OF_SEPARATOR)}
309
+ data-object-id={id}
310
+ data-testid={testId}
311
+ // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
312
+ // without alerting the user (except for in the correct link element). See also:
313
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
314
+ aria-current={current ? ('' as 'page') : undefined}
315
+ classNames={mx(
316
+ 'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-current-surface',
234
317
  hoverableControls,
235
318
  hoverableFocusedKeyboardControls,
236
319
  hoverableFocusedWithinControls,
237
320
  hoverableDescriptionIcons,
238
- ghostHover,
239
321
  ghostFocusWithin,
322
+ ghostHover,
240
323
  className,
241
- ]}
242
- data-itemid={id}
243
- data-testid={testId}
244
- // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
245
- // without alerting the user (except for in the correct link element). See also:
246
- // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
247
- aria-current={current ? ('' as 'page') : undefined}
324
+ )}
248
325
  onKeyDown={handleKeyDown}
249
- onContextMenu={(event) => {
250
- event.preventDefault();
251
- setMenuOpen(true);
252
- }}
326
+ onMouseEnter={handleItemHover}
327
+ onContextMenu={handleContextMenu}
253
328
  >
254
- <div
255
- role='none'
256
- className='indent relative grid grid-cols-subgrid col-[tree-row]'
257
- style={paddingIndentation(level)}
258
- >
329
+ <div className='indent relative grid grid-cols-subgrid col-[tree-row]' style={paddingIndentation(level)}>
259
330
  <Treegrid.Cell classNames='flex items-center'>
260
331
  <TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
261
332
  <TreeItemHeading
@@ -264,7 +335,7 @@ const RawTreeItem = <T extends HasId = any>({
264
335
  label={label}
265
336
  className={headingClassName}
266
337
  icon={icon}
267
- iconClassName={iconClassName}
338
+ iconHue={iconHue}
268
339
  onSelect={handleSelect}
269
340
  ref={buttonRef}
270
341
  />
@@ -274,22 +345,25 @@ const RawTreeItem = <T extends HasId = any>({
274
345
  </div>
275
346
  </Treegrid.Row>
276
347
  {open &&
277
- items.map((item, index) => (
278
- <TreeItem
279
- key={item.id}
280
- item={item}
281
- path={path}
282
- last={index === items.length - 1}
283
- draggable={_draggable}
284
- renderColumns={Columns}
285
- canDrop={canDrop}
286
- canSelect={canSelect}
287
- onOpenChange={onOpenChange}
288
- onSelect={onSelect}
289
- />
348
+ childIds.map((childId, index) => (
349
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
290
350
  ))}
291
351
  </>
292
352
  );
293
353
  };
294
354
 
295
355
  export const TreeItem = memo(RawTreeItem) as FC<TreeItemProps>;
356
+
357
+ /** Resolves a child ID to an item via the `item` atom and renders a TreeItem. */
358
+ export type TreeItemByIdProps = Omit<TreeItemProps, 'item'> & { id: string };
359
+
360
+ const RawTreeItemById = <T extends { id: string } = any>({ id, ...props }: TreeItemByIdProps) => {
361
+ const { item: itemAtom } = useTree();
362
+ const item = useAtomValue(itemAtom(id)) as T | undefined;
363
+ if (!item) {
364
+ return null;
365
+ }
366
+ return <TreeItem item={item} {...props} />;
367
+ };
368
+
369
+ export const TreeItemById = memo(RawTreeItemById) as FC<TreeItemByIdProps>;
@@ -6,6 +6,7 @@ import React, { type KeyboardEvent, type MouseEvent, forwardRef, memo, useCallba
6
6
 
7
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.
@@ -13,7 +14,7 @@ export type TreeItemHeadingProps = {
13
14
  label: Label;
14
15
  className?: string;
15
16
  icon?: string;
16
- iconClassName?: string;
17
+ iconHue?: string;
17
18
  disabled?: boolean;
18
19
  current?: boolean;
19
20
  onSelect?: (option: boolean) => void;
@@ -21,8 +22,9 @@ export type TreeItemHeadingProps = {
21
22
 
22
23
  export const TreeItemHeading = memo(
23
24
  forwardRef<HTMLButtonElement, TreeItemHeadingProps>(
24
- ({ label, className, icon, iconClassName, disabled, current, onSelect }, forwardedRef) => {
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,9 +56,8 @@ export const TreeItemHeading = memo(
54
56
  <Button
55
57
  data-testid='treeItem.heading'
56
58
  variant='ghost'
57
- density='fine'
58
59
  classNames={[
59
- 'grow gap-2 pis-0.5 hover:bg-transparent dark:hover:bg-transparent',
60
+ 'grow gap-2 ps-0.5 hover:bg-transparent dark:hover:bg-transparent',
60
61
  'disabled:cursor-default disabled:opacity-100',
61
62
  className,
62
63
  ]}
@@ -65,8 +66,10 @@ export const TreeItemHeading = memo(
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', iconClassName]} />}
69
- <span className='flex-1 is-0 truncate text-start text-sm font-normal' data-tooltip>
69
+ {icon && (
70
+ <Icon size={5} icon={icon ?? 'ph--placeholder--regular'} classNames={['my-1', styles?.foreground]} />
71
+ )}
72
+ <span className='flex-1 w-0 truncate text-start font-normal' data-tooltip>
70
73
  {toLocalizedString(label, t)}
71
74
  </span>
72
75
  </Button>
@@ -14,7 +14,7 @@ export type TreeItemToggleProps = Omit<IconButtonProps, 'icon' | 'size' | 'label
14
14
 
15
15
  export const TreeItemToggle = memo(
16
16
  forwardRef<HTMLButtonElement, TreeItemToggleProps>(
17
- ({ open, isBranch, hidden, classNames, ...props }, forwardedRef) => {
17
+ ({ classNames, open, isBranch, hidden, ...props }, forwardedRef) => {
18
18
  return (
19
19
  <IconButton
20
20
  ref={forwardedRef}
@@ -23,9 +23,9 @@ export const TreeItemToggle = memo(
23
23
  variant='ghost'
24
24
  density='fine'
25
25
  classNames={[
26
- 'bs-full is-6 pli-0',
27
- '[&_svg]:transition-[transform] [&_svg]:duration-200',
28
- open && '[&_svg]:rotate-90',
26
+ 'h-full w-6 px-0',
27
+ '[&_svg]:transition-transform [&_svg]:duration-200',
28
+ open ? '[&_svg]:rotate-90' : '[&_svg]:rotate-0',
29
29
  hidden ? 'hidden' : !isBranch && 'invisible',
30
30
  classNames,
31
31
  ]}
@@ -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';
@@ -5,38 +5,39 @@
5
5
  import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
6
6
  import * as Schema from 'effect/Schema';
7
7
 
8
- import { type HasId, ObjectId } from '@dxos/echo/internal';
8
+ import { Obj } from '@dxos/echo';
9
9
  import { log } from '@dxos/log';
10
- import { faker } from '@dxos/random';
10
+ import { random } 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))),
25
26
  });
26
27
 
27
28
  export const createTree = (n = 4, d = 4): TestItem => ({
28
- id: faker.string.uuid(),
29
- name: faker.commerce.productName(),
29
+ id: random.string.uuid(),
30
+ name: random.commerce.productName(),
30
31
  icon:
31
32
  d === 3
32
33
  ? undefined
33
- : faker.helpers.arrayElement([
34
+ : random.helpers.arrayElement([
34
35
  'ph--planet--regular',
35
36
  'ph--sailboat--regular',
36
37
  'ph--house--regular',
37
38
  'ph--gear--regular',
38
39
  ]),
39
- items: d > 0 ? faker.helpers.multiple(() => createTree(n, d - 1), { count: n }) : [],
40
+ items: d > 0 ? random.helpers.multiple(() => createTree(n, d - 1), { count: n }) : [],
40
41
  });
41
42
 
42
43
  const removeItem = (tree: TestItem, source: TreeData) => {
@@ -3,5 +3,9 @@
3
3
  //
4
4
 
5
5
  export * from './Accordion';
6
+ export * from './Combobox';
6
7
  export * from './List';
8
+ export * from './Listbox';
9
+ export * from './Picker';
10
+ export * from './RowList';
7
11
  export * from './Tree';