diff --git a/packages/react-arborist/src/components/default-container.tsx b/packages/react-arborist/src/components/default-container.tsx index f92dd5cf..f3975933 100644 --- a/packages/react-arborist/src/components/default-container.tsx +++ b/packages/react-arborist/src/components/default-container.tsx @@ -4,6 +4,7 @@ import { focusNextElement, focusPrevElement } from "../utils"; import { ListOuterElement } from "./list-outer-element"; import { ListInnerElement } from "./list-inner-element"; import { RowContainer } from "./row-container"; +import { ContainerProps } from "../types/renderers"; let focusSearchTerm = ""; let timeoutId: any = null; @@ -13,7 +14,9 @@ let timeoutId: any = null; * Each operation should be a given a name and separated from * the event handler. Future clean up welcome. */ -export function DefaultContainer() { +export function DefaultContainer({ + shortcutHandlers +}: ContainerProps) { useDataUpdates(); const tree = useTreeApi(); return ( @@ -42,38 +45,22 @@ export function DefaultContainer() { if (tree.isEditing) { 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; - } + + Object.values(shortcutHandlers).find(handler => handler.shortcut(e))?.function(tree, e); + + // Focus Next Element if (e.key === "Tab" && !e.shiftKey) { e.preventDefault(); focusNextElement(e.currentTarget); return; } + // Focus Previous Element if (e.key === "Tab" && e.shiftKey) { e.preventDefault(); focusPrevElement(e.currentTarget); return; } + // Select Up Tree if (e.key === "ArrowDown") { e.preventDefault(); const next = tree.nextNode; @@ -97,25 +84,10 @@ export function DefaultContainer() { return; } } + // Select Down Tree 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; - } } + // Open Tree Node if (e.key === "ArrowRight") { const node = tree.focusedNode; if (!node) return; @@ -124,6 +96,7 @@ export function DefaultContainer() { } else if (node.isInternal) tree.open(node.id); return; } + // Close Tree Node if (e.key === "ArrowLeft") { const node = tree.focusedNode; if (!node || node.isRoot) return; @@ -133,33 +106,39 @@ export function DefaultContainer() { } return; } + // Select All if (e.key === "a" && e.metaKey && !tree.props.disableMultiSelection) { e.preventDefault(); tree.selectAll(); return; } + // Create Leaf if (e.key === "a" && !e.metaKey && tree.props.onCreate) { tree.createLeaf(); return; } + // Create Internal if (e.key === "A" && !e.metaKey) { if (!tree.props.onCreate) return; tree.createInternal(); return; } + // To Top if (e.key === "Home") { // add shift keys e.preventDefault(); tree.focus(tree.firstNode); return; } + // To Bottom if (e.key === "End") { // add shift keys e.preventDefault(); tree.focus(tree.lastNode); return; } + // Edit Node if (e.key === "Enter") { const node = tree.focusedNode; if (!node) return; @@ -169,6 +148,7 @@ export function DefaultContainer() { }); return; } + // Toggle if (e.key === " ") { e.preventDefault(); const node = tree.focusedNode; @@ -181,17 +161,20 @@ export function DefaultContainer() { } return; } + // Open Siblings if (e.key === "*") { const node = tree.focusedNode; if (!node) return; tree.openSiblings(node); return; } + // Scroll up if (e.key === "PageUp") { e.preventDefault(); tree.pageUp(); return; } + // Scroll down if (e.key === "PageDown") { e.preventDefault(); tree.pageDown(); diff --git a/packages/react-arborist/src/components/tree-container.tsx b/packages/react-arborist/src/components/tree-container.tsx index d21bd980..36f2ea0f 100644 --- a/packages/react-arborist/src/components/tree-container.tsx +++ b/packages/react-arborist/src/components/tree-container.tsx @@ -1,13 +1,14 @@ import React from "react"; import { useTreeApi } from "../context"; import { DefaultContainer } from "./default-container"; +import { shortcutHandlers } from "../shortcuts"; export function TreeContainer() { const tree = useTreeApi(); const Container = tree.props.renderContainer || DefaultContainer; return ( <> - + > ); } diff --git a/packages/react-arborist/src/index.ts b/packages/react-arborist/src/index.ts index 854860fb..008512a3 100644 --- a/packages/react-arborist/src/index.ts +++ b/packages/react-arborist/src/index.ts @@ -7,3 +7,4 @@ export * from "./interfaces/node-api"; export * from "./interfaces/tree-api"; export * from "./data/simple-tree"; export * from "./hooks/use-simple-tree"; +export * from "./shortcuts"; diff --git a/packages/react-arborist/src/shortcuts.ts b/packages/react-arborist/src/shortcuts.ts new file mode 100644 index 00000000..ba4102f7 --- /dev/null +++ b/packages/react-arborist/src/shortcuts.ts @@ -0,0 +1,55 @@ +interface ShortcutHandler { + shortcut: (e:any) => boolean; + function: (tree:any) => void; +} + +export interface ShortcutHandlers { + [key: string]: ShortcutHandler +} + +export const shortcutHandlers = { + handleDeleteNode: { + shortcut: (e:any) => e.key === "Backspace", + function: (tree:any, e:any) => { + 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); + } + } + } + }, + handleSelectDownTree: { + shortcut: (e:any) => e.key === "ArrowUp", + function: (tree:any, e:any) => { + e.preventDefault(); + const prev = tree.prevNode; + if (!e.shiftKey || tree.props.disableMultiSelection) { + tree.focus(prev); + } 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); + } + } + } + } +} diff --git a/packages/react-arborist/src/types/renderers.ts b/packages/react-arborist/src/types/renderers.ts index 356ad576..1a88a854 100644 --- a/packages/react-arborist/src/types/renderers.ts +++ b/packages/react-arborist/src/types/renderers.ts @@ -3,6 +3,7 @@ import { IdObj } from "./utils"; import { NodeApi } from "../interfaces/node-api"; import { TreeApi } from "../interfaces/tree-api"; import { XYCoord } from "react-dnd"; +import { ShortcutHandlers } from "../shortcuts"; export type NodeRendererProps = { style: CSSProperties; @@ -32,3 +33,7 @@ export type CursorProps = { left: number; indent: number; }; + +export type ContainerProps = { + shortcutHandlers: ShortcutHandlers +} diff --git a/packages/react-arborist/src/types/tree-props.ts b/packages/react-arborist/src/types/tree-props.ts index 440bcc40..097ac943 100644 --- a/packages/react-arborist/src/types/tree-props.ts +++ b/packages/react-arborist/src/types/tree-props.ts @@ -5,6 +5,7 @@ import { ElementType, MouseEventHandler } from "react"; import { ListOnScrollProps } from "react-window"; import { NodeApi } from "../interfaces/node-api"; import { OpenMap } from "../state/open-slice"; +import { ShortcutHandlers } from "../shortcuts"; import { useDragDropManager } from "react-dnd"; export interface TreeProps { @@ -73,6 +74,7 @@ export interface TreeProps { className?: string | undefined; rowClassName?: string | undefined; + shortcutHandlers?: ShortcutHandlers; dndRootElement?: globalThis.Node | null; onClick?: MouseEventHandler; onContextMenu?: MouseEventHandler; diff --git a/packages/showcase/pages/cities.tsx b/packages/showcase/pages/cities.tsx index ca3f8990..269e34a7 100644 --- a/packages/showcase/pages/cities.tsx +++ b/packages/showcase/pages/cities.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { useEffect, useRef, useState } from "react"; -import { NodeApi, NodeRendererProps, Tree, TreeApi } from "react-arborist"; +import { NodeApi, NodeRendererProps, Tree, TreeApi, shortcutHandlers } from "react-arborist"; import styles from "../styles/cities.module.css"; import { cities } from "../data/cities"; import { BsMapFill, BsMap, BsGeo, BsGeoFill } from "react-icons/bs"; @@ -27,6 +27,22 @@ export default function Cities() { setCount(tree?.visibleNodes.length ?? 0); }, [tree, searchTerm]); + const customShortcutHandlers = { + ...shortcutHandlers, + // You can override shortcuts like this + handleSelectDownTree: { + shortcut: (e:any) => e.key === "ArrowUp" || e.key === "k", + function: shortcutHandlers.handleSelectDownTree.function + }, + // You can even add custom shortcuts + customHandler: { + shortcut: (e:any) => e.key === "@", + function: (tree:any, e:any) => { + alert("@"); + } + } + } + return ( @@ -56,6 +72,7 @@ export default function Cities() { setCount(tree?.visibleNodes.length ?? 0); }); }} + shortcutHandlers={customShortcutHandlers} > {Node}