@clevertask/react-sortable-tree 0.0.6 → 0.0.8
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/README.md +303 -83
- package/dist/e2e/index.js +51 -0
- package/dist/e2e/utils/drag-item.d.ts +15 -0
- package/dist/e2e/utils/expect-item-before.d.ts +2 -0
- package/dist/e2e/utils/expect-item-child-of.d.ts +3 -0
- package/dist/e2e/utils/get-tree-item-id.d.ts +2 -0
- package/dist/e2e/utils/index.d.ts +4 -0
- package/dist/{style.css → react-sortable-tree.css} +1 -1
- package/dist/react-sortable-tree.js +2404 -1381
- package/dist/react-sortable-tree.js.map +1 -1
- package/dist/src/SortableTree/SortableTree.d.ts +2 -0
- package/dist/src/SortableTree/components/TreeItem/TreeItem.d.ts +33 -0
- package/dist/{SortableTree → src/SortableTree}/components/TreeItem/index.d.ts +1 -0
- package/dist/src/SortableTree/components/TreeItemStructure/index.d.ts +39 -0
- package/dist/src/SortableTree/components/index.d.ts +4 -0
- package/dist/src/SortableTree/createSortableTreeGlobalStyles.d.ts +8 -0
- package/dist/src/SortableTree/index.d.ts +6 -0
- package/dist/{SortableTree → src/SortableTree}/types.d.ts +9 -11
- package/dist/{SortableTree → src/SortableTree}/utilities.d.ts +4 -4
- package/package.json +53 -29
- package/dist/SortableTree/SortableTree.d.ts +0 -4
- package/dist/SortableTree/components/TreeItem/TreeItem.d.ts +0 -23
- package/dist/SortableTree/components/index.d.ts +0 -1
- package/dist/SortableTree/index.d.ts +0 -3
- /package/dist/{SortableTree → src/SortableTree}/components/Action/Action.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Action/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Add/Add.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Add/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Handle/Handle.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Handle/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Remove/Remove.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/Remove/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/TreeItem/SortableTreeItem.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/keyboardCoordinates.d.ts +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{main.d.ts → src/main.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @clevertask/react-sortable-tree
|
|
2
2
|
|
|
3
|
-
A customizable React component for rendering and managing tree structures with drag-and-drop functionality.
|
|
3
|
+
A customizable React component for rendering and managing tree structures with drag-and-drop functionality. Built on top of the [dnd-kit sortable tree example](https://github.com/clauderic/dnd-kit/blob/master/stories/3%20-%20Examples/Tree/SortableTree.tsx).
|
|
4
|
+
|
|
5
|
+
This library is currently focused on **custom item rendering** and **type-safe tree structures**. A more detailed API and feature set will be released in a future major version with support for virtualization and multi-selection.
|
|
6
|
+
|
|
7
|
+
---
|
|
4
8
|
|
|
5
9
|
## Table of Contents
|
|
6
10
|
|
|
@@ -8,54 +12,210 @@ A customizable React component for rendering and managing tree structures with d
|
|
|
8
12
|
- [Usage](#usage)
|
|
9
13
|
- [Props](#props)
|
|
10
14
|
- [Types](#types)
|
|
11
|
-
- [TreeItem](#treeitem)
|
|
12
|
-
- [TreeItems](#treeitems)
|
|
13
15
|
- [Helper Functions](#helper-functions)
|
|
14
|
-
- [getItemById](#getItemById)
|
|
15
|
-
- [removeItemById](#removeitembyid)
|
|
16
|
-
- [setTreeItemProperties](#settreeitemproperties)
|
|
17
16
|
- [Roadmap](#roadmap)
|
|
18
|
-
- [Release Process](#release-process)
|
|
19
17
|
- [License](#license)
|
|
20
18
|
|
|
19
|
+
---
|
|
20
|
+
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
npm install @clevertask/react-sortable-tree
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
---
|
|
28
|
+
|
|
27
29
|
## Usage
|
|
28
30
|
|
|
31
|
+
### 1. Define your custom tree item type if needed
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
type CustomTreeItem = TreeItem<{
|
|
35
|
+
metadata?: Record<string, any>;
|
|
36
|
+
icon?: string;
|
|
37
|
+
}>;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Otherwise, the component will use the default [tree item type](#types).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### 2. Create your custom item component
|
|
45
|
+
|
|
46
|
+
This is the basic structure you can start with:
|
|
47
|
+
|
|
29
48
|
```tsx
|
|
30
|
-
import '@clevertask/react-sortable-tree
|
|
31
|
-
import React, { useState } from 'react';
|
|
32
|
-
import { SortableTree, TreeItems } from '@clevertask/react-sortable-tree';
|
|
49
|
+
import { RenderItemProps, TreeItemStructure } from '@clevertask/react-sortable-tree';
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
{ id: '1', label: 'Item 1', children: [] },
|
|
37
|
-
{ id: '2', label: 'Item 2', children: [{ id: '3', label: 'Item 2.1', children: [] }] },
|
|
38
|
-
]);
|
|
51
|
+
export const TreeItem = (props: RenderItemProps) => {
|
|
52
|
+
const { treeItem, collapsed, onCollapse, dragListeners } = props;
|
|
39
53
|
|
|
40
54
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
<TreeItemStructure {...props}>
|
|
56
|
+
{/* TreeItemStructure.DragHandler provides a default drag handle with accessible attributes and stable selectors for E2E testing.*/}
|
|
57
|
+
<TreeItemStructure.DragHandler>Drag me</TreeItemStructure.DragHandler>
|
|
58
|
+
|
|
59
|
+
{/* Or if you want to implement your own approach */}
|
|
60
|
+
<button {...dragListeners}>Drag me</button>
|
|
61
|
+
|
|
62
|
+
{onCollapse && <button onClick={onCollapse}>{collapsed ? 'Expand' : 'Collapse'}</button>}
|
|
63
|
+
|
|
64
|
+
<h5>{treeItem.label}</h5>
|
|
65
|
+
|
|
66
|
+
<button onClick={() => openItemDetailsModal(treeItem.id)}>Show treeItem info</button>
|
|
67
|
+
</TreeItemStructure>
|
|
49
68
|
);
|
|
50
|
-
}
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you need to change the indicator colors when dragging an item, you can use the `createSortableTreeGlobalStyles` for that:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const useSortableTreeGlobalStyles = createSortableTreeGlobalStyles({
|
|
76
|
+
indicatorColor: 'var(--orange-7)',
|
|
77
|
+
indicatorBorderColor: 'var(--orange-7)',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
useSortableTreeGlobalStyles();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This is a real-world example using Radix:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import {
|
|
87
|
+
RenderItemProps,
|
|
88
|
+
TreeItemStructure,
|
|
89
|
+
createSortableTreeGlobalStyles,
|
|
90
|
+
TreeItem as TTreeItem,
|
|
91
|
+
} from '@clevertask/react-sortable-tree';
|
|
92
|
+
import {
|
|
93
|
+
DragHandleDots2Icon,
|
|
94
|
+
ChevronRightIcon,
|
|
95
|
+
ChevronDownIcon,
|
|
96
|
+
TrashIcon,
|
|
97
|
+
PlusIcon,
|
|
98
|
+
} from '@radix-ui/react-icons';
|
|
99
|
+
import { Flex, Button, Text, Box } from '@radix-ui/themes';
|
|
100
|
+
import { CustomTreeItem } from '.';
|
|
101
|
+
|
|
102
|
+
export const TreeItem = (
|
|
103
|
+
props: RenderItemProps<CustomTreeItem> & {
|
|
104
|
+
onClickAddNestedItemButton: (id: string) => void;
|
|
105
|
+
onClickItemRemoveButton: (id: string) => void;
|
|
106
|
+
onItemClick: (id: string) => void;
|
|
107
|
+
},
|
|
108
|
+
) => {
|
|
109
|
+
const {
|
|
110
|
+
treeItem,
|
|
111
|
+
onCollapse,
|
|
112
|
+
collapsed,
|
|
113
|
+
onClickAddNestedItemButton,
|
|
114
|
+
onClickItemRemoveButton,
|
|
115
|
+
onItemClick,
|
|
116
|
+
} = props;
|
|
117
|
+
|
|
118
|
+
const useSortableTreeGlobalStyles = createSortableTreeGlobalStyles({
|
|
119
|
+
indicatorColor: 'var(--orange-7)',
|
|
120
|
+
indicatorBorderColor: 'var(--orange-7)',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
useSortableTreeGlobalStyles();
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<TreeItemStructure
|
|
127
|
+
{...props}
|
|
128
|
+
asDropZone={Box}
|
|
129
|
+
asDraggableItem={Box}
|
|
130
|
+
draggableItemStyle={{
|
|
131
|
+
display: 'flex',
|
|
132
|
+
justifyContent: 'space-between',
|
|
133
|
+
padding: '1rem',
|
|
134
|
+
border: '1px solid var(--gray-3)',
|
|
135
|
+
background: 'var(--color-background)',
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<Flex align="center" gap="5" direction="row">
|
|
139
|
+
<TreeItemStructure.DragHandler>
|
|
140
|
+
<DragHandleDots2Icon />
|
|
141
|
+
</TreeItemStructure.DragHandler>
|
|
142
|
+
|
|
143
|
+
{onCollapse && (
|
|
144
|
+
<Button color="gray" variant="ghost" onClick={onCollapse}>
|
|
145
|
+
{collapsed ?
|
|
146
|
+
<ChevronRightIcon />
|
|
147
|
+
: <ChevronDownIcon />}
|
|
148
|
+
</Button>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
<Text style={{ cursor: 'pointer' }} onClick={() => onItemClick(treeItem.id)}>
|
|
152
|
+
{treeItem.label} {treeItem.metadata.foo}
|
|
153
|
+
</Text>
|
|
154
|
+
</Flex>
|
|
155
|
+
|
|
156
|
+
<Flex align="center" gap="3" direction="row">
|
|
157
|
+
<Button variant="ghost" color="red" onClick={() => onClickItemRemoveButton(treeItem.id)}>
|
|
158
|
+
<TrashIcon />
|
|
159
|
+
</Button>
|
|
160
|
+
<Button
|
|
161
|
+
variant="ghost"
|
|
162
|
+
color="gray"
|
|
163
|
+
onClick={() => onClickAddNestedItemButton(treeItem.id)}
|
|
164
|
+
>
|
|
165
|
+
<PlusIcon />
|
|
166
|
+
</Button>
|
|
167
|
+
</Flex>
|
|
168
|
+
</TreeItemStructure>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `<TreeItemStructure/>` appends the dataSlots (for CSS styles), dropzone, and drag item container listeners and refs so you don't have to do it from scratch, but it's possible making your custom tree items without that component.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### 3. Use the `SortableTree` with your custom item
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import React, { useState } from 'react';
|
|
181
|
+
import { TreeItems, SortableTree } from '@clevertask/react-sortable-tree';
|
|
182
|
+
type CustomTreeItem = TreeItem<{ metadata?: Record<string, string> }>;
|
|
183
|
+
|
|
184
|
+
const [items, setItems] = useState<TreeItems<CustomTreeItem>>([
|
|
185
|
+
{ id: '1', label: 'Item 1', children: [] },
|
|
186
|
+
{
|
|
187
|
+
id: '2',
|
|
188
|
+
label: 'Item 2',
|
|
189
|
+
children: [{ id: '3', label: 'Item 2.1', children: [] }],
|
|
190
|
+
metadata: { foo: 'foo' },
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
<SortableTree<CustomTreeItem>
|
|
195
|
+
isCollapsible
|
|
196
|
+
showDropIndicator
|
|
197
|
+
items={items}
|
|
198
|
+
setItems={setItems}
|
|
199
|
+
renderItem={(props: RenderItemProps<CustomTreeItem>) => (
|
|
200
|
+
<TreeItem
|
|
201
|
+
{...props}
|
|
202
|
+
onClickAddNestedItemButton={onClickAddNestedItemButton}
|
|
203
|
+
onClickItemRemoveButton={onClickItemRemoveButton}
|
|
204
|
+
onItemClick={onItemClick}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
/>;
|
|
51
208
|
```
|
|
52
209
|
|
|
210
|
+
---
|
|
211
|
+
|
|
53
212
|
## Props
|
|
54
213
|
|
|
55
214
|
| Prop | Type | Default | Description |
|
|
56
215
|
| ------------------------- | --------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
-
| `items` | `TreeItems
|
|
58
|
-
| `setItems` | `
|
|
216
|
+
| `items` | `TreeItems<T>` | Required | The array of tree items to be rendered. |
|
|
217
|
+
| `setItems` | `(items: TreeItems<T>) => void` | Required | Callback function called when the tree items array changes. |
|
|
218
|
+
| `renderItem` | `(props: RenderItemProps<T>) => React.ReactNode` | Required | Function to render each tree item. |
|
|
59
219
|
| `indentationWidth` | `number` | `undefined` | The indentation width for children elements. |
|
|
60
220
|
| `isCollapsible` | `boolean` | `false` | Determines if tree items can be collapsed/expanded. |
|
|
61
221
|
| `onLazyLoadChildren` | `(id: UniqueIdentifier, isExpanding: boolean) => Promise<void>` | `undefined` | Callback for lazy loading child items when a parent is expanded. Useful for getting child items from an API endpoint |
|
|
@@ -67,106 +227,166 @@ function App() {
|
|
|
67
227
|
| `onDragEnd` | `(result: DropResult) => void` | `undefined` | Callback function called when a drag operation ends. |
|
|
68
228
|
| `onItemClick` | `(id: UniqueIdentifier) => void` | `undefined` | Callback function called when an item in the tree is clicked. |
|
|
69
229
|
|
|
230
|
+
---
|
|
231
|
+
|
|
70
232
|
## Types
|
|
71
233
|
|
|
72
234
|
### TreeItem
|
|
73
235
|
|
|
74
|
-
```
|
|
75
|
-
type TreeItem = {
|
|
236
|
+
```ts
|
|
237
|
+
type TreeItem<ExtraProps = unknown> = {
|
|
76
238
|
id: UniqueIdentifier;
|
|
77
239
|
label: string;
|
|
78
|
-
children: TreeItem[];
|
|
240
|
+
children: TreeItem<ExtraProps>[];
|
|
79
241
|
collapsed?: boolean;
|
|
80
242
|
canFetchChildren?: boolean;
|
|
81
243
|
disableDragging?: boolean;
|
|
82
|
-
|
|
83
|
-
};
|
|
244
|
+
} & ExtraProps;
|
|
84
245
|
```
|
|
85
246
|
|
|
86
247
|
### TreeItems
|
|
87
248
|
|
|
88
|
-
```
|
|
89
|
-
type TreeItems = TreeItem[];
|
|
249
|
+
```ts
|
|
250
|
+
type TreeItems<T = TreeItem> = T[];
|
|
90
251
|
```
|
|
91
252
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
253
|
+
### TreeStructureProps
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
export interface TreeItemStructureProps {
|
|
257
|
+
dropZoneRef: (element: HTMLElement | null) => void;
|
|
258
|
+
draggableItemRef: React.Ref<any>;
|
|
259
|
+
dropZoneStyle?: React.CSSProperties;
|
|
260
|
+
draggableItemStyle?: React.CSSProperties;
|
|
261
|
+
classNames?: {
|
|
262
|
+
dropZone?: string;
|
|
263
|
+
draggableItem?: string;
|
|
264
|
+
};
|
|
265
|
+
asDropZone?: React.ElementType;
|
|
266
|
+
asDraggableItem?: React.ElementType;
|
|
267
|
+
draggableItemProps?: Record<string, any>;
|
|
268
|
+
children?: React.ReactNode;
|
|
269
|
+
dataSlots: {
|
|
270
|
+
dropZone: Record<string, string | boolean | undefined>;
|
|
271
|
+
draggableItem: Record<string, string>;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
98
274
|
```
|
|
99
275
|
|
|
100
|
-
|
|
276
|
+
### RenderItemProps
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
export interface RenderItemProps<T extends TTreeItem = TTreeItem>
|
|
280
|
+
extends
|
|
281
|
+
Pick<
|
|
282
|
+
TreeItemStructureProps,
|
|
283
|
+
'classNames' | 'dropZoneStyle' | 'dropZoneRef' | 'draggableItemRef'
|
|
284
|
+
>,
|
|
285
|
+
Pick<
|
|
286
|
+
Props,
|
|
287
|
+
| 'onCollapse'
|
|
288
|
+
| 'childCount'
|
|
289
|
+
| 'clone'
|
|
290
|
+
| 'ghost'
|
|
291
|
+
| 'indicator'
|
|
292
|
+
| 'disableSelection'
|
|
293
|
+
| 'disableInteraction'
|
|
294
|
+
| 'collapsed'
|
|
295
|
+
> {
|
|
296
|
+
dragListeners?: any;
|
|
297
|
+
treeItem: T;
|
|
298
|
+
dataSlots: {
|
|
299
|
+
dropZone: Record<string, string | boolean | undefined>;
|
|
300
|
+
draggableItem: Record<string, string>;
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
```
|
|
101
304
|
|
|
102
|
-
|
|
305
|
+
---
|
|
103
306
|
|
|
104
|
-
|
|
105
|
-
const item = getItemById(items, '1');
|
|
106
|
-
```
|
|
307
|
+
## Helper Functions
|
|
107
308
|
|
|
108
|
-
###
|
|
309
|
+
### getItemById
|
|
109
310
|
|
|
110
|
-
```
|
|
111
|
-
function
|
|
311
|
+
```ts
|
|
312
|
+
function getItemById<T extends TreeItem>(items: TreeItems<T>, id: UniqueIdentifier): T | undefined;
|
|
112
313
|
```
|
|
113
314
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Usage example:
|
|
315
|
+
### removeItemById
|
|
117
316
|
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
|
|
317
|
+
```ts
|
|
318
|
+
function removeItemById<T extends TreeItem>(
|
|
319
|
+
items: TreeItems<T>,
|
|
320
|
+
id: UniqueIdentifier,
|
|
321
|
+
): TreeItems<T>;
|
|
121
322
|
```
|
|
122
323
|
|
|
123
324
|
### setTreeItemProperties
|
|
124
325
|
|
|
125
|
-
```
|
|
126
|
-
function setTreeItemProperties(
|
|
127
|
-
items: TreeItems
|
|
326
|
+
```ts
|
|
327
|
+
function setTreeItemProperties<T extends TreeItem>(
|
|
328
|
+
items: TreeItems<T>,
|
|
128
329
|
id: UniqueIdentifier,
|
|
129
|
-
setter: (value:
|
|
130
|
-
): TreeItems
|
|
330
|
+
setter: (value: T) => Partial<T>,
|
|
331
|
+
): TreeItems<T>;
|
|
131
332
|
```
|
|
132
333
|
|
|
133
|
-
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## E2E Helper Functions
|
|
337
|
+
|
|
338
|
+
While adding e2e tests for this library, we created some helper functions that makes e2e testing declarative and easy in a way the user would use the UI. Feel free to import these helpers on your e2e tests. Also feel free to see the tests added to this repo on the `e2e` folder to have an idea about using these helpers
|
|
339
|
+
|
|
340
|
+
### dragItem
|
|
341
|
+
|
|
342
|
+
Use it whenever you want to drag an item inside, after or before another item. This already knows how to move the item based on this library contract. You need to pass the name of the item you want to drag. There will be support for targeting items by their ID, to prevent getting items with the same name, but naturally, we get items by their name.
|
|
134
343
|
|
|
135
|
-
|
|
344
|
+
⚠️ So, when using E2E helpers that target items by name, item labels should be unique in the rendered tree.
|
|
136
345
|
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
346
|
+
```ts
|
|
347
|
+
await dragItem({
|
|
348
|
+
page,
|
|
349
|
+
expect,
|
|
350
|
+
from: { name: 'A' },
|
|
351
|
+
to: { name: 'C', position: 'inside' },
|
|
143
352
|
});
|
|
144
353
|
```
|
|
145
354
|
|
|
146
|
-
|
|
355
|
+
### expectItemToBeChildOf/expectItemNotToBeChildOf
|
|
356
|
+
|
|
357
|
+
This helper tells you if the item is a child of a given tree item. There's its opposite helper to assert if the target element is not child of a given item.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
const taskA = page.getByRole('treeitem', { name: 'A' });
|
|
361
|
+
const taskC = page.getByRole('treeitem', { name: 'C' });
|
|
362
|
+
await expectItemToBeChildOf(expect, taskA, taskC);
|
|
363
|
+
|
|
364
|
+
// or
|
|
147
365
|
|
|
148
|
-
|
|
366
|
+
await expectItemNotToBeChildOf(expect, taskA, taskC);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### expectItemBefore
|
|
149
370
|
|
|
150
|
-
|
|
151
|
-
- **Custom item rendering**: Allow users to provide custom components for rendering tree items.
|
|
152
|
-
- **Selection and Multi-selection**: Add support for selecting one or multiple items in the tree.
|
|
153
|
-
- **Drag multiple items**: Enable dragging and dropping multiple selected items at once.
|
|
154
|
-
- **API Example**: Provide a comprehensive example illustrating real-world usage with a backend API.
|
|
155
|
-
- **E2E tests**: It will ensure this component's working as expected.
|
|
371
|
+
This helper tells you if an item is before the "x" item. You don't need a function for after because it's a matter of targeting the items in their proper places.
|
|
156
372
|
|
|
157
|
-
|
|
373
|
+
```ts
|
|
374
|
+
await expectItemBefore(page, expect, 'C', 'A');
|
|
375
|
+
```
|
|
158
376
|
|
|
159
|
-
|
|
377
|
+
---
|
|
160
378
|
|
|
161
|
-
|
|
379
|
+
## Roadmap
|
|
162
380
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
381
|
+
- ✅ Custom item rendering (done!)
|
|
382
|
+
- ✅ E2E tests: It's partially done. We have yet to add item expanding tests and future tests for the next stuff we plan to add
|
|
383
|
+
- 🔜 Virtualization for large trees
|
|
384
|
+
- 🔜 Multi-selection support
|
|
385
|
+
- 🔜 Drag multiple items
|
|
386
|
+
- 🔜 Keyboard navigation
|
|
387
|
+
- 🔜 API usage example
|
|
168
388
|
|
|
169
|
-
|
|
389
|
+
---
|
|
170
390
|
|
|
171
391
|
## License
|
|
172
392
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
async function f({ page: t, expect: e, from: o, to: a }) {
|
|
2
|
+
const n = t.getByLabel(`Drag ${o.name}`, { exact: !0 }), m = t.getByRole("treeitem", { name: a.name });
|
|
3
|
+
await e(n).toBeVisible(), await e(m).toBeVisible();
|
|
4
|
+
const r = await n.boundingBox(), i = await m.boundingBox();
|
|
5
|
+
if (!r || !i)
|
|
6
|
+
throw new Error("Could not determine bounding boxes for drag operation");
|
|
7
|
+
const c = r.x + r.width / 2, w = r.y + r.height / 2;
|
|
8
|
+
let u = i.x + 8, d = i.y;
|
|
9
|
+
const b = 4;
|
|
10
|
+
switch (a.position) {
|
|
11
|
+
case "before":
|
|
12
|
+
d = i.y + b;
|
|
13
|
+
break;
|
|
14
|
+
case "after":
|
|
15
|
+
d = i.y + i.height - b;
|
|
16
|
+
break;
|
|
17
|
+
case "inside": {
|
|
18
|
+
const s = await m.locator("[data-tree-draggable]").boundingBox();
|
|
19
|
+
if (!s)
|
|
20
|
+
throw new Error("Could not determine draggable item bounds");
|
|
21
|
+
u = s.x + s.width * 0.05, d = s.y + s.height * 0.75;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await t.mouse.move(c, w), await t.mouse.down(), await t.mouse.move(c + 1, w + 1), await t.mouse.move(u, d, { steps: 10 }), await t.evaluate(() => new Promise(requestAnimationFrame)), await t.mouse.up();
|
|
26
|
+
}
|
|
27
|
+
async function l(t) {
|
|
28
|
+
const e = await t.getAttribute("data-tree-item-id");
|
|
29
|
+
if (!e)
|
|
30
|
+
throw new Error("Tree item does not have data-tree-item-id");
|
|
31
|
+
return e;
|
|
32
|
+
}
|
|
33
|
+
async function x(t, e, o, a) {
|
|
34
|
+
const n = await t.getByRole("treeitem").allTextContents();
|
|
35
|
+
e(n.indexOf(o)).toBeLessThan(n.indexOf(a));
|
|
36
|
+
}
|
|
37
|
+
async function h(t, e, o) {
|
|
38
|
+
const a = await l(o);
|
|
39
|
+
await t(e).toHaveAttribute("data-tree-item-parent-id", a);
|
|
40
|
+
}
|
|
41
|
+
async function B(t, e, o) {
|
|
42
|
+
const a = await l(o);
|
|
43
|
+
await t(e).not.toHaveAttribute("data-tree-item-parent-id", a);
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
f as dragItem,
|
|
47
|
+
x as expectItemBefore,
|
|
48
|
+
B as expectItemNotToBeChildOf,
|
|
49
|
+
h as expectItemToBeChildOf,
|
|
50
|
+
l as getTreeItemId
|
|
51
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Page, Expect } from '@playwright/test';
|
|
2
|
+
type DragPosition = 'before' | 'after' | 'inside';
|
|
3
|
+
interface DragItemOptions {
|
|
4
|
+
page: Page;
|
|
5
|
+
expect: Expect;
|
|
6
|
+
from: {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
to: {
|
|
10
|
+
name: string;
|
|
11
|
+
position: DragPosition;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function dragItem({ page, expect, from, to }: DragItemOptions): Promise<void>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Locator, Expect } from '@playwright/test';
|
|
2
|
+
export declare function expectItemToBeChildOf(expect: Expect, child: Locator, parent: Locator): Promise<void>;
|
|
3
|
+
export declare function expectItemNotToBeChildOf(expect: Expect, child: Locator, parent: Locator): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
._Wrapper_wguit_1{list-style:none;box-sizing:border-box;padding-left:var(--spacing);margin-bottom:-1px}._Wrapper_wguit_1._clone_wguit_8{display:inline-block;pointer-events:none;padding:5px 0 0 10px}._Wrapper_wguit_1._clone_wguit_8 ._TreeItem_wguit_16{--vertical-padding: 5px;padding-right:24px;border-radius:4px;box-shadow:0 15px 15px #2221511a}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23{opacity:1;position:relative;z-index:1;margin-bottom:-1px}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23 ._TreeItem_wguit_16{position:relative;padding:0;height:8px;border-color:#2389ff;background-color:#56a1f8}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23 ._TreeItem_wguit_16:before{position:absolute;left:-8px;top:-4px;display:block;content:"";width:12px;height:12px;border-radius:50%;border:1px solid #2389ff;background-color:#fff}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23 ._TreeItem_wguit_16>*{opacity:0;height:0}._Wrapper_wguit_1._ghost_wguit_23:not(._indicator_wguit_23){opacity:.5}._Wrapper_wguit_1._ghost_wguit_23 ._TreeItem_wguit_16>*{box-shadow:none;background-color:transparent}._TreeItem_wguit_16{--vertical-padding: 10px;position:relative;display:flex;align-items:center;padding:var(--vertical-padding) 10px;background-color:#fff;border:1px solid #dedede;color:#222;box-sizing:border-box}._Text_wguit_78{flex-grow:1;padding-left:.5rem;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;align-self:stretch;display:flex;align-items:center;cursor:pointer}._Count_wguit_90{position:absolute;top:-10px;right:-10px;display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background-color:#2389ff;font-size:.8rem;font-weight:600;color:#fff}._disableInteraction_wguit_106{pointer-events:none}._disableSelection_wguit_110 ._Text_wguit_78,._disableSelection_wguit_110 ._Count_wguit_90,._clone_wguit_8 ._Text_wguit_78,._clone_wguit_8 ._Count_wguit_90{user-select:none;-webkit-user-select:none}._Collapse_wguit_118 svg{transition:transform .25s ease}._Collapse_wguit_118._collapsed_wguit_122 svg{transform:rotate(-90deg)}._Action_7i4a4_1{display:flex;width:12px;padding:15px;align-items:center;justify-content:center;flex:0 0 auto;touch-action:none;cursor:var(--cursor, pointer);border-radius:5px;border:none;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;-webkit-tap-highlight-color:transparent}@media
|
|
1
|
+
._Wrapper_wguit_1{list-style:none;box-sizing:border-box;padding-left:var(--spacing);margin-bottom:-1px}._Wrapper_wguit_1._clone_wguit_8{display:inline-block;pointer-events:none;padding:5px 0 0 10px}._Wrapper_wguit_1._clone_wguit_8 ._TreeItem_wguit_16{--vertical-padding: 5px;padding-right:24px;border-radius:4px;box-shadow:0 15px 15px #2221511a}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23{opacity:1;position:relative;z-index:1;margin-bottom:-1px}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23 ._TreeItem_wguit_16{position:relative;padding:0;height:8px;border-color:#2389ff;background-color:#56a1f8}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23 ._TreeItem_wguit_16:before{position:absolute;left:-8px;top:-4px;display:block;content:"";width:12px;height:12px;border-radius:50%;border:1px solid #2389ff;background-color:#fff}._Wrapper_wguit_1._ghost_wguit_23._indicator_wguit_23 ._TreeItem_wguit_16>*{opacity:0;height:0}._Wrapper_wguit_1._ghost_wguit_23:not(._indicator_wguit_23){opacity:.5}._Wrapper_wguit_1._ghost_wguit_23 ._TreeItem_wguit_16>*{box-shadow:none;background-color:transparent}._TreeItem_wguit_16{--vertical-padding: 10px;position:relative;display:flex;align-items:center;padding:var(--vertical-padding) 10px;background-color:#fff;border:1px solid #dedede;color:#222;box-sizing:border-box}._Text_wguit_78{flex-grow:1;padding-left:.5rem;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;align-self:stretch;display:flex;align-items:center;cursor:pointer}._Count_wguit_90{position:absolute;top:-10px;right:-10px;display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background-color:#2389ff;font-size:.8rem;font-weight:600;color:#fff}._disableInteraction_wguit_106{pointer-events:none}._disableSelection_wguit_110 ._Text_wguit_78,._disableSelection_wguit_110 ._Count_wguit_90,._clone_wguit_8 ._Text_wguit_78,._clone_wguit_8 ._Count_wguit_90{user-select:none;-webkit-user-select:none}._Collapse_wguit_118 svg{transition:transform .25s ease}._Collapse_wguit_118._collapsed_wguit_122 svg{transform:rotate(-90deg)}._Action_7i4a4_1{display:flex;width:12px;padding:15px;align-items:center;justify-content:center;flex:0 0 auto;touch-action:none;cursor:var(--cursor, pointer);border-radius:5px;border:none;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;-webkit-tap-highlight-color:transparent}@media(hover:hover){._Action_7i4a4_1:hover{background-color:var(--action-background, rgba(0, 0, 0, .05))}._Action_7i4a4_1:hover svg{fill:#6f7b88}}._Action_7i4a4_1 svg{flex:0 0 auto;margin:auto;height:100%;overflow:visible;fill:#919eab}._Action_7i4a4_1:active{background-color:var(--background, rgba(0, 0, 0, .05))}._Action_7i4a4_1:active svg{fill:var(--fill, #788491)}._Action_7i4a4_1:focus-visible{outline:none;box-shadow:0 0 0 2px #fff0,0 0 0 2px #4c9ffe}
|