shelving 1.94.0 → 1.96.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/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.94.0",
14
+ "version": "1.96.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
package/react/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./useFocus.js";
1
2
  export * from "./useLazy.js";
2
3
  export * from "./useReduce.js";
3
4
  export * from "./useInstance.js";
package/react/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Utilities.
2
+ export * from "./useFocus.js";
2
3
  export * from "./useLazy.js";
3
4
  export * from "./useReduce.js";
4
5
  export * from "./useInstance.js";
@@ -0,0 +1,11 @@
1
+ /// <reference types="react" />
2
+ /**
3
+ * Hook that puts the user's focus on an element (or the first focusable element inside that element) when it is attached to the DOM.
4
+ * - When the element using `useFocus()` is attached to the DOM, finds the first focusable element and calls `element.focus()` on it.
5
+ * - If the user tabs outside the element focus will 'wrap' and refocus inside the container until the targeted element is detached from the DOM.
6
+ *
7
+ * @param active Whether this `useFocus()` is active or not (so we can skip setting focus for modals that appear in the DOM but are minimised etc).
8
+ *
9
+ * @returns React ref that should be set on the target element you want the focus to be placed on or within.
10
+ */
11
+ export declare function useFocus<T extends HTMLElement>(active?: boolean): React.RefObject<T>;
@@ -0,0 +1,61 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { addArrayItem, deleteArrayItems, getLastItem } from "../util/array.js";
3
+ /** Stack of elements that should be keeping focus. */
4
+ let FOCUSED;
5
+ /** Selector for elements that can take focus */
6
+ const FOCUSABLE = `[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])`;
7
+ /**
8
+ * Focus on the first focusable element inside a container.
9
+ * - Checks items with an `autofocus` attribute first, then other elements.
10
+ */
11
+ function _applyFocus(container, selector = FOCUSABLE) {
12
+ if (container.matches(selector))
13
+ return container.focus();
14
+ const child = container.querySelector(selector);
15
+ if (child instanceof HTMLElement)
16
+ return child.focus();
17
+ }
18
+ /**
19
+ * Hook that puts the user's focus on an element (or the first focusable element inside that element) when it is attached to the DOM.
20
+ * - When the element using `useFocus()` is attached to the DOM, finds the first focusable element and calls `element.focus()` on it.
21
+ * - If the user tabs outside the element focus will 'wrap' and refocus inside the container until the targeted element is detached from the DOM.
22
+ *
23
+ * @param active Whether this `useFocus()` is active or not (so we can skip setting focus for modals that appear in the DOM but are minimised etc).
24
+ *
25
+ * @returns React ref that should be set on the target element you want the focus to be placed on or within.
26
+ */
27
+ export function useFocus(active = true) {
28
+ // Store the element in a ref.
29
+ const ref = useRef(null);
30
+ // Effect that runs when element is first attached to the DOM.
31
+ useEffect(() => {
32
+ // Set up a global listener the first time `useFocus()` is actually used.
33
+ if (!FOCUSED) {
34
+ FOCUSED = [];
35
+ // Add a focus listener on the body that listens for changes in focus.
36
+ document.body.addEventListener("focusin", () => {
37
+ const el = getLastItem(FOCUSED);
38
+ if (el && !el.contains(document.activeElement))
39
+ _applyFocus(el);
40
+ });
41
+ }
42
+ // Only run if the element is open and the ref is attached.
43
+ const el = ref.current;
44
+ if (active && el) {
45
+ // Keep reference to the element that was focused before this element took the focus.
46
+ const lastEl = document.activeElement;
47
+ // Focus immediately on the first auto-focusable element now (e.g. this might just be the close button).
48
+ _applyFocus(el, FOCUSABLE);
49
+ // Add this element to the stack of elements to keep focused.
50
+ addArrayItem(FOCUSED, el);
51
+ return () => {
52
+ // Remove this element from the stack of elements to keep focused.
53
+ deleteArrayItems(FOCUSED, el);
54
+ // Attempt to restore the previous focus.
55
+ if (lastEl instanceof HTMLElement)
56
+ lastEl.focus();
57
+ };
58
+ }
59
+ }, [ref.current, active]);
60
+ return ref;
61
+ }
@@ -1,12 +1,14 @@
1
- import type { Schema } from "./Schema.js";
1
+ import type { Schema, SchemaOptions } from "./Schema.js";
2
2
  import { ThroughSchema } from "./ThroughSchema.js";
3
+ /** Allowed options for `OptionalSchema` */
4
+ export type OptionalSchemaOptions<T> = SchemaOptions & {
5
+ readonly source: Schema<T>;
6
+ readonly value?: T | null;
7
+ };
3
8
  /** Validate a value of a specific type or `null`. */
4
9
  export declare class OptionalSchema<T> extends ThroughSchema<T | null> {
5
10
  readonly value: T | null;
6
- constructor(options: ConstructorParameters<typeof Schema>[0] & {
7
- source: Schema<T>;
8
- value?: T | null;
9
- });
11
+ constructor(options: OptionalSchemaOptions<T>);
10
12
  validate(unsafeValue?: unknown): T | null;
11
13
  }
12
14
  /** Create a new optional schema from a source schema. */
@@ -1,6 +1,6 @@
1
1
  import type { Schema } from "./Schema.js";
2
2
  import { ThroughSchema } from "./ThroughSchema.js";
3
- /** Validate a value of a specifed type, but return `Feedback` if the validated value is falsy. */
3
+ /** Validate a value of a specifed type, but throw `Feedback` if the validated value is falsy. */
4
4
  export declare class RequiredSchema<T> extends ThroughSchema<T> {
5
5
  validate(unsafeValue: unknown): T;
6
6
  }
@@ -1,6 +1,6 @@
1
1
  import { Feedback } from "../feedback/Feedback.js";
2
2
  import { ThroughSchema } from "./ThroughSchema.js";
3
- /** Validate a value of a specifed type, but return `Feedback` if the validated value is falsy. */
3
+ /** Validate a value of a specifed type, but throw `Feedback` if the validated value is falsy. */
4
4
  export class RequiredSchema extends ThroughSchema {
5
5
  validate(unsafeValue) {
6
6
  const safeValue = super.validate(unsafeValue);
@@ -2,8 +2,9 @@ import { Schema } from "./Schema.js";
2
2
  /** Schema that passes through to a source schema. */
3
3
  export class ThroughSchema extends Schema {
4
4
  constructor(options) {
5
- super(options);
6
- this.source = options.source;
5
+ const source = options.source;
6
+ super({ ...source, ...options });
7
+ this.source = source;
7
8
  }
8
9
  validate(unsafeValue) {
9
10
  return this.source.validate(unsafeValue);