From 049a904ca673ec42e674886f74d7d85a2edae4fa Mon Sep 17 00:00:00 2001 From: Hasan Ayan Date: Thu, 21 Mar 2024 15:35:01 +0300 Subject: [PATCH 1/3] feat: support variable node height --- .../react-arborist/src/components/cursor.tsx | 2 +- .../src/components/default-container.tsx | 6 +- .../src/components/list-outer-element.tsx | 2 +- .../src/components/provider.tsx | 4 +- modules/react-arborist/src/dnd/drag-hook.ts | 1 + .../react-arborist/src/interfaces/tree-api.ts | 67 +++++++++++++++---- .../react-arborist/src/types/tree-props.ts | 22 +++++- 7 files changed, 82 insertions(+), 22 deletions(-) diff --git a/modules/react-arborist/src/components/cursor.tsx b/modules/react-arborist/src/components/cursor.tsx index 658664c1..d6804b8c 100644 --- a/modules/react-arborist/src/components/cursor.tsx +++ b/modules/react-arborist/src/components/cursor.tsx @@ -7,7 +7,7 @@ export function Cursor() { if (!cursor || cursor.type !== "line") return null; const indent = tree.indent; const top = - tree.rowHeight * cursor.index + + tree.rowTopPosition(cursor.index) + (tree.props.padding ?? tree.props.paddingTop ?? 0); const left = indent * cursor.level; const Cursor = tree.renderCursor; diff --git a/modules/react-arborist/src/components/default-container.tsx b/modules/react-arborist/src/components/default-container.tsx index f92dd5cf..47e17857 100644 --- a/modules/react-arborist/src/components/default-container.tsx +++ b/modules/react-arborist/src/components/default-container.tsx @@ -1,4 +1,4 @@ -import { FixedSizeList } from "react-window"; +import { VariableSizeList } from "react-window"; import { useDataUpdates, useTreeApi } from "../context"; import { focusNextElement, focusPrevElement } from "../utils"; import { ListOuterElement } from "./list-outer-element"; @@ -217,7 +217,7 @@ export function DefaultContainer() { }} > {/* @ts-ignore */} - {RowContainer} - + ); } diff --git a/modules/react-arborist/src/components/list-outer-element.tsx b/modules/react-arborist/src/components/list-outer-element.tsx index 672419e4..344664a1 100644 --- a/modules/react-arborist/src/components/list-outer-element.tsx +++ b/modules/react-arborist/src/components/list-outer-element.tsx @@ -29,7 +29,7 @@ const DropContainer = () => { return (
({ imperativeHandle, children, }: Props) { - const list = useRef(null); + const list = useRef(null); const listEl = useRef(null); const store = useRef>( // @ts-ignore diff --git a/modules/react-arborist/src/dnd/drag-hook.ts b/modules/react-arborist/src/dnd/drag-hook.ts index 22ac564e..9388e233 100644 --- a/modules/react-arborist/src/dnd/drag-hook.ts +++ b/modules/react-arborist/src/dnd/drag-hook.ts @@ -37,6 +37,7 @@ export function useDragHook(node: NodeApi): ConnectDragSource { }); tree.open(parentId); } + tree.list?.current?.resetAfterIndex(0); tree.dispatch(dnd.dragEnd()); }, }), diff --git a/modules/react-arborist/src/interfaces/tree-api.ts b/modules/react-arborist/src/interfaces/tree-api.ts index 50e03a84..f516a789 100644 --- a/modules/react-arborist/src/interfaces/tree-api.ts +++ b/modules/react-arborist/src/interfaces/tree-api.ts @@ -2,7 +2,11 @@ import { EditResult } from "../types/handlers"; import { Identity, IdObj } from "../types/utils"; import { TreeProps } from "../types/tree-props"; import { MutableRefObject } from "react"; -import { Align, FixedSizeList, ListOnItemsRenderedProps } from "react-window"; +import { + Align, + ListOnItemsRenderedProps, + VariableSizeList, +} from "react-window"; import * as utils from "../utils"; import { DefaultCursor } from "../components/default-cursor"; import { DefaultRow } from "../components/default-row"; @@ -34,8 +38,8 @@ export class TreeApi { constructor( public store: Store, public props: TreeProps, - public list: MutableRefObject, - public listEl: MutableRefObject + public list: MutableRefObject, + public listEl: MutableRefObject, ) { /* Changes here must also be made in update() */ this.root = createRoot(this); @@ -79,10 +83,6 @@ export class TreeApi { return this.props.indent ?? 24; } - get rowHeight() { - return this.props.rowHeight ?? 24; - } - get overscanCount() { return this.props.overscanCount ?? 1; } @@ -91,6 +91,33 @@ export class TreeApi { return (this.props.searchTerm || "").trim(); } + rowHeight = (index: number): number => { + if (!this.props.rowHeight) { + return 24; + } + + const node = this.at(index); + if (!node) { + return 0; + } + + return typeof this.props.rowHeight === "function" + ? this.props.rowHeight(node) + : this.props.rowHeight; + }; + + rowTopPosition = (index: number): number => { + let position = 0; + for (let i = 0; i < index; i++) { + position += this.rowHeight(i); + } + return position; + }; + + redrawList = (afterIndex?: number | undefined | null) => { + this.list.current?.resetAfterIndex(afterIndex ?? 0); + }; + get matchFn() { const match = this.props.searchMatch ?? @@ -194,7 +221,7 @@ export class TreeApi { type?: "internal" | "leaf"; parentId?: null | string; index?: null | number; - } = {} + } = {}, ) { const parentId = opts.parentId === undefined @@ -224,6 +251,9 @@ export class TreeApi { const idents = Array.isArray(node) ? node : [node]; const ids = idents.map(identify); const nodes = ids.map((id) => this.get(id)!).filter((n) => !!n); + + this.redrawList(Math.min(...nodes.map((node) => node.rowIndex ?? 0))); + await safeRun(this.props.onDelete, { nodes, ids }); } @@ -232,6 +262,7 @@ export class TreeApi { this.resolveEdit({ cancelled: true }); this.scrollTo(id); this.dispatch(edit(id)); + this.redrawList(this.get(id)?.rowIndex); return new Promise((resolve) => { TreeApi.editPromise = resolve; }); @@ -247,12 +278,14 @@ export class TreeApi { }); this.dispatch(edit(null)); this.resolveEdit({ cancelled: false, value }); + this.redrawList(this.get(id)?.rowIndex); setTimeout(() => this.onFocus()); // Return focus to element; } reset() { this.dispatch(edit(null)); this.resolveEdit({ cancelled: true }); + this.redrawList(); setTimeout(() => this.onFocus()); // Return focus to element; } @@ -470,19 +503,21 @@ export class TreeApi { /* Visibility */ - open(identity: Identity) { + open(identity: Identity, redraw: boolean = true) { const id = identifyNull(identity); if (!id) return; if (this.isOpen(id)) return; this.dispatch(visibility.open(id, this.isFiltered)); + redraw && this.redrawList(this.get(id)?.rowIndex); safeRun(this.props.onToggle, id); } - close(identity: Identity) { + close(identity: Identity, redraw: boolean = true) { const id = identifyNull(identity); if (!id) return; if (!this.isOpen(id)) return; this.dispatch(visibility.close(id, this.isFiltered)); + redraw && this.redrawList(this.get(id)?.rowIndex); safeRun(this.props.onToggle, id); } @@ -499,9 +534,10 @@ export class TreeApi { let parent = node?.parent; while (parent) { - this.open(parent.id); + this.open(parent.id, false); parent = parent.parent; } + this.redrawList(); } openSiblings(node: NodeApi) { @@ -512,23 +548,26 @@ export class TreeApi { const isOpen = node.isOpen; for (let sibling of parent.children) { if (sibling.isInternal) { - isOpen ? this.close(sibling.id) : this.open(sibling.id); + isOpen ? this.close(sibling.id, false) : this.open(sibling.id, false); } } + this.redrawList(); this.scrollTo(this.focusedNode); } } openAll() { utils.walk(this.root, (node) => { - if (node.isInternal) node.open(); + if (node.isInternal) this.open(node, false); }); + this.redrawList(); } closeAll() { utils.walk(this.root, (node) => { - if (node.isInternal) node.close(); + if (node.isInternal) this.close(node, false); }); + this.redrawList(); } /* Scrolling */ diff --git a/modules/react-arborist/src/types/tree-props.ts b/modules/react-arborist/src/types/tree-props.ts index 440bcc40..ed1480d4 100644 --- a/modules/react-arborist/src/types/tree-props.ts +++ b/modules/react-arborist/src/types/tree-props.ts @@ -7,6 +7,26 @@ import { NodeApi } from "../interfaces/node-api"; import { OpenMap } from "../state/open-slice"; import { useDragDropManager } from "react-dnd"; +export type RowHeightCalculatorParams = Pick< + NodeApi, + | "childIndex" + | "children" + | "data" + | "parent" + | "id" + | "rowIndex" + | "tree" + | "isRoot" + | "isLeaf" + | "isClosed" + | "isOpen" + | "isAncestorOf" +>; + +export type RowHeightCalculator = ( + params: RowHeightCalculatorParams, +) => number; + export interface TreeProps { /* Data Options */ data?: readonly T[]; @@ -26,7 +46,7 @@ export interface TreeProps { renderContainer?: ElementType<{}>; /* Sizes */ - rowHeight?: number; + rowHeight?: number | RowHeightCalculator; overscanCount?: number; width?: number | string; height?: number; From 449b4c75df9a4aa0cc045a4d2b639d30860bd073 Mon Sep 17 00:00:00 2001 From: Hasan Ayan Date: Thu, 21 Mar 2024 15:56:36 +0300 Subject: [PATCH 2/3] fix DropContainer height calculation --- .../react-arborist/src/components/list-outer-element.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/react-arborist/src/components/list-outer-element.tsx b/modules/react-arborist/src/components/list-outer-element.tsx index 344664a1..c6459311 100644 --- a/modules/react-arborist/src/components/list-outer-element.tsx +++ b/modules/react-arborist/src/components/list-outer-element.tsx @@ -5,7 +5,7 @@ import { Cursor } from "./cursor"; export const ListOuterElement = forwardRef(function Outer( props: React.HTMLProps, - ref + ref, ) { const { children, ...rest } = props; const tree = useTreeApi(); @@ -29,7 +29,9 @@ const DropContainer = () => { return (
Date: Thu, 21 Mar 2024 17:19:00 +0300 Subject: [PATCH 3/3] add "level" to RowHeightCalculatorParams --- modules/react-arborist/src/types/tree-props.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/react-arborist/src/types/tree-props.ts b/modules/react-arborist/src/types/tree-props.ts index ed1480d4..2ada067b 100644 --- a/modules/react-arborist/src/types/tree-props.ts +++ b/modules/react-arborist/src/types/tree-props.ts @@ -21,6 +21,7 @@ export type RowHeightCalculatorParams = Pick< | "isClosed" | "isOpen" | "isAncestorOf" + | "level" >; export type RowHeightCalculator = (