import { FixedSizeList, VariableSizeList } from "react-window"; import { useDataUpdates, useTreeApi } from "../context"; import { focusNextElement, focusPrevElement } from "../utils"; import { ListOuterElement } from "./list-outer-element"; import { ListInnerElement } from "./list-inner-element"; import { RowContainer } from "./row-container"; let focusSearchTerm = ""; let timeoutId: any = null; /** * All these keyboard shortcuts seem like they should be configurable. * Each operation should be a given a name and separated from * the event handler. Future clean up welcome. */ export function DefaultContainer() { useDataUpdates(); const tree = useTreeApi(); return (
{ if (!e.currentTarget.contains(e.relatedTarget)) { tree.onFocus(); } }} onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) { tree.onBlur(); } }} onKeyDown={(e) => { if (tree.isEditing) { return; } // Don't hijack keystrokes typed into a nested form field or editable // element (e.g. an inside a modal rendered within the tree). // Their events bubble up to this handler, where shortcuts like Space // would otherwise call preventDefault and swallow the character. Rows // are plain divs, so navigation is unaffected. (#257) const target = e.target; if ( target instanceof Element && target !== e.currentTarget && target.closest( "input, textarea, select, [contenteditable]:not([contenteditable='false'])", ) ) { return; } if (e.key === "Backspace") { if (!tree.props.onDelete) return; const ids = Array.from(tree.selectedIds); if (ids.length > 1) { let nextFocus = tree.mostRecentNode; while (nextFocus && nextFocus.isSelected) { nextFocus = nextFocus.nextSibling; } if (!nextFocus) nextFocus = tree.lastNode; tree.focus(nextFocus, { scroll: false }); tree.delete(Array.from(ids)); } else { const node = tree.focusedNode; if (node) { const sib = node.nextSibling; const parent = node.parent; tree.focus(sib || parent, { scroll: false }); tree.delete(node); } } return; } if (e.key === "Tab" && !e.shiftKey) { e.preventDefault(); focusNextElement(e.currentTarget); return; } if (e.key === "Tab" && e.shiftKey) { e.preventDefault(); focusPrevElement(e.currentTarget); return; } if (e.key === "ArrowDown") { e.preventDefault(); const next = tree.nextNode; if (e.metaKey) { tree.select(tree.focusedNode); tree.activate(tree.focusedNode); return; } else if (!e.shiftKey || tree.props.disableMultiSelection) { tree.focus(next); return; } else { if (!next) return; const current = tree.focusedNode; if (!current) { tree.focus(tree.firstNode); } else if (current.isSelected) { tree.selectContiguous(next); } else { tree.selectMulti(next); } return; } } if (e.key === "ArrowUp") { e.preventDefault(); const prev = tree.prevNode; if (!e.shiftKey || tree.props.disableMultiSelection) { tree.focus(prev); return; } else { if (!prev) return; const current = tree.focusedNode; if (!current) { tree.focus(tree.lastNode); // ? } else if (current.isSelected) { tree.selectContiguous(prev); } else { tree.selectMulti(prev); } return; } } if (e.key === "ArrowRight") { const node = tree.focusedNode; if (!node) return; if (node.isInternal && node.isOpen) { tree.focus(tree.nextNode); } else if (node.isInternal) tree.open(node.id); return; } if (e.key === "ArrowLeft") { const node = tree.focusedNode; if (!node || node.isRoot) return; if (node.isInternal && node.isOpen) tree.close(node.id); else if (!node.parent?.isRoot) { tree.focus(node.parent); } return; } if (e.key === "a" && (e.metaKey || e.ctrlKey) && !tree.props.disableMultiSelection) { e.preventDefault(); tree.selectAll(); return; } if (e.key === "a" && !e.metaKey && !e.ctrlKey && tree.props.onCreate) { tree.createLeaf(); return; } if (e.key === "A" && !e.metaKey && !e.ctrlKey) { if (!tree.props.onCreate) return; tree.createInternal(); return; } if (e.key === "Home") { // add shift keys e.preventDefault(); tree.focus(tree.firstNode); return; } if (e.key === "End") { // add shift keys e.preventDefault(); tree.focus(tree.lastNode); return; } if (e.key === "Enter") { const node = tree.focusedNode; if (!node) return; if (!node.isEditable || !tree.props.onRename) return; setTimeout(() => { if (node) tree.edit(node); }); return; } if (e.key === " ") { e.preventDefault(); const node = tree.focusedNode; if (!node) return; if (node.isLeaf) { node.select(); node.activate(); } else { node.toggle(); } return; } if (e.key === "*") { const node = tree.focusedNode; if (!node) return; tree.openSiblings(node); return; } if (e.key === "PageUp") { e.preventDefault(); tree.pageUp(); return; } if (e.key === "PageDown") { e.preventDefault(); tree.pageDown(); } // If they type a sequence of characters // collect them. Reset them after a timeout. // Use it to search the tree for a node, then focus it. // Clean this up a bit later clearTimeout(timeoutId); focusSearchTerm += e.key; timeoutId = setTimeout(() => { focusSearchTerm = ""; }, 600); const node = tree.visibleNodes.find((n) => { // @ts-ignore const name = n.data.name; if (typeof name === "string") { return name.toLowerCase().startsWith(focusSearchTerm); } else return false; }); if (node) tree.focus(node.id); }} >
); } /** * Fixed-height trees (numeric rowHeight) render a FixedSizeList, preserving the * original O(1) layout and avoiding VariableSizeList's measurement cache. Only * the function form, which needs per-row heights, uses VariableSizeList. */ function List() { const tree = useTreeApi(); const commonProps = { className: tree.props.className, outerRef: tree.listEl, itemCount: tree.visibleNodes.length, height: tree.height, width: tree.width, overscanCount: tree.overscanCount, itemKey: (index: number) => tree.visibleNodes[index]?.id || index, outerElementType: tree.props.outerElementType ?? ListOuterElement, innerElementType: tree.props.innerElementType ?? ListInnerElement, onScroll: tree.props.onScroll, onItemsRendered: tree.onItemsRendered.bind(tree), }; if (typeof tree.props.rowHeight === "function") { return ( // @ts-ignore {RowContainer} ); } return ( // @ts-ignore {RowContainer} ); }