🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@solid-primitives/keyboard

Package Overview
Dependencies
Maintainers
3
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@solid-primitives/keyboard - npm Package Compare versions

Comparing version
1.3.5
to
2.0.0-next.0
+30
-7
dist/index.d.ts

@@ -0,1 +1,2 @@

import { type MaybeAccessor } from "@solid-primitives/utils";
import { type Accessor } from "solid-js";

@@ -26,4 +27,4 @@ export type ModifierKey = "Alt" | "Control" | "Meta" | "Shift";

* if (e) {
* console.log(e.key) // => "Q" | "ALT" | ... or null
* e.preventDefault(); // prevent default behavior or last keydown event
* console.log(e.key) // => "Q" | "ALT" | ...
* e.preventDefault();
* }

@@ -106,4 +107,4 @@ * })

* @param key The key to check for.
* @options The options for the key hold.
* - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
* @param options The options for the key hold.
* - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
* @returns

@@ -126,3 +127,3 @@ * ```ts

/**
* Creates a keyboard shotcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
* Creates a keyboard shortcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
*

@@ -133,4 +134,4 @@ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createShortcut

* @param callback The callback to call when the keys are pressed.
* @options The options for the shortcut.
* - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
* @param options The options for the shortcut.
* - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
* - `requireReset` — If `true`, the shortcut will only be triggered once until all of the keys stop being pressed. Disabled by default.

@@ -149,1 +150,23 @@ *

}): void;
export interface CreateKeyDownOptions {
/** Whether the listener should be inactive. */
disabled?: MaybeAccessor<boolean | undefined>;
/** The document to attach the listener to. Defaults to `window.document`. Useful for iframes. */
ownerDocument?: Accessor<Document | undefined>;
}
/**
* Listens for a keydown event for a specific key on a document.
* Supports a custom `ownerDocument` for use in iframes and portals.
*
* @param key The key to listen for (matched against `event.key`).
* @param callback Handler called when the key is pressed.
* @param options `disabled` and `ownerDocument` accessors.
*
* @example
* ```ts
* createKeyDown("Escape", e => close(), {
* ownerDocument: () => iframeEl.contentDocument ?? document,
* });
* ```
*/
export declare function createKeyDown(key: KbdKey, callback: (event: KeyboardEvent) => void, options?: CreateKeyDownOptions): void;
+145
-70
import { makeEventListener } from "@solid-primitives/event-listener";
import { createSingletonRoot } from "@solid-primitives/rootless";
import { arrayEquals } from "@solid-primitives/utils";
import { createEffect, createMemo, createSignal, on, untrack } from "solid-js";
import { isServer } from "solid-js/web";
import { arrayEquals, INTERNAL_OPTIONS, access } from "@solid-primitives/utils";
import { createEffect, createMemo, createSignal, untrack } from "solid-js";
import { isServer } from "@solidjs/web";
function equalsKeyHoldSequence(sequence, model) {

@@ -36,4 +36,4 @@ for (let i = sequence.length - 1; i >= 0; i--) {

* if (e) {
* console.log(e.key) // => "Q" | "ALT" | ... or null
* e.preventDefault(); // prevent default behavior or last keydown event
* console.log(e.key) // => "Q" | "ALT" | ...
* e.preventDefault();
* }

@@ -47,3 +47,3 @@ * })

}
const [event, setEvent] = createSignal(null);
const [event, setEvent] = createSignal(null, INTERNAL_OPTIONS);
makeEventListener(window, "keydown", e => {

@@ -78,14 +78,6 @@ setEvent(e);

if (isServer) {
const keys = () => [];
// this is for backwards compatibility
// TODO remove in the next major version
keys[0] = keys;
keys[1] = { event: () => null };
keys[Symbol.iterator] = function* () {
yield keys[0];
yield keys[1];
};
return keys;
return () => [];
}
const [pressedKeys, setPressedKeys] = createSignal([]), reset = () => setPressedKeys([]), event = useKeyDownEvent();
const [pressedKeys, setPressedKeys] = createSignal([], INTERNAL_OPTIONS);
const reset = () => setPressedKeys([]);
makeEventListener(window, "keydown", e => {

@@ -128,10 +120,2 @@ // e.key may be undefined when used with <datalist> el

});
// this is for backwards compatibility
// TODO remove in the next major version
pressedKeys[0] = pressedKeys;
pressedKeys[1] = { event };
pressedKeys[Symbol.iterator] = function* () {
yield pressedKeys[0];
yield pressedKeys[1];
};
return pressedKeys;

@@ -200,7 +184,9 @@ });

const keys = useKeyDownList();
return createMemo(prev => {
// createMemo's second arg is options (not initialValue). The prev
// parameter starts as undefined; handle it with a fallback.
return createMemo((prev) => {
if (keys().length === 0)
return [];
return [...prev, keys()];
}, []);
return [...(prev ?? []), keys()];
});
});

@@ -214,4 +200,4 @@ /**

* @param key The key to check for.
* @options The options for the key hold.
* - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
* @param options The options for the key hold.
* - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
* @returns

@@ -235,7 +221,17 @@ * ```ts

key = key.toUpperCase();
const { preventDefault = true } = options, event = useKeyDownEvent(), heldKey = useCurrentlyHeldKey();
return createMemo(() => heldKey() === key && (preventDefault && event()?.preventDefault(), true));
const { preventDefault = true } = options;
const heldKey = useCurrentlyHeldKey();
if (preventDefault) {
// Use a direct event listener for synchronous preventDefault — signal reads in event
// listeners return the pre-batch committed value, so we check e.key directly.
makeEventListener(window, "keydown", (e) => {
if (e.key.toUpperCase() === key) {
e.preventDefault();
}
});
}
return createMemo(() => heldKey() === key);
}
/**
* Creates a keyboard shotcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
* Creates a keyboard shortcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
*

@@ -246,4 +242,4 @@ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createShortcut

* @param callback The callback to call when the keys are pressed.
* @options The options for the shortcut.
* - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
* @param options The options for the shortcut.
* - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
* - `requireReset` — If `true`, the shortcut will only be triggered once until all of the keys stop being pressed. Disabled by default.

@@ -263,50 +259,129 @@ *

keys = keys.map(key => key.toUpperCase());
const { preventDefault = true } = options, event = useKeyDownEvent(), sequence = useKeyDownSequence();
const { preventDefault = true, requireReset = false } = options;
// Track pressed keys and sequence locally with plain JS state rather than
// reactive signals. A signal reads from event listeners return
// the pre-batch committed value, so synchronous shortcut checking requires
// imperative state that's updated in the same event listener tick.
let pressedKeys = [];
let sequence = [];
let reset = false;
// allow to check the sequence only once the user has released all keys
const handleSequenceWithReset = (sequence) => {
if (!sequence.length)
return (reset = false);
if (reset)
const resetAll = () => {
pressedKeys = [];
sequence = [];
reset = false;
};
makeEventListener(window, "keydown", (e) => {
if (e.repeat || typeof e.key !== "string")
return;
const e = event();
if (sequence.length < keys.length) {
// optimistically preventDefault behavior if we yet don't have enough keys
if (equalsKeyHoldSequence(sequence, keys.slice(0, sequence.length))) {
preventDefault && e && e.preventDefault();
const key = e.key.toUpperCase();
if (!pressedKeys.includes(key)) {
const newKeys = [...pressedKeys];
// Detect modifiers pressed before listener attached
if (pressedKeys.length === 0 &&
key !== "ALT" &&
key !== "CONTROL" &&
key !== "META" &&
key !== "SHIFT") {
if (e.shiftKey && !newKeys.includes("SHIFT"))
newKeys.unshift("SHIFT");
if (e.altKey && !newKeys.includes("ALT"))
newKeys.unshift("ALT");
if (e.ctrlKey && !newKeys.includes("CONTROL"))
newKeys.unshift("CONTROL");
if (e.metaKey && !newKeys.includes("META"))
newKeys.unshift("META");
}
newKeys.push(key);
pressedKeys = newKeys;
sequence = [...sequence, [...pressedKeys]];
}
if (requireReset) {
if (reset)
return;
if (sequence.length < keys.length) {
if (equalsKeyHoldSequence(sequence, keys.slice(0, sequence.length))) {
preventDefault && e.preventDefault();
}
else {
reset = true;
}
}
else {
reset = true;
if (equalsKeyHoldSequence(sequence, keys)) {
preventDefault && e.preventDefault();
callback(e);
}
}
}
else {
reset = true;
if (equalsKeyHoldSequence(sequence, keys)) {
preventDefault && e && e.preventDefault();
callback(e);
const last = sequence.at(-1);
if (!last)
return;
if (preventDefault && last.length < keys.length) {
if (arrayEquals(last, keys.slice(0, keys.length - 1))) {
e.preventDefault();
}
return;
}
if (arrayEquals(last, keys)) {
const prev = sequence.at(-2);
if (!prev || arrayEquals(prev, keys.slice(0, keys.length - 1))) {
preventDefault && e.preventDefault();
callback(e);
}
}
}
};
// allow checking the sequence even if the user is still holding down keys
const handleSequenceWithoutReset = (sequence) => {
const last = sequence.at(-1);
if (!last)
});
makeEventListener(window, "keyup", (e) => {
if (typeof e.key !== "string")
return;
const e = event();
// optimistically preventDefault behavior if we yet don't have enough keys
if (preventDefault && last.length < keys.length) {
if (arrayEquals(last, keys.slice(0, keys.length - 1))) {
e && e.preventDefault();
}
return;
const key = e.key.toUpperCase();
pressedKeys = pressedKeys.filter(k => k !== key);
if (pressedKeys.length === 0) {
sequence = [];
reset = false;
}
if (arrayEquals(last, keys)) {
const prev = sequence.at(-2);
if (!prev || arrayEquals(prev, keys.slice(0, keys.length - 1))) {
preventDefault && e && e.preventDefault();
callback(e);
}
else {
// Reset sequence to remaining held keys so repeated presses of the last
// key can re-trigger the shortcut while modifier keys stay held.
sequence = [[...pressedKeys]];
}
};
createEffect(on(sequence, options.requireReset ? handleSequenceWithReset : handleSequenceWithoutReset));
});
makeEventListener(window, "blur", resetAll);
makeEventListener(window, "contextmenu", (e) => {
e.defaultPrevented || resetAll();
});
}
/**
* Listens for a keydown event for a specific key on a document.
* Supports a custom `ownerDocument` for use in iframes and portals.
*
* @param key The key to listen for (matched against `event.key`).
* @param callback Handler called when the key is pressed.
* @param options `disabled` and `ownerDocument` accessors.
*
* @example
* ```ts
* createKeyDown("Escape", e => close(), {
* ownerDocument: () => iframeEl.contentDocument ?? document,
* });
* ```
*/
export function createKeyDown(key, callback, options) {
if (isServer)
return;
createEffect(() => ({
disabled: !!access(options?.disabled),
document: options?.ownerDocument?.() ?? window.document,
}), ({ disabled, document }) => {
if (disabled)
return;
const handler = (e) => {
if (e.key === key)
callback(e);
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
});
}
{
"name": "@solid-primitives/keyboard",
"version": "1.3.5",
"version": "2.0.0-next.0",
"description": "A library of reactive promitives helping handling user's keyboard input.",

@@ -18,4 +18,5 @@ "author": "Damian Tarnwski <gthetarnav@gmail.com>",

"name": "keyboard",
"stage": 1,
"stage": 3,
"list": [
"useKeyDownEvent",
"useKeyDownList",

@@ -27,3 +28,4 @@ "useCurrentlyHeldKey",

],
"category": "Inputs"
"category": "Inputs",
"gzip": 1348
},

@@ -54,11 +56,13 @@ "keywords": [

"dependencies": {
"@solid-primitives/event-listener": "^2.4.5",
"@solid-primitives/rootless": "^1.5.3",
"@solid-primitives/utils": "^6.4.0"
"@solid-primitives/event-listener": "^3.0.0-next.0",
"@solid-primitives/rootless": "^2.0.0-next.0",
"@solid-primitives/utils": "^7.0.0-next.0"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.15",
"solid-js": "^2.0.0-beta.15"
},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.15",
"solid-js": "2.0.0-beta.15"
},

@@ -65,0 +69,0 @@ "typesVersions": {},

@@ -8,14 +8,16 @@ <p>

[![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg?style=for-the-badge)](https://lerna.js.org/)
[![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/keyboard?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/keyboard)
[![size](https://img.shields.io/badge/size-1.35_kB-blue?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/keyboard)
[![version](https://img.shields.io/npm/v/@solid-primitives/keyboard?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/keyboard)
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-1.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)
[![tested with vitest](https://img.shields.io/badge/tested_with-vitest-6E9F18?style=for-the-badge&logo=vitest)](https://vitest.dev)
A library of reactive promitives helping handling user's keyboard input.
A library of reactive primitives for handling user keyboard input.
- [`useKeyDownEvent`](#usekeydownevent) — Provides a signal with the last keydown event.
- [`useKeyDownList`](#usekeydownlist) — Provides a signal with the list of currently held keys
- [`useKeyDownList`](#usekeydownlist) — Provides a signal with the list of currently held keys.
- [`useCurrentlyHeldKey`](#usecurrentlyheldkey) — Provides a signal with the currently held single key.
- [`useKeyDownSequence`](#usekeydownsequence) — Provides a signal with a sequence of currently held keys, as they were pressed down and up.
- [`createKeyHold`](#createkeyhold) — Provides a signal indicating if provided key is currently being held down.
- [`createShortcut`](#createshortcut) — Creates a keyboard shotcut observer.
- [`createShortcut`](#createshortcut) — Creates a keyboard shortcut observer.
- [`createKeyDown`](#createkeydown) — Listens for a keydown event for a specific key on a document.

@@ -66,3 +68,3 @@ ## Installation

`useKeyDownList` takes no arguments, and returns a signal with the list of currently held keys
`useKeyDownList` takes no arguments and returns a signal with the list of currently held keys.

@@ -148,3 +150,3 @@ ```tsx

Creates a keyboard shotcut observer. The provided callback will be called when the specified keys are pressed.
Creates a keyboard shortcut observer. The provided callback will be called when the specified keys are pressed.

@@ -179,10 +181,30 @@ ### How to use it

## DEMO
## `createKeyDown`
Working demo of some of the primitives in keyboard package:
Listens for a `keydown` event for a specific key on a document. Useful for global keyboard handlers that need to respond to a single key without setting up a full shortcut.
https://codesandbox.io/s/solid-primitives-keyboard-demo-s2l84k?file=/index.tsx
### How to use it
`createKeyDown` takes three arguments:
- `key` — the key to listen for (matched against `event.key`)
- `callback` — handler called when the key is pressed, receives the `KeyboardEvent`
- `options` — additional configuration:
- `disabled` — a `boolean` or accessor; when `true` the listener is inactive. Reactive — the listener is added/removed as the value changes.
- `ownerDocument` — accessor returning the `Document` to attach the listener to. Defaults to `window.document`. Useful for iframes and portals.
```tsx
import { createKeyDown } from "@solid-primitives/keyboard";
createKeyDown("Escape", e => close());
// with options
createKeyDown("Escape", e => close(), {
disabled: () => !isOpen(),
ownerDocument: () => iframeEl.contentDocument ?? document,
});
```
## Changelog
See [CHANGELOG.md](./CHANGELOG.md)