@dxos/react-ui-list 0.8.4-main.69d29f4 → 0.8.4-main.6fa680abb7

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 (33) hide show
  1. package/dist/lib/browser/index.mjs +192 -166
  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 +192 -166
  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.map +1 -1
  8. package/dist/types/src/components/List/ListItem.d.ts +2 -2
  9. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  10. package/dist/types/src/components/Tree/Tree.d.ts +6 -5
  11. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  12. package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
  13. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Tree/TreeContext.d.ts +21 -10
  15. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  16. package/dist/types/src/components/Tree/TreeItem.d.ts +8 -0
  17. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  18. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  19. package/dist/types/src/components/Tree/index.d.ts +2 -0
  20. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  21. package/dist/types/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +18 -18
  23. package/src/components/Accordion/Accordion.stories.tsx +3 -3
  24. package/src/components/Accordion/AccordionItem.tsx +1 -1
  25. package/src/components/List/List.stories.tsx +7 -7
  26. package/src/components/List/ListItem.tsx +10 -10
  27. package/src/components/Tree/Tree.stories.tsx +102 -26
  28. package/src/components/Tree/Tree.tsx +30 -40
  29. package/src/components/Tree/TreeContext.tsx +18 -9
  30. package/src/components/Tree/TreeItem.tsx +166 -99
  31. package/src/components/Tree/TreeItemHeading.tsx +5 -3
  32. package/src/components/Tree/TreeItemToggle.tsx +1 -1
  33. package/src/components/Tree/index.ts +2 -0
@@ -10,8 +10,19 @@ 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
27
  import { invariant } from '@dxos/invariant';
17
28
  import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
@@ -21,6 +32,7 @@ import {
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,61 @@ 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;
98
132
 
99
133
  const cancelExpand = useCallback(() => {
100
134
  if (cancelExpandRef.current) {
@@ -103,19 +137,19 @@ const RawTreeItem = <T extends { id: string } = any>({
103
137
  }
104
138
  }, []);
105
139
 
140
+ const isItemDraggable = draggableProp && itemDraggable !== false;
141
+ const isItemDroppable = itemDroppable !== false;
142
+
106
143
  useEffect(() => {
107
- if (!_draggable) {
144
+ if (!draggableProp) {
108
145
  return;
109
146
  }
110
147
 
111
148
  invariant(buttonRef.current);
112
149
 
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(
150
+ const makeDraggable = () =>
117
151
  draggable({
118
- element: buttonRef.current,
152
+ element: buttonRef.current!,
119
153
  getInitialData: () => data,
120
154
  onDragStart: () => {
121
155
  setState('dragging');
@@ -130,62 +164,72 @@ const RawTreeItem = <T extends { id: string } = any>({
130
164
  onOpenChange?.({ item, path, open: true });
131
165
  }
132
166
  },
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);
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);
176
202
  }
177
- },
178
- onDragLeave: () => {
179
- cancelExpand();
180
- setInstruction(null);
181
- },
182
- onDrop: () => {
183
- 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 {
184
213
  setInstruction(null);
185
- },
186
- }),
187
- );
188
- }, [_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]);
189
233
 
190
234
  // Cancel expand on unmount.
191
235
  useEffect(() => () => cancelExpand(), [cancelExpand]);
@@ -223,6 +267,29 @@ const RawTreeItem = <T extends { id: string } = any>({
223
267
  [isBranch, open, handleOpenToggle, handleSelect],
224
268
  );
225
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
+
226
293
  return (
227
294
  <>
228
295
  <Treegrid.Row
@@ -231,27 +298,25 @@ const RawTreeItem = <T extends { id: string } = any>({
231
298
  id={id}
232
299
  aria-labelledby={`${id}__label`}
233
300
  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
- ]}
244
301
  data-object-id={id}
245
302
  data-testid={testId}
246
303
  // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
247
304
  // without alerting the user (except for in the correct link element). See also:
248
305
  // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current#description
249
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
+ )}
250
317
  onKeyDown={handleKeyDown}
251
- onContextMenu={(event) => {
252
- event.preventDefault();
253
- setMenuOpen(true);
254
- }}
318
+ onMouseEnter={handleItemHover}
319
+ onContextMenu={handleContextMenu}
255
320
  >
256
321
  <div
257
322
  role='none'
@@ -276,23 +341,25 @@ const RawTreeItem = <T extends { id: string } = any>({
276
341
  </div>
277
342
  </Treegrid.Row>
278
343
  {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
- />
344
+ childIds.map((childId, index) => (
345
+ <TreeItemById key={childId} id={childId} path={path} last={index === childIds.length - 1} {...childProps} />
293
346
  ))}
294
347
  </>
295
348
  );
296
349
  };
297
350
 
298
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>;
@@ -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';