@dxos/react-ui-list 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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.
- package/dist/lib/browser/index.mjs +230 -191
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +230 -191
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/List/List.d.ts +6 -4
- package/dist/types/src/components/List/List.d.ts.map +1 -1
- package/dist/types/src/components/List/ListItem.d.ts +8 -6
- package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.d.ts +6 -5
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeContext.d.ts +21 -10
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItem.d.ts +8 -0
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/Tree/index.d.ts +2 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -18
- package/src/components/Accordion/Accordion.stories.tsx +3 -3
- package/src/components/Accordion/AccordionItem.tsx +1 -1
- package/src/components/List/List.stories.tsx +10 -10
- package/src/components/List/List.tsx +4 -9
- package/src/components/List/ListItem.tsx +55 -35
- package/src/components/Tree/Tree.stories.tsx +103 -27
- package/src/components/Tree/Tree.tsx +30 -40
- package/src/components/Tree/TreeContext.tsx +18 -9
- package/src/components/Tree/TreeItem.tsx +176 -101
- package/src/components/Tree/TreeItemHeading.tsx +3 -4
- package/src/components/Tree/TreeItemToggle.tsx +4 -4
- package/src/components/Tree/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-list",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.bcb3aa67d6",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -30,35 +30,36 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
33
|
-
"@effect-atom/atom-react": "^0.
|
|
33
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
34
34
|
"@radix-ui/react-accordion": "1.2.3",
|
|
35
35
|
"@radix-ui/react-context": "1.1.1",
|
|
36
|
-
"@
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/react-ui-text-tooltip": "0.8.4-main.
|
|
42
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
43
|
-
"@dxos/ui-types": "0.8.4-main.
|
|
44
|
-
"@dxos/util": "0.8.4-main.
|
|
36
|
+
"@radix-ui/react-slot": "1.1.2",
|
|
37
|
+
"@dxos/debug": "0.8.4-main.bcb3aa67d6",
|
|
38
|
+
"@dxos/invariant": "0.8.4-main.bcb3aa67d6",
|
|
39
|
+
"@dxos/echo": "0.8.4-main.bcb3aa67d6",
|
|
40
|
+
"@dxos/log": "0.8.4-main.bcb3aa67d6",
|
|
41
|
+
"@dxos/react-ui-text-tooltip": "0.8.4-main.bcb3aa67d6",
|
|
42
|
+
"@dxos/ui-theme": "0.8.4-main.bcb3aa67d6",
|
|
43
|
+
"@dxos/ui-types": "0.8.4-main.bcb3aa67d6",
|
|
44
|
+
"@dxos/util": "0.8.4-main.bcb3aa67d6",
|
|
45
|
+
"@dxos/react-ui": "0.8.4-main.bcb3aa67d6"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/react": "~19.2.7",
|
|
48
49
|
"@types/react-dom": "~19.2.3",
|
|
49
|
-
"effect": "3.
|
|
50
|
+
"effect": "3.20.0",
|
|
50
51
|
"react": "~19.2.3",
|
|
51
52
|
"react-dom": "~19.2.3",
|
|
52
|
-
"vite": "7.1.
|
|
53
|
-
"@dxos/random": "0.8.4-main.
|
|
54
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
53
|
+
"vite": "^7.1.11",
|
|
54
|
+
"@dxos/random": "0.8.4-main.bcb3aa67d6",
|
|
55
|
+
"@dxos/storybook-utils": "0.8.4-main.bcb3aa67d6"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
|
-
"effect": "3.
|
|
58
|
+
"effect": "3.20.0",
|
|
58
59
|
"react": "~19.2.3",
|
|
59
60
|
"react-dom": "~19.2.3",
|
|
60
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
61
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
61
|
+
"@dxos/react-ui": "0.8.4-main.bcb3aa67d6",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.bcb3aa67d6"
|
|
62
63
|
},
|
|
63
64
|
"publishConfig": {
|
|
64
65
|
"access": "public"
|
|
@@ -22,9 +22,9 @@ const items: TestItem[] = Array.from({ length: 10 }, (_, i) => ({
|
|
|
22
22
|
|
|
23
23
|
const DefaultStory = () => {
|
|
24
24
|
return (
|
|
25
|
-
<Accordion.Root<TestItem> items={items} classNames='
|
|
25
|
+
<Accordion.Root<TestItem> items={items} classNames='w-[40rem]'>
|
|
26
26
|
{({ items }) => (
|
|
27
|
-
<div className='flex flex-col
|
|
27
|
+
<div className='flex flex-col w-full border-y border-separator divide-y divide-separator'>
|
|
28
28
|
{items.map((item) => (
|
|
29
29
|
<Accordion.Item key={item.id} item={item} classNames='border-x border-separator'>
|
|
30
30
|
<Accordion.ItemHeader>{item.name}</Accordion.ItemHeader>
|
|
@@ -42,7 +42,7 @@ const DefaultStory = () => {
|
|
|
42
42
|
const meta = {
|
|
43
43
|
title: 'ui/react-ui-list/Accordion',
|
|
44
44
|
render: DefaultStory,
|
|
45
|
-
decorators: [withTheme, withLayout({ layout: 'column' })],
|
|
45
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
46
46
|
} satisfies Meta<typeof Accordion>;
|
|
47
47
|
|
|
48
48
|
export default meta;
|
|
@@ -43,7 +43,7 @@ export type AccordionItemHeaderProps = ThemedClassName<AccordionPrimitive.Accord
|
|
|
43
43
|
export const AccordionItemHeader = ({ classNames, children, ...props }: AccordionItemHeaderProps) => {
|
|
44
44
|
return (
|
|
45
45
|
<AccordionPrimitive.Header {...props} className={mx(classNames)}>
|
|
46
|
-
<AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset
|
|
46
|
+
<AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset w-full text-start'>
|
|
47
47
|
{children}
|
|
48
48
|
<Icon
|
|
49
49
|
icon='ph--caret-right--regular'
|
|
@@ -7,16 +7,16 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
7
7
|
import * as Schema from 'effect/Schema';
|
|
8
8
|
import React, { useContext, useMemo } from 'react';
|
|
9
9
|
|
|
10
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
import { withRegistry } from '@dxos/storybook-utils';
|
|
12
|
-
import {
|
|
12
|
+
import { mx } from '@dxos/ui-theme';
|
|
13
13
|
import { arrayMove } from '@dxos/util';
|
|
14
14
|
|
|
15
15
|
import { List, type ListRootProps } from './List';
|
|
16
16
|
import { TestItemSchema, type TestItemType, type TestList, createList } from './testing';
|
|
17
17
|
|
|
18
18
|
// TODO(burdon): var-icon-size.
|
|
19
|
-
const grid = 'grid grid-cols-[32px_1fr_32px] min-
|
|
19
|
+
const grid = 'grid grid-cols-[32px_1fr_32px] min-h-[2rem] rounded-sm';
|
|
20
20
|
|
|
21
21
|
const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
22
22
|
const registry = useContext(RegistryContext);
|
|
@@ -45,15 +45,15 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
45
45
|
<List.Root<TestItemType> dragPreview items={items} getId={(item) => item.id} onMove={handleMove} {...props}>
|
|
46
46
|
{({ items }) => (
|
|
47
47
|
<>
|
|
48
|
-
<div className='flex flex-col
|
|
48
|
+
<div className='flex flex-col w-full'>
|
|
49
49
|
<div role='none' className={grid}>
|
|
50
50
|
<div />
|
|
51
51
|
<div className='flex items-center text-sm'>Items</div>
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
|
-
<div role='list' className='
|
|
54
|
+
<div role='list' className='h-full w-full overflow-auto'>
|
|
55
55
|
{items?.map((item) => (
|
|
56
|
-
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid
|
|
56
|
+
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
|
|
57
57
|
<List.ItemDragHandle />
|
|
58
58
|
<List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
|
|
59
59
|
<List.ItemDeleteButton onClick={() => handleDelete(item)} />
|
|
@@ -69,7 +69,7 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
69
69
|
|
|
70
70
|
<List.ItemDragPreview<TestItemType>>
|
|
71
71
|
{({ item }) => (
|
|
72
|
-
<List.ItemWrapper classNames={mx(grid, 'bg-
|
|
72
|
+
<List.ItemWrapper classNames={mx(grid, 'bg-modal-surface border border-separator')}>
|
|
73
73
|
<List.ItemDragHandle />
|
|
74
74
|
<div className='flex items-center'>{item.name}</div>
|
|
75
75
|
</List.ItemWrapper>
|
|
@@ -89,9 +89,9 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
89
89
|
return (
|
|
90
90
|
<List.Root<TestItemType> dragPreview items={items} {...props}>
|
|
91
91
|
{({ items }) => (
|
|
92
|
-
<div role='list' className='
|
|
92
|
+
<div role='list' className='h-full w-full overflow-auto'>
|
|
93
93
|
{items?.map((item) => (
|
|
94
|
-
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid
|
|
94
|
+
<List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
|
|
95
95
|
<List.ItemDragHandle />
|
|
96
96
|
<List.ItemTitle>{item.name}</List.ItemTitle>
|
|
97
97
|
<List.ItemDeleteButton />
|
|
@@ -106,7 +106,7 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
|
|
|
106
106
|
const meta = {
|
|
107
107
|
title: 'ui/react-ui-list/List',
|
|
108
108
|
component: List.Root,
|
|
109
|
-
decorators: [withTheme, withRegistry],
|
|
109
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withRegistry],
|
|
110
110
|
parameters: {
|
|
111
111
|
layout: 'fullscreen',
|
|
112
112
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
ListItem,
|
|
7
|
-
|
|
7
|
+
ListItemIconButton,
|
|
8
8
|
ListItemDeleteButton,
|
|
9
9
|
ListItemDragHandle,
|
|
10
10
|
ListItemDragPreview,
|
|
@@ -15,17 +15,12 @@ import {
|
|
|
15
15
|
} from './ListItem';
|
|
16
16
|
import { ListRoot, type ListRootProps } from './ListRoot';
|
|
17
17
|
|
|
18
|
-
// TODO(burdon): Multi-select model.
|
|
19
|
-
// TODO(burdon): Key nav.
|
|
20
|
-
// TODO(burdon): Animation.
|
|
21
|
-
// TODO(burdon): Constrain axis.
|
|
22
|
-
// TODO(burdon): Tree view.
|
|
23
|
-
// TODO(burdon): Fix autoscroll while dragging.
|
|
24
|
-
|
|
25
18
|
/**
|
|
26
19
|
* Draggable list.
|
|
27
20
|
* Ref: https://github.com/atlassian/pragmatic-drag-and-drop
|
|
28
21
|
* Ref: https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/task.tsx
|
|
22
|
+
*
|
|
23
|
+
* @deprecated Use react-ui-mosaic.
|
|
29
24
|
*/
|
|
30
25
|
export const List = {
|
|
31
26
|
Root: ListRoot,
|
|
@@ -33,8 +28,8 @@ export const List = {
|
|
|
33
28
|
ItemDragPreview: ListItemDragPreview,
|
|
34
29
|
ItemWrapper: ListItemWrapper,
|
|
35
30
|
ItemDragHandle: ListItemDragHandle,
|
|
31
|
+
ItemIconButton: ListItemIconButton,
|
|
36
32
|
ItemDeleteButton: ListItemDeleteButton,
|
|
37
|
-
ItemButton: ListItemButton,
|
|
38
33
|
ItemTitle: ListItemTitle,
|
|
39
34
|
};
|
|
40
35
|
|
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
extractClosestEdge,
|
|
12
12
|
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
13
13
|
import { createContext } from '@radix-ui/react-context';
|
|
14
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
14
15
|
import React, {
|
|
15
16
|
type ComponentProps,
|
|
16
17
|
type HTMLAttributes,
|
|
17
|
-
type MutableRefObject,
|
|
18
18
|
type PropsWithChildren,
|
|
19
19
|
type ReactNode,
|
|
20
|
+
RefObject,
|
|
20
21
|
useEffect,
|
|
21
22
|
useRef,
|
|
22
23
|
useState,
|
|
@@ -61,7 +62,7 @@ const stateStyles: { [Key in ItemDragState['type']]?: HTMLAttributes<HTMLDivElem
|
|
|
61
62
|
|
|
62
63
|
type ListItemContext<T extends ListItemRecord> = {
|
|
63
64
|
item: T;
|
|
64
|
-
dragHandleRef:
|
|
65
|
+
dragHandleRef: RefObject<HTMLButtonElement | null>;
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -80,6 +81,8 @@ export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
|
|
|
80
81
|
PropsWithChildren<
|
|
81
82
|
{
|
|
82
83
|
item: T;
|
|
84
|
+
asChild?: boolean;
|
|
85
|
+
selected?: boolean;
|
|
83
86
|
} & HTMLAttributes<HTMLDivElement>
|
|
84
87
|
>
|
|
85
88
|
>;
|
|
@@ -87,14 +90,22 @@ export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
|
|
|
87
90
|
/**
|
|
88
91
|
* Draggable list item.
|
|
89
92
|
*/
|
|
90
|
-
export const ListItem = <T extends ListItemRecord>({
|
|
93
|
+
export const ListItem = <T extends ListItemRecord>({
|
|
94
|
+
children,
|
|
95
|
+
classNames,
|
|
96
|
+
item,
|
|
97
|
+
asChild,
|
|
98
|
+
selected,
|
|
99
|
+
...props
|
|
100
|
+
}: ListItemProps<T>) => {
|
|
101
|
+
const Comp = asChild ? Slot : 'div';
|
|
91
102
|
const { isItem, readonly, dragPreview, setState: setRootState } = useListContext(LIST_ITEM_NAME);
|
|
92
|
-
const
|
|
93
|
-
const dragHandleRef = useRef<
|
|
103
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
104
|
+
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
|
94
105
|
const [state, setState] = useState<ItemDragState>(idle);
|
|
95
106
|
|
|
96
107
|
useEffect(() => {
|
|
97
|
-
const element =
|
|
108
|
+
const element = rootRef.current;
|
|
98
109
|
invariant(element);
|
|
99
110
|
return combine(
|
|
100
111
|
//
|
|
@@ -170,12 +181,18 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
|
|
|
170
181
|
|
|
171
182
|
return (
|
|
172
183
|
<ListItemProvider item={item} dragHandleRef={dragHandleRef}>
|
|
173
|
-
<
|
|
184
|
+
<Comp
|
|
185
|
+
{...props}
|
|
186
|
+
role='listitem'
|
|
187
|
+
aria-selected={selected}
|
|
188
|
+
className={mx('relative p-1 dx-selected dx-hover', classNames, stateStyles[state.type])}
|
|
189
|
+
ref={rootRef}
|
|
190
|
+
>
|
|
174
191
|
{children}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
</Comp>
|
|
193
|
+
{state.type === 'is-dragging-over' && state.closestEdge && (
|
|
194
|
+
<NaturalListItem.DropIndicator edge={state.closestEdge} />
|
|
195
|
+
)}
|
|
179
196
|
</ListItemProvider>
|
|
180
197
|
);
|
|
181
198
|
};
|
|
@@ -184,47 +201,48 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
|
|
|
184
201
|
// List item components
|
|
185
202
|
//
|
|
186
203
|
|
|
187
|
-
export const
|
|
204
|
+
export const ListItemIconButton = ({
|
|
188
205
|
autoHide = true,
|
|
206
|
+
iconOnly = true,
|
|
207
|
+
variant = 'ghost',
|
|
189
208
|
classNames,
|
|
190
209
|
disabled,
|
|
191
|
-
icon = 'ph--x--regular',
|
|
192
|
-
label,
|
|
193
210
|
...props
|
|
194
|
-
}:
|
|
195
|
-
|
|
196
|
-
const { state } = useListContext('DELETE_BUTTON');
|
|
211
|
+
}: IconButtonProps & { autoHide?: boolean }) => {
|
|
212
|
+
const { state } = useListContext('ITEM_BUTTON');
|
|
197
213
|
const isDisabled = state.type !== 'idle' || disabled;
|
|
198
|
-
const { t } = useTranslation(osTranslations);
|
|
199
214
|
return (
|
|
200
215
|
<IconButton
|
|
201
|
-
iconOnly
|
|
202
|
-
variant='ghost'
|
|
203
216
|
{...props}
|
|
204
|
-
icon={icon}
|
|
205
217
|
disabled={isDisabled}
|
|
206
|
-
|
|
218
|
+
iconOnly={iconOnly}
|
|
219
|
+
variant={variant}
|
|
207
220
|
classNames={[classNames, autoHide && disabled && 'hidden']}
|
|
208
221
|
/>
|
|
209
222
|
);
|
|
210
223
|
};
|
|
211
224
|
|
|
212
|
-
|
|
225
|
+
// TODO(burdon): Generalize to action button.
|
|
226
|
+
export const ListItemDeleteButton = ({
|
|
213
227
|
autoHide = true,
|
|
214
|
-
iconOnly = true,
|
|
215
|
-
variant = 'ghost',
|
|
216
228
|
classNames,
|
|
217
229
|
disabled,
|
|
230
|
+
icon = 'ph--x--regular',
|
|
231
|
+
label,
|
|
218
232
|
...props
|
|
219
|
-
}: IconButtonProps &
|
|
220
|
-
|
|
233
|
+
}: Partial<Pick<IconButtonProps, 'icon'>> &
|
|
234
|
+
Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
|
|
235
|
+
const { state } = useListContext('DELETE_BUTTON');
|
|
221
236
|
const isDisabled = state.type !== 'idle' || disabled;
|
|
237
|
+
const { t } = useTranslation(osTranslations);
|
|
222
238
|
return (
|
|
223
239
|
<IconButton
|
|
224
240
|
{...props}
|
|
241
|
+
variant='ghost'
|
|
225
242
|
disabled={isDisabled}
|
|
226
|
-
|
|
227
|
-
|
|
243
|
+
icon={icon}
|
|
244
|
+
iconOnly
|
|
245
|
+
label={label ?? t('delete.label')}
|
|
228
246
|
classNames={[classNames, autoHide && disabled && 'hidden']}
|
|
229
247
|
/>
|
|
230
248
|
);
|
|
@@ -235,12 +253,12 @@ export const ListItemDragHandle = ({ disabled }: Pick<IconButtonProps, 'disabled
|
|
|
235
253
|
const { t } = useTranslation(osTranslations);
|
|
236
254
|
return (
|
|
237
255
|
<IconButton
|
|
238
|
-
iconOnly
|
|
239
256
|
variant='ghost'
|
|
240
|
-
label={t('drag handle label')}
|
|
241
|
-
ref={dragHandleRef as any}
|
|
242
|
-
icon='ph--dots-six-vertical--regular'
|
|
243
257
|
disabled={disabled}
|
|
258
|
+
icon='ph--dots-six-vertical--regular'
|
|
259
|
+
iconOnly
|
|
260
|
+
label={t('drag-handle.label')}
|
|
261
|
+
ref={dragHandleRef}
|
|
244
262
|
/>
|
|
245
263
|
);
|
|
246
264
|
};
|
|
@@ -255,7 +273,9 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
|
|
|
255
273
|
};
|
|
256
274
|
|
|
257
275
|
export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
|
|
258
|
-
<div className={mx('flex
|
|
276
|
+
<div role='none' className={mx('flex w-full gap-2', classNames)}>
|
|
277
|
+
{children}
|
|
278
|
+
</div>
|
|
259
279
|
);
|
|
260
280
|
|
|
261
281
|
export const ListItemTitle = ({
|
|
@@ -263,7 +283,7 @@ export const ListItemTitle = ({
|
|
|
263
283
|
children,
|
|
264
284
|
...props
|
|
265
285
|
}: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
|
|
266
|
-
<div className={mx('flex grow items-center truncate', classNames)} {...props}>
|
|
286
|
+
<div role='none' className={mx('flex grow items-center truncate', classNames)} {...props}>
|
|
267
287
|
{children}
|
|
268
288
|
</div>
|
|
269
289
|
);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
6
|
import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
7
|
-
import { Atom, RegistryContext
|
|
7
|
+
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
8
8
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
9
9
|
import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
10
10
|
|
|
@@ -17,6 +17,7 @@ import { Path } from '../../util';
|
|
|
17
17
|
|
|
18
18
|
import { type TestItem, createTree, updateState } from './testing';
|
|
19
19
|
import { Tree } from './Tree';
|
|
20
|
+
import { type TreeModel } from './TreeContext';
|
|
20
21
|
import { type TreeData } from './TreeItem';
|
|
21
22
|
|
|
22
23
|
faker.seed(1234);
|
|
@@ -27,27 +28,111 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
27
28
|
const registry = useContext(RegistryContext);
|
|
28
29
|
const stateAtomsRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
29
30
|
|
|
30
|
-
const getOrCreateStateAtom = useCallback((
|
|
31
|
-
let atom = stateAtomsRef.current.get(
|
|
31
|
+
const getOrCreateStateAtom = useCallback((pathKey: string) => {
|
|
32
|
+
let atom = stateAtomsRef.current.get(pathKey);
|
|
32
33
|
if (!atom) {
|
|
33
34
|
atom = Atom.make({ open: false, current: false }).pipe(Atom.keepAlive);
|
|
34
|
-
stateAtomsRef.current.set(
|
|
35
|
+
stateAtomsRef.current.set(pathKey, atom);
|
|
35
36
|
}
|
|
36
37
|
return atom;
|
|
37
38
|
}, []);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
// Build a lookup map of all items by ID.
|
|
41
|
+
const itemMap = useMemo(() => {
|
|
42
|
+
const map = new Map<string, TestItem>();
|
|
43
|
+
const walk = (item: TestItem) => {
|
|
44
|
+
map.set(item.id, item);
|
|
45
|
+
item.items?.forEach(walk);
|
|
46
|
+
};
|
|
47
|
+
walk(tree);
|
|
48
|
+
return map;
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
// Build a child IDs map keyed by parent ID.
|
|
52
|
+
const childIdsMap = useMemo(() => {
|
|
53
|
+
const map = new Map<string, string[]>();
|
|
54
|
+
const walk = (item: TestItem) => {
|
|
55
|
+
if (item.items) {
|
|
56
|
+
map.set(
|
|
57
|
+
item.id,
|
|
58
|
+
item.items.map((child) => child.id),
|
|
59
|
+
);
|
|
60
|
+
item.items.forEach(walk);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// Root children.
|
|
64
|
+
map.set(
|
|
65
|
+
tree.id,
|
|
66
|
+
(tree.items ?? []).map((child) => child.id),
|
|
67
|
+
);
|
|
68
|
+
walk(tree);
|
|
69
|
+
return map;
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const childIdsFamily = useMemo(
|
|
73
|
+
() => Atom.family((id: string) => Atom.make(() => childIdsMap.get(id) ?? []).pipe(Atom.keepAlive)),
|
|
74
|
+
[childIdsMap],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const itemFamily = useMemo(
|
|
78
|
+
() => Atom.family((id: string) => Atom.make(() => itemMap.get(id)).pipe(Atom.keepAlive)),
|
|
79
|
+
[itemMap],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const itemPropsFamily = useMemo(
|
|
83
|
+
() =>
|
|
84
|
+
Atom.family((pathKey: string) => {
|
|
85
|
+
const id = pathKey.split('~').pop()!;
|
|
86
|
+
return Atom.make(() => {
|
|
87
|
+
const parent = itemMap.get(id);
|
|
88
|
+
if (!parent) {
|
|
89
|
+
return { id, label: id };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
id: parent.id,
|
|
93
|
+
label: parent.name,
|
|
94
|
+
icon: parent.icon,
|
|
95
|
+
...((parent.items?.length ?? 0) > 0 && {
|
|
96
|
+
parentOf: parent.items!.map(({ id }) => id),
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}).pipe(Atom.keepAlive);
|
|
100
|
+
}),
|
|
101
|
+
[itemMap],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const itemOpenFamily = useMemo(
|
|
105
|
+
() =>
|
|
106
|
+
Atom.family((pathKey: string) => {
|
|
107
|
+
const stateAtom = getOrCreateStateAtom(pathKey);
|
|
108
|
+
return Atom.make((get) => get(stateAtom).open).pipe(Atom.keepAlive);
|
|
109
|
+
}),
|
|
110
|
+
[getOrCreateStateAtom],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const itemCurrentFamily = useMemo(
|
|
114
|
+
() =>
|
|
115
|
+
Atom.family((pathKey: string) => {
|
|
116
|
+
const stateAtom = getOrCreateStateAtom(pathKey);
|
|
117
|
+
return Atom.make((get) => get(stateAtom).current).pipe(Atom.keepAlive);
|
|
118
|
+
}),
|
|
45
119
|
[getOrCreateStateAtom],
|
|
46
120
|
);
|
|
47
121
|
|
|
122
|
+
const model: TreeModel<TestItem> = useMemo(
|
|
123
|
+
() => ({
|
|
124
|
+
childIds: (parentId?: string) => childIdsFamily(parentId ?? tree.id),
|
|
125
|
+
item: (id: string) => itemFamily(id),
|
|
126
|
+
itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
|
|
127
|
+
itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
|
|
128
|
+
itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
|
|
129
|
+
}),
|
|
130
|
+
[childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
|
|
131
|
+
);
|
|
132
|
+
|
|
48
133
|
const handleOpenChange = useCallback(
|
|
49
|
-
({ path:
|
|
50
|
-
const path = Path.create(...
|
|
134
|
+
({ path: pathProp, open }: { path: string[]; open: boolean }) => {
|
|
135
|
+
const path = Path.create(...pathProp);
|
|
51
136
|
const atom = getOrCreateStateAtom(path);
|
|
52
137
|
const prev = registry.get(atom);
|
|
53
138
|
registry.set(atom, { ...prev, open });
|
|
@@ -56,8 +141,8 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
56
141
|
);
|
|
57
142
|
|
|
58
143
|
const handleSelect = useCallback(
|
|
59
|
-
({ path:
|
|
60
|
-
const path = Path.create(...
|
|
144
|
+
({ path: pathProp, current }: { path: string[]; current: boolean }) => {
|
|
145
|
+
const path = Path.create(...pathProp);
|
|
61
146
|
const atom = getOrCreateStateAtom(path);
|
|
62
147
|
const prev = registry.get(atom);
|
|
63
148
|
registry.set(atom, { ...prev, current });
|
|
@@ -89,22 +174,13 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
89
174
|
|
|
90
175
|
return (
|
|
91
176
|
<Tree
|
|
177
|
+
model={model}
|
|
92
178
|
id={tree.id}
|
|
179
|
+
rootId={tree.id}
|
|
93
180
|
draggable={draggable}
|
|
94
|
-
useItems={(parent?: TestItem) => parent?.items ?? tree.items}
|
|
95
|
-
getProps={(parent: TestItem) => ({
|
|
96
|
-
id: parent.id,
|
|
97
|
-
label: parent.name,
|
|
98
|
-
icon: parent.icon,
|
|
99
|
-
...((parent.items?.length ?? 0) > 0 && {
|
|
100
|
-
parentOf: parent.items!.map(({ id }) => id),
|
|
101
|
-
}),
|
|
102
|
-
})}
|
|
103
|
-
useIsOpen={(_path: string[]) => useItemState(_path).open}
|
|
104
|
-
useIsCurrent={(_path: string[]) => useItemState(_path).current}
|
|
105
181
|
renderColumns={() => (
|
|
106
182
|
<div className='flex items-center'>
|
|
107
|
-
<Icon icon='ph--placeholder--regular'
|
|
183
|
+
<Icon icon='ph--placeholder--regular' />
|
|
108
184
|
</div>
|
|
109
185
|
)}
|
|
110
186
|
onOpenChange={handleOpenChange}
|
|
@@ -116,7 +192,7 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
|
|
|
116
192
|
const meta = {
|
|
117
193
|
title: 'ui/react-ui-list/Tree',
|
|
118
194
|
|
|
119
|
-
decorators: [withTheme, withRegistry],
|
|
195
|
+
decorators: [withTheme(), withRegistry],
|
|
120
196
|
component: Tree,
|
|
121
197
|
render: DefaultStory,
|
|
122
198
|
} satisfies Meta<typeof Tree<TestItem>>;
|