Skip to content

Commit

Permalink
[core] feat(HotkeysProvider): support advanced nesting use cases (#4767)
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya authored Jun 22, 2021
1 parent 97de1a6 commit 4aaf980
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 65 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/components/hotkeys/hotkeys-target2.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Migrating from [HotkeysTarget](#core/components/hotkeys)?
HotkeysTarget2 is a replacement for HotkeysTarget. You are encouraged to use this new API, or
the `useHotkeys` hook directly in your function components, as they will become the standard
APIs in Blueprint v4. See the full
[migration guide](https://github.com/palantir/blueprint/wiki/useHotkeys-migration) on the wiki.
[migration guide](https://github.com/palantir/blueprint/wiki/HotkeysTarget-&-useHotkeys-migration) on the wiki.

</div>

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/hotkeys/hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Deprecated: use [useHotkeys](#core/hooks/use-hotkeys)

This API is **deprecated since @blueprintjs/core v3.39.0** in favor of the new
[`useHotkeys` hook](#core/hooks/use-hotkeys) and
[HotkeysTarget2 component](#core/components/hokeys-target2) available to React 16.8+ users.
[HotkeysTarget2 component](#core/components/hotkeys-target2) available to React 16.8+ users.
You should migrate to one of these new APIs, as they will become the standard in Blueprint v4.

</div>
Expand Down
80 changes: 79 additions & 1 deletion packages/core/src/context/hotkeys/hotkeys-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Migrating from [HotkeysTarget](#core/components/hotkeys)?

HotkeysProvider and `useHotkeys`, used together, are a replacement for HotkeysTarget.
You are encouraged to use this new API, as it will become the standard APIs in Blueprint v4.
See the full [migration guide](https://github.com/palantir/blueprint/wiki/useHotkeys-migration)
See the full [migration guide](https://github.com/palantir/blueprint/wiki/HotkeysTarget-&-useHotkeys-migration)
on the wiki.

</div>
Expand All @@ -43,6 +43,84 @@ ReactDOM.render(
);
```

@## Advanced usage

HotkeysProvider should not be nested, except in special cases. If you have a rendering boundary within your application
through which React context is not preserved (for example, a plugin system which uses `ReactDOM.render()`) and you wish
to use hotkeys in a descendant part of the tree below such a boundary, you may render a descendant provider and initialize
it with the root context instance. This ensures that there will only be one "global" hotkeys dialogs in an application
which has multiple HotkeysProviders.

```tsx
import {
HotkeyConfig,
HotkeysContext,
HotkeysProvider,
HotkeysTarget2
} from "@blueprintjs/core";
import React, { useContext, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

function App() {
const appHotkeys: HotkeyConfig[] = [
{
combo: "o",
global: true,
label: "Open",
onKeyDown: () => console.info("open"),
},
];

return (
<HotkeysProvider>
<div>
<HotkeysTarget2 hotkeys={appHotkeys}>
<div>My app has hotkeys 😎</div>
</HotkeysTarget2>
<PluginSlot>
<Plugin />
</PluginSlot>
</div>
</HotkeysProvider>
);
}

function Plugin() {
const pluginHotkeys: HotkeyConfig[] = [
{
combo: "f",
global: true,
label: "Search",
onKeyDown: () => console.info("search"),
}
];

return (
<HotkeysTarget2 hotkeys={pluginHotkeys}>
<div>This plugin also has hotkeys</div>
</HotkeysTarget2>
);
}

function PluginSlot(props) {
const hotkeysContext = useContext(HotkeysContext);
const ref = useRef<HTMLDivElement>();

useEffect(() => {
if (ref.current != null) {
ReactDOM.render(
<HotkeysProvider value={hotkeysContext}>
{props.children}
</HotkeysProvider>,
ref.current,
);
}
}, [ref, hotkeysContext, props.children]);

return <div ref={ref} />;
}
```

@## Props

@interface HotkeysProviderProps
30 changes: 22 additions & 8 deletions packages/core/src/context/hotkeys/hotkeysProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ type HotkeysAction =
| { type: "ADD_HOTKEYS" | "REMOVE_HOTKEYS"; payload: HotkeyConfig[] }
| { type: "CLOSE_DIALOG" | "OPEN_DIALOG" };

export type HotkeysContextInstance = [HotkeysContextState, React.Dispatch<HotkeysAction>];

const initialHotkeysState: HotkeysContextState = { hotkeys: [], isDialogOpen: false };
const noOpDispatch: React.Dispatch<HotkeysAction> = () => null;

// we can remove this guard once Blueprint depends on React 16
export const HotkeysContext = React.createContext?.<[HotkeysContextState, React.Dispatch<HotkeysAction>]>([
initialHotkeysState,
noOpDispatch,
]);
// N.B. we can remove this optional call guard once Blueprint depends on React 16
/**
* A React context used to register and deregister hotkeys as components are mounted and unmounted in an application.
* Users should take care to make sure that only _one_ of these is instantiated and used within an application, especially
* if using global hotkeys.
*
* You will likely not be using this HotkeysContext directly, except in cases where you need to get a direct handle on an
* exisitng context instance for advanced use cases involving nested HotkeysProviders.
*
* For more information, see the [HotkeysProvider documentation](https://blueprintjs.com/docs/#core/context/hotkeys-provider).
*/
export const HotkeysContext = React.createContext?.<HotkeysContextInstance>([initialHotkeysState, noOpDispatch]);

const hotkeysReducer = (state: HotkeysContextState, action: HotkeysAction) => {
switch (action.type) {
Expand Down Expand Up @@ -70,13 +79,17 @@ export interface HotkeysProviderProps {

/** 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.
*/
export const HotkeysProvider = ({ children, dialogProps, renderDialog }: HotkeysProviderProps) => {
const [state, dispatch] = React.useReducer(hotkeysReducer, initialHotkeysState);
export const HotkeysProvider = ({ children, dialogProps, renderDialog, value }: HotkeysProviderProps) => {
const hasExistingContext = value != null;
const [state, dispatch] = value ?? React.useReducer(hotkeysReducer, initialHotkeysState);
const handleDialogClose = React.useCallback(() => dispatch({ type: "CLOSE_DIALOG" }), []);

const dialog = renderDialog?.(state, { handleDialogClose }) ?? (
Expand All @@ -88,10 +101,11 @@ export const HotkeysProvider = ({ children, dialogProps, renderDialog }: Hotkeys
/>
);

// if we are working with an existing context, we don't need to generate our own dialog
return (
<HotkeysContext.Provider value={[state, dispatch]}>
{children}
{dialog}
{hasExistingContext ? undefined : dialog}
</HotkeysContext.Provider>
);
};
7 changes: 6 additions & 1 deletion packages/core/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@
* limitations under the License.
*/

export { HotkeysContext, HotkeysProvider, HotkeysProviderProps } from "./hotkeys/hotkeysProvider";
export {
HotkeysContext,
HotkeysContextInstance,
HotkeysProvider,
HotkeysProviderProps,
} from "./hotkeys/hotkeysProvider";
2 changes: 1 addition & 1 deletion packages/core/src/hooks/hotkeys/use-hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Migrating from [HotkeysTarget](#core/components/hotkeys)?
`useHotkeys` is a replacement for HotkeysTarget. You are encouraged to use this new API in your function
components, or the [HotkeysTarget2 component](#core/components/hotkeys-target2) in your component classes,
as they will become the standard APIs in Blueprint v4. See the full
[migration guide](https://github.com/palantir/blueprint/wiki/useHotkeys-migration) on the wiki.
[migration guide](https://github.com/palantir/blueprint/wiki/HotkeysTarget-&-useHotkeys-migration) on the wiki.

</div>

Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/hooks/hotkeys/useHotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,18 @@ export function useHotkeys(keys: HotkeyConfig[], options: UseHotkeysOptions = {}
) => {
const isTextInput = isTargetATextInput(e);
for (const key of global ? globalKeys : localKeys) {
const shouldIgnore = (isTextInput && !key.config.allowInInput) || key.config.disabled;
const {
allowInInput = false,
disabled = false,
preventDefault = false,
stopPropagation = false,
} = key.config;
const shouldIgnore = (isTextInput && !allowInInput) || disabled;
if (!shouldIgnore && comboMatches(key.combo, combo)) {
if (key.config.preventDefault) {
if (preventDefault) {
e.preventDefault();
}
if (key.config.stopPropagation) {
if (stopPropagation) {
// set a flag just for unit testing. not meant to be referenced in feature work.
(e as any).isPropagationStopped = true;
e.stopPropagation();
Expand Down
Loading

1 comment on commit 4aaf980

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[core] feat(HotkeysProvider): support advanced nesting use cases (#4767)

Previews: documentation | landing | table

Please sign in to comment.