From 6e67198c6f13dba976b4410cacdf9b32d768d467 Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Mon, 31 Jul 2023 14:22:44 +0300 Subject: [PATCH 1/9] Minor punctuation improvements --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f5e67f..16fb4eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # Testing Locally 1. Clone the repo -2. From the root, run yarn && yarn start -3. Visit localhost:3000 +2. From the root, run `yarn && yarn start` +3. Visit # Running Tests From ad8e4be08021d89c4f42747406db875f0feeeb05 Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Mon, 31 Jul 2023 14:26:15 +0300 Subject: [PATCH 2/9] feat(Tree): add `disableSelect` prop, to control which nodes are selectable --- .../react-arborist/src/interfaces/node-api.ts | 4 ++ .../react-arborist/src/interfaces/tree-api.ts | 50 +++++++++++++------ .../react-arborist/src/types/tree-props.ts | 1 + 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/react-arborist/src/interfaces/node-api.ts b/packages/react-arborist/src/interfaces/node-api.ts index cd4da42..3885ff6 100644 --- a/packages/react-arborist/src/interfaces/node-api.ts +++ b/packages/react-arborist/src/interfaces/node-api.ts @@ -59,6 +59,10 @@ export class NodeApi { return this.tree.isEditable(this.data); } + get isSelectable() { + return this.tree.isSelectable(this.data); + } + get isEditing() { return this.tree.editingId === this.id; } diff --git a/packages/react-arborist/src/interfaces/tree-api.ts b/packages/react-arborist/src/interfaces/tree-api.ts index b442a89..4048868 100644 --- a/packages/react-arborist/src/interfaces/tree-api.ts +++ b/packages/react-arborist/src/interfaces/tree-api.ts @@ -1,5 +1,5 @@ import { EditResult } from "../types/handlers"; -import { Identity, IdObj } from "../types/utils"; +import { BoolFunc, Identity, IdObj } from "../types/utils"; import { TreeProps } from "../types/tree-props"; import { MutableRefObject } from "react"; import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window"; @@ -328,9 +328,13 @@ export class TreeApi { const changeFocus = opts.focus !== false; const id = identify(node); if (changeFocus) this.dispatch(focus(id)); - this.dispatch(selection.only(id)); - this.dispatch(selection.anchor(id)); - this.dispatch(selection.mostRecent(id)); + if (this.get(id)?.isSelectable) { + this.setSelection({ + ids: [id], + anchor: id, + mostRecent: id, + }); + } this.scrollTo(id, opts.align); if (this.focusedNode && changeFocus) { safeRun(this.props.onFocus, this.focusedNode); @@ -348,9 +352,11 @@ export class TreeApi { const node = this.get(identifyNull(identity)); if (!node) return; this.dispatch(focus(node.id)); - this.dispatch(selection.add(node.id)); - this.dispatch(selection.anchor(node.id)); - this.dispatch(selection.mostRecent(node.id)); + if (node.isSelectable) { + this.dispatch(selection.add(node.id)); + this.dispatch(selection.anchor(node.id)); + this.dispatch(selection.mostRecent(node.id)); + } this.scrollTo(node); if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode); safeRun(this.props.onSelect, this.selectedNodes); @@ -359,11 +365,13 @@ export class TreeApi { selectContiguous(identity: Identity) { if (!identity) return; const id = identify(identity); - const { anchor, mostRecent } = this.state.nodes.selection; this.dispatch(focus(id)); - this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent))); - this.dispatch(selection.add(this.nodesBetween(anchor, identifyNull(id)))); - this.dispatch(selection.mostRecent(id)); + if (this.get(id)?.isSelectable) { + const {anchor, mostRecent} = this.state.nodes.selection; + this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent))); + this.dispatch(selection.add(this.nodesBetween(anchor, identifyNull(id)).filter(n => n.isSelectable))); + this.dispatch(selection.mostRecent(id)); + } this.scrollTo(id); if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode); safeRun(this.props.onSelect, this.selectedNodes); @@ -375,8 +383,12 @@ export class TreeApi { } selectAll() { + const selectableIds = Object.keys(this.idToIndex) + .map(id => this.get(id)!) + .filter(n => !!n && n.isSelectable) + .map(node => node.id); this.setSelection({ - ids: Object.keys(this.idToIndex), + ids: selectableIds, anchor: this.firstNode, mostRecent: this.lastNode, }); @@ -580,13 +592,19 @@ export class TreeApi { } isEditable(data: T) { - const check = this.props.disableEdit || (() => false); - return !utils.access(data, check) ?? true; + return this.isActionPossible(data, this.props.disableEdit); } isDraggable(data: T) { - const check = this.props.disableDrag || (() => false); - return !utils.access(data, check) ?? true; + return this.isActionPossible(data, this.props.disableDrag); + } + + isSelectable(data: T) { + return this.isActionPossible(data, this.props.disableSelect); + } + + private isActionPossible(data: T, disabler: string | boolean | BoolFunc | undefined = (() => false)) { + return !utils.access(data, disabler) ?? true; } isDragging(node: string | IdObj | null) { diff --git a/packages/react-arborist/src/types/tree-props.ts b/packages/react-arborist/src/types/tree-props.ts index de8fff4..e68b289 100644 --- a/packages/react-arborist/src/types/tree-props.ts +++ b/packages/react-arborist/src/types/tree-props.ts @@ -41,6 +41,7 @@ export interface TreeProps { openByDefault?: boolean; selectionFollowsFocus?: boolean; disableMultiSelection?: boolean; + disableSelect?: string | boolean | BoolFunc; disableEdit?: string | boolean | BoolFunc; disableDrag?: string | boolean | BoolFunc; disableDrop?: From 0880da8d609b47a0f03a2c7f2c2d8990a2236faf Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Mon, 31 Jul 2023 14:28:27 +0300 Subject: [PATCH 3/9] add `disableSelect` usage example to Gmail showcase --- packages/showcase/pages/gmail.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/showcase/pages/gmail.tsx b/packages/showcase/pages/gmail.tsx index f400962..6b1cb00 100644 --- a/packages/showcase/pages/gmail.tsx +++ b/packages/showcase/pages/gmail.tsx @@ -47,6 +47,7 @@ export default function GmailSidebar() { renderCursor={Cursor} searchTerm={term} paddingBottom={32} + disableSelect={(data) => ["Categories", "Spam"].includes(data.name)} disableEdit={(data) => data.readOnly} disableDrop={({ parentNode, dragNodes }) => { if ( @@ -78,17 +79,18 @@ export default function GmailSidebar() {

The tree is fully functional. Try the following:

  • Drag the items around
  • -
  • Try to drag Inbox into Categories (not allowed)
  • +
  • Try to drag Inbox into 'Categories' (not allowed)
  • Move focus with the arrow keys
  • Toggle folders (press spacebar)
  • - Rename (press enter, only allowed on items in {"'"}Categories{"'"} + Rename (press enter, only allowed on items in 'Categories' )
  • Create a new item (press A)
  • Create a new folder (press shift+A)
  • Delete items (press delete)
  • Select multiple items with shift or meta
  • +
  • 'Categories' and 'Spam' cannot be selected
  • Filter the tree by typing in this text box:{" "} Date: Mon, 31 Jul 2023 14:33:57 +0300 Subject: [PATCH 4/9] add `disableSelect` to Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bf80cb0..2618612 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ interface TreeProps { openByDefault?: boolean; selectionFollowsFocus?: boolean; disableMultiSelection?: boolean; + disableSelect?: string | boolean | BoolFunc; disableEdit?: string | boolean | BoolFunc; disableDrag?: string | boolean | BoolFunc; disableDrop?: From ad0142e0784eaa89660c1599cb729250bf030cba Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Sat, 19 Aug 2023 15:35:14 +0300 Subject: [PATCH 5/9] fix lint errors --- packages/showcase/pages/gmail.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/showcase/pages/gmail.tsx b/packages/showcase/pages/gmail.tsx index 6b1cb00..f42334f 100644 --- a/packages/showcase/pages/gmail.tsx +++ b/packages/showcase/pages/gmail.tsx @@ -79,18 +79,18 @@ export default function GmailSidebar() {

    The tree is fully functional. Try the following:

    • Drag the items around
    • -
    • Try to drag Inbox into 'Categories' (not allowed)
    • +
    • Try to drag Inbox into {"'"}Categories{"'"} (not allowed)
    • Move focus with the arrow keys
    • Toggle folders (press spacebar)
    • - Rename (press enter, only allowed on items in 'Categories' + Rename (press enter, only allowed on items in {"'"}Categories{"'"} )
    • Create a new item (press A)
    • Create a new folder (press shift+A)
    • Delete items (press delete)
    • Select multiple items with shift or meta
    • -
    • 'Categories' and 'Spam' cannot be selected
    • +
    • {"'"}Categories{"'"} and {"'"}Spam{"'"} cannot be selected
    • Filter the tree by typing in this text box:{" "} Date: Sat, 19 Aug 2023 16:24:16 +0300 Subject: [PATCH 6/9] add e2e for disabling selection --- packages/e2e/cypress/e2e/gmail-spec.cy.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/e2e/cypress/e2e/gmail-spec.cy.ts b/packages/e2e/cypress/e2e/gmail-spec.cy.ts index f51febd..301735a 100644 --- a/packages/e2e/cypress/e2e/gmail-spec.cy.ts +++ b/packages/e2e/cypress/e2e/gmail-spec.cy.ts @@ -139,6 +139,23 @@ describe("Testing the Gmail Demo", () => { cy.get("@item").contains("Categories").click(); // collapses cy.get("@item").should("have.length", 1); }); + + it("can select inbox but not categories", () => { + cy.get("@item").contains("Inbox").click(); + cy.focused().should("have.attr", "aria-selected", "true"); + cy.get("@item").contains("Categories").click(); + cy.focused().should("have.attr", "aria-selected", "false"); + }); + + it("select all does not select categories or spam", () => { + cy.get("@item").contains("Inbox").click(); + cy.focused().type("{meta}a"); + cy.get("[aria-selected='true']") + .should("not.contain.text", "Categories") + .should("not.contain.text", "Spam") + .should("contain.text", "Inbox") + .should("have.length", TOTAL_ITEMS - 2); + }); }); function dragAndDrop(src: any, dst: any) { From 881fe1db6bd7fe33e96848126dda89f9c54448c0 Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Sat, 19 Aug 2023 16:37:16 +0300 Subject: [PATCH 7/9] remove redundant code --- packages/react-arborist/src/interfaces/tree-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-arborist/src/interfaces/tree-api.ts b/packages/react-arborist/src/interfaces/tree-api.ts index 4048868..0ff472f 100644 --- a/packages/react-arborist/src/interfaces/tree-api.ts +++ b/packages/react-arborist/src/interfaces/tree-api.ts @@ -603,8 +603,8 @@ export class TreeApi { return this.isActionPossible(data, this.props.disableSelect); } - private isActionPossible(data: T, disabler: string | boolean | BoolFunc | undefined = (() => false)) { - return !utils.access(data, disabler) ?? true; + private isActionPossible(data: T, disabler: string | boolean | BoolFunc = (() => false)) { + return !utils.access(data, disabler); } isDragging(node: string | IdObj | null) { From fe1f6c7018d7071aa56f3dffea5d2d9aee85a98c Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Sat, 19 Aug 2023 17:22:10 +0300 Subject: [PATCH 8/9] refactor --- .../react-arborist/src/interfaces/tree-api.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/react-arborist/src/interfaces/tree-api.ts b/packages/react-arborist/src/interfaces/tree-api.ts index 0ff472f..f974cfb 100644 --- a/packages/react-arborist/src/interfaces/tree-api.ts +++ b/packages/react-arborist/src/interfaces/tree-api.ts @@ -368,8 +368,9 @@ export class TreeApi { this.dispatch(focus(id)); if (this.get(id)?.isSelectable) { const {anchor, mostRecent} = this.state.nodes.selection; + const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id))) this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent))); - this.dispatch(selection.add(this.nodesBetween(anchor, identifyNull(id)).filter(n => n.isSelectable))); + this.dispatch(selection.add(selectableNodes)); this.dispatch(selection.mostRecent(id)); } this.scrollTo(id); @@ -383,12 +384,9 @@ export class TreeApi { } selectAll() { - const selectableIds = Object.keys(this.idToIndex) - .map(id => this.get(id)!) - .filter(n => !!n && n.isSelectable) - .map(node => node.id); + const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex)); this.setSelection({ - ids: selectableIds, + ids: allSelectableNodes, anchor: this.firstNode, mostRecent: this.lastNode, }); @@ -397,6 +395,12 @@ export class TreeApi { safeRun(this.props.onSelect, this.selectedNodes); } + private filterSelectableNodes(nodes: (IdObj | string)[]) { + return nodes + .map(n => this.get(identify(n))) + .filter(n => !!n && n.isSelectable) as NodeApi[]; + } + setSelection(args: { ids: (IdObj | string)[] | null; anchor: IdObj | string | null; From 4e5af8fb80b37dbb1621a860e75139db1840e69b Mon Sep 17 00:00:00 2001 From: Boris Korneev Date: Sat, 19 Aug 2023 17:24:37 +0300 Subject: [PATCH 9/9] refactor --- packages/react-arborist/src/interfaces/tree-api.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-arborist/src/interfaces/tree-api.ts b/packages/react-arborist/src/interfaces/tree-api.ts index f974cfb..a09e9e8 100644 --- a/packages/react-arborist/src/interfaces/tree-api.ts +++ b/packages/react-arborist/src/interfaces/tree-api.ts @@ -169,7 +169,7 @@ export class TreeApi { return this.visibleNodes.slice(start, end + 1); } - indexOf(id: string | null | IdObj) { + indexOf(id: Identity) { const key = utils.identifyNull(id); if (!key) return null; return this.idToIndex[key]; @@ -219,7 +219,7 @@ export class TreeApi { } } - async delete(node: string | IdObj | null | string[] | IdObj[]) { + async delete(node: Identity | string[] | IdObj[]) { if (!node) return; const idents = Array.isArray(node) ? node : [node]; const ids = idents.map(identify); @@ -256,7 +256,7 @@ export class TreeApi { setTimeout(() => this.onFocus()); // Return focus to element; } - activate(id: string | IdObj | null) { + activate(id: Identity) { const node = this.get(identifyNull(id)); if (!node) return; safeRun(this.props.onActivate, node); @@ -403,8 +403,8 @@ export class TreeApi { setSelection(args: { ids: (IdObj | string)[] | null; - anchor: IdObj | string | null; - mostRecent: IdObj | string | null; + anchor: Identity; + mostRecent: Identity; }) { const ids = new Set(args.ids?.map(identify)); const anchor = identifyNull(args.anchor); @@ -611,7 +611,7 @@ export class TreeApi { return !utils.access(data, disabler); } - isDragging(node: string | IdObj | null) { + isDragging(node: Identity) { const id = identifyNull(node); if (!id) return false; return this.state.nodes.drag.id === id; @@ -625,7 +625,7 @@ export class TreeApi { return this.matchFn(node); } - willReceiveDrop(node: string | IdObj | null) { + willReceiveDrop(node: Identity) { const id = identifyNull(node); if (!id) return false; return id === this.state.nodes.drag.idWillReceiveDrop;