Skip to content

Instantly share code, notes, and snippets.

@stephen
Last active June 10, 2023 11:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stephen/873773683fc15d1fb290b338a6c59d38 to your computer and use it in GitHub Desktop.
Save stephen/873773683fc15d1fb290b338a6c59d38 to your computer and use it in GitHub Desktop.
blueprint hotkeys provider without unnecessary re-renders
/*
* Copyright 2021 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { shallowCompareKeys } from "@blueprintjs/core/lib/esm/common/utils";
import * as React from "react";
import {
HotkeyConfig,
HotkeysContext,
HotkeysDialog2,
} from "@blueprintjs/core";
import { HotkeysDialog2Props } from "@blueprintjs/core/lib/esm/components/hotkeys/hotkeysDialog2";
interface HotkeysContextState {
/**
* Whether the context instance is being used within a tree which has a <HotkeysProvider>.
* It's technically ok if this is false, but not recommended, since that means any hotkeys
* bound with that context instance will not show up in the hotkeys help dialog.
*/
hasProvider: boolean;
/** List of hotkeys accessible in the current scope, registered by currently mounted components, can be global or local. */
hotkeys: HotkeyConfig[];
/** Whether the global hotkeys dialog is open. */
isDialogOpen: boolean;
}
type HotkeysAction =
| { type: "ADD_HOTKEYS" | "REMOVE_HOTKEYS"; payload: HotkeyConfig[] }
| { type: "CLOSE_DIALOG" | "OPEN_DIALOG" };
export type HotkeysContextInstance = [
HotkeysContextState,
React.Dispatch<HotkeysAction>
];
const initialHotkeysState: HotkeysContextState = {
hasProvider: false,
hotkeys: [],
isDialogOpen: false,
};
const hotkeysReducer = (state: HotkeysContextState, action: HotkeysAction) => {
switch (action.type) {
case "ADD_HOTKEYS":
// only pick up unique hotkeys which haven't been registered already
const newUniqueHotkeys = [];
for (const a of action.payload) {
let isUnique = true;
for (const b of state.hotkeys) {
isUnique &&= !shallowCompareKeys(a, b, {
exclude: ["onKeyDown", "onKeyUp"],
});
}
if (isUnique) {
newUniqueHotkeys.push(a);
}
}
return {
...state,
hotkeys: [...state.hotkeys, ...newUniqueHotkeys],
};
case "REMOVE_HOTKEYS":
return {
...state,
hotkeys: state.hotkeys.filter(
(key) => action.payload.indexOf(key) === -1
),
};
case "OPEN_DIALOG":
return { ...state, isDialogOpen: true };
case "CLOSE_DIALOG":
return { ...state, isDialogOpen: false };
default:
return state;
}
};
export interface HotkeysProviderProps {
/** The component subtree which will have access to this hotkeys context. */
children: React.ReactChild;
/** Optional props to customize the rendered hotkeys dialog. */
dialogProps?: Partial<Omit<HotkeysDialog2Props, "hotkeys">>;
/** If provided, this dialog render function will be used in place of the default implementation. */
renderDialog?: (
state: HotkeysContextState,
contextActions: { handleDialogClose: () => void }
) => JSX.Element;
/** If provided, we will use this context instance instead of generating our own. */
value?: HotkeysContextInstance;
}
/**
* Hotkeys context provider, necessary for the `useHotkeys` hook.
*
* @see https://blueprintjs.com/docs/#core/context/hotkeys-provider
*/
export const HotkeysDialogProvider = ({
children,
dialogProps,
renderDialog,
value,
}: HotkeysProviderProps) => {
const hasExistingContext = value != null;
const [state, dispatch] =
value ??
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useReducer(hotkeysReducer, {
...initialHotkeysState,
hasProvider: true,
});
const handleDialogClose = React.useCallback(
() => dispatch({ type: "CLOSE_DIALOG" }),
[dispatch]
);
const dialog = renderDialog?.(state, { handleDialogClose }) ?? (
<HotkeysDialog2
{...dialogProps}
isOpen={state.isDialogOpen}
hotkeys={state.hotkeys}
onClose={handleDialogClose}
/>
);
const reducedState = React.useMemo(
() => ({ hasProvider: state.hasProvider }),
[state.hasProvider]
);
const ctxValue = React.useMemo(
() => [reducedState, dispatch],
[reducedState, dispatch]
);
// if we are working with an existing context, we don't need to generate our own dialog
return (
<HotkeysContext.Provider value={ctxValue as any}>
{children}
{hasExistingContext ? undefined : dialog}
</HotkeysContext.Provider>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment