@dxos/react-ui-list 0.8.4-main.7ace549 → 0.8.4-main.8360d9e660

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 (44) hide show
  1. package/dist/lib/browser/index.mjs +667 -723
  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 +667 -723
  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/List/List.d.ts +6 -6
  8. package/dist/types/src/components/List/List.d.ts.map +1 -1
  9. package/dist/types/src/components/List/List.stories.d.ts +2 -2
  10. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/List/ListItem.d.ts +8 -6
  12. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  13. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  14. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  15. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  16. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  17. package/dist/types/src/components/Tree/Tree.stories.d.ts +9 -28
  18. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Tree/TreeContext.d.ts +21 -8
  20. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  21. package/dist/types/src/components/Tree/TreeItem.d.ts +14 -3
  22. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  23. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  24. package/dist/types/src/components/Tree/index.d.ts +2 -0
  25. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  26. package/dist/types/src/components/Tree/testing.d.ts +2 -2
  27. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  28. package/dist/types/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +31 -28
  30. package/src/components/Accordion/Accordion.stories.tsx +3 -3
  31. package/src/components/Accordion/AccordionItem.tsx +3 -3
  32. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  33. package/src/components/List/List.stories.tsx +34 -22
  34. package/src/components/List/List.tsx +2 -2
  35. package/src/components/List/ListItem.tsx +57 -37
  36. package/src/components/List/ListRoot.tsx +2 -2
  37. package/src/components/Tree/Tree.stories.tsx +150 -60
  38. package/src/components/Tree/Tree.tsx +30 -41
  39. package/src/components/Tree/TreeContext.tsx +18 -7
  40. package/src/components/Tree/TreeItem.tsx +170 -104
  41. package/src/components/Tree/TreeItemHeading.tsx +6 -4
  42. package/src/components/Tree/TreeItemToggle.tsx +1 -1
  43. package/src/components/Tree/index.ts +2 -0
  44. package/src/components/Tree/testing.ts +2 -2
@@ -10,10 +10,20 @@ import {
10
10
  attachInstruction,
11
11
  extractInstruction,
12
12
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
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
28
  import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
19
29
  import {
@@ -22,7 +32,8 @@ import {
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,7 +62,7 @@ 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;
@@ -63,39 +74,61 @@ export type TreeItemProps<T extends HasId = any> = {
63
74
  canSelect?: (params: { item: T; path: string[] }) => boolean;
64
75
  onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
65
76
  onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
77
+ onItemHover?: (params: { item: T }) => void;
66
78
  };
67
79
 
68
- const RawTreeItem = <T extends HasId = any>({
80
+ const RawTreeItem = <T extends { id: string } = any>({
69
81
  item,
70
- path: _path,
82
+ path: pathProp,
71
83
  levelOffset = 2,
72
84
  last,
73
- draggable: _draggable,
85
+ draggable: draggableProp,
74
86
  renderColumns: Columns,
75
87
  blockInstruction,
76
88
  canDrop,
77
89
  canSelect,
78
90
  onOpenChange,
79
91
  onSelect,
92
+ onItemHover,
80
93
  }: TreeItemProps<T>) => {
81
94
  const rowRef = useRef<HTMLDivElement | null>(null);
82
95
  const buttonRef = useRef<HTMLButtonElement | null>(null);
83
96
  const openRef = useRef(false);
84
97
  const cancelExpandRef = useRef<NodeJS.Timeout | null>(null);
85
- const [_state, setState] = useState<TreeItemState>('idle');
98
+ const [_state, setState] = useState<TreeItemDragState>('idle');
86
99
  const [instruction, setInstruction] = useState<Instruction | null>(null);
87
100
  const [menuOpen, setMenuOpen] = useState(false);
88
101
 
89
- const { useItems, getProps, isOpen, isCurrent } = useTree();
90
- const items = useItems(item);
91
- const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = getProps(item, _path);
92
- const path = useMemo(() => [..._path, id], [_path, id]);
93
- const open = isOpen(path, item);
94
- 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
+
95
127
  const level = path.length - levelOffset;
96
128
  const isBranch = !!parentOf;
97
129
  const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
98
130
  const canSelectItem = canSelect?.({ item, path }) ?? true;
131
+ const data = { id, path, item } satisfies TreeData;
99
132
 
100
133
  const cancelExpand = useCallback(() => {
101
134
  if (cancelExpandRef.current) {
@@ -104,19 +137,19 @@ const RawTreeItem = <T extends HasId = any>({
104
137
  }
105
138
  }, []);
106
139
 
140
+ const isItemDraggable = draggableProp && itemDraggable !== false;
141
+ const isItemDroppable = itemDroppable !== false;
142
+
107
143
  useEffect(() => {
108
- if (!_draggable) {
144
+ if (!draggableProp) {
109
145
  return;
110
146
  }
111
147
 
112
148
  invariant(buttonRef.current);
113
149
 
114
- const data = { id, path, item } satisfies TreeData;
115
-
116
- // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
117
- return combine(
150
+ const makeDraggable = () =>
118
151
  draggable({
119
- element: buttonRef.current,
152
+ element: buttonRef.current!,
120
153
  getInitialData: () => data,
121
154
  onDragStart: () => {
122
155
  setState('dragging');
@@ -131,62 +164,72 @@ const RawTreeItem = <T extends HasId = any>({
131
164
  onOpenChange?.({ item, path, open: true });
132
165
  }
133
166
  },
134
- }),
135
- // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx
136
- dropTargetForElements({
137
- element: buttonRef.current,
138
- getData: ({ input, element }) => {
139
- return attachInstruction(data, {
140
- input,
141
- element,
142
- indentPerLevel: DEFAULT_INDENTATION,
143
- currentLevel: level,
144
- mode,
145
- block: isBranch ? [] : ['make-child'],
146
- });
147
- },
148
- canDrop: ({ source }) => {
149
- const _canDrop = canDrop ?? (() => true);
150
- return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
151
- },
152
- getIsSticky: () => true,
153
- onDrag: ({ self, source }) => {
154
- const desired = extractInstruction(self.data);
155
- const block =
156
- desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
157
- const instruction: Instruction | null =
158
- block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
159
-
160
- if (source.data.id !== id) {
161
- if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
162
- cancelExpandRef.current = setTimeout(() => {
163
- onOpenChange?.({ item, path, open: true });
164
- }, 500);
165
- }
166
-
167
- if (instruction?.type !== 'make-child') {
168
- cancelExpand();
169
- }
170
-
171
- setInstruction(instruction);
172
- } else if (instruction?.type === 'reparent') {
173
- // TODO(wittjosiah): This is not occurring in the current implementation.
174
- setInstruction(instruction);
175
- } else {
176
- setInstruction(null);
167
+ });
168
+
169
+ if (!isItemDroppable) {
170
+ return isItemDraggable ? makeDraggable() : undefined;
171
+ }
172
+
173
+ const dropTarget = dropTargetForElements({
174
+ element: buttonRef.current,
175
+ getData: ({ input, element }) => {
176
+ return attachInstruction(data, {
177
+ input,
178
+ element,
179
+ indentPerLevel: DEFAULT_INDENTATION,
180
+ currentLevel: level,
181
+ mode,
182
+ block: isBranch ? [] : ['make-child'],
183
+ });
184
+ },
185
+ canDrop: ({ source }) => {
186
+ const _canDrop = canDrop ?? (() => true);
187
+ return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
188
+ },
189
+ getIsSticky: () => true,
190
+ onDrag: ({ self, source }) => {
191
+ const desired = extractInstruction(self.data);
192
+ const block =
193
+ desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
194
+ const instruction: Instruction | null =
195
+ block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
196
+
197
+ if (source.data.id !== id) {
198
+ if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
199
+ cancelExpandRef.current = setTimeout(() => {
200
+ onOpenChange?.({ item, path, open: true });
201
+ }, 500);
177
202
  }
178
- },
179
- onDragLeave: () => {
180
- cancelExpand();
181
- setInstruction(null);
182
- },
183
- onDrop: () => {
184
- cancelExpand();
203
+
204
+ if (instruction?.type !== 'make-child') {
205
+ cancelExpand();
206
+ }
207
+
208
+ setInstruction(instruction);
209
+ } else if (instruction?.type === 'reparent') {
210
+ // TODO(wittjosiah): This is not occurring in the current implementation.
211
+ setInstruction(instruction);
212
+ } else {
185
213
  setInstruction(null);
186
- },
187
- }),
188
- );
189
- }, [_draggable, item, id, mode, path, open, blockInstruction, canDrop]);
214
+ }
215
+ },
216
+ onDragLeave: () => {
217
+ cancelExpand();
218
+ setInstruction(null);
219
+ },
220
+ onDrop: () => {
221
+ cancelExpand();
222
+ setInstruction(null);
223
+ },
224
+ });
225
+
226
+ if (!isItemDraggable) {
227
+ return dropTarget;
228
+ }
229
+
230
+ // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
231
+ return combine(makeDraggable(), dropTarget);
232
+ }, [draggableProp, isItemDraggable, isItemDroppable, item, id, mode, path, open, blockInstruction, canDrop]);
190
233
 
191
234
  // Cancel expand on unmount.
192
235
  useEffect(() => () => cancelExpand(), [cancelExpand]);
@@ -224,6 +267,29 @@ const RawTreeItem = <T extends HasId = any>({
224
267
  [isBranch, open, handleOpenToggle, handleSelect],
225
268
  );
226
269
 
270
+ const handleItemHover = useCallback(() => {
271
+ onItemHover?.({ item });
272
+ }, [onItemHover, item]);
273
+
274
+ const handleContextMenu = useCallback(
275
+ (event: MouseEvent) => {
276
+ event.preventDefault();
277
+ setMenuOpen(true);
278
+ },
279
+ [setMenuOpen],
280
+ );
281
+
282
+ const childProps = {
283
+ draggable: draggableProp,
284
+ renderColumns: Columns,
285
+ blockInstruction,
286
+ canDrop,
287
+ canSelect,
288
+ onItemHover,
289
+ onOpenChange,
290
+ onSelect,
291
+ };
292
+
227
293
  return (
228
294
  <>
229
295
  <Treegrid.Row
@@ -232,27 +298,25 @@ const RawTreeItem = <T extends HasId = any>({
232
298
  id={id}
233
299
  aria-labelledby={`${id}__label`}
234
300
  parentOf={parentOf?.join(Treegrid.PARENT_OF_SEPARATOR)}
235
- classNames={[
236
- 'grid grid-cols-subgrid col-[tree-row] mbs-0.5 aria-[current]:bg-activeSurface',
237
- hoverableControls,
238
- hoverableFocusedKeyboardControls,
239
- hoverableFocusedWithinControls,
240
- hoverableDescriptionIcons,
241
- ghostHover,
242
- ghostFocusWithin,
243
- className,
244
- ]}
245
301
  data-object-id={id}
246
302
  data-testid={testId}
247
303
  // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
248
304
  // without alerting the user (except for in the correct link element). See also:
249
305
  // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
250
306
  aria-current={current ? ('' as 'page') : undefined}
307
+ classNames={mx(
308
+ 'grid grid-cols-subgrid col-[tree-row] mt-0.5 is-current:bg-active-surface',
309
+ hoverableControls,
310
+ hoverableFocusedKeyboardControls,
311
+ hoverableFocusedWithinControls,
312
+ hoverableDescriptionIcons,
313
+ ghostFocusWithin,
314
+ ghostHover,
315
+ className,
316
+ )}
251
317
  onKeyDown={handleKeyDown}
252
- onContextMenu={(event) => {
253
- event.preventDefault();
254
- setMenuOpen(true);
255
- }}
318
+ onMouseEnter={handleItemHover}
319
+ onContextMenu={handleContextMenu}
256
320
  >
257
321
  <div
258
322
  role='none'
@@ -277,23 +341,25 @@ const RawTreeItem = <T extends HasId = any>({
277
341
  </div>
278
342
  </Treegrid.Row>
279
343
  {open &&
280
- items.map((item, index) => (
281
- <TreeItem
282
- key={item.id}
283
- item={item}
284
- path={path}
285
- last={index === items.length - 1}
286
- draggable={_draggable}
287
- renderColumns={Columns}
288
- blockInstruction={blockInstruction}
289
- canDrop={canDrop}
290
- canSelect={canSelect}
291
- onOpenChange={onOpenChange}
292
- onSelect={onSelect}
293
- />
344
+ childIds.map((childId, index) => (
345
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
294
346
  ))}
295
347
  </>
296
348
  );
297
349
  };
298
350
 
299
351
  export const TreeItem = memo(RawTreeItem) as FC<TreeItemProps>;
352
+
353
+ /** Resolves a child ID to an item via the `item` atom and renders a TreeItem. */
354
+ export type TreeItemByIdProps = Omit<TreeItemProps, 'item'> & { id: string };
355
+
356
+ const RawTreeItemById = <T extends { id: string } = any>({ id, ...props }: TreeItemByIdProps) => {
357
+ const { item: itemAtom } = useTree();
358
+ const item = useAtomValue(itemAtom(id)) as T | undefined;
359
+ if (!item) {
360
+ return null;
361
+ }
362
+ return <TreeItem item={item} {...props} />;
363
+ };
364
+
365
+ export const TreeItemById = memo(RawTreeItemById) as FC<TreeItemByIdProps>;
@@ -6,7 +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/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.
@@ -58,7 +58,7 @@ export const TreeItemHeading = memo(
58
58
  variant='ghost'
59
59
  density='fine'
60
60
  classNames={[
61
- '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',
62
62
  'disabled:cursor-default disabled:opacity-100',
63
63
  className,
64
64
  ]}
@@ -67,8 +67,10 @@ export const TreeItemHeading = memo(
67
67
  onKeyDown={handleButtonKeydown}
68
68
  {...(current && { 'aria-current': 'location' })}
69
69
  >
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>
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>
72
74
  {toLocalizedString(label, t)}
73
75
  </span>
74
76
  </Button>
@@ -23,7 +23,7 @@ export const TreeItemToggle = memo(
23
23
  variant='ghost'
24
24
  density='fine'
25
25
  classNames={[
26
- 'bs-full is-6 pli-0',
26
+ 'h-full w-6 px-0',
27
27
  '[&_svg]:transition-[transform] [&_svg]:duration-200',
28
28
  open && '[&_svg]:rotate-90',
29
29
  hidden ? 'hidden' : !isBranch && 'invisible',
@@ -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';
@@ -6,13 +6,13 @@ import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-
6
6
  import * as Schema from 'effect/Schema';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
- import { type HasId } from '@dxos/echo/internal';
10
9
  import { log } from '@dxos/log';
11
10
  import { faker } from '@dxos/random';
12
11
 
13
12
  import { type TreeData } from './TreeItem';
14
13
 
15
- export type TestItem = HasId & {
14
+ export type TestItem = {
15
+ id: string;
16
16
  name: string;
17
17
  icon?: string;
18
18
  items: TestItem[];