@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.
Files changed (33) hide show
  1. package/README.md +93 -32
  2. package/dist/e2e/index.js +51 -0
  3. package/dist/e2e/utils/drag-item.d.ts +15 -0
  4. package/dist/e2e/utils/expect-item-before.d.ts +2 -0
  5. package/dist/e2e/utils/expect-item-child-of.d.ts +3 -0
  6. package/dist/e2e/utils/get-tree-item-id.d.ts +2 -0
  7. package/dist/e2e/utils/index.d.ts +4 -0
  8. package/dist/react-sortable-tree.css +1 -1
  9. package/dist/react-sortable-tree.js +770 -731
  10. package/dist/react-sortable-tree.js.map +1 -1
  11. package/dist/src/SortableTree/components/TreeItemStructure/index.d.ts +39 -0
  12. package/package.json +34 -11
  13. package/dist/SortableTree/components/TreeItemStructure/index.d.ts +0 -19
  14. /package/dist/{SortableTree → src/SortableTree}/SortableTree.d.ts +0 -0
  15. /package/dist/{SortableTree → src/SortableTree}/components/Action/Action.d.ts +0 -0
  16. /package/dist/{SortableTree → src/SortableTree}/components/Action/index.d.ts +0 -0
  17. /package/dist/{SortableTree → src/SortableTree}/components/Add/Add.d.ts +0 -0
  18. /package/dist/{SortableTree → src/SortableTree}/components/Add/index.d.ts +0 -0
  19. /package/dist/{SortableTree → src/SortableTree}/components/Handle/Handle.d.ts +0 -0
  20. /package/dist/{SortableTree → src/SortableTree}/components/Handle/index.d.ts +0 -0
  21. /package/dist/{SortableTree → src/SortableTree}/components/Remove/Remove.d.ts +0 -0
  22. /package/dist/{SortableTree → src/SortableTree}/components/Remove/index.d.ts +0 -0
  23. /package/dist/{SortableTree → src/SortableTree}/components/TreeItem/SortableTreeItem.d.ts +0 -0
  24. /package/dist/{SortableTree → src/SortableTree}/components/TreeItem/TreeItem.d.ts +0 -0
  25. /package/dist/{SortableTree → src/SortableTree}/components/TreeItem/index.d.ts +0 -0
  26. /package/dist/{SortableTree → src/SortableTree}/components/index.d.ts +0 -0
  27. /package/dist/{SortableTree → src/SortableTree}/createSortableTreeGlobalStyles.d.ts +0 -0
  28. /package/dist/{SortableTree → src/SortableTree}/index.d.ts +0 -0
  29. /package/dist/{SortableTree → src/SortableTree}/keyboardCoordinates.d.ts +0 -0
  30. /package/dist/{SortableTree → src/SortableTree}/types.d.ts +0 -0
  31. /package/dist/{SortableTree → src/SortableTree}/utilities.d.ts +0 -0
  32. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  33. /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 = ({ treeItem, collapsed, onCollapse, ...rest }: RenderItemProps) => {
57
- <TreeItemStructure {...rest}>
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
- <h5>{treeItem.label}</h5>
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
- <button onClick={() => openItemInfoModal(treeItem.id)}>Show treeItem info</button>
64
- </TreeItemStructure>;
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
- treeItem,
89
- dragListeners,
90
- onCollapse,
91
- collapsed,
92
- onClickAddNestedItemButton,
93
- onClickItemRemoveButton,
94
- onItemClick,
95
- ...rest
96
- }: RenderItemProps<CustomTreeItem> & {
97
- onClickAddNestedItemButton: (id: string) => void;
98
- onClickItemRemoveButton: (id: string) => void;
99
- onItemClick: (id: string) => void;
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
- {...rest}
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
- <Button color="gray" variant="ghost" {...dragListeners}>
139
+ <TreeItemStructure.DragHandler>
123
140
  <DragHandleDots2Icon />
124
- </Button>
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 Pick<
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,2 @@
1
+ import { Expect, Page } from '@playwright/test';
2
+ export declare function expectItemBefore(page: Page, expect: Expect, first: string, second: string): Promise<void>;
@@ -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>;
@@ -0,0 +1,2 @@
1
+ import { Locator } from '@playwright/test';
2
+ export declare function getTreeItemId(locator: Locator): Promise<string>;
@@ -0,0 +1,4 @@
1
+ export * from './drag-item';
2
+ export * from './get-tree-item-id';
3
+ export * from './expect-item-before';
4
+ export * from './expect-item-child-of';
@@ -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 (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}
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}