Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 23 additions & 40 deletions packages/react-arborist/src/components/default-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
Expand Down Expand Up @@ -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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we run the function of the handler with a shortcut that returns true based on the pressed keys.


// Focus Next Element
if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault();
focusNextElement(e.currentTarget);
return;
}
// Focus Previous Element
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left these comments here as placeholders for the handler names. Let me know if I need to change any of these. This would turn into handleFocusPreviousElement.

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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -169,6 +148,7 @@ export function DefaultContainer() {
});
return;
}
// Toggle
if (e.key === " ") {
e.preventDefault();
const node = tree.focusedNode;
Expand All @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion packages/react-arborist/src/components/tree-container.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Container />
<Container shortcutHandlers={tree.props.shortcutHandlers || shortcutHandlers} />
</>
);
}
1 change: 1 addition & 0 deletions packages/react-arborist/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
55 changes: 55 additions & 0 deletions packages/react-arborist/src/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Comment on lines +12 to +13
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each handler consists of a shortcut and a function.

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);
}
}
}
}
}
5 changes: 5 additions & 0 deletions packages/react-arborist/src/types/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = {
style: CSSProperties;
Expand Down Expand Up @@ -32,3 +33,7 @@ export type CursorProps = {
left: number;
indent: number;
};

export type ContainerProps = {
shortcutHandlers: ShortcutHandlers
}
2 changes: 2 additions & 0 deletions packages/react-arborist/src/types/tree-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand Down Expand Up @@ -73,6 +74,7 @@ export interface TreeProps<T> {
className?: string | undefined;
rowClassName?: string | undefined;

shortcutHandlers?: ShortcutHandlers;
dndRootElement?: globalThis.Node | null;
onClick?: MouseEventHandler;
onContextMenu?: MouseEventHandler;
Expand Down
19 changes: 18 additions & 1 deletion packages/showcase/pages/cities.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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("@");
}
}
}
Comment on lines +30 to +44
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how you override or even add custom shortcuts.


return (
<div className={styles.container}>
<div className={styles.split}>
Expand Down Expand Up @@ -56,6 +72,7 @@ export default function Cities() {
setCount(tree?.visibleNodes.length ?? 0);
});
}}
shortcutHandlers={customShortcutHandlers}
>
{Node}
</Tree>
Expand Down