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
Changes from 1 commit
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
Next Next commit
WIP useHotkeys
adidahiya committed Feb 16, 2021
commit f98f481c9377376d07e906342cdb90c8177c0a84
19 changes: 19 additions & 0 deletions packages/core/src/components/hotkeys/hotkeys2.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => <div />;
17 changes: 17 additions & 0 deletions packages/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2021 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export { useHotkeys } from "./useHotkeys";
134 changes: 134 additions & 0 deletions packages/core/src/hooks/useHotkeys.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>) =>
invokeNamedCallbackIfComboRecognized(false, getKeyCombo(e.nativeEvent), "onKeyDown", e.nativeEvent),
[localKeys],
);
const handleLocalKeyUp = useCallback(
(e: React.KeyboardEvent<HTMLElement>) =>
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;
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -17,3 +17,4 @@
export * from "./accessibility";
export * from "./common";
export * from "./components";
export * from "./hooks";
55 changes: 55 additions & 0 deletions packages/docs-app/src/examples/core-examples/audio/envelope.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 20 additions & 0 deletions packages/docs-app/src/examples/core-examples/audio/index.ts
Original file line number Diff line number Diff line change
@@ -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";
26 changes: 26 additions & 0 deletions packages/docs-app/src/examples/core-examples/audio/oscillator.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
70 changes: 70 additions & 0 deletions packages/docs-app/src/examples/core-examples/audio/pianoKey.tsx
Original file line number Diff line number Diff line change
@@ -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<IPianoKeyProps> = ({ 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 (
<div className={classes}>
<div className={elevation}>
<div className="piano-key-text">
<span className="piano-key-note">{note}</span>
<br />
<kbd className="piano-key-hotkey">{hotkey}</kbd>
</div>
</div>
</div>
);
};
Loading
Oops, something went wrong.