@dxos/react-ui-list 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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 (41) hide show
  1. package/dist/lib/browser/index.mjs +233 -194
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +233 -194
  5. package/dist/lib/node-esm/index.mjs.map +3 -3
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  8. package/dist/types/src/components/List/List.d.ts +6 -4
  9. package/dist/types/src/components/List/List.d.ts.map +1 -1
  10. package/dist/types/src/components/List/ListItem.d.ts +8 -6
  11. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  12. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  13. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  14. package/dist/types/src/components/Tree/Tree.d.ts +6 -5
  15. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  16. package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
  17. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Tree/TreeContext.d.ts +21 -10
  19. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  20. package/dist/types/src/components/Tree/TreeItem.d.ts +8 -0
  21. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  22. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  23. package/dist/types/src/components/Tree/index.d.ts +2 -0
  24. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  25. package/dist/types/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +19 -18
  27. package/src/components/Accordion/Accordion.stories.tsx +6 -6
  28. package/src/components/Accordion/AccordionItem.tsx +1 -2
  29. package/src/components/List/List.stories.tsx +10 -10
  30. package/src/components/List/List.tsx +4 -9
  31. package/src/components/List/ListItem.tsx +58 -38
  32. package/src/components/List/ListRoot.tsx +3 -3
  33. package/src/components/List/testing.ts +4 -4
  34. package/src/components/Tree/Tree.stories.tsx +106 -31
  35. package/src/components/Tree/Tree.tsx +30 -40
  36. package/src/components/Tree/TreeContext.tsx +18 -9
  37. package/src/components/Tree/TreeItem.tsx +178 -103
  38. package/src/components/Tree/TreeItemHeading.tsx +3 -4
  39. package/src/components/Tree/TreeItemToggle.tsx +4 -4
  40. package/src/components/Tree/index.ts +2 -0
  41. package/src/components/Tree/testing.ts +5 -5
@@ -2,25 +2,37 @@
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
27
  import { invariant } from '@dxos/invariant';
17
- import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
28
+ import { TreeItem as NaturalTreeItem, Treegrid, TREEGRID_PARENT_OF_SEPARATOR } from '@dxos/react-ui';
18
29
  import {
19
30
  ghostFocusWithin,
20
31
  ghostHover,
21
32
  hoverableControls,
22
33
  hoverableFocusedKeyboardControls,
23
34
  hoverableFocusedWithinControls,
35
+ mx,
24
36
  } from '@dxos/ui-theme';
25
37
 
26
38
  import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
@@ -31,7 +43,7 @@ import { TreeItemToggle } from './TreeItemToggle';
31
43
  const hoverableDescriptionIcons =
32
44
  '[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
33
45
 
34
- type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
46
+ type TreeItemDragState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
35
47
 
36
48
  export const TreeDataSchema = Schema.Struct({
37
49
  id: Schema.String,
@@ -62,39 +74,62 @@ export type TreeItemProps<T extends { id: string } = any> = {
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
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,
74
87
  blockInstruction,
75
88
  canDrop,
76
89
  canSelect,
77
90
  onOpenChange,
78
91
  onSelect,
92
+ onItemHover,
79
93
  }: TreeItemProps<T>) => {
80
94
  const rowRef = useRef<HTMLDivElement | null>(null);
81
95
  const buttonRef = useRef<HTMLButtonElement | null>(null);
82
96
  const openRef = useRef(false);
83
97
  const cancelExpandRef = useRef<NodeJS.Timeout | null>(null);
84
- const [_state, setState] = useState<TreeItemState>('idle');
98
+ const [_state, setState] = useState<TreeItemDragState>('idle');
85
99
  const [instruction, setInstruction] = useState<Instruction | null>(null);
86
100
  const [menuOpen, setMenuOpen] = useState(false);
87
101
 
88
- const { useItems, getProps, useIsOpen, useIsCurrent } = useTree();
89
- const items = useItems(item);
90
- const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = getProps(item, _path);
91
- const path = useMemo(() => [..._path, id], [_path, id]);
92
- const open = useIsOpen(path, item);
93
- const current = useIsCurrent(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
+
94
127
  const level = path.length - levelOffset;
95
128
  const isBranch = !!parentOf;
96
129
  const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
97
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');
98
133
 
99
134
  const cancelExpand = useCallback(() => {
100
135
  if (cancelExpandRef.current) {
@@ -103,20 +138,27 @@ const RawTreeItem = <T extends { id: string } = any>({
103
138
  }
104
139
  }, []);
105
140
 
141
+ const isItemDraggable = draggableProp && itemDraggable !== false;
142
+ const isItemDroppable = itemDroppable !== false;
143
+ const nativeDragText = id;
144
+
106
145
  useEffect(() => {
107
- if (!_draggable) {
146
+ if (!draggableProp) {
108
147
  return;
109
148
  }
110
149
 
111
150
  invariant(buttonRef.current);
112
151
 
113
- const data = { id, path, item } satisfies TreeData;
114
-
115
- // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
116
- return combine(
152
+ const makeDraggable = () =>
117
153
  draggable({
118
- element: buttonRef.current,
154
+ element: buttonRef.current!,
119
155
  getInitialData: () => data,
156
+ getInitialDataForExternal: () => {
157
+ if (!shouldSeedNativeDragData) {
158
+ return {};
159
+ }
160
+ return { 'text/plain': nativeDragText };
161
+ },
120
162
  onDragStart: () => {
121
163
  setState('dragging');
122
164
  if (open) {
@@ -130,62 +172,72 @@ const RawTreeItem = <T extends { id: string } = any>({
130
172
  onOpenChange?.({ item, path, open: true });
131
173
  }
132
174
  },
133
- }),
134
- // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx
135
- dropTargetForElements({
136
- element: buttonRef.current,
137
- getData: ({ input, element }) => {
138
- return attachInstruction(data, {
139
- input,
140
- element,
141
- indentPerLevel: DEFAULT_INDENTATION,
142
- currentLevel: level,
143
- mode,
144
- block: isBranch ? [] : ['make-child'],
145
- });
146
- },
147
- canDrop: ({ source }) => {
148
- const _canDrop = canDrop ?? (() => true);
149
- return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
150
- },
151
- getIsSticky: () => true,
152
- onDrag: ({ self, source }) => {
153
- const desired = extractInstruction(self.data);
154
- const block =
155
- desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
156
- const instruction: Instruction | null =
157
- block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
158
-
159
- if (source.data.id !== id) {
160
- if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
161
- cancelExpandRef.current = setTimeout(() => {
162
- onOpenChange?.({ item, path, open: true });
163
- }, 500);
164
- }
165
-
166
- if (instruction?.type !== 'make-child') {
167
- cancelExpand();
168
- }
169
-
170
- setInstruction(instruction);
171
- } else if (instruction?.type === 'reparent') {
172
- // TODO(wittjosiah): This is not occurring in the current implementation.
173
- setInstruction(instruction);
174
- } else {
175
- 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);
176
210
  }
177
- },
178
- onDragLeave: () => {
179
- cancelExpand();
180
- setInstruction(null);
181
- },
182
- onDrop: () => {
183
- 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 {
184
221
  setInstruction(null);
185
- },
186
- }),
187
- );
188
- }, [_draggable, item, id, mode, path, open, blockInstruction, 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]);
189
241
 
190
242
  // Cancel expand on unmount.
191
243
  useEffect(() => () => cancelExpand(), [cancelExpand]);
@@ -223,6 +275,29 @@ const RawTreeItem = <T extends { id: string } = any>({
223
275
  [isBranch, open, handleOpenToggle, handleSelect],
224
276
  );
225
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
+
226
301
  return (
227
302
  <>
228
303
  <Treegrid.Row
@@ -230,28 +305,26 @@ const RawTreeItem = <T extends { id: string } = any>({
230
305
  key={id}
231
306
  id={id}
232
307
  aria-labelledby={`${id}__label`}
233
- parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
234
- classNames={[
235
- 'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
236
- hoverableControls,
237
- hoverableFocusedKeyboardControls,
238
- hoverableFocusedWithinControls,
239
- hoverableDescriptionIcons,
240
- ghostHover,
241
- ghostFocusWithin,
242
- className,
243
- ]}
308
+ parentOf={parentOf?.join(TREEGRID_PARENT_OF_SEPARATOR)}
244
309
  data-object-id={id}
245
310
  data-testid={testId}
246
311
  // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
247
312
  // without alerting the user (except for in the correct link element). See also:
248
313
  // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
249
314
  aria-current={current ? ('' as 'page') : undefined}
315
+ classNames={mx(
316
+ 'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-active-surface',
317
+ hoverableControls,
318
+ hoverableFocusedKeyboardControls,
319
+ hoverableFocusedWithinControls,
320
+ hoverableDescriptionIcons,
321
+ ghostFocusWithin,
322
+ ghostHover,
323
+ className,
324
+ )}
250
325
  onKeyDown={handleKeyDown}
251
- onContextMenu={(event) => {
252
- event.preventDefault();
253
- setMenuOpen(true);
254
- }}
326
+ onMouseEnter={handleItemHover}
327
+ onContextMenu={handleContextMenu}
255
328
  >
256
329
  <div
257
330
  role='none'
@@ -276,23 +349,25 @@ const RawTreeItem = <T extends { id: string } = any>({
276
349
  </div>
277
350
  </Treegrid.Row>
278
351
  {open &&
279
- items.map((item, index) => (
280
- <TreeItem
281
- key={item.id}
282
- item={item}
283
- path={path}
284
- last={index === items.length - 1}
285
- draggable={_draggable}
286
- renderColumns={Columns}
287
- blockInstruction={blockInstruction}
288
- canDrop={canDrop}
289
- canSelect={canSelect}
290
- onOpenChange={onOpenChange}
291
- onSelect={onSelect}
292
- />
352
+ childIds.map((childId, index) => (
353
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
293
354
  ))}
294
355
  </>
295
356
  );
296
357
  };
297
358
 
298
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>;
@@ -56,9 +56,8 @@ export const TreeItemHeading = memo(
56
56
  <Button
57
57
  data-testid='treeItem.heading'
58
58
  variant='ghost'
59
- density='fine'
60
59
  classNames={[
61
- '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',
62
61
  'disabled:cursor-default disabled:opacity-100',
63
62
  className,
64
63
  ]}
@@ -67,8 +66,8 @@ export const TreeItemHeading = memo(
67
66
  onKeyDown={handleButtonKeydown}
68
67
  {...(current && { 'aria-current': 'location' })}
69
68
  >
70
- {icon && <Icon icon={icon ?? 'ph--placeholder--regular'} size={5} classNames={['mlb-1', styles?.icon]} />}
71
- <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>
72
71
  {toLocalizedString(label, t)}
73
72
  </span>
74
73
  </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';
@@ -7,7 +7,7 @@ import * as Schema from 'effect/Schema';
7
7
 
8
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
 
@@ -26,18 +26,18 @@ export const TestItemSchema = Schema.Struct({
26
26
  });
27
27
 
28
28
  export const createTree = (n = 4, d = 4): TestItem => ({
29
- id: faker.string.uuid(),
30
- name: faker.commerce.productName(),
29
+ id: random.string.uuid(),
30
+ name: random.commerce.productName(),
31
31
  icon:
32
32
  d === 3
33
33
  ? undefined
34
- : faker.helpers.arrayElement([
34
+ : random.helpers.arrayElement([
35
35
  'ph--planet--regular',
36
36
  'ph--sailboat--regular',
37
37
  'ph--house--regular',
38
38
  'ph--gear--regular',
39
39
  ]),
40
- 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 }) : [],
41
41
  });
42
42
 
43
43
  const removeItem = (tree: TestItem, source: TreeData) => {