-
Notifications
You must be signed in to change notification settings - Fork 181
Keyboard shortcut proof of concept #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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(); | ||
|
|
||
| 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} /> | ||
| </> | ||
| ); | ||
| } |
| 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 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"; | ||
|
|
@@ -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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}> | ||
|
|
@@ -56,6 +72,7 @@ export default function Cities() { | |
| setCount(tree?.visibleNodes.length ?? 0); | ||
| }); | ||
| }} | ||
| shortcutHandlers={customShortcutHandlers} | ||
| > | ||
| {Node} | ||
| </Tree> | ||
|
|
||
There was a problem hiding this comment.
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.