Skip to content

Commit

Permalink
Make all keybindings rebindable (#831)
Browse files Browse the repository at this point in the history
* Make all keybindings rebindable

* Cleanup

* Fix backwards compatibility

* Add story to make it easy to play

* Improve hotkey detection

* Fix key check

* Fix using a-z chars in keybindings

* Fix test that now needs keycode mocked

* Remove dead code

* remove extra redirect

* Cleanup further and reduce code
  • Loading branch information
jassmith committed Dec 28, 2023
1 parent 03be30d commit 2d2256b
Show file tree
Hide file tree
Showing 6 changed files with 623 additions and 390 deletions.
36 changes: 31 additions & 5 deletions packages/core/src/common/is-hotkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,47 @@ function checkKey(key: string | undefined, args: GridKeyEventArgs): boolean {
if (key === undefined) return false;
if (key.length > 1 && key.startsWith("_")) {
const keycode = Number.parseInt(key.slice(1));
if (keycode !== args.keyCode) return false;
} else {
if (key !== args.key) return false;
return keycode === args.keyCode;
}
return true;
if (key.length === 1 && key >= "a" && key <= "z") {
return key.toUpperCase().codePointAt(0) === args.keyCode;
}

return key === args.key;
}

interface HotkeyResultDetails {
didMatch: boolean;
}
export function isHotkey(hotkey: string, args: GridKeyEventArgs): boolean {

export function isHotkey(hotkey: string, args: GridKeyEventArgs, details: HotkeyResultDetails): boolean {
const result = isHotkeyInner(hotkey, args);
if (result) details.didMatch = true;
return result;
}

function isHotkeyInner(hotkey: string, args: GridKeyEventArgs): boolean {
if (hotkey.length === 0) return false;

if (hotkey.includes("|")) {
const parts = hotkey.split("|");
for (const part of parts) {
if (isHotkeyInner(part, args)) return true;
}
return false;
}

let wantCtrl = false;
let wantShift = false;
let wantAlt = false;
let wantMeta = false;

const split = hotkey.split("+");
const key = split.pop();

if (!checkKey(key, args)) return false;
if (split[0] === "any") return true;

for (const accel of split) {
switch (accel) {
case "ctrl":
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/common/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react";
import debounce from "lodash/debounce.js";
import { deepEqual } from "./support.js";

export function useEventListener<K extends keyof HTMLElementEventMap>(
eventName: K,
Expand Down Expand Up @@ -272,3 +273,14 @@ export function makeAccessibilityStringForArray(arr: readonly string[]): string
}
return arr.slice(0, index).join(", ");
}

export function useDeepMemo<T>(value: T): T {
const ref = React.useRef<T>(value);

if (!deepEqual(value, ref.current)) {
ref.current = value;
}

// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useMemo(() => ref.current, [ref.current]);
}
195 changes: 195 additions & 0 deletions packages/core/src/data-editor/data-editor-keybindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React from "react";
import { browserIsOSX } from "../common/browser-detect.js";
import { useDeepMemo } from "../common/utils.js";

export type Keybind = boolean | string;

interface ForcedKeybinds {
copy: boolean;
cut: boolean;
paste: boolean;
}

interface BackCompatKeybinds {
readonly pageUp: boolean;
readonly pageDown: boolean;
readonly first: boolean;
readonly last: boolean;
}

export interface ConfigurableKeybinds {
readonly downFill: Keybind;
readonly rightFill: Keybind;
readonly clear: Keybind;
readonly closeOverlay: Keybind;
readonly acceptOverlayDown: Keybind;
readonly acceptOverlayUp: Keybind;
readonly acceptOverlayLeft: Keybind;
readonly acceptOverlayRight: Keybind;
readonly search: Keybind;
readonly delete: Keybind;
readonly activateCell: Keybind;

// Navigation Keybinds
readonly goToFirstColumn: Keybind;
readonly goToLastColumn: Keybind;
readonly goToFirstCell: Keybind;
readonly goToLastCell: Keybind;
readonly goToFirstRow: Keybind;
readonly goToLastRow: Keybind;
readonly goToNextPage: Keybind;
readonly goToPreviousPage: Keybind;

readonly goUpCell: Keybind;
readonly goDownCell: Keybind;
readonly goLeftCell: Keybind;
readonly goRightCell: Keybind;

readonly goUpCellRetainSelection: Keybind;
readonly goDownCellRetainSelection: Keybind;
readonly goLeftCellRetainSelection: Keybind;
readonly goRightCellRetainSelection: Keybind;

// Selection Keybinds
readonly selectToFirstColumn: Keybind;
readonly selectToLastColumn: Keybind;
readonly selectToFirstCell: Keybind;
readonly selectToLastCell: Keybind;
readonly selectToFirstRow: Keybind;
readonly selectToLastRow: Keybind;

readonly selectGrowUp: Keybind;
readonly selectGrowDown: Keybind;
readonly selectGrowLeft: Keybind;
readonly selectGrowRight: Keybind;

readonly selectAll: Keybind;
readonly selectRow: Keybind;
readonly selectColumn: Keybind;
}

export type Keybinds = ConfigurableKeybinds & ForcedKeybinds & Partial<BackCompatKeybinds>;

export type RealizedKeybinds = Readonly<Record<keyof ConfigurableKeybinds, string>> & ForcedKeybinds;

export const keybindingDefaults: Keybinds = {
downFill: false,
rightFill: false,
clear: true,
closeOverlay: true,
acceptOverlayDown: true,
acceptOverlayUp: true,
acceptOverlayLeft: true,
acceptOverlayRight: true,
copy: true,
paste: true,
cut: true,
search: false,
delete: true,
activateCell: true,
goToFirstCell: true,
goToFirstColumn: true,
goToFirstRow: true,
goToLastCell: true,
goToLastColumn: true,
goToLastRow: true,
goToNextPage: true,
goToPreviousPage: true,
selectToFirstCell: true,
selectToFirstColumn: true,
selectToFirstRow: true,
selectToLastCell: true,
selectToLastColumn: true,
selectToLastRow: true,
selectAll: true,
selectRow: true,
selectColumn: true,
goUpCell: true,
goRightCell: true,
goDownCell: true,
goLeftCell: true,
goUpCellRetainSelection: true,
goRightCellRetainSelection: true,
goDownCellRetainSelection: true,
goLeftCellRetainSelection: true,
selectGrowUp: true,
selectGrowRight: true,
selectGrowDown: true,
selectGrowLeft: true,
};

function realizeKeybind(keybind: Keybind, defaultVal: string): string {
if (keybind === true) return defaultVal;
if (keybind === false) return "";
return keybind;
}

export function realizeKeybinds(keybinds: Keybinds): RealizedKeybinds {
const isOSX = browserIsOSX.value;

return {
activateCell: realizeKeybind(keybinds.activateCell, " |Enter|shift+Enter"),
clear: realizeKeybind(keybinds.clear, "any+Escape"),
closeOverlay: realizeKeybind(keybinds.closeOverlay, "any+Escape"),
acceptOverlayDown: realizeKeybind(keybinds.acceptOverlayDown, "Enter"),
acceptOverlayUp: realizeKeybind(keybinds.acceptOverlayUp, "shift+Enter"),
acceptOverlayLeft: realizeKeybind(keybinds.acceptOverlayLeft, "shift+Tab"),
acceptOverlayRight: realizeKeybind(keybinds.acceptOverlayRight, "Tab"),
copy: keybinds.copy,
cut: keybinds.cut,
delete: realizeKeybind(keybinds.delete, isOSX ? "Backspace|Delete" : "Delete"),
downFill: realizeKeybind(keybinds.downFill, "primary+_68"),
goDownCell: realizeKeybind(keybinds.goDownCell, "ArrowDown"),
goDownCellRetainSelection: realizeKeybind(keybinds.goDownCellRetainSelection, "alt+ArrowDown"),
goLeftCell: realizeKeybind(keybinds.goLeftCell, "ArrowLeft|shift+Tab"),
goLeftCellRetainSelection: realizeKeybind(keybinds.goLeftCellRetainSelection, "alt+ArrowLeft"),
goRightCell: realizeKeybind(keybinds.goRightCell, "ArrowRight|Tab"),
goRightCellRetainSelection: realizeKeybind(keybinds.goRightCellRetainSelection, "alt+ArrowRight"),
goUpCell: realizeKeybind(keybinds.goUpCell, "ArrowUp"),
goUpCellRetainSelection: realizeKeybind(keybinds.goUpCellRetainSelection, "alt+ArrowUp"),
goToFirstCell: realizeKeybind(keybinds.goToFirstCell, "primary+Home"),
goToFirstColumn: realizeKeybind(keybinds.goToFirstColumn, "Home|primary+ArrowLeft"),
goToFirstRow: realizeKeybind(keybinds.goToFirstRow, "primary+ArrowUp"),
goToLastCell: realizeKeybind(keybinds.goToLastCell, "primary+End"),
goToLastColumn: realizeKeybind(keybinds.goToLastColumn, "End|primary+ArrowRight"),
goToLastRow: realizeKeybind(keybinds.goToLastRow, "primary+ArrowDown"),
goToNextPage: realizeKeybind(keybinds.goToNextPage, "PageDown"),
goToPreviousPage: realizeKeybind(keybinds.goToPreviousPage, "PageUp"),
paste: keybinds.paste,
rightFill: realizeKeybind(keybinds.rightFill, "primary+_82"),
search: realizeKeybind(keybinds.search, "primary+f"),
selectAll: realizeKeybind(keybinds.selectAll, "primary+a"),
selectColumn: realizeKeybind(keybinds.selectColumn, "ctrl+ "),
selectGrowDown: realizeKeybind(keybinds.selectGrowDown, "shift+ArrowDown"),
selectGrowLeft: realizeKeybind(keybinds.selectGrowLeft, "shift+ArrowLeft"),
selectGrowRight: realizeKeybind(keybinds.selectGrowRight, "shift+ArrowRight"),
selectGrowUp: realizeKeybind(keybinds.selectGrowUp, "shift+ArrowUp"),
selectRow: realizeKeybind(keybinds.selectRow, "shift+ "),
selectToFirstCell: realizeKeybind(keybinds.selectToFirstCell, "primary+shift+Home"),
selectToFirstColumn: realizeKeybind(keybinds.selectToFirstColumn, "primary+shift+ArrowLeft"),
selectToFirstRow: realizeKeybind(keybinds.selectToFirstRow, "primary+shift+ArrowUp"),
selectToLastCell: realizeKeybind(keybinds.selectToLastCell, "primary+shift+End"),
selectToLastColumn: realizeKeybind(keybinds.selectToLastColumn, "primary+shift+ArrowRight"),
selectToLastRow: realizeKeybind(keybinds.selectToLastRow, "primary+shift+ArrowDown"),
};
}

export function useKeybindingsWithDefaults(keybindingsIn?: Partial<Keybinds>): RealizedKeybinds {
const keys = useDeepMemo(keybindingsIn);
return React.useMemo(() => {
if (keys === undefined) return realizeKeybinds(keybindingDefaults);
const withBackCompatApplied = {
...keys,
goToNextPage: keys?.goToNextPage ?? keys?.pageDown ?? keybindingDefaults.goToNextPage,
goToPreviousPage: keys?.goToPreviousPage ?? keys?.pageUp ?? keybindingDefaults.goToPreviousPage,
goToFirstCell: keys?.goToFirstCell ?? keys?.first ?? keybindingDefaults.goToFirstCell,
goToLastCell: keys?.goToLastCell ?? keys?.last ?? keybindingDefaults.goToLastCell,
selectToFirstCell: keys?.selectToFirstCell ?? keys?.first ?? keybindingDefaults.selectToFirstCell,
selectToLastCell: keys?.selectToLastCell ?? keys?.last ?? keybindingDefaults.selectToLastCell,
};
return realizeKeybinds({
...keybindingDefaults,
...withBackCompatApplied,
});
}, [keys]);
}
Loading

0 comments on commit 2d2256b

Please sign in to comment.