shelving 1.95.0 → 1.96.1

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.95.0",
14
+ "version": "1.96.1",
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";
@@ -1,14 +1,15 @@
1
1
  /// <reference types="react" />
2
2
  import type { AsyncQueryReference } from "../db/QueryReference.js";
3
3
  import type { ImmutableArray } from "../util/array.js";
4
+ import type { Nullish } from "../util/null.js";
4
5
  import { AsyncItemReference } from "../db/ItemReference.js";
5
6
  import { ItemState } from "../db/ItemState.js";
6
7
  import { QueryState } from "../db/QueryState.js";
7
- type RefToState<T> = T extends undefined ? undefined : T extends AsyncItemReference<infer X> ? ItemState<X> : T extends AsyncQueryReference<infer X> ? QueryState<X> : never;
8
+ type NullishReferenceState<T> = T extends undefined | null ? undefined : T extends AsyncItemReference<infer X> ? ItemState<X> : T extends AsyncQueryReference<infer X> ? QueryState<X> : never;
8
9
  /** Use one or more data items or queries. */
9
- export declare function useData<T extends AsyncItemReference | AsyncQueryReference | undefined>(ref: T): RefToState<T>;
10
- export declare function useData<T extends ImmutableArray<AsyncItemReference | AsyncQueryReference | undefined>>(...refs: T): {
11
- [K in keyof T]: RefToState<T[K]>;
10
+ export declare function useData<T extends Nullish<AsyncItemReference | AsyncQueryReference>>(ref: T): NullishReferenceState<T>;
11
+ export declare function useData<T extends ImmutableArray<Nullish<AsyncItemReference | AsyncQueryReference>>>(...refs: T): {
12
+ [K in keyof T]: NullishReferenceState<T[K]>;
12
13
  };
13
14
  /** Wrap components with `<DataCache>` to allow the use of `useData()`. */
14
15
  export declare const DataCache: ({ children }: {
@@ -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,51 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { addArrayItem, deleteArrayItems, getLastItem } from "../util/array.js";
3
+ import { getFirstFocusable } from "../util/focus.js";
4
+ /** Stack of elements that should be keeping focus. */
5
+ let FOCUS_STACK;
6
+ /**
7
+ * 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.
8
+ * - When the element using `useFocus()` is attached to the DOM, finds the first focusable element and calls `element.focus()` on it.
9
+ * - If the user tabs outside the element focus will 'wrap' and refocus inside the container until the targeted element is detached from the DOM.
10
+ *
11
+ * @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).
12
+ *
13
+ * @returns React ref that should be set on the target element you want the focus to be placed on or within.
14
+ */
15
+ export function useFocus(active = true) {
16
+ // Store the element in a ref.
17
+ const ref = useRef(null);
18
+ // Effect that runs when element is first attached to the DOM.
19
+ useEffect(() => {
20
+ var _a;
21
+ // Set up a global listener the first time `useFocus()` is actually used.
22
+ if (!FOCUS_STACK) {
23
+ FOCUS_STACK = [];
24
+ // Add a focus listener on the body that listens for changes in focus.
25
+ document.body.addEventListener("focusin", () => {
26
+ var _a;
27
+ const el = getLastItem(FOCUS_STACK);
28
+ if (el && !el.contains(document.activeElement))
29
+ (_a = getFirstFocusable(el)) === null || _a === void 0 ? void 0 : _a.focus();
30
+ });
31
+ }
32
+ // Only run if the element is open and the ref is attached.
33
+ const el = ref.current;
34
+ if (active && el) {
35
+ // Keep reference to the element that was focused before this element took the focus.
36
+ const lastEl = document.activeElement;
37
+ // Focus immediately on the first auto-focusable element now (e.g. this might just be the close button).
38
+ (_a = getFirstFocusable(el)) === null || _a === void 0 ? void 0 : _a.focus();
39
+ // Add this element to the stack of elements to keep focused.
40
+ addArrayItem(FOCUS_STACK, el);
41
+ return () => {
42
+ // Remove this element from the stack of elements to keep focused.
43
+ deleteArrayItems(FOCUS_STACK, el);
44
+ // Attempt to restore the previous focus.
45
+ if (lastEl instanceof HTMLElement)
46
+ lastEl.focus();
47
+ };
48
+ }
49
+ }, [ref.current, active]);
50
+ return ref;
51
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AnyState } from "../state/State.js";
2
2
  import type { ImmutableArray } from "../util/array.js";
3
+ import type { Nullish } from "../util/null.js";
3
4
  /**
4
5
  * Subscribe to one or more `State` instances.
5
6
  *
@@ -9,5 +10,5 @@ import type { ImmutableArray } from "../util/array.js";
9
10
  * - If the value is a `State` instance
10
11
  */
11
12
  export declare function useState<T extends AnyState>(state: T): T;
12
- export declare function useState<T extends AnyState>(state?: T | undefined): T | undefined;
13
- export declare function useState<T extends ImmutableArray<AnyState | undefined>>(...states: T): T;
13
+ export declare function useState<T extends AnyState>(state?: Nullish<T>): Nullish<T>;
14
+ export declare function useState<T extends ImmutableArray<Nullish<AnyState>>>(...states: T): T;
@@ -0,0 +1,2 @@
1
+ /** Find the first focusable element inside HTML element (including the element itself). */
2
+ export declare function getFirstFocusable(el: HTMLElement): HTMLElement | null;
package/util/focus.js ADDED
@@ -0,0 +1,6 @@
1
+ /** Selector for elements that can take focus */
2
+ const FOCUSABLE = `a:link, button:enabled, input:enabled, select:enabled, textarea:enabled, [tabindex]:not([tabindex="-1"]):not(:disabled)`;
3
+ /** Find the first focusable element inside HTML element (including the element itself). */
4
+ export function getFirstFocusable(el) {
5
+ return el.matches(FOCUSABLE) ? el : el.querySelector(FOCUSABLE);
6
+ }
package/util/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./duration.js";
15
15
  export * from "./entry.js";
16
16
  export * from "./equal.js";
17
17
  export * from "./error.js";
18
+ export * from "./focus.js";
18
19
  export * from "./function.js";
19
20
  export * from "./hydrate.js";
20
21
  export * from "./iterate.js";
package/util/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./duration.js";
15
15
  export * from "./entry.js";
16
16
  export * from "./equal.js";
17
17
  export * from "./error.js";
18
+ export * from "./focus.js";
18
19
  export * from "./function.js";
19
20
  export * from "./hydrate.js";
20
21
  export * from "./iterate.js";
package/util/null.d.ts CHANGED
@@ -1,17 +1,23 @@
1
1
  /** Function that always returns null. */
2
2
  export declare const getNull: () => null;
3
+ /** Nullable is the value or `null` */
4
+ export type Nullable<T> = T | null;
3
5
  /** Is a value null? */
4
6
  export declare const isNull: (value: unknown) => value is null;
7
+ /** Assert that a value is not null. */
8
+ export declare function assertNull<T>(value: Nullable<T>): asserts value is T;
5
9
  /** Is a value not null? */
6
- export declare const notNull: <T>(value: T | null) => value is T;
10
+ export declare const notNull: <T>(value: Nullable<T>) => value is T;
7
11
  /** Assert that a value is not null. */
8
- export declare function assertNotNull<T>(value: T | null): asserts value is T;
12
+ export declare function assertNotNull<T>(value: Nullable<T>): asserts value is T;
9
13
  /** Get the not-nullish version of value. */
10
- export declare function getNotNull<T>(value: T | null): T;
11
- /** Nullish is `null` or `undefined` */
14
+ export declare function getNotNull<T>(value: Nullable<T>): T;
15
+ /** Nullish is the value or `null` or `undefined` */
12
16
  export type Nullish<T> = T | null | undefined;
13
17
  /** Is a value nullish? */
14
18
  export declare const isNullish: <T>(value: Nullish<T>) => value is null | undefined;
19
+ /** Assert that a value is not nullish. */
20
+ export declare function assertNullish<T>(value: Nullish<T>): asserts value is T;
15
21
  /** Is a value not nullish? */
16
22
  export declare const notNullish: <T>(value: Nullish<T>) => value is T;
17
23
  /** Assert that a value is not nullish. */
package/util/null.js CHANGED
@@ -4,6 +4,11 @@ import { RequiredError } from "../error/RequiredError.js";
4
4
  export const getNull = () => null;
5
5
  /** Is a value null? */
6
6
  export const isNull = (value) => value === null;
7
+ /** Assert that a value is not null. */
8
+ export function assertNull(value) {
9
+ if (value !== null)
10
+ throw new AssertionError("Must be null", value);
11
+ }
7
12
  /** Is a value not null? */
8
13
  export const notNull = (value) => value !== null;
9
14
  /** Assert that a value is not null. */
@@ -18,6 +23,11 @@ export function getNotNull(value) {
18
23
  }
19
24
  /** Is a value nullish? */
20
25
  export const isNullish = (value) => value === null || value === undefined;
26
+ /** Assert that a value is not nullish. */
27
+ export function assertNullish(value) {
28
+ if (value !== null && value !== undefined)
29
+ throw new AssertionError("Must be null or undefined", value);
30
+ }
21
31
  /** Is a value not nullish? */
22
32
  export const notNullish = (value) => value !== null && value !== undefined;
23
33
  /** Assert that a value is not nullish. */
@@ -4,6 +4,8 @@ export declare const getUndefined: () => undefined;
4
4
  export declare const isUndefined: (value: unknown) => value is undefined;
5
5
  /** Is a value defined? */
6
6
  export declare const isDefined: <T>(value: T | undefined) => value is T;
7
+ /** Is a value defined? */
8
+ export declare const notUndefined: <T>(value: T | undefined) => value is T;
7
9
  /** Assert that a value is not `undefined` */
8
10
  export declare function assertDefined<T>(value: T | undefined): asserts value is T;
9
11
  /** Get a defined value. */
package/util/undefined.js CHANGED
@@ -5,6 +5,8 @@ export const getUndefined = () => undefined;
5
5
  export const isUndefined = (value) => value === undefined;
6
6
  /** Is a value defined? */
7
7
  export const isDefined = (value) => value !== undefined;
8
+ /** Is a value defined? */
9
+ export const notUndefined = isDefined;
8
10
  /** Assert that a value is not `undefined` */
9
11
  export function assertDefined(value) {
10
12
  if (value === undefined)