Skip to content

Commit

Permalink
Add HotkeysProvider, HotkeysDialog2, fix examples
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya committed Feb 17, 2021
1 parent 02b4539 commit c702efb
Show file tree
Hide file tree
Showing 22 changed files with 590 additions and 345 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const HOTKEYS_WARN_DECORATOR_NO_METHOD = ns + ` @HotkeysTarget-decorated
export const HOTKEYS_WARN_DECORATOR_NEEDS_REACT_ELEMENT =
ns + ` "@HotkeysTarget-decorated components must return a single JSX.Element or an empty render.`;

export const HOTKEYS_TARGET2_CHILDREN_LOCAL_HOTKEYS =
ns +
` <HotkeysTarget2> was configured with local hotkeys, but you did not use the generated event handlers to bind their event handlers. Try using a render function as the child of this component.`;

export const INPUT_WARN_LEFT_ELEMENT_LEFT_ICON_MUTEX =
ns + ` <InputGroup> leftElement and leftIcon prop are mutually exclusive, with leftElement taking priority.`;

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/hotkeys/hotkey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class Hotkey extends AbstractPureComponent2<IHotkeyProps> {

protected validateProps(props: IHotkeyProps) {
if (props.global !== true && props.group == null) {
throw new Error("non-global <Hotkey>s must define a group");
console.error("non-global <Hotkey>s must define a group");
}
}
}
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 @@ -39,7 +39,7 @@ export default class extends React.PureComponent {

public render() {
return (
<HotkeysTarget2 hotkeys={hotkeys}>
<HotkeysTarget2 hotkeys={this.hotkeys}>
{({ handleKeyDown, handleKeyUp }) => (
<div tabIndex={0} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp}>
Need help?
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/components/hotkeys/hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Deprecated: use [useHotkeys](#core/hooks/useHotkeys)
</h4>

This API is **deprecated since @blueprintjs/core v3.39.0** in favor of the new
`useHotkeys` hook available to React 16.8+ users. You should migrate
to this new API which will become the standard in Blueprint v4.
[`useHotkeys` hook](#core/hooks/useHotkeys) and
[HotkeysTarget2 component](#core/components/hokeys-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
34 changes: 33 additions & 1 deletion packages/core/src/components/hotkeys/hotkeysDialog2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,38 @@
* limitations under the License.
*/

import classNames from "classnames";
import React from "react";

export const HotkeysDialog2: React.FC = () => <div />;
import { Classes } from "../../common";
import { Dialog, IDialogProps } from "../dialog/dialog";
import { IHotkeyProps, Hotkey } from "./hotkey";
import { Hotkeys } from "./hotkeys";

export interface HotkeysDialog2Props extends IDialogProps {
/**
* This string displayed as the group name in the hotkeys dialog for all
* global hotkeys.
*/
globalGroupName?: string;

hotkeys: IHotkeyProps[];
}

export const HotkeysDialog2: React.FC<HotkeysDialog2Props> = ({ globalGroupName = "Global", hotkeys, ...props }) => {
return (
<Dialog {...props} className={classNames(Classes.HOTKEY_DIALOG, props.className)}>
<div className={Classes.DIALOG_BODY}>
<Hotkeys>
{hotkeys.map((hotkey, index) => (
<Hotkey
key={index}
{...hotkey}
group={hotkey.global === true && hotkey.group == null ? globalGroupName : hotkey.group}
/>
))}
</Hotkeys>
</div>
</Dialog>
);
};
1 change: 1 addition & 0 deletions packages/core/src/components/hotkeys/hotkeysTarget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface IHotkeysTargetComponent extends React.Component {
renderHotkeys: () => React.ReactElement<IHotkeysProps>;
}

/** @deprecated use `useHotkeys` hook or `<HotkeysTarget2>` component */
export function HotkeysTarget<T extends IConstructor<IHotkeysTargetComponent>>(WrappedComponent: T) {
if (!isFunction(WrappedComponent.prototype.renderHotkeys)) {
console.warn(HOTKEYS_WARN_DECORATOR_NO_METHOD);
Expand Down
29 changes: 24 additions & 5 deletions packages/core/src/components/hotkeys/hotkeysTarget2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
* limitations under the License.
*/

import React from "react";
import React, { useEffect } from "react";

import * as Errors from "../../common/errors";
import { isNodeEnv } from "../../common/utils";
import { useHotkeys } from "../../hooks/useHotkeys";
import { IHotkeyProps } from "./hotkey";

Expand All @@ -26,8 +28,11 @@ export interface HotkeysTarget2RenderProps {
}

export interface HotkeysTarget2Props {
/** Render prop which receives the same callback handlers generated by the `useHotkeys` hook. */
children: (props: HotkeysTarget2RenderProps) => JSX.Element;
/**
* Render prop which receives the same callback handlers generated by the `useHotkeys` hook.
* If your hotkey definitions are all global, you may supply an element instead.
*/
children: JSX.Element | ((props: HotkeysTarget2RenderProps) => JSX.Element);

/** Hotkey definitions. */
hotkeys: IHotkeyProps[];
Expand All @@ -37,7 +42,21 @@ export interface HotkeysTarget2Props {
* Utility component which allows consumers to use the new `useHotkeys` hook inside
* React component classes. The implementation simply passes through to the hook.
*/
export const HotkeysTarget2: React.FC<HotkeysTarget2Props> = ({ children, hotkeys }) => {
export const HotkeysTarget2 = ({ children, hotkeys }: HotkeysTarget2Props): JSX.Element => {
const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys);
return children({ handleKeyDown, handleKeyUp });

// run props validation
useEffect(() => {
if (!isNodeEnv("production")) {
if (typeof children !== "function" && hotkeys.some(h => !h.global)) {
console.error(Errors.HOTKEYS_TARGET2_CHILDREN_LOCAL_HOTKEYS);
}
}
}, [hotkeys]);

if (typeof children === "function") {
return children({ handleKeyDown, handleKeyUp });
} else {
return children;
}
};
92 changes: 92 additions & 0 deletions packages/core/src/context/hotkeysProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 React, { createContext, useReducer, Dispatch, useCallback } from "react";

import { IHotkeyProps, HotkeysDialog2 } from "../components";
import { HotkeysDialog2Props } from "../components/hotkeys/hotkeysDialog2";

interface HotkeysContextState {
/** List of hotkeys accessible in the current scope, registered by currently mounted components, can be global or local. */
hotkeys: IHotkeyProps[];

/** Whether the global hotkeys dialog is open. */
isDialogOpen: boolean;
}

type HotkeysAction =
| { type: "ADD_HOTKEYS" | "REMOVE_HOTKEYS"; payload: IHotkeyProps[] }
| { type: "CLOSE_DIALOG" | "OPEN_DIALOG" };

const initialHotkeysState: HotkeysContextState = { hotkeys: [], isDialogOpen: false };

export const HotkeysContext = createContext<[HotkeysContextState, Dispatch<HotkeysAction>]>([
initialHotkeysState,
() => null,
]);

const hotkeysReducer = (state: HotkeysContextState, action: HotkeysAction) => {
switch (action.type) {
case "ADD_HOTKEYS":
return {
...state,
hotkeys: [...state.hotkeys, ...action.payload],
};
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;
}

export const HotkeysProvider = ({ children, dialogProps, renderDialog }: HotkeysProviderProps) => {
const [state, dispatch] = useReducer(hotkeysReducer, initialHotkeysState);
const handleDialogClose = useCallback(() => dispatch({ type: "CLOSE_DIALOG" }), []);

const dialog = renderDialog?.(state, { handleDialogClose }) ?? (
<HotkeysDialog2
{...dialogProps}
isOpen={state.isDialogOpen}
hotkeys={state.hotkeys}
onClose={handleDialogClose}
/>
);

return (
<HotkeysContext.Provider value={[state, dispatch]}>
{children}
{dialog}
</HotkeysContext.Provider>
);
};
17 changes: 17 additions & 0 deletions packages/core/src/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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.
*/

export { HotkeysProvider, HotkeysProviderProps } from "./hotkeysProvider";
4 changes: 2 additions & 2 deletions packages/core/src/hooks/useHotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Migrating from [HotkeysTarget](#core/components/hotkeys)?
</h4>

`useHotkeys` is a replacement for HotkeysTarget. You are encouraged to use this new API in your function
components, or the HotkeysTarget2 component in your component classes, as they will be come the standard
APIs in Blueprint v4. See the full
components, or the [HotkeysTarget2 component](#core/components/hotkeys-target2) in your component classes,
as they will be come the standard APIs in Blueprint v4. See the full
[migration guide](https://github.com/palantir/blueprint/wiki/useHotkeys-migration) on the wiki.

</div>
Expand Down
44 changes: 32 additions & 12 deletions packages/core/src/hooks/useHotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@
* limitations under the License.
*/

import React, { useCallback, useEffect, useMemo } from "react";
import React, { useCallback, useContext, useEffect, useMemo } from "react";

import { IHotkeyProps } from "../components/hotkeys/hotkey";
import { comboMatches, getKeyCombo, IKeyCombo, parseKeyCombo } from "../components/hotkeys/hotkeyParser";
import { HotkeysEvents, HotkeyScope } from "../components/hotkeys/hotkeysEvents";

export function useHotkeys(keys: IHotkeyProps[]) {
const localHotkeysEvents = useMemo(() => new HotkeysEvents(HotkeyScope.LOCAL), []);
const globalHotkeysEvents = useMemo(() => new HotkeysEvents(HotkeyScope.GLOBAL), []);
import { HotkeysContext } from "../context/hotkeysProvider";

export interface UseHotkeysOptions {
/**
* The key combo which will trigger the hotkeys dialog to open.
*
* @default "?"
*/
showDialogKeyCombo?: string;
}

export function useHotkeys(keys: IHotkeyProps[], { showDialogKeyCombo = "?" }: UseHotkeysOptions = {}) {
const localKeys = useMemo(
() =>
keys
Expand All @@ -32,7 +38,7 @@ export function useHotkeys(keys: IHotkeyProps[]) {
combo: parseKeyCombo(k.combo),
props: k,
})),
keys,
[keys],
);
const globalKeys = useMemo(
() =>
Expand All @@ -42,9 +48,17 @@ export function useHotkeys(keys: IHotkeyProps[]) {
combo: parseKeyCombo(k.combo),
props: k,
})),
keys,
[keys],
);

// register keys with global context
const [, dispatch] = useContext(HotkeysContext);
useEffect(() => {
const payload = [...globalKeys.map(k => k.props), ...localKeys.map(k => k.props)];
dispatch({ type: "ADD_HOTKEYS", payload });
return () => dispatch({ type: "REMOVE_HOTKEYS", payload });
}, [keys]);

const invokeNamedCallbackIfComboRecognized = (
global: boolean,
combo: IKeyCombo,
Expand All @@ -69,7 +83,16 @@ export function useHotkeys(keys: IHotkeyProps[]) {
};

const handleGlobalKeyDown = useCallback(
(e: KeyboardEvent) => invokeNamedCallbackIfComboRecognized(true, getKeyCombo(e), "onKeyDown", e),
(e: KeyboardEvent) => {
// special case for global keydown: if '?' is pressed, open the hotkeys dialog
const combo = getKeyCombo(e);
const isTextInput = isTargetATextInput(e);
if (!isTextInput && comboMatches(parseKeyCombo(showDialogKeyCombo), combo)) {
dispatch({ type: "OPEN_DIALOG" });
} else {
invokeNamedCallbackIfComboRecognized(true, getKeyCombo(e), "onKeyDown", e);
}
},
[globalKeys],
);
const handleGlobalKeyUp = useCallback(
Expand All @@ -94,9 +117,6 @@ export function useHotkeys(keys: IHotkeyProps[]) {
return () => {
document.removeEventListener("keydown", handleGlobalKeyDown);
document.removeEventListener("keyup", handleGlobalKeyUp);

globalHotkeysEvents.clear();
localHotkeysEvents.clear();
};
}, []);

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
export * from "./accessibility";
export * from "./common";
export * from "./components";
export * from "./context";
export * from "./hooks";
26 changes: 14 additions & 12 deletions packages/docs-app/src/components/blueprintDocs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { IHeadingNode, IPageData, isPageNode, ITsDocBase } from "@documentalist/
import classNames from "classnames";
import * as React from "react";

import { AnchorButton, Classes, setHotkeysDialogProps, Tag } from "@blueprintjs/core";
import { AnchorButton, Classes, HotkeysProvider, setHotkeysDialogProps, Tag } from "@blueprintjs/core";
import { IDocsCompleteData } from "@blueprintjs/docs-data";
import { Documentation, IDocumentationProps, INavMenuItemProps, NavMenuItem } from "@blueprintjs/docs-theme";

Expand Down Expand Up @@ -80,17 +80,19 @@ export class BlueprintDocs extends React.Component<IBlueprintDocsProps, { themeN
/>
);
return (
<Documentation
{...this.props}
className={this.state.themeName}
footer={footer}
header={header}
navigatorExclude={isNavSection}
onComponentUpdate={this.handleComponentUpdate}
renderNavMenuItem={this.renderNavMenuItem}
renderPageActions={this.renderPageActions}
renderViewSourceLinkText={this.renderViewSourceLinkText}
/>
<HotkeysProvider>
<Documentation
{...this.props}
className={this.state.themeName}
footer={footer}
header={header}
navigatorExclude={isNavSection}
onComponentUpdate={this.handleComponentUpdate}
renderNavMenuItem={this.renderNavMenuItem}
renderPageActions={this.renderPageActions}
renderViewSourceLinkText={this.renderViewSourceLinkText}
/>
</HotkeysProvider>
);
}

Expand Down
Loading

1 comment on commit c702efb

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

Add HotkeysProvider, HotkeysDialog2, fix examples

Previews: documentation | landing | table

Please sign in to comment.