Deprecated: Function get_magic_quotes_gpc() is deprecated in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 99

Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 619

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1169

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176
10000 Updating tiny keys, adding shortcut tests by npfitz · Pull Request #375 · timc1/kbar · GitHub
Nothing Special   »   [go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/InternalEvents.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { ActionImpl } from "./action";
import tinykeys from "./tinykeys";
import { tinykeys } from "./tinykeys";
import { VisualState } from "./types";
import { useKBar } from "./useKBar";
import { getScrollbarWidth, shouldRejectKeystrokes } from "./utils";
Expand Down
56 changes: 56 additions & 0 deletions src/__tests__/useMatches.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import { useKBar } from "../useKBar";
import { KBarProvider } from "../KBarContextProvider";
import { render, fireEvent, RenderResult } from "@testing-library/react";
import UserEvent from "@testing-library/user-event";
import * as React from "react";
import { createAction, Priority } from "../utils";
import { useMatches } from "../useMatches";
import { Action } from "..";

jest.mock("../utils", () => {
return {
Expand Down Expand Up @@ -108,6 +110,15 @@ function WithLongNamesComponent() {
);
}

function WithShortcutsComoonent({ actions }: { actions: Action[] }) {
return (
<KBarProvider actions={actions}>
<Search />
<Results />
</KBarProvider>
);
}

const setup = (Component: React.ComponentType) => {
const utils = render(<Component />);
const input = utils.getByLabelText("search-input");
Expand Down Expand Up @@ -181,4 +192,49 @@ describe("useMatches", () => {
);
});
});

describe("Shortcuts", () => {
it("triggers a shortcut when the apropriate keys are pressed", async () => {
const action1Fn = jest.fn();
const action2Fn = jest.fn();
const action3Fn = jest.fn();

render(
<KBarProvider
actions={[
createAction({
name: "foo",
shortcut: ["t"],
perform: action1Fn,
}),
createAction({
name: "bar",
shortcut: ["Control+o"],
perform: action2Fn,
}),
createAction({
name: "metaFoo",
shortcut: ["Control+]"],
perform: action3Fn,
}),
]}
/>
);

await UserEvent.keyboard("t");
expect(action1Fn).toHaveBeenCalledTimes(1);
await UserEvent.keyboard("t");
expect(action1Fn).toHaveBeenCalledTimes(2);

const keyboardState = await UserEvent.keyboard("{Control>}");
await UserEvent.keyboard("o", { keyboardState });
expect(action2Fn).toHaveBeenCalled();

await UserEvent.keyboard("]");
expect(action3Fn).not.toHaveBeenCalled();

await UserEvent.keyboard("]", { keyboardState });
expect(action3Fn).toHaveBeenCalled();
});
});
});
149 changes: 100 additions & 49 deletions src/tinykeys.ts
8000
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Fixes special character issues; `?` -> `shift+/` + build issue
// https://github.com/jamiebuilds/tinykeys

type KeyBindingPress = [string[], string];
/**
* A single press of a keybinding sequence
*/
export type KeyBindingPress = [mods: string[], key: string | RegExp];

/**
* A map of keybinding strings to event handlers.
Expand All @@ -10,23 +10,30 @@ export interface KeyBindingMap {
[keybinding: string]: (event: KeyboardEvent) => void;
}

export interface KeyBindingHandlerOptions {
/**
* Keybinding sequences will wait this long between key presses before
* cancelling (default: 1000).
*
* **Note:** Setting this value too low (i.e. `300`) will be too fast for many
* of your users.
*/
timeout?: number;
}

/**
* Options to configure the behavior of keybindings.
*/
export interface KeyBindingOptions {
export inte 10000 rface KeyBindingOptions extends KeyBindingHandlerOptions {
/**
* Key presses will listen to this event (default: "keydown").
*/
event?: "keydown" | "keyup";

/**
* Keybinding sequences will wait this long between key presses before
* cancelling (default: 1000).
*
* **Note:** Setting this value too low (i.e. `300`) will be too fast for many
* of your users.
* Key presses will use a capture listener (default: false)
*/
timeout?: number;
capture?: boolean;
}

/**
Expand All @@ -45,24 +52,39 @@ let DEFAULT_TIMEOUT = 1000;
/**
* Keybinding sequences should bind to this event by default.
*/
let DEFAULT_EVENT = "keydown";
let DEFAULT_EVENT = "keydown" as const;

/**
* Platform detection code.
* @see https://github.com/jamiebuilds/tinykeys/issues/184
*/
let PLATFORM = typeof navigator === "object" ? navigator.platform : "";
let APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM);

/**
* An alias for creating platform-specific keybinding aliases.
*/
let MOD =
typeof navigator === "object" &&
/Mac|iPod|iPhone|iPad/.test(navigator.platform)
? "Meta"
: "Control";
let MOD = APPLE_DEVICE ? "Meta" : "Control";

/**
* Meaning of `AltGraph`, from MDN:
* - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed
* - Mac: ⌥ Option key pressed
* - Linux: Level 3 Shift key (or Level 5 Shift key) pressed
* - Android: Not supported
* @see https://github.com/jamiebuilds/tinykeys/issues/185
*/
let ALT_GRAPH_ALIASES =
PLATFORM === "Win32" ? ["Control", "Alt"] : APPLE_DEVICE ? ["Alt"] : [];

/**
* There's a bug in Chrome that causes event.getModifierState not to exist on
* KeyboardEvent's for F1/F2/etc keys.
*/
function getModifierState(event: KeyboardEvent, mod: string) {
return typeof event.getModifierState === "function"
? event.getModifierState(mod)
? event.getModifierState(mod) ||
(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph"))
: false;
}

Expand All @@ -73,63 +95,65 @@ function getModifierState(event: KeyboardEvent, mod: string) {
* <sequence> = `<press> <press> <press> ...`
* <press> = `<key>` or `<mods>+<key>`
* <mods> = `<mod>+<mod>+...`
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
* <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)
*/
function parse(str: string): KeyBindingPress[] {
export function parseKeybinding(str: string): KeyBindingPress[] {
return str
.trim()
.split(" ")
.map((press) => {
let mods = press.split(/\b\+/);
let key = mods.pop() as string;
let key: string | RegExp = mods.pop() as string;
let match = key.match(/^\((.+)\)$/);
if (match) {
key = new RegExp(`^${match[1]}$`);
}
mods = mods.map((mod) => (mod === "$mod" ? MOD : mod));
return [mods, key];
});
}

/**
* This tells us if a series of events matches a key binding sequence either
* partially or exactly.
* This tells us if a single keyboard event matches a single keybinding press.
*/
function match(event: KeyboardEvent, press: KeyBindingPress): boolean {
// Special characters; `?` `!`
if (/^[^A-Za-z0-9]$/.test(event.key) && press[1] === event.key) {
return true;
}

export function matchKeyBindingPress(
event: KeyboardEvent,
[mods, key]: KeyBindingPress
): boolean {
// prettier-ignore
return !(
// Allow either the `event.key` or the `event.code`
// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
(
press[1].toUpperCase() !== event.key.toUpperCase() &&
press[1] !== event.code
key instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :
(key.toUpperCase() !== event.key.toUpperCase() &&
key !== event.code)
) ||

// Ensure all the modifiers in the keybinding are pressed.
press[0].find(mod => {
mods.find(mod => {
return !getModifierState(event, mod)
}) ||

// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a
// keybinding. So if they are pressed but aren't part of the current
// keybinding press, then we don't have a match.
KEYBINDING_MODIFIER_KEYS.find(mod => {
return !press[0].includes(mod) && press[1] !== mod && getModifierState(event, mod)
return !mods.includes(mod) && key !== mod && getModifierState(event, mod)
})
)
}

/**
* Subscribes to keybindings.
*
* Returns an unsubscribe method.
* Creates an event listener for handling keybindings.
*
* @example
* ```js
* import keybindings from "../src/keybindings"
* import { createKeybindingsHandler } from "../src/keybindings"
*
* keybindings(window, {
* let handler = createKeybindingsHandler({
* "Shift+d": () => {
* alert("The 'Shift' and 'd' keys were pressed at the same time")
* },
Expand All @@ -140,24 +164,24 @@ function match(event: KeyboardEvent, press: KeyBindingPress): boolean {
* alert("Either 'Control+d' or 'Meta+d' were pressed")
* },
* })
*
* window.addEvenListener("keydown", handler)
* ```
*/
export default function keybindings(
target: Window | HTMLElement,
export function createKeybindingsHandler(
keyBindingMap: KeyBindingMap,
options: KeyBindingOptions = {}
): () => void {
options: KeyBindingHandlerOptions = {}
): EventListener {
let timeout = options.timeout ?? DEFAULT_TIMEOUT;
let event = options.event ?? DEFAULT_EVENT;

let keyBindings = Object.keys(keyBindingMap).map((key) => {
return [parse(key), keyBindingMap[key]] as const;
return [parseKeybinding(key), keyBindingMap[key]] as const;
});

let possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>();
let timer: number | null = null;

let onKeyEvent: EventListener = (event) => {
return (event) => {
// Ensure and stop any event that isn't a full keyboard event.
// Autocomplete option navigation and selection would fire a instanceof Event,
// instead of the expected KeyboardEvent
Expand All @@ -173,7 +197,7 @@ export default function keybindings(
let remainingExpectedPresses = prev ? prev : sequence;
let currentExpectedPress = remainingExpectedPresses[0];

let matches = match(event, currentExpectedPress);
let matches = matchKeyBindingPress(event, currentExpectedPress);

if (!matches) {
// Modifier keydown events shouldn't break sequences
Expand All @@ -196,13 +220,40 @@ export default function keybindings(
clearTimeout(timer);
}

// @ts-ignore
timer = setTimeout(possibleMatches.clear.bind(possibleMatches), timeout);
};
}

target.addEventListener(event, onKeyEvent);

/**
* Subscribes to keybindings.
*
* Returns an unsubscribe method.
*
* @example
* ```js
* import { tinykeys } from "../src/tinykeys"
*
* tinykeys(window, {
* "Shift+d": () => {
* alert("The 'Shift' and 'd' keys were pressed at the same time")
* },
* "y e e t": () => {
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
* },
* "$mod+d": () => {
* alert("Either 'Control+d' or 'Meta+d' were pressed")
* },
* })
* ```
*/
export function tinykeys(
target: Window | HTMLElement,
keyBindingMap: KeyBindingMap,
{ event = DEFAULT_EVENT, capture, timeout }: KeyBindingOptions = {}
): () => void {
let { timeout });
target.addEventListener(event, onKeyEvent, capture);
return () => {
target.removeEventListener(event, onKeyEvent);
target.removeEventListener(event, onKeyEvent, capture);
};
}
0