[Task 3071878] Tab Navigation Keyboard Shortcuts (#1808)

* [Task 3071878] Tab Navigation Keyboard Shortcuts

* throw in development on duplicate handlers

* refmt
This commit is contained in:
Ashley Stanton-Nurse
2024-04-19 13:44:30 -07:00
committed by GitHub
parent a5a5a95973
commit c220a8b070
4 changed files with 118 additions and 9 deletions

View File

@@ -12,6 +12,15 @@ export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void;
export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHandler>>;
/**
* The groups of keyboard actions that can be managed by the application.
* Each group can be updated separately, but, when updated, must be completely replaced.
*/
export enum KeyboardActionGroup {
TABS = "TABS",
COMMAND_BAR = "COMMAND_BAR",
}
/**
* The possible actions that can be triggered by keyboard shortcuts.
*/
@@ -30,6 +39,9 @@ export enum KeyboardAction {
NEW_ITEM = "NEW_ITEM",
DELETE_ITEM = "DELETE_ITEM",
TOGGLE_COPILOT = "TOGGLE_COPILOT",
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
CLOSE_TAB = "CLOSE_TAB",
}
/**
@@ -55,6 +67,9 @@ const bindings: Record<KeyboardAction, string[]> = {
[KeyboardAction.NEW_ITEM]: ["Alt+N I"],
[KeyboardAction.DELETE_ITEM]: ["Alt+D"],
[KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"],
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+["],
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"],
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
};
interface KeyboardShortcutState {
@@ -64,15 +79,47 @@ interface KeyboardShortcutState {
allHandlers: KeyboardHandlerMap;
/**
* Sets the keyboard shortcut handlers.
* A set of all the groups of keyboard shortcuts handlers.
*/
setHandlers: (handlers: KeyboardHandlerMap) => void;
groups: Partial<Record<KeyboardActionGroup, KeyboardHandlerMap>>;
/**
* Sets the keyboard shortcut handlers for the given group.
*/
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
}
export const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set) => ({
/**
* Defines the calling component as the manager of the keyboard actions for the given group.
* @param group The group of keyboard actions to manage.
* @returns A function that can be used to set the keyboard action handlers for the given group.
*/
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
allHandlers: {},
setHandlers: (handlers: Partial<Record<KeyboardAction, KeyboardActionHandler>>) => {
set({ allHandlers: handlers });
groups: {},
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {
const state = get();
const groups = { ...state.groups, [group]: handlers };
// Combine all the handlers from all the groups in the correct order.
const allHandlers: KeyboardHandlerMap = {};
eachKey(groups).forEach((group) => {
const groupHandlers = groups[group];
if (groupHandlers) {
eachKey(groupHandlers).forEach((action) => {
// Check for duplicate handlers in development mode.
// We don't want to raise an error here in production, but having duplicate handlers is a mistake.
if (process.env.NODE_ENV === "development" && allHandlers[action]) {
throw new Error(`Duplicate handler for Keyboard Action "${action}".`);
}
allHandlers[action] = groupHandlers[action];
});
}
});
set({ groups, allHandlers });
},
}));
@@ -88,7 +135,7 @@ function createHandler(action: KeyboardAction): KeyboardActionHandler {
}
const allHandlers: KeyBindingMap = {};
(Object.keys(bindings) as KeyboardAction[]).forEach((action) => {
eachKey(bindings).forEach((action) => {
const shortcuts = bindings[action];
shortcuts.forEach((shortcut) => {
allHandlers[shortcut] = createHandler(action);
@@ -103,3 +150,8 @@ export function KeyboardShortcutRoot({ children }: PropsWithChildren<unknown>) {
return <>{children}</>;
}
/** A _typed_ version of `Object.keys` that preserves the original key type */
function eachKey<K extends string | number | symbol, V>(record: Partial<Record<K, V>>): K[] {
return Object.keys(record) as K[];
}