@solid-primitives/focus 1.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/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Solid Primitives Working Group
4
+
5
+ The `createFocusTrap` primitive is ported from solid-focus-trap:
6
+ Copyright (c) 2023 Jasmin Noetzli (GiyoMoon)
7
+ https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ <p>
2
+ <img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=focus" alt="Solid Primitives Focus">
3
+ </p>
4
+
5
+ # @solid-primitives/focus
6
+
7
+ [![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/focus?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/focus)
8
+ [![version](https://img.shields.io/npm/v/@solid-primitives/focus?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/focus)
9
+ [![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)
10
+ [![tested with vitest](https://img.shields.io/badge/tested_with-vitest-6E9F18?style=for-the-badge&logo=vitest)](https://vitest.dev)
11
+
12
+ Primitives for autofocusing HTML elements and trapping focus within a container.
13
+
14
+ The native `autofocus` attribute only works on page load, which makes it incompatible with SolidJS. These primitives run on render, allowing autofocus on initial render as well as dynamically added components.
15
+
16
+ - [`autofocus`](#autofocus) - Ref callback factory to autofocus an element on render.
17
+ - [`createAutofocus`](#createautofocus) - Reactive primitive to autofocus an element on render.
18
+ - [`createFocusTrap`](#createfocustrap) - Traps focus inside a given DOM element.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @solid-primitives/focus
24
+ # or
25
+ yarn add @solid-primitives/focus
26
+ # or
27
+ pnpm add @solid-primitives/focus
28
+ ```
29
+
30
+ ## `autofocus`
31
+
32
+ ### How to use it
33
+
34
+ `autofocus` is a ref callback factory. It uses the native `autofocus` attribute to determine whether to focus the element.
35
+
36
+ ```tsx
37
+ import { autofocus } from "@solid-primitives/focus";
38
+
39
+ <button ref={autofocus()} autofocus>
40
+ Autofocused
41
+ </button>;
42
+ ```
43
+
44
+ To conditionally enable autofocus, control the `autofocus` attribute directly — the `autofocus()` ref only focuses when the attribute is present, so removing it is sufficient to opt out:
45
+
46
+ ```tsx
47
+ // Conditionally autofocus by toggling the attribute
48
+ <button ref={autofocus()} autofocus={shouldFocus()}>
49
+ Maybe Autofocused
50
+ </button>
51
+ ```
52
+
53
+ > **Note:** The `enabled` parameter was removed because it was redundant — the same effect is achieved by omitting the `autofocus` attribute. Previously, Solid directives always received an accessor argument whether you used it or not, which gave the impression an explicit toggle was necessary.
54
+
55
+ ## `createAutofocus`
56
+
57
+ `createAutofocus` reactively autofocuses an element passed in as a signal.
58
+
59
+ ```tsx
60
+ import { createAutofocus } from "@solid-primitives/focus";
61
+
62
+ // Using ref
63
+ let ref!: HTMLButtonElement;
64
+ createAutofocus(() => ref);
65
+
66
+ <button ref={ref}>Autofocused</button>;
67
+
68
+ // Using ref signal
69
+ const [ref, setRef] = createSignal<HTMLButtonElement>();
70
+ createAutofocus(ref);
71
+
72
+ <button ref={setRef}>Autofocused</button>;
73
+ ```
74
+
75
+ ## `createFocusTrap`
76
+
77
+ `createFocusTrap` traps keyboard focus inside a given DOM element, cycling through focusable children on Tab / Shift+Tab. It uses a `MutationObserver` to stay up to date with DOM changes and restores focus to the previously focused element when deactivated.
78
+
79
+ > Ported from [solid-focus-trap](https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap) by [Jasmin Noetzli (GiyoMoon)](https://github.com/GiyoMoon), adapted for Solid.js 2.0.
80
+
81
+ ### How to use it
82
+
83
+ ```tsx
84
+ import { createFocusTrap } from "@solid-primitives/focus";
85
+
86
+ const DialogContent: Component<{ open: boolean }> = props => {
87
+ const [contentRef, setContentRef] = createSignal<HTMLElement | null>(null);
88
+
89
+ createFocusTrap({
90
+ element: contentRef,
91
+ enabled: () => props.open,
92
+ });
93
+
94
+ return (
95
+ <Show when={props.open}>
96
+ <div ref={setContentRef}>
97
+ <button>Close</button>
98
+ <input />
99
+ </div>
100
+ </Show>
101
+ );
102
+ };
103
+ ```
104
+
105
+ ### Props
106
+
107
+ | Prop | Type | Default | Description |
108
+ | --------------------- | ---------------------------------- | -------------------------- | --------------------------------------------------------------------------------- |
109
+ | `element` | `MaybeAccessor<HTMLElement\|null>` | — | Element to trap focus within. |
110
+ | `enabled` | `MaybeAccessor<boolean>` | `true` | Whether the trap is active. |
111
+ | `observeChanges` | `MaybeAccessor<boolean>` | `true` | Watch for DOM mutations inside the container and refresh focusable elements. |
112
+ | `initialFocusElement` | `MaybeAccessor<HTMLElement\|null>` | First focusable element | Element to focus when the trap activates. |
113
+ | `restoreFocus` | `MaybeAccessor<boolean>` | `true` | Restore focus to the previously focused element when the trap deactivates. |
114
+ | `finalFocusElement` | `MaybeAccessor<HTMLElement\|null>` | Previously focused element | Element to focus when the trap deactivates. |
115
+ | `onInitialFocus` | `(event: Event) => void` | — | Callback when focus moves into the trap. Call `event.preventDefault()` to cancel. |
116
+ | `onFinalFocus` | `(event: Event) => void` | — | Callback when focus restores. Call `event.preventDefault()` to cancel. |
117
+
118
+ ### Custom initial focus
119
+
120
+ ```tsx
121
+ const [contentRef, setContentRef] = createSignal<HTMLElement | null>(null);
122
+ const [inputRef, setInputRef] = createSignal<HTMLElement | null>(null);
123
+
124
+ createFocusTrap({
125
+ element: contentRef,
126
+ enabled: () => props.open,
127
+ initialFocusElement: inputRef,
128
+ });
129
+
130
+ return (
131
+ <Show when={props.open}>
132
+ <div ref={setContentRef}>
133
+ <button>Close</button>
134
+ <input ref={setInputRef} />
135
+ </div>
136
+ </Show>
137
+ );
138
+ ```
139
+
140
+ ### Preventing focus moves
141
+
142
+ ```tsx
143
+ createFocusTrap({
144
+ element: contentRef,
145
+ onInitialFocus: event => {
146
+ event.preventDefault(); // focus won't move on activation
147
+ },
148
+ onFinalFocus: event => {
149
+ event.preventDefault(); // focus won't restore on deactivation
150
+ },
151
+ });
152
+ ```
153
+
154
+ ## Credits
155
+
156
+ `createFocusTrap` is ported from [solid-focus-trap](https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap), part of the [corvu](https://corvu.dev) UI toolkit by [Jasmin Noetzli (GiyoMoon)](https://github.com/GiyoMoon). Licensed under the MIT License.
157
+
158
+ ## Changelog
159
+
160
+ See [CHANGELOG.md](./CHANGELOG.md)
@@ -0,0 +1,40 @@
1
+ import { type Accessor } from "solid-js";
2
+ import type { JSX } from "@solidjs/web";
3
+ import { type FalsyValue } from "@solid-primitives/utils";
4
+ /**
5
+ * Ref callback factory to autofocus an element on render.
6
+ * Uses the native `autofocus` attribute to determine whether to focus.
7
+ *
8
+ * To disable autofocus, simply omit the `autofocus` attribute on the element —
9
+ * no `enabled` parameter is needed or provided.
10
+ *
11
+ * @returns Ref callback to attach to the element.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <button ref={autofocus()} autofocus>Autofocused</button>
16
+ * ```
17
+ */
18
+ export declare const autofocus: () => (element: HTMLElement) => void;
19
+ /**
20
+ * Creates a new reactive primitive for autofocusing the element on render.
21
+ *
22
+ * @param ref - Element to focus.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * let ref!: HTMLButtonElement;
27
+ *
28
+ * createAutofocus(() => ref);
29
+ *
30
+ * <button ref={ref}>Autofocused</button>;
31
+ *
32
+ * // Using ref signal
33
+ * const [ref, setRef] = createSignal<HTMLButtonElement>();
34
+ * createAutofocus(ref);
35
+ *
36
+ * <button ref={setRef}>Autofocused</button>;
37
+ * ```
38
+ */
39
+ export declare const createAutofocus: (ref: Accessor<HTMLElement | FalsyValue>) => void;
40
+ export type E = JSX.Element;
@@ -0,0 +1,56 @@
1
+ import { createEffect, onSettled } from "solid-js";
2
+ import {} from "@solid-primitives/utils";
3
+ /**
4
+ * Ref callback factory to autofocus an element on render.
5
+ * Uses the native `autofocus` attribute to determine whether to focus.
6
+ *
7
+ * To disable autofocus, simply omit the `autofocus` attribute on the element —
8
+ * no `enabled` parameter is needed or provided.
9
+ *
10
+ * @returns Ref callback to attach to the element.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <button ref={autofocus()} autofocus>Autofocused</button>
15
+ * ```
16
+ */
17
+ export const autofocus = () => {
18
+ let el;
19
+ onSettled(() => {
20
+ if (!el?.hasAttribute("autofocus"))
21
+ return;
22
+ const id = setTimeout(() => el?.focus());
23
+ return () => clearTimeout(id);
24
+ });
25
+ return (element) => {
26
+ el = element;
27
+ };
28
+ };
29
+ /**
30
+ * Creates a new reactive primitive for autofocusing the element on render.
31
+ *
32
+ * @param ref - Element to focus.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * let ref!: HTMLButtonElement;
37
+ *
38
+ * createAutofocus(() => ref);
39
+ *
40
+ * <button ref={ref}>Autofocused</button>;
41
+ *
42
+ * // Using ref signal
43
+ * const [ref, setRef] = createSignal<HTMLButtonElement>();
44
+ * createAutofocus(ref);
45
+ *
46
+ * <button ref={setRef}>Autofocused</button>;
47
+ * ```
48
+ */
49
+ export const createAutofocus = (ref) => {
50
+ createEffect(() => ref(), el => {
51
+ if (!el)
52
+ return;
53
+ const id = setTimeout(() => el.focus());
54
+ return () => clearTimeout(id);
55
+ });
56
+ };
@@ -0,0 +1,26 @@
1
+ import { type Accessor } from "solid-js";
2
+ import { type MaybeAccessor } from "@solid-primitives/utils";
3
+ /**
4
+ * Attaches "blur" and "focus" event listeners to the element.
5
+ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#makeFocusListener
6
+ * @param target element
7
+ * @param callback handle focus change
8
+ * @param useCapture activates capturing, which allows to listen on events at the root that don't support bubbling.
9
+ * @returns function for clearing event listeners
10
+ * @example
11
+ * const [isFocused, setIsFocused] = createSignal(false)
12
+ * const clear = makeFocusListener(el, focused => setIsFocused(focused));
13
+ * // remove listeners (happens also on cleanup)
14
+ * clear();
15
+ */
16
+ export declare function makeFocusListener(target: Element, callback: (isActive: boolean) => void, useCapture?: boolean): VoidFunction;
17
+ /**
18
+ * Provides a signal representing element's focus state.
19
+ * @param target element or a reactive function returning one
20
+ * @returns boolean signal representing element's focus state
21
+ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#createFocusSignal
22
+ * @example
23
+ * const isFocused = createFocusSignal(() => el)
24
+ * isFocused() // T: boolean
25
+ */
26
+ export declare function createFocusSignal(target: MaybeAccessor<Element>): Accessor<boolean>;
@@ -0,0 +1,43 @@
1
+ import {} from "solid-js";
2
+ import { isServer } from "@solidjs/web";
3
+ import { createHydratableSignal } from "@solid-primitives/utils";
4
+ import { makeEventListener, createEventListener } from "@solid-primitives/event-listener";
5
+ /**
6
+ * Attaches "blur" and "focus" event listeners to the element.
7
+ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#makeFocusListener
8
+ * @param target element
9
+ * @param callback handle focus change
10
+ * @param useCapture activates capturing, which allows to listen on events at the root that don't support bubbling.
11
+ * @returns function for clearing event listeners
12
+ * @example
13
+ * const [isFocused, setIsFocused] = createSignal(false)
14
+ * const clear = makeFocusListener(el, focused => setIsFocused(focused));
15
+ * // remove listeners (happens also on cleanup)
16
+ * clear();
17
+ */
18
+ export function makeFocusListener(target, callback, useCapture = true) {
19
+ if (isServer) {
20
+ return () => { };
21
+ }
22
+ const clear1 = makeEventListener(target, "blur", callback.bind(undefined, false), useCapture);
23
+ const clear2 = makeEventListener(target, "focus", callback.bind(undefined, true), useCapture);
24
+ return () => (clear1(), clear2());
25
+ }
26
+ /**
27
+ * Provides a signal representing element's focus state.
28
+ * @param target element or a reactive function returning one
29
+ * @returns boolean signal representing element's focus state
30
+ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#createFocusSignal
31
+ * @example
32
+ * const isFocused = createFocusSignal(() => el)
33
+ * isFocused() // T: boolean
34
+ */
35
+ export function createFocusSignal(target) {
36
+ if (isServer) {
37
+ return () => false;
38
+ }
39
+ const [isActive, setIsActive] = createHydratableSignal(false, () => document.activeElement === target);
40
+ createEventListener(target, "blur", () => setIsActive(false), true);
41
+ createEventListener(target, "focus", () => setIsActive(true), true);
42
+ return isActive;
43
+ }
@@ -0,0 +1,52 @@
1
+ import { type MaybeAccessor } from "@solid-primitives/utils";
2
+ export type CreateFocusTrapProps = {
3
+ /** Element to trap focus within. */
4
+ element: MaybeAccessor<HTMLElement | undefined>;
5
+ /** Whether the focus trap is active. Default: `true` */
6
+ enabled?: MaybeAccessor<boolean>;
7
+ /**
8
+ * Watch for DOM mutations inside the container and reload the list of
9
+ * focusable elements accordingly. Default: `true`
10
+ */
11
+ observeChanges?: MaybeAccessor<boolean>;
12
+ /**
13
+ * Element to focus when the trap activates.
14
+ * Default: the first focusable element inside `element`.
15
+ */
16
+ initialFocusElement?: MaybeAccessor<HTMLElement | undefined>;
17
+ /**
18
+ * Restore focus to the element that was focused before the trap activated
19
+ * when the trap is deactivated. Default: `true`
20
+ */
21
+ restoreFocus?: MaybeAccessor<boolean>;
22
+ /**
23
+ * Element to focus when the trap deactivates.
24
+ * Default: the element that was focused before the trap activated.
25
+ */
26
+ finalFocusElement?: MaybeAccessor<HTMLElement | undefined>;
27
+ /**
28
+ * Callback fired when focus moves into the trap.
29
+ * Call `event.preventDefault()` to suppress the focus move.
30
+ */
31
+ onInitialFocus?: (event: Event) => void;
32
+ /**
33
+ * Callback fired when focus is restored after deactivation.
34
+ * Call `event.preventDefault()` to suppress the focus move.
35
+ */
36
+ onFinalFocus?: (event: Event) => void;
37
+ };
38
+ /**
39
+ * Traps focus inside the given element. Aware of DOM changes inside the trap
40
+ * via a MutationObserver. Properly restores focus when deactivated.
41
+ *
42
+ * Ported from [solid-focus-trap](https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap)
43
+ * by Jasmin Noetzli (GiyoMoon), adapted for Solid.js 2.0.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * const [ref, setRef] = createSignal<HTMLElement | null>(null);
48
+ * createFocusTrap({ element: ref, enabled: () => isOpen() });
49
+ * <div ref={setRef}>...</div>
50
+ * ```
51
+ */
52
+ export declare const createFocusTrap: (props: CreateFocusTrapProps) => void;
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Ported from solid-focus-trap by Jasmin Noetzli (GiyoMoon)
3
+ * MIT License — https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap
4
+ * Adapted for Solid.js 2.0 and @solid-primitives/focus by the Solid Primitives Working Group.
5
+ */
6
+ import { access, afterPaint, INTERNAL_OPTIONS } from "@solid-primitives/utils";
7
+ import { createEffect, createMemo, createSignal } from "solid-js";
8
+ const FOCUSABLE_SELECTOR = 'a[href]:not([tabindex="-1"]), button:not([tabindex="-1"]), input:not([tabindex="-1"]), textarea:not([tabindex="-1"]), select:not([tabindex="-1"]), details:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])';
9
+ const EVENT_INITIAL_FOCUS = "focusTrap.initialFocus";
10
+ const EVENT_FINAL_FOCUS = "focusTrap.finalFocus";
11
+ const EVENT_OPTIONS = { bubbles: false, cancelable: true };
12
+ /**
13
+ * Traps focus inside the given element. Aware of DOM changes inside the trap
14
+ * via a MutationObserver. Properly restores focus when deactivated.
15
+ *
16
+ * Ported from [solid-focus-trap](https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap)
17
+ * by Jasmin Noetzli (GiyoMoon), adapted for Solid.js 2.0.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const [ref, setRef] = createSignal<HTMLElement | null>(null);
22
+ * createFocusTrap({ element: ref, enabled: () => isOpen() });
23
+ * <div ref={setRef}>...</div>
24
+ * ```
25
+ */
26
+ export const createFocusTrap = (props) => {
27
+ const [focusableElements, setFocusableElements] = createSignal(undefined, INTERNAL_OPTIONS);
28
+ const firstFocusElement = createMemo(() => {
29
+ const els = focusableElements();
30
+ return els ? (els[0] ?? null) : null;
31
+ });
32
+ const lastFocusElement = createMemo(() => {
33
+ const els = focusableElements();
34
+ return els ? (els[els.length - 1] ?? null) : null;
35
+ });
36
+ let originalFocusedElement = null;
37
+ const loadFocusableElements = (container) => {
38
+ const sorted = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR))
39
+ .map((element, domIndex) => ({ element, domIndex, tabIndex: element.tabIndex }))
40
+ .sort((a, b) => a.tabIndex === b.tabIndex ? a.domIndex - b.domIndex : a.tabIndex - b.tabIndex);
41
+ setFocusableElements(sorted.map(({ element }) => element));
42
+ };
43
+ const triggerInitialFocus = (container) => {
44
+ afterPaint(() => {
45
+ const target = access(props.initialFocusElement ?? null) ?? firstFocusElement() ?? container;
46
+ const { onInitialFocus } = props;
47
+ if (onInitialFocus) {
48
+ const event = new CustomEvent(EVENT_INITIAL_FOCUS, EVENT_OPTIONS);
49
+ container.addEventListener(EVENT_INITIAL_FOCUS, onInitialFocus);
50
+ container.dispatchEvent(event);
51
+ container.removeEventListener(EVENT_INITIAL_FOCUS, onInitialFocus);
52
+ if (event.defaultPrevented)
53
+ return;
54
+ }
55
+ target.focus();
56
+ });
57
+ };
58
+ const triggerRestoreFocus = (container) => {
59
+ afterPaint(() => {
60
+ if (!access(props.restoreFocus ?? true))
61
+ return;
62
+ const target = access(props.finalFocusElement ?? null) ?? originalFocusedElement;
63
+ if (!target)
64
+ return;
65
+ const { onFinalFocus } = props;
66
+ if (onFinalFocus) {
67
+ const event = new CustomEvent(EVENT_FINAL_FOCUS, EVENT_OPTIONS);
68
+ container.addEventListener(EVENT_FINAL_FOCUS, onFinalFocus);
69
+ container.dispatchEvent(event);
70
+ container.removeEventListener(EVENT_FINAL_FOCUS, onFinalFocus);
71
+ if (event.defaultPrevented)
72
+ return;
73
+ }
74
+ target.focus();
75
+ });
76
+ };
77
+ const onFirstElementKeyDown = (event) => {
78
+ if (event.key === "Tab" && event.shiftKey) {
79
+ event.preventDefault();
80
+ lastFocusElement().focus();
81
+ }
82
+ };
83
+ const onLastElementKeyDown = (event) => {
84
+ if (event.key === "Tab" && !event.shiftKey) {
85
+ event.preventDefault();
86
+ firstFocusElement().focus();
87
+ }
88
+ };
89
+ const preventTab = (event) => {
90
+ if (event.key === "Tab")
91
+ event.preventDefault();
92
+ };
93
+ // Activate / deactivate the trap when element or enabled changes.
94
+ createEffect(() => ({
95
+ container: access(props.element),
96
+ enabled: access(props.enabled ?? true),
97
+ observeChanges: access(props.observeChanges ?? true),
98
+ }), ({ container, enabled, observeChanges }) => {
99
+ if (!container || !enabled)
100
+ return;
101
+ originalFocusedElement = document.activeElement;
102
+ loadFocusableElements(container);
103
+ triggerInitialFocus(container);
104
+ const observer = new MutationObserver(() => {
105
+ afterPaint(() => {
106
+ loadFocusableElements(container);
107
+ if (!document.activeElement || document.activeElement === document.body) {
108
+ triggerInitialFocus(container);
109
+ }
110
+ });
111
+ });
112
+ if (observeChanges) {
113
+ observer.observe(container, {
114
+ subtree: true,
115
+ childList: true,
116
+ attributes: true,
117
+ attributeFilter: ["tabindex"],
118
+ });
119
+ }
120
+ return () => {
121
+ if (observeChanges)
122
+ observer.disconnect();
123
+ setFocusableElements(undefined);
124
+ triggerRestoreFocus(container);
125
+ };
126
+ });
127
+ // When there are no focusable elements, block all Tab key presses.
128
+ createEffect(() => focusableElements(), elements => {
129
+ if (!elements || elements.length !== 0)
130
+ return;
131
+ document.addEventListener("keydown", preventTab);
132
+ return () => document.removeEventListener("keydown", preventTab);
133
+ });
134
+ // Shift+Tab on the first element → wrap to last.
135
+ createEffect(() => firstFocusElement(), el => {
136
+ if (!el)
137
+ return;
138
+ el.addEventListener("keydown", onFirstElementKeyDown);
139
+ return () => el.removeEventListener("keydown", onFirstElementKeyDown);
140
+ });
141
+ // Tab on the last element → wrap to first.
142
+ createEffect(() => lastFocusElement(), el => {
143
+ if (!el)
144
+ return;
145
+ el.addEventListener("keydown", onLastElementKeyDown);
146
+ return () => el.removeEventListener("keydown", onLastElementKeyDown);
147
+ });
148
+ };
@@ -0,0 +1,5 @@
1
+ export { autofocus, createAutofocus } from "./autofocus.js";
2
+ export type { E } from "./autofocus.js";
3
+ export { createFocusTrap } from "./focusTrap.js";
4
+ export type { CreateFocusTrapProps } from "./focusTrap.js";
5
+ export { makeFocusListener, createFocusSignal } from "./focusSignal.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { autofocus, createAutofocus } from "./autofocus.js";
2
+ export { createFocusTrap } from "./focusTrap.js";
3
+ export { makeFocusListener, createFocusSignal } from "./focusSignal.js";
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@solid-primitives/focus",
3
+ "version": "1.0.0-next.0",
4
+ "description": "Primitives for autofocusing HTML elements and trapping focus within a container",
5
+ "author": "jer3m01 <jer3m01@jer3m01.com>",
6
+ "contributors": [
7
+ {
8
+ "name": "Jasmin Noetzli",
9
+ "url": "https://github.com/GiyoMoon"
10
+ },
11
+ {
12
+ "name": "David Di Biase",
13
+ "url": "https://github.com/davedbase"
14
+ }
15
+ ],
16
+ "license": "MIT",
17
+ "homepage": "https://primitives.solidjs.community/package/focus",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/solidjs-community/solid-primitives.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/solidjs-community/solid-primitives/issues"
24
+ },
25
+ "primitive": {
26
+ "name": "focus",
27
+ "stage": 3,
28
+ "list": [
29
+ "autofocus",
30
+ "createAutofocus",
31
+ "createFocusTrap",
32
+ "makeFocusListener",
33
+ "createFocusSignal"
34
+ ],
35
+ "category": "Inputs",
36
+ "gzip": 191
37
+ },
38
+ "keywords": [
39
+ "solid",
40
+ "primitives",
41
+ "focus",
42
+ "autofocus",
43
+ "focus-trap",
44
+ "trap",
45
+ "accessibility",
46
+ "a11y"
47
+ ],
48
+ "private": false,
49
+ "sideEffects": false,
50
+ "files": [
51
+ "dist"
52
+ ],
53
+ "type": "module",
54
+ "module": "./dist/index.js",
55
+ "browser": {},
56
+ "types": "./dist/index.d.ts",
57
+ "exports": {
58
+ "import": {
59
+ "@solid-primitives/source": "./src/index.ts",
60
+ "types": "./dist/index.d.ts",
61
+ "default": "./dist/index.js"
62
+ }
63
+ },
64
+ "peerDependencies": {
65
+ "@solidjs/web": "^2.0.0-beta.15",
66
+ "solid-js": "^2.0.0-beta.15"
67
+ },
68
+ "dependencies": {
69
+ "@solid-primitives/event-listener": "^3.0.0-next.0",
70
+ "@solid-primitives/utils": "^7.0.0-next.0"
71
+ },
72
+ "typesVersions": {},
73
+ "devDependencies": {
74
+ "@solidjs/web": "2.0.0-beta.15",
75
+ "solid-js": "2.0.0-beta.15"
76
+ },
77
+ "scripts": {
78
+ "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts",
79
+ "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts",
80
+ "vitest": "vitest -c ../../configs/vitest.config.ts",
81
+ "test": "pnpm run vitest",
82
+ "test:ssr": "pnpm run vitest --mode ssr"
83
+ }
84
+ }