@clevertask/react-sortable-tree 0.0.7 → 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 +93 -32
- 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/react-sortable-tree.css +1 -1
- package/dist/react-sortable-tree.js +770 -731
- package/dist/react-sortable-tree.js.map +1 -1
- package/dist/src/SortableTree/components/TreeItemStructure/index.d.ts +39 -0
- package/package.json +34 -11
- package/dist/SortableTree/components/TreeItemStructure/index.d.ts +0 -19
- /package/dist/{SortableTree → src/SortableTree}/SortableTree.d.ts +0 -0
- /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}/components/TreeItem/TreeItem.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/TreeItem/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/components/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/createSortableTreeGlobalStyles.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/index.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/keyboardCoordinates.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/types.d.ts +0 -0
- /package/dist/{SortableTree → src/SortableTree}/utilities.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
|
@@ -46,25 +46,40 @@ Otherwise, the component will use the default [tree item type](#types).
|
|
|
46
46
|
This is the basic structure you can start with:
|
|
47
47
|
|
|
48
48
|
```tsx
|
|
49
|
-
import {
|
|
50
|
-
RenderItemProps,
|
|
51
|
-
TreeItemStructure,
|
|
52
|
-
createSortableTreeGlobalStyles,
|
|
53
|
-
RenderItemProps,
|
|
54
|
-
} from '@clevertask/react-sortable-tree';
|
|
49
|
+
import { RenderItemProps, TreeItemStructure } from '@clevertask/react-sortable-tree';
|
|
55
50
|
|
|
56
|
-
export const TreeItem = (
|
|
57
|
-
|
|
58
|
-
<button {...dragListeners}>Drag me</button>
|
|
59
|
-
{onCollapse && <button onClick={onCollapse}>{collapsed ? 'Expand' : 'Collapse'}</button>}
|
|
51
|
+
export const TreeItem = (props: RenderItemProps) => {
|
|
52
|
+
const { treeItem, collapsed, onCollapse, dragListeners } = props;
|
|
60
53
|
|
|
61
|
-
|
|
54
|
+
return (
|
|
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>}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
<h5>{treeItem.label}</h5>
|
|
65
|
+
|
|
66
|
+
<button onClick={() => openItemDetailsModal(treeItem.id)}>Show treeItem info</button>
|
|
67
|
+
</TreeItemStructure>
|
|
68
|
+
);
|
|
65
69
|
};
|
|
66
70
|
```
|
|
67
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
|
+
|
|
68
83
|
This is a real-world example using Radix:
|
|
69
84
|
|
|
70
85
|
```tsx
|
|
@@ -84,20 +99,22 @@ import {
|
|
|
84
99
|
import { Flex, Button, Text, Box } from '@radix-ui/themes';
|
|
85
100
|
import { CustomTreeItem } from '.';
|
|
86
101
|
|
|
87
|
-
export const TreeItem = (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
|
|
101
118
|
const useSortableTreeGlobalStyles = createSortableTreeGlobalStyles({
|
|
102
119
|
indicatorColor: 'var(--orange-7)',
|
|
103
120
|
indicatorBorderColor: 'var(--orange-7)',
|
|
@@ -107,7 +124,7 @@ export const TreeItem = ({
|
|
|
107
124
|
|
|
108
125
|
return (
|
|
109
126
|
<TreeItemStructure
|
|
110
|
-
{...
|
|
127
|
+
{...props}
|
|
111
128
|
asDropZone={Box}
|
|
112
129
|
asDraggableItem={Box}
|
|
113
130
|
draggableItemStyle={{
|
|
@@ -119,9 +136,9 @@ export const TreeItem = ({
|
|
|
119
136
|
}}
|
|
120
137
|
>
|
|
121
138
|
<Flex align="center" gap="5" direction="row">
|
|
122
|
-
<
|
|
139
|
+
<TreeItemStructure.DragHandler>
|
|
123
140
|
<DragHandleDots2Icon />
|
|
124
|
-
</
|
|
141
|
+
</TreeItemStructure.DragHandler>
|
|
125
142
|
|
|
126
143
|
{onCollapse && (
|
|
127
144
|
<Button color="gray" variant="ghost" onClick={onCollapse}>
|
|
@@ -260,7 +277,8 @@ export interface TreeItemStructureProps {
|
|
|
260
277
|
|
|
261
278
|
```ts
|
|
262
279
|
export interface RenderItemProps<T extends TTreeItem = TTreeItem>
|
|
263
|
-
extends
|
|
280
|
+
extends
|
|
281
|
+
Pick<
|
|
264
282
|
TreeItemStructureProps,
|
|
265
283
|
'classNames' | 'dropZoneStyle' | 'dropZoneRef' | 'draggableItemRef'
|
|
266
284
|
>,
|
|
@@ -315,15 +333,58 @@ function setTreeItemProperties<T extends TreeItem>(
|
|
|
315
333
|
|
|
316
334
|
---
|
|
317
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.
|
|
343
|
+
|
|
344
|
+
⚠️ So, when using E2E helpers that target items by name, item labels should be unique in the rendered tree.
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
await dragItem({
|
|
348
|
+
page,
|
|
349
|
+
expect,
|
|
350
|
+
from: { name: 'A' },
|
|
351
|
+
to: { name: 'C', position: 'inside' },
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
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
|
|
365
|
+
|
|
366
|
+
await expectItemNotToBeChildOf(expect, taskA, taskC);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### expectItemBefore
|
|
370
|
+
|
|
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.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
await expectItemBefore(page, expect, 'C', 'A');
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
318
379
|
## Roadmap
|
|
319
380
|
|
|
320
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
|
|
321
383
|
- 🔜 Virtualization for large trees
|
|
322
384
|
- 🔜 Multi-selection support
|
|
323
385
|
- 🔜 Drag multiple items
|
|
324
386
|
- 🔜 Keyboard navigation
|
|
325
387
|
- 🔜 API usage example
|
|
326
|
-
- 🔜 E2E tests
|
|
327
388
|
|
|
328
389
|
---
|
|
329
390
|
|
|
@@ -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}
|