@solid-primitives/keyboard 1.3.4 → 2.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,18 +5,20 @@
5
5
  # @solid-primitives/keyboard
6
6
 
7
7
  [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg?style=for-the-badge)](https://lerna.js.org/)
8
- [![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/keyboard?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/keyboard)
8
+ [![size](https://img.shields.io/badge/size-1.35_kB-blue?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/keyboard)
9
9
  [![version](https://img.shields.io/npm/v/@solid-primitives/keyboard?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/keyboard)
10
10
  [![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)
11
+ [![tested with vitest](https://img.shields.io/badge/tested_with-vitest-6E9F18?style=for-the-badge&logo=vitest)](https://vitest.dev)
11
12
 
12
- A library of reactive promitives helping handling user's keyboard input.
13
+ A library of reactive primitives for handling user keyboard input.
13
14
 
14
15
  - [`useKeyDownEvent`](#usekeydownevent) — Provides a signal with the last keydown event.
15
- - [`useKeyDownList`](#usekeydownlist) — Provides a signal with the list of currently held keys
16
+ - [`useKeyDownList`](#usekeydownlist) — Provides a signal with the list of currently held keys.
16
17
  - [`useCurrentlyHeldKey`](#usecurrentlyheldkey) — Provides a signal with the currently held single key.
17
18
  - [`useKeyDownSequence`](#usekeydownsequence) — Provides a signal with a sequence of currently held keys, as they were pressed down and up.
18
19
  - [`createKeyHold`](#createkeyhold) — Provides a signal indicating if provided key is currently being held down.
19
- - [`createShortcut`](#createshortcut) — Creates a keyboard shotcut observer.
20
+ - [`createShortcut`](#createshortcut) — Creates a keyboard shortcut observer.
21
+ - [`createKeyDown`](#createkeydown) — Listens for a keydown event for a specific key on a document.
20
22
 
21
23
  ## Installation
22
24
 
@@ -62,7 +64,7 @@ This is a [singleton root](https://github.com/solidjs-community/solid-primitives
62
64
 
63
65
  ### How to use it
64
66
 
65
- `useKeyDownList` takes no arguments, and returns a signal with the list of currently held keys
67
+ `useKeyDownList` takes no arguments and returns a signal with the list of currently held keys.
66
68
 
67
69
  ```tsx
68
70
  import { useKeyDownList } from "@solid-primitives/keyboard";
@@ -143,7 +145,7 @@ const pressing = createKeyHold("Alt", { preventDefault: false });
143
145
 
144
146
  ## `createShortcut`
145
147
 
146
- Creates a keyboard shotcut observer. The provided callback will be called when the specified keys are pressed.
148
+ Creates a keyboard shortcut observer. The provided callback will be called when the specified keys are pressed.
147
149
 
148
150
  ### How to use it
149
151
 
@@ -173,11 +175,31 @@ When `preventDefault` is `true`, `e.preventDefault()` will be called not only on
173
175
 
174
176
  E.g. when listening for `Control + Shift + A`, all three keydown events will be prevented.
175
177
 
176
- ## DEMO
178
+ ## `createKeyDown`
177
179
 
178
- Working demo of some of the primitives in keyboard package:
180
+ 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.
179
181
 
180
- https://codesandbox.io/s/solid-primitives-keyboard-demo-s2l84k?file=/index.tsx
182
+ ### How to use it
183
+
184
+ `createKeyDown` takes three arguments:
185
+
186
+ - `key` — the key to listen for (matched against `event.key`)
187
+ - `callback` — handler called when the key is pressed, receives the `KeyboardEvent`
188
+ - `options` — additional configuration:
189
+ - `disabled` — a `boolean` or accessor; when `true` the listener is inactive. Reactive — the listener is added/removed as the value changes.
190
+ - `ownerDocument` — accessor returning the `Document` to attach the listener to. Defaults to `window.document`. Useful for iframes and portals.
191
+
192
+ ```tsx
193
+ import { createKeyDown } from "@solid-primitives/keyboard";
194
+
195
+ createKeyDown("Escape", e => close());
196
+
197
+ // with options
198
+ createKeyDown("Escape", e => close(), {
199
+ disabled: () => !isOpen(),
200
+ ownerDocument: () => iframeEl.contentDocument ?? document,
201
+ });
202
+ ```
181
203
 
182
204
  ## Changelog
183
205
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type MaybeAccessor } from "@solid-primitives/utils";
1
2
  import { type Accessor } from "solid-js";
2
3
  export type ModifierKey = "Alt" | "Control" | "Meta" | "Shift";
3
4
  export type KbdKey = ModifierKey | (string & {});
@@ -23,8 +24,8 @@ export type KbdKey = ModifierKey | (string & {});
23
24
  * console.log(e) // => KeyboardEvent | null
24
25
  *
25
26
  * if (e) {
26
- * console.log(e.key) // => "Q" | "ALT" | ... or null
27
- * e.preventDefault(); // prevent default behavior or last keydown event
27
+ * console.log(e.key) // => "Q" | "ALT" | ...
28
+ * e.preventDefault();
28
29
  * }
29
30
  * })
30
31
  * ```
@@ -102,8 +103,8 @@ export declare const useKeyDownSequence: () => Accessor<string[][]>;
102
103
  * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createKeyHold
103
104
  *
104
105
  * @param key The key to check for.
105
- * @options The options for the key hold.
106
- * - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
106
+ * @param options The options for the key hold.
107
+ * - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
107
108
  * @returns
108
109
  * ```ts
109
110
  * Accessor<boolean>
@@ -121,14 +122,14 @@ export declare function createKeyHold(key: KbdKey, options?: {
121
122
  preventDefault?: boolean;
122
123
  }): Accessor<boolean>;
123
124
  /**
124
- * Creates a keyboard shotcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
125
+ * Creates a keyboard shortcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
125
126
  *
126
127
  * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createShortcut
127
128
  *
128
129
  * @param keys The sequence of keys to watch for.
129
130
  * @param callback The callback to call when the keys are pressed.
130
- * @options The options for the shortcut.
131
- * - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
131
+ * @param options The options for the shortcut.
132
+ * - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
132
133
  * - `requireReset` — If `true`, the shortcut will only be triggered once until all of the keys stop being pressed. Disabled by default.
133
134
  *
134
135
  * @example
@@ -142,3 +143,25 @@ export declare function createShortcut(keys: KbdKey[], callback: (event: Keyboar
142
143
  preventDefault?: boolean;
143
144
  requireReset?: boolean;
144
145
  }): void;
146
+ export interface CreateKeyDownOptions {
147
+ /** Whether the listener should be inactive. */
148
+ disabled?: MaybeAccessor<boolean | undefined>;
149
+ /** The document to attach the listener to. Defaults to `window.document`. Useful for iframes. */
150
+ ownerDocument?: Accessor<Document | undefined>;
151
+ }
152
+ /**
153
+ * Listens for a keydown event for a specific key on a document.
154
+ * Supports a custom `ownerDocument` for use in iframes and portals.
155
+ *
156
+ * @param key The key to listen for (matched against `event.key`).
157
+ * @param callback Handler called when the key is pressed.
158
+ * @param options `disabled` and `ownerDocument` accessors.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * createKeyDown("Escape", e => close(), {
163
+ * ownerDocument: () => iframeEl.contentDocument ?? document,
164
+ * });
165
+ * ```
166
+ */
167
+ export declare function createKeyDown(key: KbdKey, callback: (event: KeyboardEvent) => void, options?: CreateKeyDownOptions): void;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { makeEventListener } from "@solid-primitives/event-listener";
2
2
  import { createSingletonRoot } from "@solid-primitives/rootless";
3
- import { arrayEquals } from "@solid-primitives/utils";
4
- import { createEffect, createMemo, createSignal, on, untrack } from "solid-js";
5
- import { isServer } from "solid-js/web";
3
+ import { arrayEquals, INTERNAL_OPTIONS, access } from "@solid-primitives/utils";
4
+ import { createEffect, createMemo, createSignal, untrack } from "solid-js";
5
+ import { isServer } from "@solidjs/web";
6
6
  function equalsKeyHoldSequence(sequence, model) {
7
7
  for (let i = sequence.length - 1; i >= 0; i--) {
8
8
  const _model = model.slice(0, i + 1);
@@ -33,8 +33,8 @@ function equalsKeyHoldSequence(sequence, model) {
33
33
  * console.log(e) // => KeyboardEvent | null
34
34
  *
35
35
  * if (e) {
36
- * console.log(e.key) // => "Q" | "ALT" | ... or null
37
- * e.preventDefault(); // prevent default behavior or last keydown event
36
+ * console.log(e.key) // => "Q" | "ALT" | ...
37
+ * e.preventDefault();
38
38
  * }
39
39
  * })
40
40
  * ```
@@ -43,7 +43,7 @@ export const useKeyDownEvent = /*#__PURE__*/ createSingletonRoot(() => {
43
43
  if (isServer) {
44
44
  return () => null;
45
45
  }
46
- const [event, setEvent] = createSignal(null);
46
+ const [event, setEvent] = createSignal(null, INTERNAL_OPTIONS);
47
47
  makeEventListener(window, "keydown", e => {
48
48
  setEvent(e);
49
49
  setTimeout(() => setEvent(null));
@@ -73,18 +73,10 @@ export const useKeyDownEvent = /*#__PURE__*/ createSingletonRoot(() => {
73
73
  */
74
74
  export const useKeyDownList = /*#__PURE__*/ createSingletonRoot(() => {
75
75
  if (isServer) {
76
- const keys = () => [];
77
- // this is for backwards compatibility
78
- // TODO remove in the next major version
79
- keys[0] = keys;
80
- keys[1] = { event: () => null };
81
- keys[Symbol.iterator] = function* () {
82
- yield keys[0];
83
- yield keys[1];
84
- };
85
- return keys;
76
+ return () => [];
86
77
  }
87
- const [pressedKeys, setPressedKeys] = createSignal([]), reset = () => setPressedKeys([]), event = useKeyDownEvent();
78
+ const [pressedKeys, setPressedKeys] = createSignal([], INTERNAL_OPTIONS);
79
+ const reset = () => setPressedKeys([]);
88
80
  makeEventListener(window, "keydown", e => {
89
81
  // e.key may be undefined when used with <datalist> el
90
82
  // gh issue: https://github.com/solidjs-community/solid-primitives/issues/246
@@ -122,14 +114,6 @@ export const useKeyDownList = /*#__PURE__*/ createSingletonRoot(() => {
122
114
  makeEventListener(window, "contextmenu", e => {
123
115
  e.defaultPrevented || reset();
124
116
  });
125
- // this is for backwards compatibility
126
- // TODO remove in the next major version
127
- pressedKeys[0] = pressedKeys;
128
- pressedKeys[1] = { event };
129
- pressedKeys[Symbol.iterator] = function* () {
130
- yield pressedKeys[0];
131
- yield pressedKeys[1];
132
- };
133
117
  return pressedKeys;
134
118
  });
135
119
  /**
@@ -193,11 +177,13 @@ export const useKeyDownSequence = /*#__PURE__*/ createSingletonRoot(() => {
193
177
  return () => [];
194
178
  }
195
179
  const keys = useKeyDownList();
196
- return createMemo(prev => {
180
+ // createMemo's second arg is options (not initialValue). The prev
181
+ // parameter starts as undefined; handle it with a fallback.
182
+ return createMemo((prev) => {
197
183
  if (keys().length === 0)
198
184
  return [];
199
- return [...prev, keys()];
200
- }, []);
185
+ return [...(prev ?? []), keys()];
186
+ });
201
187
  });
202
188
  /**
203
189
  * Provides a `boolean` signal indicating if provided {@link key} is currently being held down.
@@ -206,8 +192,8 @@ export const useKeyDownSequence = /*#__PURE__*/ createSingletonRoot(() => {
206
192
  * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createKeyHold
207
193
  *
208
194
  * @param key The key to check for.
209
- * @options The options for the key hold.
210
- * - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
195
+ * @param options The options for the key hold.
196
+ * - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
211
197
  * @returns
212
198
  * ```ts
213
199
  * Accessor<boolean>
@@ -226,18 +212,28 @@ export function createKeyHold(key, options = {}) {
226
212
  return () => false;
227
213
  }
228
214
  key = key.toUpperCase();
229
- const { preventDefault = true } = options, event = useKeyDownEvent(), heldKey = useCurrentlyHeldKey();
230
- return createMemo(() => heldKey() === key && (preventDefault && event()?.preventDefault(), true));
215
+ const { preventDefault = true } = options;
216
+ const heldKey = useCurrentlyHeldKey();
217
+ if (preventDefault) {
218
+ // Use a direct event listener for synchronous preventDefault — signal reads in event
219
+ // listeners return the pre-batch committed value, so we check e.key directly.
220
+ makeEventListener(window, "keydown", (e) => {
221
+ if (e.key.toUpperCase() === key) {
222
+ e.preventDefault();
223
+ }
224
+ });
225
+ }
226
+ return createMemo(() => heldKey() === key);
231
227
  }
232
228
  /**
233
- * Creates a keyboard shotcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
229
+ * Creates a keyboard shortcut observer. The provided {@link callback} will be called when the specified {@link keys} are pressed.
234
230
  *
235
231
  * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createShortcut
236
232
  *
237
233
  * @param keys The sequence of keys to watch for.
238
234
  * @param callback The callback to call when the keys are pressed.
239
- * @options The options for the shortcut.
240
- * - `preventDefault` — Controlls in the keydown event should have it's default action prevented. Enabled by default.
235
+ * @param options The options for the shortcut.
236
+ * - `preventDefault` — Controls if the keydown event should have its default action prevented. Enabled by default.
241
237
  * - `requireReset` — If `true`, the shortcut will only be triggered once until all of the keys stop being pressed. Disabled by default.
242
238
  *
243
239
  * @example
@@ -252,52 +248,131 @@ export function createShortcut(keys, callback, options = {}) {
252
248
  return;
253
249
  }
254
250
  keys = keys.map(key => key.toUpperCase());
255
- const { preventDefault = true } = options, event = useKeyDownEvent(), sequence = useKeyDownSequence();
251
+ const { preventDefault = true, requireReset = false } = options;
252
+ // Track pressed keys and sequence locally with plain JS state rather than
253
+ // reactive signals. A signal reads from event listeners return
254
+ // the pre-batch committed value, so synchronous shortcut checking requires
255
+ // imperative state that's updated in the same event listener tick.
256
+ let pressedKeys = [];
257
+ let sequence = [];
256
258
  let reset = false;
257
- // allow to check the sequence only once the user has released all keys
258
- const handleSequenceWithReset = (sequence) => {
259
- if (!sequence.length)
260
- return (reset = false);
261
- if (reset)
259
+ const resetAll = () => {
260
+ pressedKeys = [];
261
+ sequence = [];
262
+ reset = false;
263
+ };
264
+ makeEventListener(window, "keydown", (e) => {
265
+ if (e.repeat || typeof e.key !== "string")
262
266
  return;
263
- const e = event();
264
- if (sequence.length < keys.length) {
265
- // optimistically preventDefault behavior if we yet don't have enough keys
266
- if (equalsKeyHoldSequence(sequence, keys.slice(0, sequence.length))) {
267
- preventDefault && e && e.preventDefault();
267
+ const key = e.key.toUpperCase();
268
+ if (!pressedKeys.includes(key)) {
269
+ const newKeys = [...pressedKeys];
270
+ // Detect modifiers pressed before listener attached
271
+ if (pressedKeys.length === 0 &&
272
+ key !== "ALT" &&
273
+ key !== "CONTROL" &&
274
+ key !== "META" &&
275
+ key !== "SHIFT") {
276
+ if (e.shiftKey && !newKeys.includes("SHIFT"))
277
+ newKeys.unshift("SHIFT");
278
+ if (e.altKey && !newKeys.includes("ALT"))
279
+ newKeys.unshift("ALT");
280
+ if (e.ctrlKey && !newKeys.includes("CONTROL"))
281
+ newKeys.unshift("CONTROL");
282
+ if (e.metaKey && !newKeys.includes("META"))
283
+ newKeys.unshift("META");
284
+ }
285
+ newKeys.push(key);
286
+ pressedKeys = newKeys;
287
+ sequence = [...sequence, [...pressedKeys]];
288
+ }
289
+ if (requireReset) {
290
+ if (reset)
291
+ return;
292
+ if (sequence.length < keys.length) {
293
+ if (equalsKeyHoldSequence(sequence, keys.slice(0, sequence.length))) {
294
+ preventDefault && e.preventDefault();
295
+ }
296
+ else {
297
+ reset = true;
298
+ }
268
299
  }
269
300
  else {
270
301
  reset = true;
302
+ if (equalsKeyHoldSequence(sequence, keys)) {
303
+ preventDefault && e.preventDefault();
304
+ callback(e);
305
+ }
271
306
  }
272
307
  }
273
308
  else {
274
- reset = true;
275
- if (equalsKeyHoldSequence(sequence, keys)) {
276
- preventDefault && e && e.preventDefault();
277
- callback(e);
309
+ const last = sequence.at(-1);
310
+ if (!last)
311
+ return;
312
+ if (preventDefault && last.length < keys.length) {
313
+ if (arrayEquals(last, keys.slice(0, keys.length - 1))) {
314
+ e.preventDefault();
315
+ }
316
+ return;
278
317
  }
279
- }
280
- };
281
- // allow checking the sequence even if the user is still holding down keys
282
- const handleSequenceWithoutReset = (sequence) => {
283
- const last = sequence.at(-1);
284
- if (!last)
285
- return;
286
- const e = event();
287
- // optimistically preventDefault behavior if we yet don't have enough keys
288
- if (preventDefault && last.length < keys.length) {
289
- if (arrayEquals(last, keys.slice(0, keys.length - 1))) {
290
- e && e.preventDefault();
318
+ if (arrayEquals(last, keys)) {
319
+ const prev = sequence.at(-2);
320
+ if (!prev || arrayEquals(prev, keys.slice(0, keys.length - 1))) {
321
+ preventDefault && e.preventDefault();
322
+ callback(e);
323
+ }
291
324
  }
325
+ }
326
+ });
327
+ makeEventListener(window, "keyup", (e) => {
328
+ if (typeof e.key !== "string")
292
329
  return;
330
+ const key = e.key.toUpperCase();
331
+ pressedKeys = pressedKeys.filter(k => k !== key);
332
+ if (pressedKeys.length === 0) {
333
+ sequence = [];
334
+ reset = false;
293
335
  }
294
- if (arrayEquals(last, keys)) {
295
- const prev = sequence.at(-2);
296
- if (!prev || arrayEquals(prev, keys.slice(0, keys.length - 1))) {
297
- preventDefault && e && e.preventDefault();
298
- callback(e);
299
- }
336
+ else {
337
+ // Reset sequence to remaining held keys so repeated presses of the last
338
+ // key can re-trigger the shortcut while modifier keys stay held.
339
+ sequence = [[...pressedKeys]];
300
340
  }
301
- };
302
- createEffect(on(sequence, options.requireReset ? handleSequenceWithReset : handleSequenceWithoutReset));
341
+ });
342
+ makeEventListener(window, "blur", resetAll);
343
+ makeEventListener(window, "contextmenu", (e) => {
344
+ e.defaultPrevented || resetAll();
345
+ });
346
+ }
347
+ /**
348
+ * Listens for a keydown event for a specific key on a document.
349
+ * Supports a custom `ownerDocument` for use in iframes and portals.
350
+ *
351
+ * @param key The key to listen for (matched against `event.key`).
352
+ * @param callback Handler called when the key is pressed.
353
+ * @param options `disabled` and `ownerDocument` accessors.
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * createKeyDown("Escape", e => close(), {
358
+ * ownerDocument: () => iframeEl.contentDocument ?? document,
359
+ * });
360
+ * ```
361
+ */
362
+ export function createKeyDown(key, callback, options) {
363
+ if (isServer)
364
+ return;
365
+ createEffect(() => ({
366
+ disabled: !!access(options?.disabled),
367
+ document: options?.ownerDocument?.() ?? window.document,
368
+ }), ({ disabled, document }) => {
369
+ if (disabled)
370
+ return;
371
+ const handler = (e) => {
372
+ if (e.key === key)
373
+ callback(e);
374
+ };
375
+ document.addEventListener("keydown", handler);
376
+ return () => document.removeEventListener("keydown", handler);
377
+ });
303
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solid-primitives/keyboard",
3
- "version": "1.3.4",
3
+ "version": "2.0.0-next.0",
4
4
  "description": "A library of reactive promitives helping handling user's keyboard input.",
5
5
  "author": "Damian Tarnwski <gthetarnav@gmail.com>",
6
6
  "contributors": [],
@@ -15,15 +15,17 @@
15
15
  },
16
16
  "primitive": {
17
17
  "name": "keyboard",
18
- "stage": 1,
18
+ "stage": 3,
19
19
  "list": [
20
+ "useKeyDownEvent",
20
21
  "useKeyDownList",
21
22
  "useCurrentlyHeldKey",
22
23
  "useKeyDownSequence",
23
24
  "createKeyHold",
24
25
  "createShortcut"
25
26
  ],
26
- "category": "Inputs"
27
+ "category": "Inputs",
28
+ "gzip": 1348
27
29
  },
28
30
  "keywords": [
29
31
  "solid",
@@ -49,15 +51,17 @@
49
51
  }
50
52
  },
51
53
  "dependencies": {
52
- "@solid-primitives/event-listener": "^2.4.4",
53
- "@solid-primitives/rootless": "^1.5.2",
54
- "@solid-primitives/utils": "^6.3.2"
54
+ "@solid-primitives/event-listener": "^3.0.0-next.0",
55
+ "@solid-primitives/rootless": "^2.0.0-next.0",
56
+ "@solid-primitives/utils": "^7.0.0-next.0"
55
57
  },
56
58
  "peerDependencies": {
57
- "solid-js": "^1.6.12"
59
+ "@solidjs/web": "^2.0.0-beta.15",
60
+ "solid-js": "^2.0.0-beta.15"
58
61
  },
59
62
  "devDependencies": {
60
- "solid-js": "^1.9.7"
63
+ "@solidjs/web": "2.0.0-beta.15",
64
+ "solid-js": "2.0.0-beta.15"
61
65
  },
62
66
  "typesVersions": {},
63
67
  "scripts": {