summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/KeyboardNavigation.tsx106
1 files changed, 102 insertions, 4 deletions
diff --git a/gui/src/renderer/components/KeyboardNavigation.tsx b/gui/src/renderer/components/KeyboardNavigation.tsx
index 94b5097389..4cfba89452 100644
--- a/gui/src/renderer/components/KeyboardNavigation.tsx
+++ b/gui/src/renderer/components/KeyboardNavigation.tsx
@@ -1,20 +1,26 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useHistory } from '../lib/history';
interface IKeyboardNavigationProps {
children: React.ReactElement;
}
+// Listens for and handles keyboard shortcuts
export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
const history = useHistory();
+ const [backAction, setBackAction] = useState<IBackActionConfiguration>();
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape') {
- history.dismiss(true);
+ if (event.shiftKey) {
+ history.dismiss(true);
+ } else {
+ backAction?.action();
+ }
}
},
- [history.reset],
+ [history.dismiss, backAction],
);
useEffect(() => {
@@ -22,5 +28,97 @@ export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
- return props.children;
+ return <BackActionTracker registerBackAction={setBackAction}>{props.children}</BackActionTracker>;
+}
+
+type BackActionIcon = 'back' | 'close';
+type BackActionFn = () => void;
+
+interface IBackActionConfiguration {
+ icon: BackActionIcon;
+ action: BackActionFn;
+}
+
+interface IBackActionContext {
+ parentBackAction?: IBackActionConfiguration;
+ registerBackAction: (backAction: IBackActionConfiguration) => void;
+ removeBackAction: (backAction: IBackActionConfiguration) => void;
+}
+
+export const BackActionContext = React.createContext<IBackActionContext>({
+ registerBackAction(_backAction) {
+ throw new Error('Missing BackActionContext');
+ },
+ removeBackAction(_backAction) {
+ throw new Error('Missing BackActionContext');
+ },
+});
+
+interface IBackActionProps {
+ disabled?: boolean;
+ icon?: BackActionIcon;
+ action: BackActionFn;
+ children: React.ReactNode;
+}
+
+// Component for registering back actions, e.g. navigate back or close modal. These are called
+// either by pressing the back button in the navigation bar or by pressing escape.
+export function BackAction(props: IBackActionProps) {
+ const backActionContext = useContext(BackActionContext);
+ const [childrenBackAction, setChildrenBackAction] = useState<IBackActionConfiguration>();
+
+ const parentBackAction = useMemo<IBackActionConfiguration>(
+ () => ({ icon: props.icon ?? 'back', action: props.action }),
+ [props.icon, props.action],
+ );
+ const backActionConfiguration = childrenBackAction ?? parentBackAction;
+
+ // Every time the action or the disabled property changes the action needs to be reregistered.
+ useEffect((): (() => void) | void => {
+ if (!props.disabled && backActionConfiguration) {
+ backActionContext.registerBackAction(backActionConfiguration);
+ return () => backActionContext.removeBackAction(backActionConfiguration);
+ }
+ }, [props.disabled, backActionConfiguration]);
+
+ // Every back action keeps track of the back actions in its subtree. This makes it possible to
+ // always use the action furthest down in the tree.
+ return (
+ <BackActionTracker
+ registerBackAction={setChildrenBackAction}
+ parentBackAction={parentBackAction}>
+ {props.children}
+ </BackActionTracker>
+ );
+}
+
+interface IBackActionTracker {
+ parentBackAction?: IBackActionConfiguration;
+ registerBackAction: (backAction: IBackActionConfiguration | undefined) => void;
+ children: React.ReactNode;
+}
+
+// This component keeps track of all registered back actions in it's subtree and reports one of them
+// to it's parent.
+function BackActionTracker(props: IBackActionTracker) {
+ const [backActions, setBackActions] = useState<Array<IBackActionConfiguration>>([]);
+
+ const registerBackAction = useCallback((backAction: IBackActionConfiguration) => {
+ setBackActions((backActions) => [...backActions, backAction]);
+ }, []);
+ const removeBackAction = useCallback((backAction: IBackActionConfiguration) => {
+ setBackActions((backActions) => backActions.filter((action) => action !== backAction));
+ }, []);
+ const backActionContext = useMemo(
+ () => ({ parentBackAction: props.parentBackAction, registerBackAction, removeBackAction }),
+ [backActions],
+ );
+
+ useEffect(() => props.registerBackAction(backActions.at(0)), [backActions]);
+
+ return (
+ <BackActionContext.Provider value={backActionContext}>
+ {props.children}
+ </BackActionContext.Provider>
+ );
}