@solid-primitives/keyboard
Advanced tools
+30
-7
@@ -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); | ||
| }); | ||
| } |
+12
-8
| { | ||
| "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": {}, |
+31
-9
@@ -8,14 +8,16 @@ <p> | ||
| [](https://lerna.js.org/) | ||
| [](https://bundlephobia.com/package/@solid-primitives/keyboard) | ||
| [](https://bundlephobia.com/package/@solid-primitives/keyboard) | ||
| [](https://www.npmjs.com/package/@solid-primitives/keyboard) | ||
| [](https://github.com/solidjs-community/solid-primitives#contribution-process) | ||
| [](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) |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
29866
18.78%545
21.92%207
11.89%5
25%2
100%1
Infinity%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed