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

Hotkeys v2 with hooks #4532

Merged
merged 10 commits into from
Feb 18, 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
38 changes: 26 additions & 12 deletions packages/core/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@

const { createKarmaConfig } = require("@blueprintjs/karma-build-scripts");

const REACT = process.env.REACT || "16";

module.exports = function (config) {
const coverageExcludes = [
// not worth full coverage
"src/accessibility/*",
"src/common/abstractComponent*",
"src/common/abstractPureComponent*",
"src/compatibility/*",
// deprecations
"src/common/utils/functionUtils.ts",
"src/common/utils/safeInvokeMember.ts",
// HACKHACK: for karma upgrade only
"src/common/refs.ts",
// HACKHACK: need to add hotkeys v2 tests
"src/components/hotkeys/hotkeysDialog2.tsx",
"src/components/hotkeys/hotkeysTarget2.tsx",
"src/hooks/hotkeys/useHotkeys.ts",
"src/context/hotkeys/hotkeysProvider.tsx",
];

if (REACT === "15") {
// features require React 16.8+
coverageExcludes.push("src/context/*", "src/hooks/*");
}

const baseConfig = createKarmaConfig({
dirname: __dirname,
coverageExcludes: [
// not worth full coverage
"src/accessibility/*",
"src/common/abstractComponent*",
"src/common/abstractPureComponent*",
"src/compatibility/*",
// deprecations
"src/common/utils/functionUtils.ts",
"src/common/utils/safeInvokeMember.ts",
// HACKHACK: for karma upgrade only
"src/common/refs.ts",
],
coverageExcludes,
});
config.set(baseConfig);
config.set({
Expand Down
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
3 changes: 2 additions & 1 deletion packages/core/src/components/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@# Components

<!-- Exact ordering of components in the navbar: -->
<!-- Exact ordering of items in the navbar: -->

@page breadcrumbs
@page button
Expand All @@ -14,6 +14,7 @@
@page html
@page html-table
@page hotkeys
@page hotkeys-target2
@page icon
@page menu
@page navbar
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/components/drawer/drawer.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
---
tag: new
---

@# Drawer

Drawers overlay content over existing parts of the UI and are anchored to the edge of the screen.
Expand Down
71 changes: 3 additions & 68 deletions packages/core/src/components/hotkeys/hotkey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,75 +19,10 @@ import * as React from "react";
import { polyfill } from "react-lifecycles-compat";

import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, IProps } from "../../common";
import { HotkeyConfig } from "../../hooks";
import { KeyCombo } from "./keyCombo";

export interface IHotkeyProps extends IProps {
/**
* Whether the hotkey should be triggerable when focused in a text input.
*
* @default false
*/
allowInInput?: boolean;

/**
* Hotkey combination string, such as "space" or "cmd+n".
*/
combo: string;

/**
* Whether the hotkey cannot be triggered.
*
* @default false
*/
disabled?: boolean;

/**
* Human-friendly label for the hotkey.
*/
label: React.ReactNode;

/**
* If `false`, the hotkey is active only when the target is focused. If
* `true`, the hotkey can be triggered regardless of what component is
* focused.
*
* @default false
*/
global?: boolean;

/**
* Unless the hotkey is global, you must specify a group where the hotkey
* will be displayed in the hotkeys dialog. This string will be displayed
* in a header at the start of the group of hotkeys.
*/
group?: string;

/**
* When `true`, invokes `event.preventDefault()` before the respective `onKeyDown` and
* `onKeyUp` callbacks are invoked. Enabling this can simplify handler implementations.
*
* @default false
*/
preventDefault?: boolean;

/**
* When `true`, invokes `event.stopPropagation()` before the respective `onKeyDown` and
* `onKeyUp` callbacks are invoked. Enabling this can simplify handler implementations.
*
* @default false
*/
stopPropagation?: boolean;

/**
* `keydown` event handler.
*/
onKeyDown?(e: KeyboardEvent): any;

/**
* `keyup` event handler.
*/
onKeyUp?(e: KeyboardEvent): any;
}
export type IHotkeyProps = IProps & HotkeyConfig;

@polyfill
export class Hotkey extends AbstractPureComponent2<IHotkeyProps> {
Expand Down Expand Up @@ -115,7 +50,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");
}
}
}
89 changes: 89 additions & 0 deletions packages/core/src/components/hotkeys/hotkeys-target2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
tag: new
---

@# HotkeysTarget2

<div class="@ns-callout @ns-intent-warning @ns-icon-warning-sign">
<h4 class="@ns-heading">This API requires React 16.8+</h4>
</div>

<div class="@ns-callout @ns-intent-primary @ns-icon-info-sign">
<h4 class="@ns-heading">

Migrating from [HotkeysTarget](#core/components/hotkeys)?

</h4>

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.

</div>


The `HotkeysTarget2` component is a utility component which allows you to use the new
[`useHotkeys` hook](#core/hooks/use-hotkeys) inside a React component class. It's useful
if you want to switch to the new hotkeys API without refactoring your class components
into functional components.

Focus on the piano below to try its hotkeys. The global hotkeys dialog can be shown using the "?" key.

@reactExample HotkeysTarget2Example

@## Usage

First, make sure [HotkeysProvider](#core/context/hotkeys-provider) is configured correctly at the root of your
React application.

Then, to register hotkeys and generate the relevant event handlers, use the component like so:

```tsx
import React from "react";
import { HotkeysTarget2, InputGroup } from "@blueprintjs/core";

export default class extends React.PureComponent {
private inputEl: HTMLInputElement | null = null;
private handleInputRef = (el: HTMLInputElement) => (this.inputEl = el);

private hotkeys = [
{
combo: "R",
global: true,
label: "Refresh data",
onKeyDown: () => console.info("Refreshing data..."),
}
{
combo: "F",
group: "Input",
label: "Focus text input",
onKeyDown: this.inputEl?.focus(),
},
];

public render() {
return (
<HotkeysTarget2 hotkeys={this.hotkeys}>
{({ handleKeyDown, handleKeyUp }) => (
<div tabIndex={0} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp}>
Press "R" to refresh data, "F" to focus the input...
<InputGroup ref={this.handleInputRef} />
</div>
)}
</HotkeysTarget2>
)
}
}
```

Hotkeys must define a group, or be marked as global. The component will automatically bind global event handlers
and configure the <kbd>?</kbd> key to open the generated hotkeys dialog, but it is up to you to bind _local_
event handlers with the `handleKeyDown` and `handleKeyUp` functions in the child render function.
The component takes an optional `options` prop which can customize some of the hook's default behavior.

@## Props

@interface HotkeysTarget2Props

@interface HotkeyConfig
14 changes: 14 additions & 0 deletions packages/core/src/components/hotkeys/hotkeys.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
@# Hotkeys

<div class="@ns-callout @ns-intent-danger @ns-icon-error">
<h4 class="@ns-heading">

Deprecated: use [useHotkeys](#core/hooks/use-hotkeys)

</h4>

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.
You should migrate to one of these new APIs, as they will become the standard in Blueprint v4.

</div>

Hotkeys enable you to create interactions based on user keyboard events.

To add hotkeys to your React component, use the `@HotkeysTarget` class decorator
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/components/hotkeys/hotkeysDialog2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 classNames from "classnames";
import React from "react";

import { Classes } from "../../common";
import { HotkeyConfig } from "../../hooks";
import { Dialog, IDialogProps } from "../dialog/dialog";
import { 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: HotkeyConfig[];
}

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
Loading