@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 +25 -0
- package/README.md +160 -0
- package/dist/autofocus.d.ts +40 -0
- package/dist/autofocus.js +56 -0
- package/dist/focusSignal.d.ts +26 -0
- package/dist/focusSignal.js +43 -0
- package/dist/focusTrap.d.ts +52 -0
- package/dist/focusTrap.js +148 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/package.json +84 -0
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
|
+
[](https://bundlephobia.com/package/@solid-primitives/focus)
|
|
8
|
+
[](https://www.npmjs.com/package/@solid-primitives/focus)
|
|
9
|
+
[](https://github.com/solidjs-community/solid-primitives#contribution-process)
|
|
10
|
+
[](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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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
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
|
+
}
|