@dxos/react-ui-list 0.9.1-main.c7dcc2e112 → 0.10.0

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 (101) hide show
  1. package/dist/lib/browser/index.mjs +628 -167
  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 +628 -167
  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/aspects/useListDisclosure.d.ts +4 -4
  8. package/dist/types/src/aspects/useListDisclosure.d.ts.map +1 -1
  9. package/dist/types/src/aspects/useListNavigation.d.ts +9 -7
  10. package/dist/types/src/aspects/useListNavigation.d.ts.map +1 -1
  11. package/dist/types/src/aspects/useListSelection.d.ts +3 -3
  12. package/dist/types/src/aspects/useListSelection.d.ts.map +1 -1
  13. package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
  14. package/dist/types/src/components/Accordion/AccordionItem.d.ts +3 -1
  15. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  16. package/dist/types/src/components/Combobox/Combobox.d.ts +10 -3
  17. package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -1
  18. package/dist/types/src/components/DropIndicator/DropIndicator.d.ts +12 -0
  19. package/dist/types/src/components/DropIndicator/DropIndicator.d.ts.map +1 -0
  20. package/dist/types/src/components/DropIndicator/index.d.ts +2 -0
  21. package/dist/types/src/components/DropIndicator/index.d.ts.map +1 -0
  22. package/dist/types/src/components/Empty/Empty.d.ts +17 -0
  23. package/dist/types/src/components/Empty/Empty.d.ts.map +1 -0
  24. package/dist/types/src/components/Empty/Empty.stories.d.ts +18 -0
  25. package/dist/types/src/components/Empty/Empty.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/Empty/index.d.ts +2 -0
  27. package/dist/types/src/components/Empty/index.d.ts.map +1 -0
  28. package/dist/types/src/components/List.theme.d.ts +193 -0
  29. package/dist/types/src/components/List.theme.d.ts.map +1 -0
  30. package/dist/types/src/components/Listbox/ListItemContent.d.ts +27 -0
  31. package/dist/types/src/components/Listbox/ListItemContent.d.ts.map +1 -0
  32. package/dist/types/src/components/Listbox/ListItemContent.stories.d.ts +24 -0
  33. package/dist/types/src/components/Listbox/ListItemContent.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Listbox/Listbox.d.ts +19 -2
  35. package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -1
  36. package/dist/types/src/components/Listbox/Listbox.stories.d.ts +4 -0
  37. package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/OrderedList/OrderedList.d.ts +1 -1
  39. package/dist/types/src/components/OrderedList/OrderedList.d.ts.map +1 -1
  40. package/dist/types/src/components/OrderedList/OrderedListItem.d.ts.map +1 -1
  41. package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts +1 -1
  42. package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts.map +1 -1
  43. package/dist/types/src/components/Picker/Picker.d.ts +12 -4
  44. package/dist/types/src/components/Picker/Picker.d.ts.map +1 -1
  45. package/dist/types/src/components/Picker/index.d.ts +1 -1
  46. package/dist/types/src/components/Picker/index.d.ts.map +1 -1
  47. package/dist/types/src/components/Tree/Tree.d.ts +1 -1
  48. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  49. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -2
  50. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/Tree/TreeContext.d.ts +2 -0
  52. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  53. package/dist/types/src/components/Tree/TreeDropIndicator.d.ts +10 -0
  54. package/dist/types/src/components/Tree/TreeDropIndicator.d.ts.map +1 -0
  55. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  56. package/dist/types/src/components/Tree/testing.d.ts +4 -1
  57. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  58. package/dist/types/src/components/Treegrid/Treegrid.d.ts +33 -0
  59. package/dist/types/src/components/Treegrid/Treegrid.d.ts.map +1 -0
  60. package/dist/types/src/components/Treegrid/Treegrid.stories.d.ts +11 -0
  61. package/dist/types/src/components/Treegrid/Treegrid.stories.d.ts.map +1 -0
  62. package/dist/types/src/components/Treegrid/Treegrid.theme.d.ts +37 -0
  63. package/dist/types/src/components/Treegrid/Treegrid.theme.d.ts.map +1 -0
  64. package/dist/types/src/components/Treegrid/index.d.ts +2 -0
  65. package/dist/types/src/components/Treegrid/index.d.ts.map +1 -0
  66. package/dist/types/src/components/index.d.ts +3 -0
  67. package/dist/types/src/components/index.d.ts.map +1 -1
  68. package/dist/types/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +23 -22
  70. package/src/aspects/useListDisclosure.ts +8 -8
  71. package/src/aspects/useListNavigation.ts +9 -7
  72. package/src/aspects/useListSelection.ts +4 -4
  73. package/src/components/Accordion/AccordionItem.tsx +31 -13
  74. package/src/components/Combobox/Combobox.tsx +46 -38
  75. package/src/components/DropIndicator/DropIndicator.tsx +74 -0
  76. package/src/components/DropIndicator/index.ts +5 -0
  77. package/src/components/Empty/Empty.stories.tsx +37 -0
  78. package/src/components/Empty/Empty.tsx +41 -0
  79. package/src/components/Empty/index.ts +5 -0
  80. package/src/components/List.theme.ts +99 -0
  81. package/src/components/Listbox/ListItemContent.stories.tsx +41 -0
  82. package/src/components/Listbox/ListItemContent.tsx +49 -0
  83. package/src/components/Listbox/Listbox.stories.tsx +106 -7
  84. package/src/components/Listbox/Listbox.tsx +85 -42
  85. package/src/components/OrderedList/OrderedList.tsx +1 -1
  86. package/src/components/OrderedList/OrderedListItem.tsx +13 -13
  87. package/src/components/OrderedList/OrderedListRoot.tsx +6 -3
  88. package/src/components/Picker/Picker.tsx +25 -15
  89. package/src/components/Picker/index.ts +1 -1
  90. package/src/components/Tree/Tree.stories.tsx +39 -15
  91. package/src/components/Tree/Tree.tsx +13 -3
  92. package/src/components/Tree/TreeContext.tsx +2 -0
  93. package/src/components/Tree/TreeDropIndicator.tsx +72 -0
  94. package/src/components/Tree/TreeItem.tsx +43 -3
  95. package/src/components/Tree/TreeItemHeading.tsx +2 -2
  96. package/src/components/Tree/testing.ts +19 -14
  97. package/src/components/Treegrid/Treegrid.stories.tsx +132 -0
  98. package/src/components/Treegrid/Treegrid.theme.ts +38 -0
  99. package/src/components/Treegrid/Treegrid.tsx +187 -0
  100. package/src/components/Treegrid/index.ts +5 -0
  101. package/src/components/index.ts +3 -0
@@ -0,0 +1,132 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { random } from '@dxos/random';
9
+ import { Icon } from '@dxos/react-ui';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { Treegrid, TREEGRID_PARENT_OF_SEPARATOR, TREEGRID_PATH_SEPARATOR } from './Treegrid';
13
+
14
+ random.seed(1234);
15
+
16
+ type StorybookNode = {
17
+ id: string;
18
+ title: string;
19
+ icon?: string;
20
+ nodes?: StorybookNode[];
21
+ };
22
+
23
+ type StorybookIteratorNode = {
24
+ node: StorybookNode;
25
+ path: string[];
26
+ parentOf?: string[];
27
+ };
28
+
29
+ const content = {
30
+ id: 'root',
31
+ title: 'Root',
32
+ nodes: [
33
+ {
34
+ id: random.string.uuid(),
35
+ title: 'Personal Space',
36
+ icon: 'ph--house--regular',
37
+ nodes: [
38
+ { id: random.string.uuid(), title: random.commerce.productName() },
39
+ { id: random.string.uuid(), title: random.commerce.productName() },
40
+ {
41
+ id: random.string.uuid(),
42
+ title: random.commerce.productName(),
43
+ nodes: [
44
+ {
45
+ id: random.string.uuid(),
46
+ title: random.commerce.productName(),
47
+ nodes: [
48
+ { id: random.string.uuid(), title: random.commerce.productName() },
49
+ { id: random.string.uuid(), title: random.commerce.productName() },
50
+ ],
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ },
56
+ {
57
+ id: random.string.uuid(),
58
+ title: random.commerce.productName(),
59
+ icon: 'ph--planet--regular',
60
+ nodes: [{ id: random.string.uuid(), title: random.commerce.productName() }],
61
+ },
62
+ { id: random.string.uuid(), title: random.commerce.productName(), icon: 'ph--sailboat--regular' },
63
+ { id: random.string.uuid(), title: random.commerce.productName(), icon: 'ph--planet--regular' },
64
+ ],
65
+ } as StorybookNode;
66
+
67
+ function* visitor(node: StorybookNode, isOpen?: (node: StorybookNode) => boolean): Generator<StorybookIteratorNode> {
68
+ // `parentOf` must reference rows by their full path-based id (the row's DOM id) so `aria-owns` resolves.
69
+ const stack: StorybookIteratorNode[] = [
70
+ {
71
+ node,
72
+ path: [node.id],
73
+ parentOf: (node.nodes ?? []).map(({ id }) => [node.id, id].join(TREEGRID_PATH_SEPARATOR)),
74
+ },
75
+ ];
76
+ while (stack.length > 0) {
77
+ const { node, path, parentOf } = stack.pop()!;
78
+ if (path.length > 1) {
79
+ yield { node, path, parentOf };
80
+ }
81
+
82
+ const children = Array.from(node.nodes ?? []);
83
+ if (path.length === 1 || isOpen?.(node)) {
84
+ for (let i = children.length - 1; i >= 0; i--) {
85
+ const child = children[i];
86
+ const childPath = [...path, child.id];
87
+ stack.push({
88
+ node: child,
89
+ path: childPath,
90
+ ...((child.nodes?.length ?? 0) > 0 && {
91
+ parentOf: child.nodes!.map(({ id }) => [...childPath, id].join(TREEGRID_PATH_SEPARATOR)),
92
+ }),
93
+ });
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ const flattenedContent = Array.from(visitor(content, () => true));
100
+
101
+ // Two columns (name + child count) with row dividers so the grid structure is visible — a
102
+ // single-column treegrid reads as a plain list.
103
+ const DefaultStory = () => (
104
+ <Treegrid.Root gridTemplateColumns='1fr min-content' classNames='gap-x-4 divide-y divide-separator'>
105
+ {flattenedContent.map(({ node, parentOf, path }) => (
106
+ <Treegrid.Row
107
+ key={node.id}
108
+ id={path.join(TREEGRID_PATH_SEPARATOR)}
109
+ classNames='grid grid-cols-subgrid col-span-full items-center py-1'
110
+ {...(parentOf && { parentOf: parentOf.join(TREEGRID_PARENT_OF_SEPARATOR) })}
111
+ >
112
+ <Treegrid.Cell indent classNames='flex items-center gap-1'>
113
+ {node.icon && <Icon icon={node.icon} classNames='w-[1em] h-[1em]' />}
114
+ {node.title}
115
+ </Treegrid.Cell>
116
+ <Treegrid.Cell classNames='text-end text-description tabular-nums'>{node.nodes?.length ?? 0}</Treegrid.Cell>
117
+ </Treegrid.Row>
118
+ ))}
119
+ </Treegrid.Root>
120
+ );
121
+
122
+ const meta = {
123
+ title: 'ui/react-ui-list/Treegrid',
124
+ render: DefaultStory,
125
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
126
+ } satisfies Meta<typeof DefaultStory>;
127
+
128
+ export default meta;
129
+
130
+ type Story = StoryObj<typeof meta>;
131
+
132
+ export const Default: Story = {};
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { tv } from '@dxos/ui-theme';
6
+
7
+ // Per-level indentation of the first `.indent` cell, plus a bolder root row. Clamped to 1–8.
8
+ const levelStyles = new Map<number, string>([
9
+ [1, '[&>.indent:first-of-type]:pl-0 font-medium'],
10
+ [2, '[&>.indent:first-of-type]:pl-0'],
11
+ [3, '[&>.indent:first-of-type]:pl-1'],
12
+ [4, '[&>.indent:first-of-type]:pl-2'],
13
+ [5, '[&>.indent:first-of-type]:pl-3'],
14
+ [6, '[&>.indent:first-of-type]:pl-4'],
15
+ [7, '[&>.indent:first-of-type]:pl-5'],
16
+ [8, '[&>.indent:first-of-type]:pl-6'],
17
+ ]);
18
+
19
+ const treegridStyles = tv({
20
+ slots: {
21
+ root: 'grid',
22
+ row: '',
23
+ cell: '',
24
+ },
25
+ variants: {
26
+ // `indent` marks the cell that carries the per-level left padding (selected via `.indent`).
27
+ indent: {
28
+ true: { cell: 'indent' },
29
+ },
30
+ },
31
+ });
32
+
33
+ /** Self-contained {@link Treegrid} theme (tailwind-variants), plus the level→row-class lookup. */
34
+ export const treegridTheme = {
35
+ styles: treegridStyles,
36
+ /** Row class for a 1-based depth level (clamped to 1–8). */
37
+ rowLevel: (level: number): string => levelStyles.get(Math.min(Math.max(Math.round(level), 1), 8)) ?? '',
38
+ };
@@ -0,0 +1,187 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useFocusFinders } from '@fluentui/react-tabster';
6
+ import { type Scope, createContextScope } from '@radix-ui/react-context';
7
+ import { Primitive } from '@radix-ui/react-primitive';
8
+ import { Slot } from '@radix-ui/react-slot';
9
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
10
+ import React, {
11
+ type ComponentPropsWithRef,
12
+ type CSSProperties,
13
+ type KeyboardEvent,
14
+ forwardRef,
15
+ useCallback,
16
+ } from 'react';
17
+
18
+ import { type ThemedClassName, composable, composableProps } from '@dxos/react-ui';
19
+ import { mx } from '@dxos/ui-theme';
20
+
21
+ import { treegridTheme } from './Treegrid.theme';
22
+
23
+ // TODO(thure): https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/treegrid_role
24
+
25
+ const styles = treegridTheme.styles();
26
+
27
+ const TREEGRID_ROW_NAME = 'Treegrid.Row';
28
+
29
+ type TreegridRowScopedProps<P> = P & { __treegridRowScope?: Scope };
30
+
31
+ const [createTreegridRowContext, createTreegridRowScope] = createContextScope(TREEGRID_ROW_NAME, []);
32
+
33
+ type TreegridRowContextValue = {
34
+ open?: boolean;
35
+ onOpenChange?: (nextOpen: boolean) => void;
36
+ };
37
+
38
+ const [TreegridRowProvider, useTreegridRowContext] =
39
+ createTreegridRowContext<TreegridRowContextValue>(TREEGRID_ROW_NAME);
40
+
41
+ // TODO(burdon): Replace with functions.
42
+ export const TREEGRID_PATH_SEPARATOR = '~';
43
+ export const TREEGRID_PARENT_OF_SEPARATOR = ' ';
44
+
45
+ type TreegridRootProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.div>> & {
46
+ gridTemplateColumns?: CSSProperties['gridTemplateColumns'];
47
+ asChild?: boolean;
48
+ };
49
+
50
+ const TreegridRoot = composable<HTMLDivElement, TreegridRootProps>(
51
+ ({ asChild, classNames, children, style, gridTemplateColumns, onKeyDown: onKeyDownProp, ...props }, forwardedRef) => {
52
+ const { className, role: _role, ...rest } = composableProps<HTMLDivElement>(props, { classNames });
53
+ const Comp = asChild ? Slot : Primitive.div;
54
+ const { findFirstFocusable } = useFocusFinders();
55
+
56
+ const handleKeyDown = useCallback(
57
+ (event: KeyboardEvent<HTMLDivElement>) => {
58
+ switch (event.key) {
59
+ case 'ArrowDown':
60
+ case 'ArrowUp': {
61
+ const direction = event.key === 'ArrowDown' ? 'down' : 'up';
62
+ const target = event.target as HTMLElement;
63
+
64
+ // Find ancestor with data-arrow-keys containing the relevant direction.
65
+ const ancestorWithArrowKeys = target.closest(`[data-arrow-keys*="${direction}"], [data-arrow-keys="all"]`);
66
+
67
+ // If no ancestor with data-arrow-keys found, proceed with row navigation.
68
+ if (!ancestorWithArrowKeys) {
69
+ // Find the closest row
70
+ const currentRow = target.closest('[role="row"]');
71
+ if (currentRow) {
72
+ // Find the treegrid container.
73
+ const treegrid = currentRow.closest('[role="treegrid"]');
74
+ if (treegrid) {
75
+ // Get all rows in the treegrid.
76
+ const rows = Array.from(treegrid.querySelectorAll('[role="row"]'));
77
+ const currentIndex = rows.indexOf(currentRow as Element);
78
+
79
+ // Find next or previous row.
80
+ const nextIndex = direction === 'down' ? currentIndex + 1 : currentIndex - 1;
81
+ const targetRow = rows[nextIndex];
82
+
83
+ if (targetRow) {
84
+ // Focus the first focusable element in the target row.
85
+ const firstFocusable = findFirstFocusable(targetRow as HTMLElement);
86
+ if (firstFocusable) {
87
+ event.preventDefault();
88
+ firstFocusable.focus();
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ break;
95
+ }
96
+ }
97
+ onKeyDownProp?.(event);
98
+ },
99
+ [findFirstFocusable, onKeyDownProp],
100
+ );
101
+
102
+ return (
103
+ <Comp
104
+ role='treegrid'
105
+ {...rest}
106
+ className={styles.root({ class: mx(className) })}
107
+ style={{ ...style, gridTemplateColumns }}
108
+ onKeyDown={handleKeyDown}
109
+ ref={forwardedRef}
110
+ >
111
+ {children}
112
+ </Comp>
113
+ );
114
+ },
115
+ );
116
+
117
+ type TreegridRowProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.div>> & {
118
+ id: string;
119
+ asChild?: boolean;
120
+ parentOf?: string;
121
+ defaultOpen?: boolean;
122
+ open?: boolean;
123
+ onOpenChange?(open: boolean): void;
124
+ };
125
+
126
+ const TreegridRow = forwardRef<HTMLDivElement, TreegridRowScopedProps<TreegridRowProps>>(
127
+ (
128
+ {
129
+ __treegridRowScope,
130
+ asChild,
131
+ classNames,
132
+ children,
133
+ id,
134
+ parentOf,
135
+ open: propsOpen,
136
+ defaultOpen,
137
+ onOpenChange: propsOnOpenChange,
138
+ ...props
139
+ },
140
+ forwardedRef,
141
+ ) => {
142
+ const Comp = asChild ? Slot : Primitive.div;
143
+ const pathParts = id.split(TREEGRID_PATH_SEPARATOR);
144
+ const level = pathParts.length - 1;
145
+ const [open, onOpenChange] = useControllableState({
146
+ prop: propsOpen,
147
+ onChange: propsOnOpenChange,
148
+ defaultProp: defaultOpen,
149
+ });
150
+
151
+ return (
152
+ <TreegridRowProvider open={open} onOpenChange={onOpenChange} scope={__treegridRowScope}>
153
+ <Comp
154
+ role='row'
155
+ aria-level={level}
156
+ className={styles.row({ class: mx(treegridTheme.rowLevel(level), classNames) })}
157
+ {...(parentOf && { 'aria-expanded': open, 'aria-owns': parentOf })}
158
+ {...props}
159
+ id={id}
160
+ ref={forwardedRef}
161
+ >
162
+ {children}
163
+ </Comp>
164
+ </TreegridRowProvider>
165
+ );
166
+ },
167
+ );
168
+
169
+ type TreegridCellProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.div>> & { indent?: boolean };
170
+
171
+ const TreegridCell = forwardRef<HTMLDivElement, TreegridCellProps>(
172
+ ({ classNames, children, indent, ...props }, forwardedRef) => {
173
+ return (
174
+ <div role='gridcell' className={styles.cell({ indent, class: mx(classNames) })} {...props} ref={forwardedRef}>
175
+ {children}
176
+ </div>
177
+ );
178
+ },
179
+ );
180
+
181
+ export type { TreegridRootProps, TreegridRowProps };
182
+
183
+ export const Treegrid = {
184
+ Root: TreegridRoot,
185
+ Row: TreegridRow,
186
+ Cell: TreegridCell,
187
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './Treegrid';
@@ -4,7 +4,10 @@
4
4
 
5
5
  export * from './Accordion';
6
6
  export * from './Combobox';
7
+ export * from './DropIndicator';
8
+ export * from './Empty';
7
9
  export * from './Listbox';
8
10
  export * from './OrderedList';
9
11
  export * from './Picker';
10
12
  export * from './Tree';
13
+ export * from './Treegrid';