Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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