From f98f481c9377376d07e906342cdb90c8177c0a84 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Tue, 16 Feb 2021 01:09:28 +0000 Subject: [PATCH 01/10] WIP useHotkeys --- .../core/src/components/hotkeys/hotkeys2.tsx | 19 +++ packages/core/src/hooks/index.ts | 17 +++ packages/core/src/hooks/useHotkeys.ts | 134 +++++++++++++++++ packages/core/src/index.ts | 1 + .../examples/core-examples/audio/envelope.ts | 55 +++++++ .../src/examples/core-examples/audio/index.ts | 20 +++ .../core-examples/audio/oscillator.ts | 26 ++++ .../examples/core-examples/audio/pianoKey.tsx | 70 +++++++++ .../src/examples/core-examples/audio/scale.ts | 48 ++++++ .../examples/core-examples/hotkeyPiano.tsx | 140 +----------------- .../core-examples/hotkeysHookExample.tsx | 116 +++++++++++++++ 11 files changed, 509 insertions(+), 137 deletions(-) create mode 100644 packages/core/src/components/hotkeys/hotkeys2.tsx create mode 100644 packages/core/src/hooks/index.ts create mode 100644 packages/core/src/hooks/useHotkeys.ts create mode 100644 packages/docs-app/src/examples/core-examples/audio/envelope.ts create mode 100644 packages/docs-app/src/examples/core-examples/audio/index.ts create mode 100644 packages/docs-app/src/examples/core-examples/audio/oscillator.ts create mode 100644 packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx create mode 100644 packages/docs-app/src/examples/core-examples/audio/scale.ts create mode 100644 packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx diff --git a/packages/core/src/components/hotkeys/hotkeys2.tsx b/packages/core/src/components/hotkeys/hotkeys2.tsx new file mode 100644 index 0000000000..db35b62397 --- /dev/null +++ b/packages/core/src/components/hotkeys/hotkeys2.tsx @@ -0,0 +1,19 @@ +/* + * 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 from "react"; + +export const Hotkeys2: React.FC = () =>
; diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts new file mode 100644 index 0000000000..54ff8e62da --- /dev/null +++ b/packages/core/src/hooks/index.ts @@ -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 { useHotkeys } from "./useHotkeys"; diff --git a/packages/core/src/hooks/useHotkeys.ts b/packages/core/src/hooks/useHotkeys.ts new file mode 100644 index 0000000000..4016bf55ee --- /dev/null +++ b/packages/core/src/hooks/useHotkeys.ts @@ -0,0 +1,134 @@ +/* + * 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, { useCallback, 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), []); + + const localKeys = useMemo( + () => + keys + .filter(k => !k.global) + .map(k => ({ + combo: parseKeyCombo(k.combo), + props: k, + })), + keys, + ); + const globalKeys = useMemo( + () => + keys + .filter(k => k.global) + .map(k => ({ + combo: parseKeyCombo(k.combo), + props: k, + })), + keys, + ); + + const invokeNamedCallbackIfComboRecognized = ( + global: boolean, + combo: IKeyCombo, + callbackName: "onKeyDown" | "onKeyUp", + e: KeyboardEvent, + ) => { + const isTextInput = isTargetATextInput(e); + for (const action of global ? globalKeys : localKeys) { + const shouldIgnore = (isTextInput && !action.props.allowInInput) || action.props.disabled; + if (!shouldIgnore && comboMatches(action.combo, combo)) { + if (action.props.preventDefault) { + e.preventDefault(); + } + if (action.props.stopPropagation) { + // set a flag just for unit testing. not meant to be referenced in feature work. + (e as any).isPropagationStopped = true; + e.stopPropagation(); + } + action.props[callbackName]?.(e); + } + } + }; + + const handleGlobalKeyDown = useCallback( + (e: KeyboardEvent) => invokeNamedCallbackIfComboRecognized(true, getKeyCombo(e), "onKeyDown", e), + [globalKeys], + ); + const handleGlobalKeyUp = useCallback( + (e: KeyboardEvent) => invokeNamedCallbackIfComboRecognized(true, getKeyCombo(e), "onKeyUp", e), + [globalKeys], + ); + + const handleLocalKeyDown = useCallback( + (e: React.KeyboardEvent) => + invokeNamedCallbackIfComboRecognized(false, getKeyCombo(e.nativeEvent), "onKeyDown", e.nativeEvent), + [localKeys], + ); + const handleLocalKeyUp = useCallback( + (e: React.KeyboardEvent) => + invokeNamedCallbackIfComboRecognized(false, getKeyCombo(e.nativeEvent), "onKeyUp", e.nativeEvent), + [localKeys], + ); + + useEffect(() => { + document.addEventListener("keydown", handleGlobalKeyDown); + document.addEventListener("keyup", handleGlobalKeyUp); + return () => { + document.removeEventListener("keydown", handleGlobalKeyDown); + document.removeEventListener("keyup", handleGlobalKeyUp); + + globalHotkeysEvents.clear(); + localHotkeysEvents.clear(); + }; + }, []); + + return { handleKeyDown: handleLocalKeyDown, handleKeyUp: handleLocalKeyUp }; +} + +function isTargetATextInput(e: KeyboardEvent) { + const elem = e.target as HTMLElement; + // we check these cases for unit testing, but this should not happen + // during normal operation + if (elem == null || elem.closest == null) { + return false; + } + + const editable = elem.closest("input, textarea, [contenteditable=true]"); + + if (editable == null) { + return false; + } + + // don't let checkboxes, switches, and radio buttons prevent hotkey behavior + if (editable.tagName.toLowerCase() === "input") { + const inputType = (editable as HTMLInputElement).type; + if (inputType === "checkbox" || inputType === "radio") { + return false; + } + } + + // don't let read-only fields prevent hotkey behavior + if ((editable as HTMLInputElement).readOnly) { + return false; + } + + return true; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fe7bfd54eb..176cbe9632 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,3 +17,4 @@ export * from "./accessibility"; export * from "./common"; export * from "./components"; +export * from "./hooks"; diff --git a/packages/docs-app/src/examples/core-examples/audio/envelope.ts b/packages/docs-app/src/examples/core-examples/audio/envelope.ts new file mode 100644 index 0000000000..4cc1f99c6e --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/audio/envelope.ts @@ -0,0 +1,55 @@ +/* + * 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 class Envelope { + public amplitude: AudioParam; + + public gain: GainNode; + + private attackLevel = 0.8; + + private attackTime = 0.1; + + private sustainLevel = 0.3; + + private sustainTime = 0.1; + + private releaseTime = 0.4; + + public constructor(private context: AudioContext) { + this.gain = this.context.createGain(); + this.amplitude = this.gain.gain; + this.amplitude.value = 0; + } + + public on() { + const now = this.context.currentTime; + this.amplitude.cancelScheduledValues(now); + this.amplitude.setValueAtTime(this.amplitude.value, now); + this.amplitude.linearRampToValueAtTime(this.attackLevel, now + this.attackTime); + this.amplitude.exponentialRampToValueAtTime(this.sustainLevel, now + this.attackTime + this.sustainTime); + } + + public off() { + const now = this.context.currentTime; + // The below code helps remove waveform popping artifacts, but there is + // a bug in Firefox that breaks the whole example if we use it. + // this.amplitude.cancelScheduledValues(now); + // this.amplitude.setValueAtTime(this.amplitude.value, now); + this.amplitude.exponentialRampToValueAtTime(0.01, now + this.releaseTime); + this.amplitude.linearRampToValueAtTime(0, now + this.releaseTime + 0.01); + } +} diff --git a/packages/docs-app/src/examples/core-examples/audio/index.ts b/packages/docs-app/src/examples/core-examples/audio/index.ts new file mode 100644 index 0000000000..752102f40b --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/audio/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { Envelope } from "./envelope"; +export { Oscillator } from "./oscillator"; +export { PianoKey } from "./pianoKey"; +export { Scale } from "./scale"; diff --git a/packages/docs-app/src/examples/core-examples/audio/oscillator.ts b/packages/docs-app/src/examples/core-examples/audio/oscillator.ts new file mode 100644 index 0000000000..3ef43e0aa6 --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/audio/oscillator.ts @@ -0,0 +1,26 @@ +/* + * 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 class Oscillator { + public oscillator: OscillatorNode; + + public constructor(private context: AudioContext, freq: number) { + this.oscillator = this.context.createOscillator(); + this.oscillator.type = "sine"; + this.oscillator.frequency.value = freq; + this.oscillator.start(0); + } +} diff --git a/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx b/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx new file mode 100644 index 0000000000..39d90a7806 --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx @@ -0,0 +1,70 @@ +/* + * 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, { useEffect } from "react"; + +import { Classes } from "@blueprintjs/core"; + +import { Envelope } from "./envelope"; +import { Oscillator } from "./oscillator"; +import { Scale } from "./scale"; + +interface IPianoKeyProps { + note: string; + hotkey: string; + pressed: boolean; + context: AudioContext | undefined; +} + +export const PianoKey: React.FC = ({ context, hotkey, note, pressed }) => { + let oscillator: Oscillator; + let envelope: Envelope; + + if (context !== undefined) { + oscillator = new Oscillator(context, Scale[note]); + envelope = new Envelope(context); + oscillator.oscillator.connect(envelope.gain); + envelope.gain.connect(context.destination); + } + + useEffect(() => { + if (envelope !== undefined) { + if (pressed) { + envelope.on(); + } else { + envelope.off(); + } + } + }, [pressed]); + + const classes = classNames("piano-key", { + "piano-key-pressed": pressed, + "piano-key-sharp": /\#/.test(note), + }); + const elevation = classNames(pressed ? Classes.ELEVATION_0 : Classes.ELEVATION_2); + return ( +
+
+
+ {note} +
+ {hotkey} +
+
+
+ ); +}; diff --git a/packages/docs-app/src/examples/core-examples/audio/scale.ts b/packages/docs-app/src/examples/core-examples/audio/scale.ts new file mode 100644 index 0000000000..de107b818a --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/audio/scale.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +// alph sorting does not follow a logical order here +// tslint:disable object-literal-sort-keys +export const Scale: { [note: string]: number } = { + A3: 220.0, + "A#3": 233.08, + B3: 246.94, + C4: 261.63, + "C#4": 277.18, + D4: 293.66, + "D#4": 311.13, + E4: 329.63, + F4: 349.23, + "F#4": 369.99, + G4: 392.0, + "G#4": 415.3, + A4: 440.0, + "A#4": 466.16, + B4: 493.88, + C5: 523.25, + "C#5": 554.37, + D5: 587.33, + "D#5": 622.25, + E5: 659.25, + F5: 698.46, + "F#5": 739.99, + G5: 783.99, + "G#5": 830.61, + A5: 880.0, + "A#5": 932.33, + B5: 987.77, +}; +// tslint:enable object-literal-sort-keys diff --git a/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx b/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx index 006ab2dd04..15421083e9 100644 --- a/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx +++ b/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx @@ -16,146 +16,12 @@ /* eslint-disable max-classes-per-file */ -import classNames from "classnames"; -import * as React from "react"; +import React from "react"; -import { Classes, Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core"; +import { Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core"; import { Example, IExampleProps } from "@blueprintjs/docs-theme"; -class Oscillator { - public oscillator: OscillatorNode; - - public constructor(private context: AudioContext, freq: number) { - this.oscillator = this.context.createOscillator(); - this.oscillator.type = "sine"; - this.oscillator.frequency.value = freq; - this.oscillator.start(0); - } -} - -class Envelope { - public amplitude: AudioParam; - - public gain: GainNode; - - private attackLevel = 0.8; - - private attackTime = 0.1; - - private sustainLevel = 0.3; - - private sustainTime = 0.1; - - private releaseTime = 0.4; - - public constructor(private context: AudioContext) { - this.gain = this.context.createGain(); - this.amplitude = this.gain.gain; - this.amplitude.value = 0; - } - - public on() { - const now = this.context.currentTime; - this.amplitude.cancelScheduledValues(now); - this.amplitude.setValueAtTime(this.amplitude.value, now); - this.amplitude.linearRampToValueAtTime(this.attackLevel, now + this.attackTime); - this.amplitude.exponentialRampToValueAtTime(this.sustainLevel, now + this.attackTime + this.sustainTime); - } - - public off() { - const now = this.context.currentTime; - // The below code helps remove waveform popping artifacts, but there is - // a bug in Firefox that breaks the whole example if we use it. - // this.amplitude.cancelScheduledValues(now); - // this.amplitude.setValueAtTime(this.amplitude.value, now); - this.amplitude.exponentialRampToValueAtTime(0.01, now + this.releaseTime); - this.amplitude.linearRampToValueAtTime(0, now + this.releaseTime + 0.01); - } -} - -// alph sorting does not follow a logical order here -// tslint:disable object-literal-sort-keys -const Scale: { [note: string]: number } = { - A3: 220.0, - "A#3": 233.08, - B3: 246.94, - C4: 261.63, - "C#4": 277.18, - D4: 293.66, - "D#4": 311.13, - E4: 329.63, - F4: 349.23, - "F#4": 369.99, - G4: 392.0, - "G#4": 415.3, - A4: 440.0, - "A#4": 466.16, - B4: 493.88, - C5: 523.25, - "C#5": 554.37, - D5: 587.33, - "D#5": 622.25, - E5: 659.25, - F5: 698.46, - "F#5": 739.99, - G5: 783.99, - "G#5": 830.61, - A5: 880.0, - "A#5": 932.33, - B5: 987.77, -}; -// tslint:enable object-literal-sort-keys - -interface IPianoKeyProps { - note: string; - hotkey: string; - pressed: boolean; - context: AudioContext; -} - -class PianoKey extends React.Component { - private oscillator: Oscillator; - - private envelope: Envelope; - - public constructor(props: IPianoKeyProps) { - super(props); - - const { context, note } = this.props; - this.oscillator = new Oscillator(context, Scale[note]); - this.envelope = new Envelope(context); - this.oscillator.oscillator.connect(this.envelope.gain); - this.envelope.gain.connect(context.destination); - } - - public componentDidUpdate(prevProps: IPianoKeyProps) { - if (prevProps.pressed === false && this.props.pressed === true) { - this.envelope.on(); - } else if (prevProps.pressed === true && this.props.pressed === false) { - this.envelope.off(); - } - } - - public render() { - const { hotkey, note, pressed } = this.props; - const classes = classNames("piano-key", { - "piano-key-pressed": pressed, - "piano-key-sharp": /\#/.test(note), - }); - const elevation = classNames(pressed ? Classes.ELEVATION_0 : Classes.ELEVATION_2); - return ( -
-
-
- {note} -
- {hotkey} -
-
-
- ); - } -} +import { PianoKey } from "./audio"; export interface IHotkeyPianoState { keys: boolean[]; diff --git a/packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx b/packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx new file mode 100644 index 0000000000..1793bf3b60 --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx @@ -0,0 +1,116 @@ +/* + * 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 from "react"; + +import { useHotkeys } from "@blueprintjs/core"; +import { Example, IExampleProps } from "@blueprintjs/docs-theme"; + +import { PianoKey } from "./audio"; + +export const UseHotkeysExample: React.FC = props => { + const [audioContext, setAudioContext] = React.useState(); + + const pianoRef = React.useRef(); + const focusPiano = React.useCallback(() => { + pianoRef?.current.focus(); + if (typeof window.AudioContext !== "undefined" && audioContext === undefined) { + setAudioContext(new AudioContext()); + } + }, [pianoRef]); + + const keys = Array.apply(null, Array(24)) + .map(() => React.useState(() => false), []) + .map(([pressed, setPressed]) => ({ + pressed, + setPressed, + })); + + const { handleKeyDown, handleKeyUp } = useHotkeys([ + { + combo: "shift + P", + global: true, + label: "Focus the piano", + onKeyDown: focusPiano, + }, + { + combo: "Q", + label: "Play a C5", + onKeyDown: () => keys[0].setPressed(true), + onKeyUp: () => keys[0].setPressed(true), + }, + { + combo: "2", + label: "Play a C#5", + onKeyDown: () => keys[1].setPressed(true), + onKeyUp: () => keys[1].setPressed(true), + }, + { + combo: "W", + label: "Play a D5", + onKeyDown: () => keys[2].setPressed(true), + onKeyUp: () => keys[2].setPressed(true), + }, + { + combo: "3", + label: "Play a D#5", + onKeyDown: () => keys[3].setPressed(true), + onKeyUp: () => keys[3].setPressed(true), + }, + ]); + + return ( + +
+
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + +
+
+
+ ); +}; From 89c5a9b1d613451554e2365a2787f49a036c75da Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Tue, 16 Feb 2021 16:01:31 +0000 Subject: [PATCH 02/10] useHotkeys example, HotkeysTarget2 --- packages/core/src/components/components.md | 3 +- .../src/components/hotkeys/hotkeys-target2.md | 41 +++ .../core/src/components/hotkeys/hotkeys.md | 13 + .../{hotkeys2.tsx => hotkeysDialog2.tsx} | 2 +- .../src/components/hotkeys/hotkeysTarget2.tsx | 43 +++ packages/core/src/components/hotkeys/index.ts | 3 + packages/core/src/docs/index.md | 1 + packages/core/src/hooks/hooks.md | 5 + packages/core/src/hooks/useHotkeys.md | 19 ++ packages/core/src/hooks/useHotkeys.ts | 3 + .../docs-app/src/components/blueprintDocs.tsx | 4 +- .../examples/core-examples/audio/envelope.ts | 6 +- .../examples/core-examples/audio/pianoKey.tsx | 13 +- .../examples/core-examples/hotkeyPiano.tsx | 2 - .../core-examples/hotkeysTarget2Example.tsx | 262 ++++++++++++++++++ .../src/examples/core-examples/index.ts | 3 + ...sHookExample.tsx => useHotkeysExample.tsx} | 129 ++++++++- 17 files changed, 533 insertions(+), 19 deletions(-) create mode 100644 packages/core/src/components/hotkeys/hotkeys-target2.md rename packages/core/src/components/hotkeys/{hotkeys2.tsx => hotkeysDialog2.tsx} (92%) create mode 100644 packages/core/src/components/hotkeys/hotkeysTarget2.tsx create mode 100644 packages/core/src/hooks/hooks.md create mode 100644 packages/core/src/hooks/useHotkeys.md create mode 100644 packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx rename packages/docs-app/src/examples/core-examples/{hotkeysHookExample.tsx => useHotkeysExample.tsx} (56%) diff --git a/packages/core/src/components/components.md b/packages/core/src/components/components.md index ab1a8a2642..61bff89775 100644 --- a/packages/core/src/components/components.md +++ b/packages/core/src/components/components.md @@ -1,6 +1,6 @@ @# Components - + @page breadcrumbs @page button @@ -14,6 +14,7 @@ @page html @page html-table @page hotkeys +@page hotkeys-target2 @page icon @page menu @page navbar diff --git a/packages/core/src/components/hotkeys/hotkeys-target2.md b/packages/core/src/components/hotkeys/hotkeys-target2.md new file mode 100644 index 0000000000..e67b32a183 --- /dev/null +++ b/packages/core/src/components/hotkeys/hotkeys-target2.md @@ -0,0 +1,41 @@ +@# HotkeysTarget2 + +The `HotkeysTarget2` component is a utility component which allows you to use the new +[`useHotkeys` hook](#core/hooks/useHotkeys) 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. + +@reactExample HotkeysTarget2Example + +@## Usage + +```tsx +import React from "react"; +import { HotkeysTarget2 } from "@blueprintjs/core"; + +export default class extends React.PureComponent { + private hotkeys = [ + { + combo: "?", + label: "Open help dialog", + onKeyDown: () => alert("Opened help dialog!"), + }, + ]; + + public render() { + return ( + + {({ handleKeyDown, handleKeyUp }) => ( +
+ Need help? +
+ )} +
+ ) + } +} +``` + +@## Props + +@interface HotkeysTarget2Props diff --git a/packages/core/src/components/hotkeys/hotkeys.md b/packages/core/src/components/hotkeys/hotkeys.md index 388551b570..bc6ea91a18 100644 --- a/packages/core/src/components/hotkeys/hotkeys.md +++ b/packages/core/src/components/hotkeys/hotkeys.md @@ -1,5 +1,18 @@ @# Hotkeys +
+

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

+ +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. + +
+ Hotkeys enable you to create interactions based on user keyboard events. To add hotkeys to your React component, use the `@HotkeysTarget` class decorator diff --git a/packages/core/src/components/hotkeys/hotkeys2.tsx b/packages/core/src/components/hotkeys/hotkeysDialog2.tsx similarity index 92% rename from packages/core/src/components/hotkeys/hotkeys2.tsx rename to packages/core/src/components/hotkeys/hotkeysDialog2.tsx index db35b62397..693f5bfad2 100644 --- a/packages/core/src/components/hotkeys/hotkeys2.tsx +++ b/packages/core/src/components/hotkeys/hotkeysDialog2.tsx @@ -16,4 +16,4 @@ import React from "react"; -export const Hotkeys2: React.FC = () =>
; +export const HotkeysDialog2: React.FC = () =>
; diff --git a/packages/core/src/components/hotkeys/hotkeysTarget2.tsx b/packages/core/src/components/hotkeys/hotkeysTarget2.tsx new file mode 100644 index 0000000000..2793991052 --- /dev/null +++ b/packages/core/src/components/hotkeys/hotkeysTarget2.tsx @@ -0,0 +1,43 @@ +/* + * 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 from "react"; + +import { useHotkeys } from "../../hooks/useHotkeys"; +import { IHotkeyProps } from "./hotkey"; + +/** Identical to the return type of `useHotkeys` hook. */ +export interface HotkeysTarget2RenderProps { + handleKeyDown: React.KeyboardEventHandler; + handleKeyUp: React.KeyboardEventHandler; +} + +export interface HotkeysTarget2Props { + /** Render prop which receives the same callback handlers generated by the `useHotkeys` hook. */ + children: (props: HotkeysTarget2RenderProps) => JSX.Element; + + /** Hotkey definitions. */ + hotkeys: IHotkeyProps[]; +} + +/** + * 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 = ({ children, hotkeys }) => { + const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys); + return children({ handleKeyDown, handleKeyUp }); +}; diff --git a/packages/core/src/components/hotkeys/index.ts b/packages/core/src/components/hotkeys/index.ts index b47c3bd58a..e1ac8ed019 100644 --- a/packages/core/src/components/hotkeys/index.ts +++ b/packages/core/src/components/hotkeys/index.ts @@ -23,3 +23,6 @@ export { HotkeysTarget, IHotkeysTargetComponent } from "./hotkeysTarget"; export { IKeyCombo, comboMatches, getKeyCombo, getKeyComboString, parseKeyCombo } from "./hotkeyParser"; // eslint-disable-next-line import/no-cycle export { IHotkeysDialogProps, hideHotkeysDialog, setHotkeysDialogProps } from "./hotkeysDialog"; + +export { HotkeysDialog2 } from "./hotkeysDialog2"; +export { HotkeysTarget2, HotkeysTarget2Props } from "./hotkeysTarget2"; diff --git a/packages/core/src/docs/index.md b/packages/core/src/docs/index.md index 59d41b51d7..6b9039daac 100644 --- a/packages/core/src/docs/index.md +++ b/packages/core/src/docs/index.md @@ -18,3 +18,4 @@ Be sure to include the icons CSS file in your app alongside the core CSS file. @page typography @page variables @page components +@page hooks diff --git a/packages/core/src/hooks/hooks.md b/packages/core/src/hooks/hooks.md new file mode 100644 index 0000000000..1b53d96fef --- /dev/null +++ b/packages/core/src/hooks/hooks.md @@ -0,0 +1,5 @@ +@# Hooks + + + +@page useHotkeys diff --git a/packages/core/src/hooks/useHotkeys.md b/packages/core/src/hooks/useHotkeys.md new file mode 100644 index 0000000000..53bb9c5980 --- /dev/null +++ b/packages/core/src/hooks/useHotkeys.md @@ -0,0 +1,19 @@ +--- +tag: new +--- + +@# useHotkeys + +The `useHotkeys` hook adds hotkey / keyboard shortcut interactions to your application using a custom React hook. +Compared to the deprecated [Hotkeys](#core/components/hotkeys) API, it works with function components and allows +more customization of the explanatory hotkeys dialog. + +@reactExample UseHotkeysExample + +@## Usage + +TODO(adahiya) + +@## Dialog + +TODO(adahiya) diff --git a/packages/core/src/hooks/useHotkeys.ts b/packages/core/src/hooks/useHotkeys.ts index 4016bf55ee..9fb2f74409 100644 --- a/packages/core/src/hooks/useHotkeys.ts +++ b/packages/core/src/hooks/useHotkeys.ts @@ -103,6 +103,9 @@ export function useHotkeys(keys: IHotkeyProps[]) { return { handleKeyDown: handleLocalKeyDown, handleKeyUp: handleLocalKeyUp }; } +/** + * @returns true if the event target is a text input which should take priority over hotkey bindings + */ function isTargetATextInput(e: KeyboardEvent) { const elem = e.target as HTMLElement; // we check these cases for unit testing, but this should not happen diff --git a/packages/docs-app/src/components/blueprintDocs.tsx b/packages/docs-app/src/components/blueprintDocs.tsx index 2a92306ddb..5498c0ea69 100644 --- a/packages/docs-app/src/components/blueprintDocs.tsx +++ b/packages/docs-app/src/components/blueprintDocs.tsx @@ -32,9 +32,11 @@ const THEME_LOCAL_STORAGE_KEY = "blueprint-docs-theme"; const GITHUB_SOURCE_URL = "https://github.com/palantir/blueprint/blob/develop"; const NPM_URL = "https://www.npmjs.com/package"; +// HACKHACK: this is brittle // detect Components page and subheadings const COMPONENTS_PATTERN = /\/components(\.[\w-]+)?$/; -const isNavSection = ({ route }: IHeadingNode) => COMPONENTS_PATTERN.test(route); +const HOOKS_PATTERN = /\/hooks(\.[\w-]+)?$/; +const isNavSection = ({ route }: IHeadingNode) => COMPONENTS_PATTERN.test(route) || HOOKS_PATTERN.test(route); /** Return the current theme className. */ export function getTheme(): string { diff --git a/packages/docs-app/src/examples/core-examples/audio/envelope.ts b/packages/docs-app/src/examples/core-examples/audio/envelope.ts index 4cc1f99c6e..614773495c 100644 --- a/packages/docs-app/src/examples/core-examples/audio/envelope.ts +++ b/packages/docs-app/src/examples/core-examples/audio/envelope.ts @@ -19,15 +19,15 @@ export class Envelope { public gain: GainNode; - private attackLevel = 0.8; + private attackLevel = 0.4; private attackTime = 0.1; - private sustainLevel = 0.3; + private sustainLevel = 0.2; private sustainTime = 0.1; - private releaseTime = 0.4; + private releaseTime = 0.2; public constructor(private context: AudioContext) { this.gain = this.context.createGain(); diff --git a/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx b/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx index 39d90a7806..6f8d2269d6 100644 --- a/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx +++ b/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx @@ -42,14 +42,13 @@ export const PianoKey: React.FC = ({ context, hotkey, note, pres } useEffect(() => { - if (envelope !== undefined) { - if (pressed) { - envelope.on(); - } else { - envelope.off(); - } + if (pressed) { + envelope?.on(); + } else { + envelope?.off(); } - }, [pressed]); + return () => envelope?.off(); + }, [envelope, pressed]); const classes = classNames("piano-key", { "piano-key-pressed": pressed, diff --git a/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx b/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx index 15421083e9..3fc61693e7 100644 --- a/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx +++ b/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -/* eslint-disable max-classes-per-file */ - import React from "react"; import { Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core"; diff --git a/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx b/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx new file mode 100644 index 0000000000..1b024cf974 --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx @@ -0,0 +1,262 @@ +/* + * 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 from "react"; + +import { HotkeysTarget2, IHotkeyProps } from "@blueprintjs/core"; +import { Example, IExampleProps } from "@blueprintjs/docs-theme"; + +import { PianoKey } from "./audio"; + +export interface IHotkeysTarget2ExampleState { + audioContext?: AudioContext; + // pressed state of each key + keys: boolean[]; +} + +/** + * Similar to UseHotkeysExample, but using a component class API pattern. + * We may deprecate and remove this in the future if we encourage everyone to switch to hooks. + */ +export class HotkeysTarget2Example extends React.PureComponent { + public state: IHotkeysTarget2ExampleState = { + keys: Array.apply(null, Array(24)).map(() => false), + }; + + private pianoRef: HTMLDivElement | null = null; + + private handlePianoRef = (ref: HTMLDivElement | null) => (this.pianoRef = ref); + + private focusPiano = () => { + if (this.pianoRef !== null) { + this.pianoRef.focus(); + if (typeof window.AudioContext !== "undefined" && this.state.audioContext === undefined) { + this.setState({ audioContext: new AudioContext() }); + } + } + }; + + private setKey = (index: number, keyState: boolean) => { + return () => { + const keys = this.state.keys.slice(); + keys[index] = keyState; + this.setState({ keys }); + }; + }; + + private hotkeys: IHotkeyProps[] = [ + { + combo: "shift + P", + global: true, + label: "Focus the piano", + onKeyDown: this.focusPiano, + }, + { + combo: "Q", + label: "Play a C5", + onKeyDown: () => this.setKey(0, true), + onKeyUp: () => this.setKey(0, false), + }, + { + combo: "2", + label: "Play a C#5", + onKeyDown: () => this.setKey(1, true), + onKeyUp: () => this.setKey(1, false), + }, + { + combo: "W", + label: "Play a D5", + onKeyDown: () => this.setKey(2, true), + onKeyUp: () => this.setKey(2, false), + }, + { + combo: "3", + label: "Play a D#5", + onKeyDown: () => this.setKey(3, true), + onKeyUp: () => this.setKey(3, false), + }, + { + combo: "E", + label: "Play a E5", + onKeyDown: () => this.setKey(4, true), + onKeyUp: () => this.setKey(4, false), + }, + { + combo: "R", + label: "Play a F5", + onKeyDown: () => this.setKey(5, true), + onKeyUp: () => this.setKey(5, false), + }, + { + combo: "5", + label: "Play a F#5", + onKeyDown: () => this.setKey(6, true), + onKeyUp: () => this.setKey(6, false), + }, + { + combo: "T", + label: "Play a G5", + onKeyDown: () => this.setKey(7, true), + onKeyUp: () => this.setKey(7, false), + }, + { + combo: "6", + label: "Play a G#5", + onKeyDown: () => this.setKey(8, true), + onKeyUp: () => this.setKey(8, false), + }, + { + combo: "Y", + label: "Play a A5", + onKeyDown: () => this.setKey(9, true), + onKeyUp: () => this.setKey(9, false), + }, + { + combo: "7", + label: "Play a A#5", + onKeyDown: () => this.setKey(10, true), + onKeyUp: () => this.setKey(10, false), + }, + { + combo: "U", + label: "Play a B5", + onKeyDown: () => this.setKey(11, true), + onKeyUp: () => this.setKey(11, false), + }, + { + combo: "Z", + label: "Play a C4", + onKeyDown: () => this.setKey(12, true), + onKeyUp: () => this.setKey(12, false), + }, + { + combo: "S", + label: "Play a C#4", + onKeyDown: () => this.setKey(13, true), + onKeyUp: () => this.setKey(13, false), + }, + { + combo: "X", + label: "Play a D4", + onKeyDown: () => this.setKey(14, true), + onKeyUp: () => this.setKey(14, false), + }, + { + combo: "D", + label: "Play a D#4", + onKeyDown: () => this.setKey(15, true), + onKeyUp: () => this.setKey(15, false), + }, + { + combo: "C", + label: "Play a E4", + onKeyDown: () => this.setKey(16, true), + onKeyUp: () => this.setKey(16, false), + }, + { + combo: "V", + label: "Play a F4", + onKeyDown: () => this.setKey(17, true), + onKeyUp: () => this.setKey(17, false), + }, + { + combo: "G", + label: "Play a F#4", + onKeyDown: () => this.setKey(18, true), + onKeyUp: () => this.setKey(18, false), + }, + { + combo: "B", + label: "Play a G4", + onKeyDown: () => this.setKey(19, true), + onKeyUp: () => this.setKey(19, false), + }, + { + combo: "H", + label: "Play a G#4", + onKeyDown: () => this.setKey(20, true), + onKeyUp: () => this.setKey(20, false), + }, + { + combo: "N", + label: "Play a A4", + onKeyDown: () => this.setKey(21, true), + onKeyUp: () => this.setKey(21, false), + }, + { + combo: "J", + label: "Play a A#4", + onKeyDown: () => this.setKey(22, true), + onKeyUp: () => this.setKey(22, false), + }, + { + combo: "M", + label: "Play a B4", + onKeyDown: () => this.setKey(23, true), + onKeyUp: () => this.setKey(23, false), + }, + ]; + + public render() { + const { audioContext, keys } = this.state; + + return ( + + + {({ handleKeyDown, handleKeyUp }) => ( +
+
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + +
+
+ )} +
+
+ ); + } +} diff --git a/packages/docs-app/src/examples/core-examples/index.ts b/packages/docs-app/src/examples/core-examples/index.ts index 41e62d6e01..bceebeb8d5 100644 --- a/packages/docs-app/src/examples/core-examples/index.ts +++ b/packages/docs-app/src/examples/core-examples/index.ts @@ -39,6 +39,7 @@ export * from "./focusExample"; export * from "./formGroupExample"; export * from "./hotkeyPiano"; export * from "./hotkeyTester"; +export { HotkeysTarget2Example } from "./hotkeysTarget2Example"; export * from "./iconExample"; export * from "./menuExample"; export * from "./multiSliderExample"; @@ -70,3 +71,5 @@ export * from "./tagExample"; export * from "./toastExample"; export * from "./tooltipExample"; export * from "./treeExample"; + +export { UseHotkeysExample } from "./useHotkeysExample"; diff --git a/packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx b/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx similarity index 56% rename from packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx rename to packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx index 1793bf3b60..78277c557c 100644 --- a/packages/docs-app/src/examples/core-examples/hotkeysHookExample.tsx +++ b/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx @@ -32,6 +32,7 @@ export const UseHotkeysExample: React.FC = props => { } }, [pianoRef]); + // create a dictionary of key states and updater functions const keys = Array.apply(null, Array(24)) .map(() => React.useState(() => false), []) .map(([pressed, setPressed]) => ({ @@ -50,25 +51,145 @@ export const UseHotkeysExample: React.FC = props => { combo: "Q", label: "Play a C5", onKeyDown: () => keys[0].setPressed(true), - onKeyUp: () => keys[0].setPressed(true), + onKeyUp: () => keys[0].setPressed(false), }, { combo: "2", label: "Play a C#5", onKeyDown: () => keys[1].setPressed(true), - onKeyUp: () => keys[1].setPressed(true), + onKeyUp: () => keys[1].setPressed(false), }, { combo: "W", label: "Play a D5", onKeyDown: () => keys[2].setPressed(true), - onKeyUp: () => keys[2].setPressed(true), + onKeyUp: () => keys[2].setPressed(false), }, { combo: "3", label: "Play a D#5", onKeyDown: () => keys[3].setPressed(true), - onKeyUp: () => keys[3].setPressed(true), + onKeyUp: () => keys[3].setPressed(false), + }, + { + combo: "E", + label: "Play a E5", + onKeyDown: () => keys[4].setPressed(true), + onKeyUp: () => keys[4].setPressed(false), + }, + { + combo: "R", + label: "Play a F5", + onKeyDown: () => keys[5].setPressed(true), + onKeyUp: () => keys[5].setPressed(false), + }, + { + combo: "5", + label: "Play a F#5", + onKeyDown: () => keys[6].setPressed(true), + onKeyUp: () => keys[6].setPressed(false), + }, + { + combo: "T", + label: "Play a G5", + onKeyDown: () => keys[7].setPressed(true), + onKeyUp: () => keys[7].setPressed(false), + }, + { + combo: "6", + label: "Play a G#5", + onKeyDown: () => keys[8].setPressed(true), + onKeyUp: () => keys[8].setPressed(false), + }, + { + combo: "Y", + label: "Play a A5", + onKeyDown: () => keys[9].setPressed(true), + onKeyUp: () => keys[9].setPressed(false), + }, + { + combo: "7", + label: "Play a A#5", + onKeyDown: () => keys[10].setPressed(true), + onKeyUp: () => keys[10].setPressed(false), + }, + { + combo: "U", + label: "Play a B5", + onKeyDown: () => keys[11].setPressed(true), + onKeyUp: () => keys[11].setPressed(false), + }, + { + combo: "Z", + label: "Play a C4", + onKeyDown: () => keys[12].setPressed(true), + onKeyUp: () => keys[12].setPressed(false), + }, + { + combo: "S", + label: "Play a C#4", + onKeyDown: () => keys[13].setPressed(true), + onKeyUp: () => keys[13].setPressed(false), + }, + { + combo: "X", + label: "Play a D4", + onKeyDown: () => keys[14].setPressed(true), + onKeyUp: () => keys[14].setPressed(false), + }, + { + combo: "D", + label: "Play a D#4", + onKeyDown: () => keys[15].setPressed(true), + onKeyUp: () => keys[15].setPressed(false), + }, + { + combo: "C", + label: "Play a E4", + onKeyDown: () => keys[16].setPressed(true), + onKeyUp: () => keys[16].setPressed(false), + }, + { + combo: "V", + label: "Play a F4", + onKeyDown: () => keys[17].setPressed(true), + onKeyUp: () => keys[17].setPressed(false), + }, + { + combo: "G", + label: "Play a F#4", + onKeyDown: () => keys[18].setPressed(true), + onKeyUp: () => keys[18].setPressed(false), + }, + { + combo: "B", + label: "Play a G4", + onKeyDown: () => keys[19].setPressed(true), + onKeyUp: () => keys[19].setPressed(false), + }, + { + combo: "H", + label: "Play a G#4", + onKeyDown: () => keys[20].setPressed(true), + onKeyUp: () => keys[20].setPressed(false), + }, + { + combo: "N", + label: "Play a A4", + onKeyDown: () => keys[21].setPressed(true), + onKeyUp: () => keys[21].setPressed(false), + }, + { + combo: "J", + label: "Play a A#4", + onKeyDown: () => keys[22].setPressed(true), + onKeyUp: () => keys[22].setPressed(false), + }, + { + combo: "M", + label: "Play a B4", + onKeyDown: () => keys[23].setPressed(true), + onKeyUp: () => keys[23].setPressed(false), }, ]); From 07b4dca71ead349f6e5fc7691dfb1c403a09431c Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Tue, 16 Feb 2021 17:03:59 +0000 Subject: [PATCH 03/10] [cherry pick me] remove 'new' tag from some docs pages --- packages/core/src/components/drawer/drawer.md | 4 ---- packages/core/src/components/panel-stack/panel-stack.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/packages/core/src/components/drawer/drawer.md b/packages/core/src/components/drawer/drawer.md index a3319bc1d3..cb2637b1ee 100644 --- a/packages/core/src/components/drawer/drawer.md +++ b/packages/core/src/components/drawer/drawer.md @@ -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. diff --git a/packages/core/src/components/panel-stack/panel-stack.md b/packages/core/src/components/panel-stack/panel-stack.md index 42ca6e5f68..3f71f55e53 100644 --- a/packages/core/src/components/panel-stack/panel-stack.md +++ b/packages/core/src/components/panel-stack/panel-stack.md @@ -1,7 +1,3 @@ ---- -tag: new ---- - @# Panel stack `PanelStack` manages a stack of panels and displays only the topmost panel. From 02b453903546a542009263868045070c9f994bfe Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Tue, 16 Feb 2021 17:15:14 +0000 Subject: [PATCH 04/10] migration docs --- .../src/components/hotkeys/hotkeys-target2.md | 15 +++++++++++++++ packages/core/src/hooks/useHotkeys.md | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/hotkeys/hotkeys-target2.md b/packages/core/src/components/hotkeys/hotkeys-target2.md index e67b32a183..c9b5f2a902 100644 --- a/packages/core/src/components/hotkeys/hotkeys-target2.md +++ b/packages/core/src/components/hotkeys/hotkeys-target2.md @@ -1,5 +1,20 @@ @# HotkeysTarget2 +
+

+ +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 be come the standard +APIs in Blueprint v4. See the full +[migration guide](https://github.com/palantir/blueprint/wiki/useHotkeys-migration) on the wiki. + +
+ + The `HotkeysTarget2` component is a utility component which allows you to use the new [`useHotkeys` hook](#core/hooks/useHotkeys) inside a React component class. It's useful if you want to switch to the new hotkeys API without refactoring your class components diff --git a/packages/core/src/hooks/useHotkeys.md b/packages/core/src/hooks/useHotkeys.md index 53bb9c5980..6744d6051e 100644 --- a/packages/core/src/hooks/useHotkeys.md +++ b/packages/core/src/hooks/useHotkeys.md @@ -4,9 +4,23 @@ tag: new @# useHotkeys +
+

+ +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 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. + +
+ The `useHotkeys` hook adds hotkey / keyboard shortcut interactions to your application using a custom React hook. Compared to the deprecated [Hotkeys](#core/components/hotkeys) API, it works with function components and allows -more customization of the explanatory hotkeys dialog. +more customization of the hotkeys dialog. @reactExample UseHotkeysExample From c702efb6501d862d6afb6e9cfa4a01dbce6b6aa7 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Wed, 17 Feb 2021 19:09:36 +0000 Subject: [PATCH 05/10] Add HotkeysProvider, HotkeysDialog2, fix examples --- packages/core/src/common/errors.ts | 4 + .../core/src/components/hotkeys/hotkey.tsx | 2 +- .../src/components/hotkeys/hotkeys-target2.md | 2 +- .../core/src/components/hotkeys/hotkeys.md | 5 +- .../src/components/hotkeys/hotkeysDialog2.tsx | 34 +- .../src/components/hotkeys/hotkeysTarget.tsx | 1 + .../src/components/hotkeys/hotkeysTarget2.tsx | 29 +- packages/core/src/context/hotkeysProvider.tsx | 92 +++++ packages/core/src/context/index.ts | 17 + packages/core/src/hooks/useHotkeys.md | 4 +- packages/core/src/hooks/useHotkeys.ts | 44 ++- packages/core/src/index.ts | 1 + .../docs-app/src/components/blueprintDocs.tsx | 26 +- .../docs-app/src/components/navHeader.tsx | 75 ++-- .../examples/core-examples/audio/pianoKey.tsx | 22 +- .../examples/core-examples/hotkeyPiano.tsx | 1 + .../core-examples/hotkeysTarget2Example.tsx | 24 ++ .../core-examples/useHotkeysExample.tsx | 334 ++++++++++-------- .../select-examples/omnibarExample.tsx | 80 ++--- .../src/components/documentation.tsx | 134 +++---- packages/table/src/cell/editableCell.tsx | 2 + packages/table/src/table.tsx | 2 + 22 files changed, 590 insertions(+), 345 deletions(-) create mode 100644 packages/core/src/context/hotkeysProvider.tsx create mode 100644 packages/core/src/context/index.ts diff --git a/packages/core/src/common/errors.ts b/packages/core/src/common/errors.ts index c3e2887456..088b6cca33 100644 --- a/packages/core/src/common/errors.ts +++ b/packages/core/src/common/errors.ts @@ -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 + + ` 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 + ` leftElement and leftIcon prop are mutually exclusive, with leftElement taking priority.`; diff --git a/packages/core/src/components/hotkeys/hotkey.tsx b/packages/core/src/components/hotkeys/hotkey.tsx index 24a436ef52..f257742f61 100644 --- a/packages/core/src/components/hotkeys/hotkey.tsx +++ b/packages/core/src/components/hotkeys/hotkey.tsx @@ -115,7 +115,7 @@ export class Hotkey extends AbstractPureComponent2 { protected validateProps(props: IHotkeyProps) { if (props.global !== true && props.group == null) { - throw new Error("non-global s must define a group"); + console.error("non-global s must define a group"); } } } diff --git a/packages/core/src/components/hotkeys/hotkeys-target2.md b/packages/core/src/components/hotkeys/hotkeys-target2.md index c9b5f2a902..223bc0bbb8 100644 --- a/packages/core/src/components/hotkeys/hotkeys-target2.md +++ b/packages/core/src/components/hotkeys/hotkeys-target2.md @@ -39,7 +39,7 @@ export default class extends React.PureComponent { public render() { return ( - + {({ handleKeyDown, handleKeyUp }) => (
Need help? diff --git a/packages/core/src/components/hotkeys/hotkeys.md b/packages/core/src/components/hotkeys/hotkeys.md index bc6ea91a18..1653727088 100644 --- a/packages/core/src/components/hotkeys/hotkeys.md +++ b/packages/core/src/components/hotkeys/hotkeys.md @@ -8,8 +8,9 @@ Deprecated: use [useHotkeys](#core/hooks/useHotkeys) 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.
diff --git a/packages/core/src/components/hotkeys/hotkeysDialog2.tsx b/packages/core/src/components/hotkeys/hotkeysDialog2.tsx index 693f5bfad2..4ff34d0d59 100644 --- a/packages/core/src/components/hotkeys/hotkeysDialog2.tsx +++ b/packages/core/src/components/hotkeys/hotkeysDialog2.tsx @@ -14,6 +14,38 @@ * limitations under the License. */ +import classNames from "classnames"; import React from "react"; -export const HotkeysDialog2: React.FC = () =>
; +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 = ({ globalGroupName = "Global", hotkeys, ...props }) => { + return ( + +
+ + {hotkeys.map((hotkey, index) => ( + + ))} + +
+
+ ); +}; diff --git a/packages/core/src/components/hotkeys/hotkeysTarget.tsx b/packages/core/src/components/hotkeys/hotkeysTarget.tsx index 106b87c337..ce856e870f 100644 --- a/packages/core/src/components/hotkeys/hotkeysTarget.tsx +++ b/packages/core/src/components/hotkeys/hotkeysTarget.tsx @@ -33,6 +33,7 @@ export interface IHotkeysTargetComponent extends React.Component { renderHotkeys: () => React.ReactElement; } +/** @deprecated use `useHotkeys` hook or `` component */ export function HotkeysTarget>(WrappedComponent: T) { if (!isFunction(WrappedComponent.prototype.renderHotkeys)) { console.warn(HOTKEYS_WARN_DECORATOR_NO_METHOD); diff --git a/packages/core/src/components/hotkeys/hotkeysTarget2.tsx b/packages/core/src/components/hotkeys/hotkeysTarget2.tsx index 2793991052..7d811439b0 100644 --- a/packages/core/src/components/hotkeys/hotkeysTarget2.tsx +++ b/packages/core/src/components/hotkeys/hotkeysTarget2.tsx @@ -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"; @@ -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[]; @@ -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 = ({ 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; + } }; diff --git a/packages/core/src/context/hotkeysProvider.tsx b/packages/core/src/context/hotkeysProvider.tsx new file mode 100644 index 0000000000..272dc7be1f --- /dev/null +++ b/packages/core/src/context/hotkeysProvider.tsx @@ -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]>([ + 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>; + + /** 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 }) ?? ( + + ); + + return ( + + {children} + {dialog} + + ); +}; diff --git a/packages/core/src/context/index.ts b/packages/core/src/context/index.ts new file mode 100644 index 0000000000..ec600e80f8 --- /dev/null +++ b/packages/core/src/context/index.ts @@ -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"; diff --git a/packages/core/src/hooks/useHotkeys.md b/packages/core/src/hooks/useHotkeys.md index 6744d6051e..d931a06d6e 100644 --- a/packages/core/src/hooks/useHotkeys.md +++ b/packages/core/src/hooks/useHotkeys.md @@ -12,8 +12,8 @@ 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 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.
diff --git a/packages/core/src/hooks/useHotkeys.ts b/packages/core/src/hooks/useHotkeys.ts index 9fb2f74409..a0e2439bf8 100644 --- a/packages/core/src/hooks/useHotkeys.ts +++ b/packages/core/src/hooks/useHotkeys.ts @@ -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 @@ -32,7 +38,7 @@ export function useHotkeys(keys: IHotkeyProps[]) { combo: parseKeyCombo(k.combo), props: k, })), - keys, + [keys], ); const globalKeys = useMemo( () => @@ -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, @@ -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( @@ -94,9 +117,6 @@ export function useHotkeys(keys: IHotkeyProps[]) { return () => { document.removeEventListener("keydown", handleGlobalKeyDown); document.removeEventListener("keyup", handleGlobalKeyUp); - - globalHotkeysEvents.clear(); - localHotkeysEvents.clear(); }; }, []); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 176cbe9632..7da0baedff 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,4 +17,5 @@ export * from "./accessibility"; export * from "./common"; export * from "./components"; +export * from "./context"; export * from "./hooks"; diff --git a/packages/docs-app/src/components/blueprintDocs.tsx b/packages/docs-app/src/components/blueprintDocs.tsx index 5498c0ea69..862def970c 100644 --- a/packages/docs-app/src/components/blueprintDocs.tsx +++ b/packages/docs-app/src/components/blueprintDocs.tsx @@ -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"; @@ -80,17 +80,19 @@ export class BlueprintDocs extends React.Component ); return ( - + + + ); } diff --git a/packages/docs-app/src/components/navHeader.tsx b/packages/docs-app/src/components/navHeader.tsx index 7cadaaaea9..ebccca795e 100644 --- a/packages/docs-app/src/components/navHeader.tsx +++ b/packages/docs-app/src/components/navHeader.tsx @@ -17,7 +17,7 @@ import { INpmPackage } from "@documentalist/client"; import * as React from "react"; -import { Classes, Hotkey, Hotkeys, HotkeysTarget, Menu, MenuItem, NavbarHeading, Tag } from "@blueprintjs/core"; +import { Classes, HotkeysTarget2, Menu, MenuItem, NavbarHeading, Tag } from "@blueprintjs/core"; import { NavButton } from "@blueprintjs/docs-theme"; import { Popover2 } from "@blueprintjs/popover2"; @@ -30,46 +30,47 @@ export interface INavHeaderProps { packageData: INpmPackage; } -@HotkeysTarget export class NavHeader extends React.PureComponent { public render() { const { useDarkTheme } = this.props; return ( - <> -
- - - -
- - Blueprint {this.renderVersionsMenu()} - - - View on GitHub + + <> +
+ + +
+ + Blueprint {this.renderVersionsMenu()} + + + View on GitHub + +
-
-
- - - ); - } - - public renderHotkeys() { - return ( - - - +
+ + + ); } @@ -95,9 +96,7 @@ export class NavHeader extends React.PureComponent { ); } - private handleDarkSwitchChange = () => { - this.props.onToggleDark(!this.props.useDarkTheme); - }; + private handleDarkSwitchChange = () => this.props.onToggleDark(!this.props.useDarkTheme); } /** Get major component of semver string. */ diff --git a/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx b/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx index 6f8d2269d6..d6c88e9522 100644 --- a/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx +++ b/packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx @@ -15,7 +15,7 @@ */ import classNames from "classnames"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Classes } from "@blueprintjs/core"; @@ -31,16 +31,20 @@ interface IPianoKeyProps { } export const PianoKey: React.FC = ({ context, hotkey, note, pressed }) => { - let oscillator: Oscillator; - let envelope: Envelope; + const [envelope, setEnvelope] = useState(); - if (context !== undefined) { - oscillator = new Oscillator(context, Scale[note]); - envelope = new Envelope(context); - oscillator.oscillator.connect(envelope.gain); - envelope.gain.connect(context.destination); - } + // only create oscillator and envelop once on mount + useEffect(() => { + if (context !== undefined) { + const oscillator = new Oscillator(context, Scale[note]); + const newEnvelope = new Envelope(context); + oscillator.oscillator.connect(newEnvelope.gain); + newEnvelope.gain.connect(context.destination); + setEnvelope(newEnvelope); + } + }, [context]); + // start/stop envelope when this key is pressed down/up useEffect(() => { if (pressed) { envelope?.on(); diff --git a/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx b/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx index 3fc61693e7..3c882ac719 100644 --- a/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx +++ b/packages/docs-app/src/examples/core-examples/hotkeyPiano.tsx @@ -28,6 +28,7 @@ export interface IHotkeyPianoState { // eslint-disable-next-line @typescript-eslint/dot-notation const AUDIO_CONTEXT = (window as any)["AudioContext"] != null ? new AudioContext() : null; +// eslint-disable-next-line deprecation/deprecation @HotkeysTarget export class HotkeyPiano extends React.PureComponent { public state: IHotkeyPianoState = { diff --git a/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx b/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx index 1b024cf974..bf42274d7b 100644 --- a/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx +++ b/packages/docs-app/src/examples/core-examples/hotkeysTarget2Example.tsx @@ -66,144 +66,168 @@ export class HotkeysTarget2Example extends React.PureComponent this.setKey(0, true), onKeyUp: () => this.setKey(0, false), }, { combo: "2", + group: "HotkeysTarget2 Example", label: "Play a C#5", onKeyDown: () => this.setKey(1, true), onKeyUp: () => this.setKey(1, false), }, { combo: "W", + group: "HotkeysTarget2 Example", label: "Play a D5", onKeyDown: () => this.setKey(2, true), onKeyUp: () => this.setKey(2, false), }, { combo: "3", + group: "HotkeysTarget2 Example", label: "Play a D#5", onKeyDown: () => this.setKey(3, true), onKeyUp: () => this.setKey(3, false), }, { combo: "E", + group: "HotkeysTarget2 Example", label: "Play a E5", onKeyDown: () => this.setKey(4, true), onKeyUp: () => this.setKey(4, false), }, { combo: "R", + group: "HotkeysTarget2 Example", label: "Play a F5", onKeyDown: () => this.setKey(5, true), onKeyUp: () => this.setKey(5, false), }, { combo: "5", + group: "HotkeysTarget2 Example", label: "Play a F#5", onKeyDown: () => this.setKey(6, true), onKeyUp: () => this.setKey(6, false), }, { combo: "T", + group: "HotkeysTarget2 Example", label: "Play a G5", onKeyDown: () => this.setKey(7, true), onKeyUp: () => this.setKey(7, false), }, { combo: "6", + group: "HotkeysTarget2 Example", label: "Play a G#5", onKeyDown: () => this.setKey(8, true), onKeyUp: () => this.setKey(8, false), }, { combo: "Y", + group: "HotkeysTarget2 Example", label: "Play a A5", onKeyDown: () => this.setKey(9, true), onKeyUp: () => this.setKey(9, false), }, { combo: "7", + group: "HotkeysTarget2 Example", label: "Play a A#5", onKeyDown: () => this.setKey(10, true), onKeyUp: () => this.setKey(10, false), }, { combo: "U", + group: "HotkeysTarget2 Example", label: "Play a B5", onKeyDown: () => this.setKey(11, true), onKeyUp: () => this.setKey(11, false), }, { combo: "Z", + group: "HotkeysTarget2 Example", label: "Play a C4", onKeyDown: () => this.setKey(12, true), onKeyUp: () => this.setKey(12, false), }, { combo: "S", + group: "HotkeysTarget2 Example", label: "Play a C#4", onKeyDown: () => this.setKey(13, true), onKeyUp: () => this.setKey(13, false), }, { combo: "X", + group: "HotkeysTarget2 Example", label: "Play a D4", onKeyDown: () => this.setKey(14, true), onKeyUp: () => this.setKey(14, false), }, { combo: "D", + group: "HotkeysTarget2 Example", label: "Play a D#4", onKeyDown: () => this.setKey(15, true), onKeyUp: () => this.setKey(15, false), }, { combo: "C", + group: "HotkeysTarget2 Example", label: "Play a E4", onKeyDown: () => this.setKey(16, true), onKeyUp: () => this.setKey(16, false), }, { combo: "V", + group: "HotkeysTarget2 Example", label: "Play a F4", onKeyDown: () => this.setKey(17, true), onKeyUp: () => this.setKey(17, false), }, { combo: "G", + group: "HotkeysTarget2 Example", label: "Play a F#4", onKeyDown: () => this.setKey(18, true), onKeyUp: () => this.setKey(18, false), }, { combo: "B", + group: "HotkeysTarget2 Example", label: "Play a G4", onKeyDown: () => this.setKey(19, true), onKeyUp: () => this.setKey(19, false), }, { combo: "H", + group: "HotkeysTarget2 Example", label: "Play a G#4", onKeyDown: () => this.setKey(20, true), onKeyUp: () => this.setKey(20, false), }, { combo: "N", + group: "HotkeysTarget2 Example", label: "Play a A4", onKeyDown: () => this.setKey(21, true), onKeyUp: () => this.setKey(21, false), }, { combo: "J", + group: "HotkeysTarget2 Example", label: "Play a A#4", onKeyDown: () => this.setKey(22, true), onKeyUp: () => this.setKey(22, false), }, { combo: "M", + group: "HotkeysTarget2 Example", label: "Play a B4", onKeyDown: () => this.setKey(23, true), onKeyUp: () => this.setKey(23, false), diff --git a/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx b/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx index 78277c557c..78ef3d80b5 100644 --- a/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx +++ b/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from "react"; +import React, { useMemo } from "react"; import { useHotkeys } from "@blueprintjs/core"; import { Example, IExampleProps } from "@blueprintjs/docs-theme"; @@ -40,158 +40,186 @@ export const UseHotkeysExample: React.FC = props => { setPressed, })); - const { handleKeyDown, handleKeyUp } = useHotkeys([ - { - combo: "shift + P", - global: true, - label: "Focus the piano", - onKeyDown: focusPiano, - }, - { - combo: "Q", - label: "Play a C5", - onKeyDown: () => keys[0].setPressed(true), - onKeyUp: () => keys[0].setPressed(false), - }, - { - combo: "2", - label: "Play a C#5", - onKeyDown: () => keys[1].setPressed(true), - onKeyUp: () => keys[1].setPressed(false), - }, - { - combo: "W", - label: "Play a D5", - onKeyDown: () => keys[2].setPressed(true), - onKeyUp: () => keys[2].setPressed(false), - }, - { - combo: "3", - label: "Play a D#5", - onKeyDown: () => keys[3].setPressed(true), - onKeyUp: () => keys[3].setPressed(false), - }, - { - combo: "E", - label: "Play a E5", - onKeyDown: () => keys[4].setPressed(true), - onKeyUp: () => keys[4].setPressed(false), - }, - { - combo: "R", - label: "Play a F5", - onKeyDown: () => keys[5].setPressed(true), - onKeyUp: () => keys[5].setPressed(false), - }, - { - combo: "5", - label: "Play a F#5", - onKeyDown: () => keys[6].setPressed(true), - onKeyUp: () => keys[6].setPressed(false), - }, - { - combo: "T", - label: "Play a G5", - onKeyDown: () => keys[7].setPressed(true), - onKeyUp: () => keys[7].setPressed(false), - }, - { - combo: "6", - label: "Play a G#5", - onKeyDown: () => keys[8].setPressed(true), - onKeyUp: () => keys[8].setPressed(false), - }, - { - combo: "Y", - label: "Play a A5", - onKeyDown: () => keys[9].setPressed(true), - onKeyUp: () => keys[9].setPressed(false), - }, - { - combo: "7", - label: "Play a A#5", - onKeyDown: () => keys[10].setPressed(true), - onKeyUp: () => keys[10].setPressed(false), - }, - { - combo: "U", - label: "Play a B5", - onKeyDown: () => keys[11].setPressed(true), - onKeyUp: () => keys[11].setPressed(false), - }, - { - combo: "Z", - label: "Play a C4", - onKeyDown: () => keys[12].setPressed(true), - onKeyUp: () => keys[12].setPressed(false), - }, - { - combo: "S", - label: "Play a C#4", - onKeyDown: () => keys[13].setPressed(true), - onKeyUp: () => keys[13].setPressed(false), - }, - { - combo: "X", - label: "Play a D4", - onKeyDown: () => keys[14].setPressed(true), - onKeyUp: () => keys[14].setPressed(false), - }, - { - combo: "D", - label: "Play a D#4", - onKeyDown: () => keys[15].setPressed(true), - onKeyUp: () => keys[15].setPressed(false), - }, - { - combo: "C", - label: "Play a E4", - onKeyDown: () => keys[16].setPressed(true), - onKeyUp: () => keys[16].setPressed(false), - }, - { - combo: "V", - label: "Play a F4", - onKeyDown: () => keys[17].setPressed(true), - onKeyUp: () => keys[17].setPressed(false), - }, - { - combo: "G", - label: "Play a F#4", - onKeyDown: () => keys[18].setPressed(true), - onKeyUp: () => keys[18].setPressed(false), - }, - { - combo: "B", - label: "Play a G4", - onKeyDown: () => keys[19].setPressed(true), - onKeyUp: () => keys[19].setPressed(false), - }, - { - combo: "H", - label: "Play a G#4", - onKeyDown: () => keys[20].setPressed(true), - onKeyUp: () => keys[20].setPressed(false), - }, - { - combo: "N", - label: "Play a A4", - onKeyDown: () => keys[21].setPressed(true), - onKeyUp: () => keys[21].setPressed(false), - }, - { - combo: "J", - label: "Play a A#4", - onKeyDown: () => keys[22].setPressed(true), - onKeyUp: () => keys[22].setPressed(false), - }, - { - combo: "M", - label: "Play a B4", - onKeyDown: () => keys[23].setPressed(true), - onKeyUp: () => keys[23].setPressed(false), - }, - ]); + const hotkeys = useMemo( + () => [ + { + combo: "shift + P", + global: true, + label: "Focus the piano", + onKeyDown: focusPiano, + }, + { + combo: "Q", + group: "useHotkeys Example", + label: "Play a C5", + onKeyDown: () => keys[0].setPressed(true), + onKeyUp: () => keys[0].setPressed(false), + }, + { + combo: "2", + group: "useHotkeys Example", + label: "Play a C#5", + onKeyDown: () => keys[1].setPressed(true), + onKeyUp: () => keys[1].setPressed(false), + }, + { + combo: "W", + group: "useHotkeys Example", + label: "Play a D5", + onKeyDown: () => keys[2].setPressed(true), + onKeyUp: () => keys[2].setPressed(false), + }, + { + combo: "3", + group: "useHotkeys Example", + label: "Play a D#5", + onKeyDown: () => keys[3].setPressed(true), + onKeyUp: () => keys[3].setPressed(false), + }, + { + combo: "E", + group: "useHotkeys Example", + label: "Play a E5", + onKeyDown: () => keys[4].setPressed(true), + onKeyUp: () => keys[4].setPressed(false), + }, + { + combo: "R", + group: "useHotkeys Example", + label: "Play a F5", + onKeyDown: () => keys[5].setPressed(true), + onKeyUp: () => keys[5].setPressed(false), + }, + { + combo: "5", + group: "useHotkeys Example", + label: "Play a F#5", + onKeyDown: () => keys[6].setPressed(true), + onKeyUp: () => keys[6].setPressed(false), + }, + { + combo: "T", + group: "useHotkeys Example", + label: "Play a G5", + onKeyDown: () => keys[7].setPressed(true), + onKeyUp: () => keys[7].setPressed(false), + }, + { + combo: "6", + group: "useHotkeys Example", + label: "Play a G#5", + onKeyDown: () => keys[8].setPressed(true), + onKeyUp: () => keys[8].setPressed(false), + }, + { + combo: "Y", + group: "useHotkeys Example", + label: "Play a A5", + onKeyDown: () => keys[9].setPressed(true), + onKeyUp: () => keys[9].setPressed(false), + }, + { + combo: "7", + group: "useHotkeys Example", + label: "Play a A#5", + onKeyDown: () => keys[10].setPressed(true), + onKeyUp: () => keys[10].setPressed(false), + }, + { + combo: "U", + group: "useHotkeys Example", + label: "Play a B5", + onKeyDown: () => keys[11].setPressed(true), + onKeyUp: () => keys[11].setPressed(false), + }, + { + combo: "Z", + group: "useHotkeys Example", + label: "Play a C4", + onKeyDown: () => keys[12].setPressed(true), + onKeyUp: () => keys[12].setPressed(false), + }, + { + combo: "S", + group: "useHotkeys Example", + label: "Play a C#4", + onKeyDown: () => keys[13].setPressed(true), + onKeyUp: () => keys[13].setPressed(false), + }, + { + combo: "X", + group: "useHotkeys Example", + label: "Play a D4", + onKeyDown: () => keys[14].setPressed(true), + onKeyUp: () => keys[14].setPressed(false), + }, + { + combo: "D", + group: "useHotkeys Example", + label: "Play a D#4", + onKeyDown: () => keys[15].setPressed(true), + onKeyUp: () => keys[15].setPressed(false), + }, + { + combo: "C", + group: "useHotkeys Example", + label: "Play a E4", + onKeyDown: () => keys[16].setPressed(true), + onKeyUp: () => keys[16].setPressed(false), + }, + { + combo: "V", + group: "useHotkeys Example", + label: "Play a F4", + onKeyDown: () => keys[17].setPressed(true), + onKeyUp: () => keys[17].setPressed(false), + }, + { + combo: "G", + group: "useHotkeys Example", + label: "Play a F#4", + onKeyDown: () => keys[18].setPressed(true), + onKeyUp: () => keys[18].setPressed(false), + }, + { + combo: "B", + group: "useHotkeys Example", + label: "Play a G4", + onKeyDown: () => keys[19].setPressed(true), + onKeyUp: () => keys[19].setPressed(false), + }, + { + combo: "H", + group: "useHotkeys Example", + label: "Play a G#4", + onKeyDown: () => keys[20].setPressed(true), + onKeyUp: () => keys[20].setPressed(false), + }, + { + combo: "N", + group: "useHotkeys Example", + label: "Play a A4", + onKeyDown: () => keys[21].setPressed(true), + onKeyUp: () => keys[21].setPressed(false), + }, + { + combo: "J", + group: "useHotkeys Example", + label: "Play a A#4", + onKeyDown: () => keys[22].setPressed(true), + onKeyUp: () => keys[22].setPressed(false), + }, + { + combo: "M", + group: "useHotkeys Example", + label: "Play a B4", + onKeyDown: () => keys[23].setPressed(true), + onKeyUp: () => keys[23].setPressed(false), + }, + ], + [], + ); + const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys); return ( diff --git a/packages/docs-app/src/examples/select-examples/omnibarExample.tsx b/packages/docs-app/src/examples/select-examples/omnibarExample.tsx index 42a410249c..4749d86a22 100644 --- a/packages/docs-app/src/examples/select-examples/omnibarExample.tsx +++ b/packages/docs-app/src/examples/select-examples/omnibarExample.tsx @@ -16,18 +16,7 @@ import * as React from "react"; -import { - Button, - H5, - Hotkey, - Hotkeys, - HotkeysTarget, - KeyCombo, - MenuItem, - Position, - Switch, - Toaster, -} from "@blueprintjs/core"; +import { Button, H5, HotkeysTarget2, KeyCombo, MenuItem, Position, Switch, Toaster } from "@blueprintjs/core"; import { Example, handleBooleanChange, IExampleProps } from "@blueprintjs/docs-theme"; import { Omnibar } from "@blueprintjs/select"; @@ -41,7 +30,6 @@ export interface IOmnibarExampleState { resetOnSelect: boolean; } -@HotkeysTarget export class OmnibarExample extends React.PureComponent { public state: IOmnibarExampleState = { allowCreate: false, @@ -59,21 +47,6 @@ export class OmnibarExample extends React.PureComponent (this.toaster = ref), }; - public renderHotkeys() { - return ( - - - - ); - } - public render() { const { allowCreate } = this.state; @@ -81,25 +54,38 @@ export class OmnibarExample extends React.PureComponent - -