@dxos/react-ui-list 0.8.4-main.3a94e84 → 0.8.4-main.3c1ae3b

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 (50) hide show
  1. package/dist/lib/browser/index.mjs +107 -91
  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 +107 -91
  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 +6 -6
  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 +4 -7
  16. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  17. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  18. package/dist/types/src/components/List/testing.d.ts +1 -1
  19. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  20. package/dist/types/src/components/Tree/Tree.d.ts +3 -3
  21. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  22. package/dist/types/src/components/Tree/Tree.stories.d.ts +36 -6
  23. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Tree/TreeContext.d.ts +3 -2
  25. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  26. package/dist/types/src/components/Tree/TreeItem.d.ts +20 -9
  27. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  28. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
  29. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  30. package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
  31. package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
  32. package/dist/types/src/components/Tree/testing.d.ts +2 -2
  33. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  34. package/dist/types/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +25 -24
  36. package/src/components/Accordion/Accordion.stories.tsx +7 -9
  37. package/src/components/Accordion/Accordion.tsx +1 -1
  38. package/src/components/Accordion/AccordionItem.tsx +5 -2
  39. package/src/components/List/List.stories.tsx +19 -17
  40. package/src/components/List/List.tsx +2 -5
  41. package/src/components/List/ListItem.tsx +39 -27
  42. package/src/components/List/ListRoot.tsx +1 -1
  43. package/src/components/List/testing.ts +3 -3
  44. package/src/components/Tree/Tree.stories.tsx +51 -48
  45. package/src/components/Tree/Tree.tsx +16 -2
  46. package/src/components/Tree/TreeContext.tsx +3 -2
  47. package/src/components/Tree/TreeItem.tsx +59 -45
  48. package/src/components/Tree/TreeItemHeading.tsx +9 -6
  49. package/src/components/Tree/TreeItemToggle.tsx +29 -18
  50. package/src/components/Tree/testing.ts +4 -3
@@ -5,34 +5,35 @@
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 * as Schema from 'effect/Schema';
14
+ import React, { type FC, type KeyboardEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
15
 
16
- import { type HasId } from '@dxos/echo-schema';
16
+ import { type HasId } from '@dxos/echo/internal';
17
17
  import { invariant } from '@dxos/invariant';
18
- import { Treegrid, TreeItem as NaturalTreeItem } from '@dxos/react-ui';
18
+ import { TreeItem as NaturalTreeItem, Treegrid } from '@dxos/react-ui';
19
19
  import {
20
+ ghostFocusWithin,
20
21
  ghostHover,
21
22
  hoverableControls,
22
23
  hoverableFocusedKeyboardControls,
23
24
  hoverableFocusedWithinControls,
24
25
  } from '@dxos/react-ui-theme';
25
26
 
27
+ import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
26
28
  import { useTree } from './TreeContext';
27
29
  import { TreeItemHeading } from './TreeItemHeading';
28
30
  import { TreeItemToggle } from './TreeItemToggle';
29
- import { DEFAULT_INDENTATION, paddingIndentation } from './helpers';
30
-
31
- type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
32
31
 
33
32
  const hoverableDescriptionIcons =
34
33
  '[--icons-color:inherit] hover-hover:[--icons-color:var(--description-text)] hover-hover:hover:[--icons-color:inherit] focus-within:[--icons-color:inherit]';
35
34
 
35
+ type TreeItemState = 'idle' | 'dragging' | 'preview' | 'parent-of-instruction';
36
+
36
37
  export const TreeDataSchema = Schema.Struct({
37
38
  id: Schema.String,
38
39
  path: Schema.Array(Schema.String),
@@ -40,23 +41,26 @@ export const TreeDataSchema = Schema.Struct({
40
41
  });
41
42
 
42
43
  export type TreeData = Schema.Schema.Type<typeof TreeDataSchema>;
43
-
44
44
  export const isTreeData = (data: unknown): data is TreeData => Schema.is(TreeDataSchema)(data);
45
45
 
46
+ export type ColumnRenderer<T extends HasId = any> = FC<{
47
+ item: T;
48
+ path: string[];
49
+ open: boolean;
50
+ menuOpen: boolean;
51
+ setMenuOpen: (open: boolean) => void;
52
+ }>;
53
+
46
54
  export type TreeItemProps<T extends HasId = any> = {
47
55
  item: T;
48
56
  path: string[];
49
57
  levelOffset?: number;
50
58
  last: boolean;
51
59
  draggable?: boolean;
52
- renderColumns?: FC<{
53
- item: T;
54
- path: string[];
55
- open: boolean;
56
- menuOpen: boolean;
57
- setMenuOpen: (open: boolean) => void;
58
- }>;
60
+ renderColumns?: ColumnRenderer<T>;
61
+ blockInstruction?: (params: { instruction: Instruction; source: TreeData; target: TreeData }) => boolean;
59
62
  canDrop?: (params: { source: TreeData; target: TreeData }) => boolean;
63
+ canSelect?: (params: { item: T; path: string[] }) => boolean;
60
64
  onOpenChange?: (params: { item: T; path: string[]; open: boolean }) => void;
61
65
  onSelect?: (params: { item: T; path: string[]; current: boolean; option: boolean }) => void;
62
66
  };
@@ -64,13 +68,15 @@ export type TreeItemProps<T extends HasId = any> = {
64
68
  const RawTreeItem = <T extends HasId = any>({
65
69
  item,
66
70
  path: _path,
71
+ levelOffset = 2,
67
72
  last,
68
73
  draggable: _draggable,
69
74
  renderColumns: Columns,
75
+ blockInstruction,
70
76
  canDrop,
77
+ canSelect,
71
78
  onOpenChange,
72
79
  onSelect,
73
- levelOffset = 2,
74
80
  }: TreeItemProps<T>) => {
75
81
  const rowRef = useRef<HTMLDivElement | null>(null);
76
82
  const buttonRef = useRef<HTMLButtonElement | null>(null);
@@ -82,13 +88,14 @@ const RawTreeItem = <T extends HasId = any>({
82
88
 
83
89
  const { useItems, getProps, isOpen, isCurrent } = useTree();
84
90
  const items = useItems(item);
85
- const { id, label, parentOf, icon, disabled, className, headingClassName, testId } = getProps(item, _path);
91
+ const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = getProps(item, _path);
86
92
  const path = useMemo(() => [..._path, id], [_path, id]);
87
93
  const open = isOpen(path, item);
88
94
  const current = isCurrent(path, item);
89
95
  const level = path.length - levelOffset;
90
96
  const isBranch = !!parentOf;
91
97
  const mode: ItemMode = last ? 'last-in-group' : open ? 'expanded' : 'standard';
98
+ const canSelectItem = canSelect?.({ item, path }) ?? true;
92
99
 
93
100
  const cancelExpand = useCallback(() => {
94
101
  if (cancelExpandRef.current) {
@@ -144,7 +151,11 @@ const RawTreeItem = <T extends HasId = any>({
144
151
  },
145
152
  getIsSticky: () => true,
146
153
  onDrag: ({ self, source }) => {
147
- const instruction = extractInstruction(self.data);
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;
148
159
 
149
160
  if (source.data.id !== id) {
150
161
  if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
@@ -175,43 +186,42 @@ const RawTreeItem = <T extends HasId = any>({
175
186
  },
176
187
  }),
177
188
  );
178
- }, [_draggable, item, id, mode, path, open, canDrop]);
189
+ }, [_draggable, item, id, mode, path, open, blockInstruction, canDrop]);
179
190
 
180
191
  // Cancel expand on unmount.
181
192
  useEffect(() => () => cancelExpand(), [cancelExpand]);
182
193
 
183
- const handleOpenChange = useCallback(
194
+ const handleOpenToggle = useCallback(
184
195
  () => onOpenChange?.({ item, path, open: !open }),
185
196
  [onOpenChange, item, path, open],
186
197
  );
187
198
 
188
199
  const handleSelect = useCallback(
189
200
  (option = false) => {
190
- if (isBranch) {
191
- handleOpenChange();
192
- } else {
201
+ // If the item is a branch, toggle it if:
202
+ // - also holding down the option key
203
+ // - or the item is currently selected
204
+ if (isBranch && (option || current)) {
205
+ handleOpenToggle();
206
+ } else if (canSelectItem) {
207
+ canSelect?.({ item, path });
193
208
  rowRef.current?.focus();
194
209
  onSelect?.({ item, path, current: !current, option });
195
210
  }
196
211
  },
197
- [item, path, current, isBranch, handleOpenChange, onSelect],
212
+ [item, path, current, isBranch, canSelectItem, handleOpenToggle, onSelect],
198
213
  );
199
214
 
200
215
  const handleKeyDown = useCallback(
201
216
  (event: KeyboardEvent) => {
202
217
  switch (event.key) {
203
218
  case 'ArrowRight':
204
- isBranch && !open && handleOpenChange();
205
- break;
206
219
  case 'ArrowLeft':
207
- isBranch && open && handleOpenChange();
208
- break;
209
- case ' ':
210
- handleSelect(event.altKey);
220
+ isBranch && handleOpenToggle();
211
221
  break;
212
222
  }
213
223
  },
214
- [isBranch, open, handleOpenChange, handleSelect],
224
+ [isBranch, open, handleOpenToggle, handleSelect],
215
225
  );
216
226
 
217
227
  return (
@@ -229,9 +239,10 @@ const RawTreeItem = <T extends HasId = any>({
229
239
  hoverableFocusedWithinControls,
230
240
  hoverableDescriptionIcons,
231
241
  ghostHover,
242
+ ghostFocusWithin,
232
243
  className,
233
244
  ]}
234
- data-itemid={id}
245
+ data-object-id={id}
235
246
  data-testid={testId}
236
247
  // NOTE(thure): This is intentionally an empty string to for descendents to select by in the CSS
237
248
  // without alerting the user (except for in the correct link element). See also:
@@ -243,26 +254,27 @@ const RawTreeItem = <T extends HasId = any>({
243
254
  setMenuOpen(true);
244
255
  }}
245
256
  >
246
- <Treegrid.Cell
247
- indent
248
- classNames='relative grid grid-cols-subgrid col-[tree-row]'
257
+ <div
258
+ role='none'
259
+ className='indent relative grid grid-cols-subgrid col-[tree-row]'
249
260
  style={paddingIndentation(level)}
250
261
  >
251
- <div role='none' className='flex items-center'>
252
- <TreeItemToggle isBranch={isBranch} open={open} onToggle={handleOpenChange} />
262
+ <Treegrid.Cell classNames='flex items-center'>
263
+ <TreeItemToggle isBranch={isBranch} open={open} onClick={handleOpenToggle} />
253
264
  <TreeItemHeading
254
- ref={buttonRef}
255
- label={label}
256
- icon={icon}
257
- className={headingClassName}
258
265
  disabled={disabled}
259
266
  current={current}
267
+ label={label}
268
+ className={headingClassName}
269
+ icon={icon}
270
+ iconHue={iconHue}
260
271
  onSelect={handleSelect}
272
+ ref={buttonRef}
261
273
  />
262
- </div>
274
+ </Treegrid.Cell>
263
275
  {Columns && <Columns item={item} path={path} open={open} menuOpen={menuOpen} setMenuOpen={setMenuOpen} />}
264
276
  {instruction && <NaturalTreeItem.DropIndicator instruction={instruction} gap={2} />}
265
- </Treegrid.Cell>
277
+ </div>
266
278
  </Treegrid.Row>
267
279
  {open &&
268
280
  items.map((item, index) => (
@@ -273,7 +285,9 @@ const RawTreeItem = <T extends HasId = any>({
273
285
  last={index === items.length - 1}
274
286
  draggable={_draggable}
275
287
  renderColumns={Columns}
288
+ blockInstruction={blockInstruction}
276
289
  canDrop={canDrop}
290
+ canSelect={canSelect}
277
291
  onOpenChange={onOpenChange}
278
292
  onSelect={onSelect}
279
293
  />
@@ -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/react-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) => {
@@ -64,7 +67,7 @@ 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' />}
70
+ {icon && <Icon icon={icon ?? 'ph--placeholder--regular'} size={5} classNames={['mlb-1', styles?.icon]} />}
68
71
  <span className='flex-1 is-0 truncate text-start text-sm font-normal' data-tooltip>
69
72
  {toLocalizedString(label, t)}
70
73
  </span>
@@ -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
+ 'bs-full is-6 pli-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
  );
@@ -3,9 +3,10 @@
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
+ import { type HasId } from '@dxos/echo/internal';
9
10
  import { log } from '@dxos/log';
10
11
  import { faker } from '@dxos/random';
11
12
 
@@ -18,7 +19,7 @@ export type TestItem = HasId & {
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))),