From ddf4ecd5b2e942227265a1bc1379f34512ab27d1 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Wed, 13 May 2026 09:49:47 -0400
Subject: [PATCH 1/7] Rename to focus package
---
...migration.md => focus-solid2-migration.md} | 2 +-
.changeset/pre.json | 2 +-
README.md | 2 +-
packages/{autofocus => focus}/CHANGELOG.md | 2 +-
packages/{autofocus => focus}/LICENSE | 0
packages/{autofocus => focus}/README.md | 22 ++++++++--------
packages/{autofocus => focus}/dev/index.tsx | 0
packages/{autofocus => focus}/package.json | 6 ++---
packages/{autofocus => focus}/src/index.ts | 0
.../{autofocus => focus}/test/index.test.tsx | 0
.../{autofocus => focus}/test/server.test.ts | 0
packages/{autofocus => focus}/tsconfig.json | 0
pnpm-lock.yaml | 26 +++++++++----------
13 files changed, 31 insertions(+), 31 deletions(-)
rename .changeset/{autofocus-solid2-migration.md => focus-solid2-migration.md} (95%)
rename packages/{autofocus => focus}/CHANGELOG.md (98%)
rename packages/{autofocus => focus}/LICENSE (100%)
rename packages/{autofocus => focus}/README.md (77%)
rename packages/{autofocus => focus}/dev/index.tsx (100%)
rename packages/{autofocus => focus}/package.json (92%)
rename packages/{autofocus => focus}/src/index.ts (100%)
rename packages/{autofocus => focus}/test/index.test.tsx (100%)
rename packages/{autofocus => focus}/test/server.test.ts (100%)
rename packages/{autofocus => focus}/tsconfig.json (100%)
diff --git a/.changeset/autofocus-solid2-migration.md b/.changeset/focus-solid2-migration.md
similarity index 95%
rename from .changeset/autofocus-solid2-migration.md
rename to .changeset/focus-solid2-migration.md
index a9c43b560..8720ad291 100644
--- a/.changeset/autofocus-solid2-migration.md
+++ b/.changeset/focus-solid2-migration.md
@@ -1,5 +1,5 @@
---
-"@solid-primitives/autofocus": major
+"@solid-primitives/focus": major
---
Migrate to Solid.js v2.0 (beta.10)
diff --git a/.changeset/pre.json b/.changeset/pre.json
index ae0287918..c41689783 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -5,7 +5,7 @@
"@solid-primitives/active-element": "2.1.5",
"@solid-primitives/analytics": "0.2.1",
"@solid-primitives/audio": "1.4.4",
- "@solid-primitives/autofocus": "0.1.4",
+ "@solid-primitives/focus": "0.1.4",
"@solid-primitives/bounds": "0.1.5",
"@solid-primitives/broadcast-channel": "0.1.1",
"@solid-primitives/clipboard": "1.6.4",
diff --git a/README.md b/README.md
index 1ddd4dda8..210b3535f 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ The goal of Solid Primitives is to wrap client and server side functionality to
|----|----|----|----|----|----|
|
*Inputs* |
|[active-element](https://github.com/solidjs-community/solid-primitives/tree/main/packages/active-element#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createActiveElement](https://github.com/solidjs-community/solid-primitives/tree/main/packages/active-element#createactiveelement) [createFocusSignal](https://github.com/solidjs-community/solid-primitives/tree/main/packages/active-element#createfocussignal)|[](https://bundlephobia.com/package/@solid-primitives/active-element)|[](https://www.npmjs.com/package/@solid-primitives/active-element)||
-|[autofocus](https://github.com/solidjs-community/solid-primitives/tree/main/packages/autofocus#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[autofocus](https://github.com/solidjs-community/solid-primitives/tree/main/packages/autofocus#autofocus) [createAutofocus](https://github.com/solidjs-community/solid-primitives/tree/main/packages/autofocus#createautofocus)|[](https://bundlephobia.com/package/@solid-primitives/autofocus)|[](https://www.npmjs.com/package/@solid-primitives/autofocus)|✓|
+|[focus](https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[autofocus](https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#autofocus) [createAutofocus](https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#createautofocus)|[](https://bundlephobia.com/package/@solid-primitives/focus)|[](https://www.npmjs.com/package/@solid-primitives/focus)|✓|
|[input-mask](https://github.com/solidjs-community/solid-primitives/tree/main/packages/input-mask#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createInputMask](https://github.com/solidjs-community/solid-primitives/tree/main/packages/input-mask#createinputmask) [createMaskPattern](https://github.com/solidjs-community/solid-primitives/tree/main/packages/input-mask#createmaskpattern)|[](https://bundlephobia.com/package/@solid-primitives/input-mask)|[](https://www.npmjs.com/package/@solid-primitives/input-mask)|✓|
|[keyboard](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[useKeyDownList](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#usekeydownlist) [useCurrentlyHeldKey](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#usecurrentlyheldkey) [useKeyDownSequence](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#usekeydownsequence) [createKeyHold](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createkeyhold) [createShortcut](https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyboard#createshortcut)|[](https://bundlephobia.com/package/@solid-primitives/keyboard)|[](https://www.npmjs.com/package/@solid-primitives/keyboard)||
|[mouse](https://github.com/solidjs-community/solid-primitives/tree/main/packages/mouse#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createMousePosition](https://github.com/solidjs-community/solid-primitives/tree/main/packages/mouse#createmouseposition) [createPositionToElement](https://github.com/solidjs-community/solid-primitives/tree/main/packages/mouse#createpositiontoelement)|[](https://bundlephobia.com/package/@solid-primitives/mouse)|[](https://www.npmjs.com/package/@solid-primitives/mouse)||
diff --git a/packages/autofocus/CHANGELOG.md b/packages/focus/CHANGELOG.md
similarity index 98%
rename from packages/autofocus/CHANGELOG.md
rename to packages/focus/CHANGELOG.md
index 976ecae9d..ac009a623 100644
--- a/packages/autofocus/CHANGELOG.md
+++ b/packages/focus/CHANGELOG.md
@@ -1,4 +1,4 @@
-# @solid-primitives/autofocus
+# @solid-primitives/focus
## 0.1.4
diff --git a/packages/autofocus/LICENSE b/packages/focus/LICENSE
similarity index 100%
rename from packages/autofocus/LICENSE
rename to packages/focus/LICENSE
diff --git a/packages/autofocus/README.md b/packages/focus/README.md
similarity index 77%
rename from packages/autofocus/README.md
rename to packages/focus/README.md
index d07bdfa56..7d3b64079 100644
--- a/packages/autofocus/README.md
+++ b/packages/focus/README.md
@@ -1,11 +1,11 @@
-
+
-# @solid-primitives/autofocus
+# @solid-primitives/focus
-[](https://bundlephobia.com/package/@solid-primitives/autofocus)
-[](https://www.npmjs.com/package/@solid-primitives/autofocus)
+[](https://bundlephobia.com/package/@solid-primitives/focus)
+[](https://www.npmjs.com/package/@solid-primitives/focus)
[](https://github.com/solidjs-community/solid-primitives#contribution-process)
Primitives for autofocusing HTML elements.
@@ -18,11 +18,11 @@ The native autofocus attribute only works on page load, which makes it incompati
## Installation
```bash
-npm install @solid-primitives/autofocus
+npm install @solid-primitives/focus
# or
-yarn add @solid-primitives/autofocus
+yarn add @solid-primitives/focus
# or
-pnpm add @solid-primitives/autofocus
+pnpm add @solid-primitives/focus
```
## `autofocus`
@@ -32,7 +32,7 @@ pnpm add @solid-primitives/autofocus
`autofocus` is a ref callback factory. It uses the native `autofocus` attribute to determine whether to focus the element.
```tsx
-import { autofocus } from "@solid-primitives/autofocus";
+import { autofocus } from "@solid-primitives/focus";
Autofocused
@@ -55,7 +55,7 @@ To conditionally enable autofocus, control the `autofocus` attribute directly
`createAutofocus` reactively autofocuses an element passed in as a signal.
```tsx
-import { createAutofocus } from "@solid-primitives/autofocus";
+import { createAutofocus } from "@solid-primitives/focus";
// Using ref
let ref!: HTMLButtonElement;
@@ -72,9 +72,9 @@ createAutofocus(ref);
## Demo
-You may see the working example here: https://primitives.solidjs.community/playground/autofocus/
+You may see the working example here: https://primitives.solidjs.community/playground/focus/
-Source code: https://github.com/solidjs-community/solid-primitives/blob/main/packages/autofocus/dev/index.tsx
+Source code: https://github.com/solidjs-community/solid-primitives/blob/main/packages/focus/dev/index.tsx
## Changelog
diff --git a/packages/autofocus/dev/index.tsx b/packages/focus/dev/index.tsx
similarity index 100%
rename from packages/autofocus/dev/index.tsx
rename to packages/focus/dev/index.tsx
diff --git a/packages/autofocus/package.json b/packages/focus/package.json
similarity index 92%
rename from packages/autofocus/package.json
rename to packages/focus/package.json
index 57e15d953..27f0b8602 100644
--- a/packages/autofocus/package.json
+++ b/packages/focus/package.json
@@ -1,11 +1,11 @@
{
- "name": "@solid-primitives/autofocus",
+ "name": "@solid-primitives/focus",
"version": "0.2.0",
"description": "Primitives for autofocusing HTML elements",
"author": "jer3m01 ",
"contributors": [],
"license": "MIT",
- "homepage": "https://primitives.solidjs.community/package/autofocus",
+ "homepage": "https://primitives.solidjs.community/package/focus",
"repository": {
"type": "git",
"url": "git+https://github.com/solidjs-community/solid-primitives.git"
@@ -14,7 +14,7 @@
"url": "https://github.com/solidjs-community/solid-primitives/issues"
},
"primitive": {
- "name": "autofocus",
+ "name": "focus",
"stage": 1,
"list": [
"autofocus",
diff --git a/packages/autofocus/src/index.ts b/packages/focus/src/index.ts
similarity index 100%
rename from packages/autofocus/src/index.ts
rename to packages/focus/src/index.ts
diff --git a/packages/autofocus/test/index.test.tsx b/packages/focus/test/index.test.tsx
similarity index 100%
rename from packages/autofocus/test/index.test.tsx
rename to packages/focus/test/index.test.tsx
diff --git a/packages/autofocus/test/server.test.ts b/packages/focus/test/server.test.ts
similarity index 100%
rename from packages/autofocus/test/server.test.ts
rename to packages/focus/test/server.test.ts
diff --git a/packages/autofocus/tsconfig.json b/packages/focus/tsconfig.json
similarity index 100%
rename from packages/autofocus/tsconfig.json
rename to packages/focus/tsconfig.json
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 70787571b..5d3727113 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -119,19 +119,6 @@ importers:
specifier: ^1.9.7
version: 1.9.7
- packages/autofocus:
- dependencies:
- '@solid-primitives/utils':
- specifier: workspace:^
- version: link:../utils
- devDependencies:
- '@solidjs/web':
- specifier: 2.0.0-beta.10
- version: 2.0.0-beta.10(solid-js@2.0.0-beta.10)
- solid-js:
- specifier: 2.0.0-beta.10
- version: 2.0.0-beta.10
-
packages/bounds:
dependencies:
'@solid-primitives/event-listener':
@@ -345,6 +332,19 @@ importers:
specifier: ^1.9.7
version: 1.9.7
+ packages/focus:
+ dependencies:
+ '@solid-primitives/utils':
+ specifier: workspace:^
+ version: link:../utils
+ devDependencies:
+ '@solidjs/web':
+ specifier: 2.0.0-beta.10
+ version: 2.0.0-beta.10(solid-js@2.0.0-beta.10)
+ solid-js:
+ specifier: 2.0.0-beta.10
+ version: 2.0.0-beta.10
+
packages/fullscreen:
dependencies:
'@solid-primitives/utils':
From 32d8eace3fe599e5afd5b3c03d12c44a139e3427 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Wed, 13 May 2026 10:21:04 -0400
Subject: [PATCH 2/7] Migrated autofocus from corvu, add licensing and
attribution
---
packages/focus/LICENSE | 6 +-
packages/focus/README.md | 92 ++++++-
packages/focus/package.json | 18 +-
packages/focus/src/autofocus.ts | 65 +++++
packages/focus/src/focusTrap.ts | 214 ++++++++++++++++
packages/focus/src/index.ts | 69 +-----
packages/focus/test/index.test.tsx | 384 ++++++++++++++++++++++++++---
packages/focus/test/server.test.ts | 9 +-
packages/utils/src/index.ts | 10 +
9 files changed, 761 insertions(+), 106 deletions(-)
create mode 100644 packages/focus/src/autofocus.ts
create mode 100644 packages/focus/src/focusTrap.ts
diff --git a/packages/focus/LICENSE b/packages/focus/LICENSE
index 38b41d975..7a35c2e14 100644
--- a/packages/focus/LICENSE
+++ b/packages/focus/LICENSE
@@ -2,6 +2,10 @@ MIT License
Copyright (c) 2021 Solid Primitives Working Group
+The `createFocusTrap` primitive is ported from solid-focus-trap:
+ Copyright (c) 2023 Jasmin Noetzli (GiyoMoon)
+ https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap
+
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
@@ -18,4 +22,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/packages/focus/README.md b/packages/focus/README.md
index 7d3b64079..92772a3e6 100644
--- a/packages/focus/README.md
+++ b/packages/focus/README.md
@@ -8,12 +8,13 @@
[](https://www.npmjs.com/package/@solid-primitives/focus)
[](https://github.com/solidjs-community/solid-primitives#contribution-process)
-Primitives for autofocusing HTML elements.
+Primitives for autofocusing HTML elements and trapping focus within a container.
-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.
+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.
-- [`autofocus`](#autofocus) - Directive to autofocus an element on render.
+- [`autofocus`](#autofocus) - Ref callback factory to autofocus an element on render.
- [`createAutofocus`](#createautofocus) - Reactive primitive to autofocus an element on render.
+- [`createFocusTrap`](#createfocustrap) - Traps focus inside a given DOM element.
## Installation
@@ -50,7 +51,7 @@ To conditionally enable autofocus, control the `autofocus` attribute directly
> **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.
-### `createAutofocus`
+## `createAutofocus`
`createAutofocus` reactively autofocuses an element passed in as a signal.
@@ -70,12 +71,95 @@ createAutofocus(ref);
Autofocused ;
```
+## `createFocusTrap`
+
+`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.
+
+> 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.
+
+### How to use it
+
+```tsx
+import { createFocusTrap } from "@solid-primitives/focus";
+
+const DialogContent: Component<{ open: boolean }> = props => {
+ const [contentRef, setContentRef] = createSignal(null);
+
+ createFocusTrap({
+ element: contentRef,
+ enabled: () => props.open,
+ });
+
+ return (
+
+
+ Close
+
+
+
+ );
+};
+```
+
+### Props
+
+| Prop | Type | Default | Description |
+| -------------------- | --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------- |
+| `element` | `MaybeAccessor` | — | Element to trap focus within. |
+| `enabled` | `MaybeAccessor` | `true` | Whether the trap is active. |
+| `observeChanges` | `MaybeAccessor` | `true` | Watch for DOM mutations inside the container and refresh focusable elements. |
+| `initialFocusElement`| `MaybeAccessor` | First focusable element | Element to focus when the trap activates. |
+| `restoreFocus` | `MaybeAccessor` | `true` | Restore focus to the previously focused element when the trap deactivates. |
+| `finalFocusElement` | `MaybeAccessor` | Previously focused element | Element to focus when the trap deactivates. |
+| `onInitialFocus` | `(event: Event) => void` | — | Callback when focus moves into the trap. Call `event.preventDefault()` to cancel.|
+| `onFinalFocus` | `(event: Event) => void` | — | Callback when focus restores. Call `event.preventDefault()` to cancel. |
+
+### Custom initial focus
+
+```tsx
+const [contentRef, setContentRef] = createSignal(null);
+const [inputRef, setInputRef] = createSignal(null);
+
+createFocusTrap({
+ element: contentRef,
+ enabled: () => props.open,
+ initialFocusElement: inputRef,
+});
+
+return (
+
+
+ Close
+
+
+
+);
+```
+
+### Preventing focus moves
+
+```tsx
+createFocusTrap({
+ element: contentRef,
+ onInitialFocus: event => {
+ event.preventDefault(); // focus won't move on activation
+ },
+ onFinalFocus: event => {
+ event.preventDefault(); // focus won't restore on deactivation
+ },
+});
+```
+
## Demo
You may see the working example here: https://primitives.solidjs.community/playground/focus/
Source code: https://github.com/solidjs-community/solid-primitives/blob/main/packages/focus/dev/index.tsx
+## Credits
+
+`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.
+
## Changelog
See [CHANGELOG.md](./CHANGELOG.md)
diff --git a/packages/focus/package.json b/packages/focus/package.json
index 27f0b8602..0c1539fae 100644
--- a/packages/focus/package.json
+++ b/packages/focus/package.json
@@ -1,9 +1,14 @@
{
"name": "@solid-primitives/focus",
"version": "0.2.0",
- "description": "Primitives for autofocusing HTML elements",
+ "description": "Primitives for autofocusing HTML elements and trapping focus within a container",
"author": "jer3m01 ",
- "contributors": [],
+ "contributors": [
+ {
+ "name": "Jasmin Noetzli",
+ "url": "https://github.com/GiyoMoon"
+ }
+ ],
"license": "MIT",
"homepage": "https://primitives.solidjs.community/package/focus",
"repository": {
@@ -18,7 +23,8 @@
"stage": 1,
"list": [
"autofocus",
- "createAutofocus"
+ "createAutofocus",
+ "createFocusTrap"
],
"category": "Inputs"
},
@@ -26,7 +32,11 @@
"solid",
"primitives",
"focus",
- "autofocus"
+ "autofocus",
+ "focus-trap",
+ "trap",
+ "accessibility",
+ "a11y"
],
"private": false,
"sideEffects": false,
diff --git a/packages/focus/src/autofocus.ts b/packages/focus/src/autofocus.ts
new file mode 100644
index 000000000..5a5cf0e06
--- /dev/null
+++ b/packages/focus/src/autofocus.ts
@@ -0,0 +1,65 @@
+import { createEffect, onSettled, type Accessor } from "solid-js";
+import type { JSX } from "@solidjs/web";
+import { type FalsyValue } from "@solid-primitives/utils";
+
+/**
+ * Ref callback factory to autofocus an element on render.
+ * Uses the native `autofocus` attribute to determine whether to focus.
+ *
+ * To disable autofocus, simply omit the `autofocus` attribute on the element —
+ * no `enabled` parameter is needed or provided.
+ *
+ * @returns Ref callback to attach to the element.
+ *
+ * @example
+ * ```tsx
+ * Autofocused
+ * ```
+ */
+export const autofocus = () => {
+ let el: HTMLElement | undefined;
+
+ onSettled(() => {
+ if (!el?.hasAttribute("autofocus")) return;
+ const id = setTimeout(() => el?.focus());
+ return () => clearTimeout(id);
+ });
+
+ return (element: HTMLElement) => {
+ el = element;
+ };
+};
+
+/**
+ * Creates a new reactive primitive for autofocusing the element on render.
+ *
+ * @param ref - Element to focus.
+ *
+ * @example
+ * ```ts
+ * let ref!: HTMLButtonElement;
+ *
+ * createAutofocus(() => ref);
+ *
+ * Autofocused ;
+ *
+ * // Using ref signal
+ * const [ref, setRef] = createSignal();
+ * createAutofocus(ref);
+ *
+ * Autofocused ;
+ * ```
+ */
+export const createAutofocus = (ref: Accessor) => {
+ createEffect(
+ () => ref(),
+ el => {
+ if (!el) return;
+ const id = setTimeout(() => el.focus());
+ return () => clearTimeout(id);
+ },
+ );
+};
+
+// only here so the `JSX` import won't be shaken off the tree:
+export type E = JSX.Element;
diff --git a/packages/focus/src/focusTrap.ts b/packages/focus/src/focusTrap.ts
new file mode 100644
index 000000000..669b0312b
--- /dev/null
+++ b/packages/focus/src/focusTrap.ts
@@ -0,0 +1,214 @@
+/*
+ * Ported from solid-focus-trap by Jasmin Noetzli (GiyoMoon)
+ * MIT License — https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap
+ * Adapted for Solid.js 2.0 and @solid-primitives/focus by the Solid Primitives Working Group.
+ */
+
+import { access, afterPaint, INTERNAL_OPTIONS, type MaybeAccessor } from "@solid-primitives/utils";
+import { createEffect, createMemo, createSignal } from "solid-js";
+
+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"])';
+
+const EVENT_INITIAL_FOCUS = "focusTrap.initialFocus";
+const EVENT_FINAL_FOCUS = "focusTrap.finalFocus";
+const EVENT_OPTIONS = { bubbles: false, cancelable: true } as const;
+
+export type CreateFocusTrapProps = {
+ /** Element to trap focus within. */
+ element: MaybeAccessor;
+ /** Whether the focus trap is active. Default: `true` */
+ enabled?: MaybeAccessor;
+ /**
+ * Watch for DOM mutations inside the container and reload the list of
+ * focusable elements accordingly. Default: `true`
+ */
+ observeChanges?: MaybeAccessor;
+ /**
+ * Element to focus when the trap activates.
+ * Default: the first focusable element inside `element`.
+ */
+ initialFocusElement?: MaybeAccessor;
+ /**
+ * Restore focus to the element that was focused before the trap activated
+ * when the trap is deactivated. Default: `true`
+ */
+ restoreFocus?: MaybeAccessor;
+ /**
+ * Element to focus when the trap deactivates.
+ * Default: the element that was focused before the trap activated.
+ */
+ finalFocusElement?: MaybeAccessor;
+ /**
+ * Callback fired when focus moves into the trap.
+ * Call `event.preventDefault()` to suppress the focus move.
+ */
+ onInitialFocus?: (event: Event) => void;
+ /**
+ * Callback fired when focus is restored after deactivation.
+ * Call `event.preventDefault()` to suppress the focus move.
+ */
+ onFinalFocus?: (event: Event) => void;
+};
+
+/**
+ * Traps focus inside the given element. Aware of DOM changes inside the trap
+ * via a MutationObserver. Properly restores focus when deactivated.
+ *
+ * Ported from [solid-focus-trap](https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap)
+ * by Jasmin Noetzli (GiyoMoon), adapted for Solid.js 2.0.
+ *
+ * @example
+ * ```tsx
+ * const [ref, setRef] = createSignal(null);
+ * createFocusTrap({ element: ref, enabled: () => isOpen() });
+ * ...
+ * ```
+ */
+export const createFocusTrap = (props: CreateFocusTrapProps): void => {
+ const [focusableElements, setFocusableElements] = createSignal(
+ null,
+ INTERNAL_OPTIONS,
+ );
+
+ const firstFocusElement = createMemo(() => {
+ const els = focusableElements();
+ return els ? (els[0] ?? null) : null;
+ });
+
+ const lastFocusElement = createMemo(() => {
+ const els = focusableElements();
+ return els ? (els[els.length - 1] ?? null) : null;
+ });
+
+ let originalFocusedElement: HTMLElement | null = null;
+
+ const loadFocusableElements = (container: HTMLElement) => {
+ const sorted = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR))
+ .map((element, domIndex) => ({ element, domIndex, tabIndex: element.tabIndex }))
+ .sort((a, b) =>
+ a.tabIndex === b.tabIndex ? a.domIndex - b.domIndex : a.tabIndex - b.tabIndex,
+ );
+ setFocusableElements(sorted.map(({ element }) => element));
+ };
+
+ const triggerInitialFocus = (container: HTMLElement) => {
+ afterPaint(() => {
+ const target = access(props.initialFocusElement ?? null) ?? firstFocusElement() ?? container;
+ const { onInitialFocus } = props;
+ if (onInitialFocus) {
+ const event = new CustomEvent(EVENT_INITIAL_FOCUS, EVENT_OPTIONS);
+ container.addEventListener(EVENT_INITIAL_FOCUS, onInitialFocus);
+ container.dispatchEvent(event);
+ container.removeEventListener(EVENT_INITIAL_FOCUS, onInitialFocus);
+ if (event.defaultPrevented) return;
+ }
+ target.focus();
+ });
+ };
+
+ const triggerRestoreFocus = (container: HTMLElement) => {
+ afterPaint(() => {
+ if (!access(props.restoreFocus ?? true)) return;
+ const target = access(props.finalFocusElement ?? null) ?? originalFocusedElement;
+ if (!target) return;
+ const { onFinalFocus } = props;
+ if (onFinalFocus) {
+ const event = new CustomEvent(EVENT_FINAL_FOCUS, EVENT_OPTIONS);
+ container.addEventListener(EVENT_FINAL_FOCUS, onFinalFocus);
+ container.dispatchEvent(event);
+ container.removeEventListener(EVENT_FINAL_FOCUS, onFinalFocus);
+ if (event.defaultPrevented) return;
+ }
+ target.focus();
+ });
+ };
+
+ const onFirstElementKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Tab" && event.shiftKey) {
+ event.preventDefault();
+ lastFocusElement()!.focus();
+ }
+ };
+
+ const onLastElementKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Tab" && !event.shiftKey) {
+ event.preventDefault();
+ firstFocusElement()!.focus();
+ }
+ };
+
+ const preventTab = (event: KeyboardEvent) => {
+ if (event.key === "Tab") event.preventDefault();
+ };
+
+ // Activate / deactivate the trap when element or enabled changes.
+ createEffect(
+ () => ({
+ container: access(props.element),
+ enabled: access(props.enabled ?? true),
+ observeChanges: access(props.observeChanges ?? true),
+ }),
+ ({ container, enabled, observeChanges }) => {
+ if (!container || !enabled) return;
+
+ originalFocusedElement = document.activeElement as HTMLElement | null;
+ loadFocusableElements(container);
+ triggerInitialFocus(container);
+
+ const observer = new MutationObserver(() => {
+ afterPaint(() => {
+ loadFocusableElements(container);
+ if (!document.activeElement || document.activeElement === document.body) {
+ triggerInitialFocus(container);
+ }
+ });
+ });
+
+ if (observeChanges) {
+ observer.observe(container, {
+ subtree: true,
+ childList: true,
+ attributes: true,
+ attributeFilter: ["tabindex"],
+ });
+ }
+
+ return () => {
+ if (observeChanges) observer.disconnect();
+ setFocusableElements(null);
+ triggerRestoreFocus(container);
+ };
+ },
+ );
+
+ // When there are no focusable elements, block all Tab key presses.
+ createEffect(
+ () => focusableElements(),
+ elements => {
+ if (elements === null || elements.length !== 0) return;
+ document.addEventListener("keydown", preventTab);
+ return () => document.removeEventListener("keydown", preventTab);
+ },
+ );
+
+ // Shift+Tab on the first element → wrap to last.
+ createEffect(
+ () => firstFocusElement(),
+ el => {
+ if (!el) return;
+ el.addEventListener("keydown", onFirstElementKeyDown);
+ return () => el.removeEventListener("keydown", onFirstElementKeyDown);
+ },
+ );
+
+ // Tab on the last element → wrap to first.
+ createEffect(
+ () => lastFocusElement(),
+ el => {
+ if (!el) return;
+ el.addEventListener("keydown", onLastElementKeyDown);
+ return () => el.removeEventListener("keydown", onLastElementKeyDown);
+ },
+ );
+};
diff --git a/packages/focus/src/index.ts b/packages/focus/src/index.ts
index 5a5cf0e06..bc812c568 100644
--- a/packages/focus/src/index.ts
+++ b/packages/focus/src/index.ts
@@ -1,65 +1,4 @@
-import { createEffect, onSettled, type Accessor } from "solid-js";
-import type { JSX } from "@solidjs/web";
-import { type FalsyValue } from "@solid-primitives/utils";
-
-/**
- * Ref callback factory to autofocus an element on render.
- * Uses the native `autofocus` attribute to determine whether to focus.
- *
- * To disable autofocus, simply omit the `autofocus` attribute on the element —
- * no `enabled` parameter is needed or provided.
- *
- * @returns Ref callback to attach to the element.
- *
- * @example
- * ```tsx
- * Autofocused
- * ```
- */
-export const autofocus = () => {
- let el: HTMLElement | undefined;
-
- onSettled(() => {
- if (!el?.hasAttribute("autofocus")) return;
- const id = setTimeout(() => el?.focus());
- return () => clearTimeout(id);
- });
-
- return (element: HTMLElement) => {
- el = element;
- };
-};
-
-/**
- * Creates a new reactive primitive for autofocusing the element on render.
- *
- * @param ref - Element to focus.
- *
- * @example
- * ```ts
- * let ref!: HTMLButtonElement;
- *
- * createAutofocus(() => ref);
- *
- * Autofocused ;
- *
- * // Using ref signal
- * const [ref, setRef] = createSignal();
- * createAutofocus(ref);
- *
- * Autofocused ;
- * ```
- */
-export const createAutofocus = (ref: Accessor) => {
- createEffect(
- () => ref(),
- el => {
- if (!el) return;
- const id = setTimeout(() => el.focus());
- return () => clearTimeout(id);
- },
- );
-};
-
-// only here so the `JSX` import won't be shaken off the tree:
-export type E = JSX.Element;
+export { autofocus, createAutofocus } from "./autofocus.js";
+export type { E } from "./autofocus.js";
+export { createFocusTrap } from "./focusTrap.js";
+export type { CreateFocusTrapProps } from "./focusTrap.js";
diff --git a/packages/focus/test/index.test.tsx b/packages/focus/test/index.test.tsx
index d7bc0b901..d1c8aa578 100644
--- a/packages/focus/test/index.test.tsx
+++ b/packages/focus/test/index.test.tsx
@@ -1,16 +1,25 @@
import { describe, test, expect, vi, beforeEach, afterAll, beforeAll } from "vitest";
import { createRoot, createSignal, flush } from "solid-js";
-import { autofocus, createAutofocus } from "../src/index.js";
+import { autofocus, createAutofocus, createFocusTrap } from "../src/index.js";
+
+// ─── Shared focus tracking ────────────────────────────────────────────────────
let focused: HTMLElement | null = null;
const original_focus = HTMLElement.prototype.focus;
-HTMLElement.prototype.focus = function (this) {
+HTMLElement.prototype.focus = function (this: HTMLElement) {
focused = this;
};
+// ─── Fake timers + rAF stub ───────────────────────────────────────────────────
+
beforeAll(() => {
vi.useFakeTimers();
+ // afterPaint uses double rAF; stub it as setTimeout so vi.runAllTimers() drives it.
+ vi.stubGlobal("requestAnimationFrame", (fn: FrameRequestCallback) =>
+ setTimeout(() => fn(performance.now()), 0),
+ );
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => clearTimeout(id));
});
beforeEach(() => {
@@ -20,31 +29,37 @@ beforeEach(() => {
afterAll(() => {
vi.useRealTimers();
+ vi.unstubAllGlobals();
HTMLElement.prototype.focus = original_focus;
});
+// ─── Helper ───────────────────────────────────────────────────────────────────
+
+/** Run all pending effects then drain all timers (including nested rAFs). */
+const settle = () => {
+ flush();
+ vi.runAllTimers();
+};
+
+// ─── autofocus ────────────────────────────────────────────────────────────────
+
describe("autofocus", () => {
test("focuses the element with autofocus attribute", () => {
const el = document.createElement("button");
el.setAttribute("autofocus", "");
const dispose = createRoot(dispose => {
- // Phase 1: factory registers onSettled
const ref = autofocus();
- // Phase 2: ref callback receives the element
ref(el);
return dispose;
});
- flush();
- expect(focused).toBe(null);
- vi.runAllTimers();
+ settle();
expect(focused).toBe(el);
-
dispose();
});
- test("doesn't focus when autofocus HTML attribute is absent", () => {
+ test("doesn't focus when autofocus attribute is absent", () => {
const el = document.createElement("button");
const dispose = createRoot(dispose => {
@@ -53,35 +68,30 @@ describe("autofocus", () => {
return dispose;
});
- flush();
+ settle();
expect(focused).toBe(null);
- vi.runAllTimers();
- expect(focused).toBe(null);
-
dispose();
});
-
});
+// ─── createAutofocus ──────────────────────────────────────────────────────────
+
describe("createAutofocus", () => {
const el = document.createElement("button"),
el2 = document.createElement("button");
- test("createAutofocus focuses the element", () => {
+ test("focuses the element", () => {
const dispose = createRoot(dispose => {
createAutofocus(() => el);
return dispose;
});
- flush();
- expect(focused).toBe(null);
- vi.runAllTimers();
+ settle();
expect(focused).toBe(el);
-
dispose();
});
- test("createAutofocus works with signal", () => {
+ test("works with signal — focuses when signal is set", () => {
const [ref, setRef] = createSignal();
const dispose = createRoot(dispose => {
@@ -89,28 +99,340 @@ describe("createAutofocus", () => {
return dispose;
});
- flush();
- expect(focused).toBe(null);
- vi.runAllTimers();
+ settle();
expect(focused).toBe(null);
setRef(el);
- flush();
- expect(focused).toBe(null);
- vi.runAllTimers();
+ settle();
expect(focused).toBe(el);
setRef(el2);
- flush();
- expect(focused).toBe(el);
- vi.runAllTimers();
+ settle();
expect(focused).toBe(el2);
dispose();
setRef(el);
- expect(focused).toBe(el2);
vi.runAllTimers();
- expect(focused).toBe(el2);
+ expect(focused).toBe(el2); // no focus after dispose
+ });
+});
+
+// ─── createFocusTrap ──────────────────────────────────────────────────────────
+
+/** Build a container with `n` focusable buttons and return them. */
+function makeContainer(n: number): { container: HTMLElement; buttons: HTMLButtonElement[] } {
+ const container = document.createElement("div");
+ const buttons: HTMLButtonElement[] = [];
+ for (let i = 0; i < n; i++) {
+ const btn = document.createElement("button");
+ btn.textContent = `btn${i}`;
+ container.appendChild(btn);
+ buttons.push(btn);
+ }
+ return { container, buttons };
+}
+
+function tabKey(shiftKey = false) {
+ return new KeyboardEvent("keydown", { key: "Tab", shiftKey, bubbles: true, cancelable: true });
+}
+
+describe("createFocusTrap", () => {
+ test("focuses the first focusable element on activation", () => {
+ const { container, buttons } = makeContainer(3);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(buttons[0]);
+ dispose();
+ });
+
+ test("Tab on last element wraps to first", () => {
+ const { container, buttons } = makeContainer(3);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container });
+ return dispose;
+ });
+
+ settle();
+ buttons[2]!.dispatchEvent(tabKey(false));
+ expect(focused).toBe(buttons[0]);
+ dispose();
+ });
+
+ test("Shift+Tab on first element wraps to last", () => {
+ const { container, buttons } = makeContainer(3);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container });
+ return dispose;
+ });
+
+ settle();
+ buttons[0]!.dispatchEvent(tabKey(true));
+ expect(focused).toBe(buttons[2]);
+ dispose();
+ });
+
+ test("blocks Tab when there are no focusable elements", () => {
+ const container = document.createElement("div"); // no children
+
+ let tabPrevented = false;
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container });
+ return dispose;
+ });
+
+ flush(); // run effects so preventTab listener is added
+
+ const event = tabKey();
+ Object.defineProperty(event, "defaultPrevented", { get: () => tabPrevented });
+ const originalPreventDefault = event.preventDefault.bind(event);
+ event.preventDefault = () => {
+ tabPrevented = true;
+ originalPreventDefault();
+ };
+
+ document.dispatchEvent(event);
+ expect(tabPrevented).toBe(true);
+ dispose();
+ });
+
+ test("does not activate when enabled is false", () => {
+ const { container } = makeContainer(2);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, enabled: false });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(null);
+ dispose();
+ });
+
+ test("activates and deactivates reactively via enabled signal", () => {
+ const { container, buttons } = makeContainer(2);
+ const [enabled, setEnabled] = createSignal(false);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, enabled });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(null); // not yet enabled
+
+ setEnabled(true);
+ settle();
+ expect(focused).toBe(buttons[0]); // initial focus
+
+ dispose();
+ });
+
+ test("restores focus to the previously focused element on deactivation", () => {
+ const { container, buttons } = makeContainer(2);
+ const trigger = document.createElement("button");
+ const [enabled, setEnabled] = createSignal(true);
+
+ // Pretend `trigger` is the element that was focused before the trap.
+ const origActiveElement = Object.getOwnPropertyDescriptor(Document.prototype, "activeElement")!;
+ Object.defineProperty(document, "activeElement", {
+ get: () => trigger,
+ configurable: true,
+ });
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, enabled });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(buttons[0]); // initial focus inside trap
+
+ // Restore the real activeElement descriptor before deactivating
+ Object.defineProperty(document, "activeElement", origActiveElement);
+
+ setEnabled(false);
+ settle();
+ expect(focused).toBe(trigger); // focus restored
+ dispose();
+ });
+
+ test("uses initialFocusElement when provided", () => {
+ const { container, buttons } = makeContainer(3);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, initialFocusElement: buttons[2] });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(buttons[2]);
+ dispose();
+ });
+
+ test("uses finalFocusElement when provided on deactivation", () => {
+ const { container } = makeContainer(2);
+ const customFinal = document.createElement("button");
+ const [enabled, setEnabled] = createSignal(true);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, enabled, finalFocusElement: customFinal });
+ return dispose;
+ });
+
+ settle();
+
+ setEnabled(false);
+ settle();
+ expect(focused).toBe(customFinal);
+ dispose();
+ });
+
+ test("onInitialFocus callback is called when trap activates", () => {
+ const { container } = makeContainer(1);
+ const onInitialFocus = vi.fn();
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, onInitialFocus });
+ return dispose;
+ });
+
+ settle();
+ expect(onInitialFocus).toHaveBeenCalledOnce();
+ dispose();
+ });
+
+ test("onInitialFocus preventDefault suppresses initial focus", () => {
+ const { container } = makeContainer(1);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({
+ element: container,
+ onInitialFocus: e => e.preventDefault(),
+ });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(null);
+ dispose();
+ });
+
+ test("onFinalFocus callback is called on deactivation", () => {
+ const { container } = makeContainer(1);
+ const [enabled, setEnabled] = createSignal(true);
+ const onFinalFocus = vi.fn();
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, enabled, onFinalFocus });
+ return dispose;
+ });
+
+ settle();
+ setEnabled(false);
+ settle();
+ expect(onFinalFocus).toHaveBeenCalledOnce();
+ dispose();
+ });
+
+ test("onFinalFocus preventDefault suppresses focus restore", () => {
+ const { container } = makeContainer(1);
+ const trigger = document.createElement("button");
+ const [enabled, setEnabled] = createSignal(true);
+
+ const origActiveElement = Object.getOwnPropertyDescriptor(Document.prototype, "activeElement")!;
+ Object.defineProperty(document, "activeElement", {
+ get: () => trigger,
+ configurable: true,
+ });
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({
+ element: container,
+ enabled,
+ onFinalFocus: e => e.preventDefault(),
+ });
+ return dispose;
+ });
+
+ settle();
+ Object.defineProperty(document, "activeElement", origActiveElement);
+
+ focused = null;
+ setEnabled(false);
+ settle();
+ expect(focused).toBe(null); // prevented
+ dispose();
+ });
+
+ test("does not restore focus when restoreFocus is false", () => {
+ const { container } = makeContainer(1);
+ const [enabled, setEnabled] = createSignal(true);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container, enabled, restoreFocus: false });
+ return dispose;
+ });
+
+ settle();
+ focused = null;
+ setEnabled(false);
+ settle();
+ expect(focused).toBe(null);
+ dispose();
+ });
+
+ test("respects tabIndex ordering for focusable elements", () => {
+ const container = document.createElement("div");
+ const a = document.createElement("button"); // tabIndex 0
+ const b = document.createElement("button");
+ b.tabIndex = 2;
+ const c = document.createElement("button");
+ c.tabIndex = 1;
+ // DOM order: a(0), b(2), c(1) → sorted: a(0), c(1), b(2)
+ container.append(a, b, c);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: container });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(a); // first by tabIndex order
+
+ // Tab on last (b, tabIndex=2) wraps to first (a, tabIndex=0)
+ b.dispatchEvent(tabKey(false));
+ expect(focused).toBe(a);
+
+ // Shift+Tab on first (a) wraps to last (b)
+ a.dispatchEvent(tabKey(true));
+ expect(focused).toBe(b);
+
+ dispose();
+ });
+
+ test("element as reactive signal — activates when signal becomes non-null", () => {
+ const { container, buttons } = makeContainer(2);
+ const [el, setEl] = createSignal(null);
+
+ const dispose = createRoot(dispose => {
+ createFocusTrap({ element: el });
+ return dispose;
+ });
+
+ settle();
+ expect(focused).toBe(null);
+
+ setEl(container);
+ settle();
+ expect(focused).toBe(buttons[0]);
+ dispose();
});
});
diff --git a/packages/focus/test/server.test.ts b/packages/focus/test/server.test.ts
index d8c2b5f8b..9b4ae4800 100644
--- a/packages/focus/test/server.test.ts
+++ b/packages/focus/test/server.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { createRoot } from "solid-js";
-import { createAutofocus } from "../src/index.js";
+import { createAutofocus, createFocusTrap } from "../src/index.js";
describe("API doesn't break in SSR", () => {
it("createAutofocus() - SSR", () => {
@@ -9,4 +9,11 @@ describe("API doesn't break in SSR", () => {
dispose();
});
});
+
+ it("createFocusTrap() - SSR", () => {
+ createRoot(dispose => {
+ expect(() => createFocusTrap({ element: null })).not.toThrow();
+ dispose();
+ });
+ });
});
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 7c5e33cb1..49c91bdd5 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -311,6 +311,16 @@ export function handleDiffArray(
}
}
+/**
+ * Schedules `fn` to run after the browser has painted by nesting two
+ * requestAnimationFrame calls. No-op in non-browser environments.
+ */
+export const afterPaint = (fn: () => void): void => {
+ if (typeof requestAnimationFrame === "function") {
+ requestAnimationFrame(() => requestAnimationFrame(fn));
+ }
+};
+
// ─── String transforms ────────────────────────────────────────────────────────
/**
From fe09eb93c3c6bc831e154e2698ac1232ac4a0fc9 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Wed, 13 May 2026 12:15:30 -0400
Subject: [PATCH 3/7] Move createFocusSignal to focus package out of
active-element
---
packages/active-element/package.json | 3 +-
packages/active-element/src/index.ts | 59 ++-------------------
packages/active-element/test/index.test.ts | 47 +---------------
packages/active-element/test/server.test.ts | 9 +---
packages/focus/package.json | 5 +-
packages/focus/src/focusSignal.ts | 52 ++++++++++++++++++
packages/focus/src/index.ts | 1 +
packages/focus/tsconfig.json | 3 ++
pnpm-lock.yaml | 3 ++
pnpm-workspace.yaml | 4 ++
10 files changed, 74 insertions(+), 112 deletions(-)
create mode 100644 packages/focus/src/focusSignal.ts
diff --git a/packages/active-element/package.json b/packages/active-element/package.json
index 0c43a6a2a..728c38941 100644
--- a/packages/active-element/package.json
+++ b/packages/active-element/package.json
@@ -16,8 +16,7 @@
"name": "active-element",
"stage": 3,
"list": [
- "createActiveElement",
- "createFocusSignal"
+ "createActiveElement"
],
"category": "Inputs"
},
diff --git a/packages/active-element/src/index.ts b/packages/active-element/src/index.ts
index 307bcb7c3..f32293dab 100644
--- a/packages/active-element/src/index.ts
+++ b/packages/active-element/src/index.ts
@@ -1,11 +1,7 @@
import { type Accessor, type JSX } from "solid-js";
import { isServer } from "solid-js/web";
-import {
- type MaybeAccessor,
- type Directive,
- createHydratableSignal,
-} from "@solid-primitives/utils";
-import { makeEventListener, createEventListener } from "@solid-primitives/event-listener";
+import { type Directive, createHydratableSignal } from "@solid-primitives/utils";
+import { makeEventListener } from "@solid-primitives/event-listener";
declare module "solid-js" {
namespace JSX {
@@ -60,54 +56,6 @@ export function createActiveElement(): Accessor {
return active;
}
-/**
- * Attaches "blur" and "focus" event listeners to the element.
- * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/active-element#makeFocusListener
- * @param target element
- * @param callback handle focus change
- * @param useCapture activates capturing, which allows to listen on events at the root that don't support bubbling.
- * @returns function for clearing event listeners
- * @example
- * const [isFocused, setIsFocused] = createSignal(false)
- * const clear = makeFocusListener(focused => setIsFocused(focused));
- * // remove listeners (happens also on cleanup)
- * clear();
- */
-export function makeFocusListener(
- target: Element,
- callback: (isActive: boolean) => void,
- useCapture = true,
-): VoidFunction {
- if (isServer) {
- return () => void 0;
- }
- const clear1 = makeEventListener(target, "blur", callback.bind(void 0, false), useCapture);
- const clear2 = makeEventListener(target, "focus", callback.bind(void 0, true), useCapture);
- return () => (clear1(), clear2());
-}
-
-/**
- * Provides a signal representing element's focus state.
- * @param target element or a reactive function returning one
- * @returns boolean signal representing element's focus state
- * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/active-element#createFocusSignal
- * @example
- * const isFocused = createFocusSignal(() => el)
- * isFocused() // T: boolean
- */
-export function createFocusSignal(target: MaybeAccessor): Accessor {
- if (isServer) {
- return () => false;
- }
- const [isActive, setIsActive] = createHydratableSignal(
- false,
- () => document.activeElement === target,
- );
- createEventListener(target, "blur", () => setIsActive(false), true);
- createEventListener(target, "focus", () => setIsActive(true), true);
- return isActive;
-}
-
/**
* A directive that notifies you when the element becomes active or inactive.
*
@@ -123,5 +71,6 @@ export const focus: Directive<(isActive: boolean) => void> = (target, props) =>
}
const callback = props();
callback(document.activeElement === target);
- makeFocusListener(target, callback);
+ makeEventListener(target, "blur", callback.bind(void 0, false), true);
+ makeEventListener(target, "focus", callback.bind(void 0, true), true);
};
diff --git a/packages/active-element/test/index.test.ts b/packages/active-element/test/index.test.ts
index de3e574ac..a47b39bcc 100644
--- a/packages/active-element/test/index.test.ts
+++ b/packages/active-element/test/index.test.ts
@@ -1,12 +1,6 @@
import { createRoot } from "solid-js";
import { describe, test, expect } from "vitest";
-import {
- makeActiveElementListener,
- createActiveElement,
- makeFocusListener,
- createFocusSignal,
- focus,
-} from "../src/index.js";
+import { makeActiveElementListener, createActiveElement, focus } from "../src/index.js";
const dispatchFocusEvent = (
target: Element | Window = window,
@@ -40,29 +34,6 @@ describe("makeActiveElementListener", () => {
}));
});
-describe("makeFocusListener", () => {
- test("works properly", () =>
- createRoot(dispose => {
- const el = document.createElement("div");
- const captured: any[] = [];
- const clear = makeFocusListener(el, e => captured.push(e));
- expect(captured).toEqual([]);
- dispatchFocusEvent(el, "focus");
- expect(captured).toEqual([true]);
- dispatchFocusEvent(el, "blur");
- expect(captured).toEqual([true, false]);
- clear();
- dispatchFocusEvent(el, "focus");
- expect(captured).toEqual([true, false]);
- makeFocusListener(el, e => captured.push(e));
- dispatchFocusEvent(el, "blur");
- expect(captured).toEqual([true, false, false]);
- dispose();
- dispatchFocusEvent(el, "focus");
- expect(captured).toEqual([true, false, false]);
- }));
-});
-
describe("createActiveElement", () => {
test("works properly", () =>
createRoot(dispose => {
@@ -72,22 +43,6 @@ describe("createActiveElement", () => {
}));
});
-describe("createFocusSignal", () => {
- test("works properly", () =>
- createRoot(dispose => {
- const el = document.createElement("div");
- const activeEl = createFocusSignal(el);
- expect(activeEl()).toBe(false);
- dispatchFocusEvent(el, "focus");
- expect(activeEl()).toBe(true);
- dispatchFocusEvent(el, "blur");
- expect(activeEl()).toBe(false);
- dispose();
- dispatchFocusEvent(el, "focus");
- expect(activeEl()).toBe(false);
- }));
-});
-
describe("use:focus", () => {
test("works properly", () =>
createRoot(dispose => {
diff --git a/packages/active-element/test/server.test.ts b/packages/active-element/test/server.test.ts
index 830c9fb3d..558d9566d 100644
--- a/packages/active-element/test/server.test.ts
+++ b/packages/active-element/test/server.test.ts
@@ -1,8 +1,7 @@
import { describe, test, expect, vi } from "vitest";
-import { makeActiveElementListener, createActiveElement, createFocusSignal } from "../src/index.js";
+import { makeActiveElementListener, createActiveElement } from "../src/index.js";
describe("API doesn't break in SSR", () => {
- // check if the API doesn't throw when calling it in SSR
test("makeActiveElementListener() - SSR", () => {
const cb = vi.fn();
expect(() => makeActiveElementListener(cb)).not.toThrow();
@@ -12,10 +11,4 @@ describe("API doesn't break in SSR", () => {
test("createActiveElement() - SSR", () => {
expect(() => createActiveElement()).not.toThrow();
});
-
- test("createFocusSignal() - SSR", () => {
- const el = vi.fn();
- expect(() => createFocusSignal(el)).not.toThrow();
- expect(el).not.toBeCalled();
- });
});
diff --git a/packages/focus/package.json b/packages/focus/package.json
index 02e232612..14b74bf26 100644
--- a/packages/focus/package.json
+++ b/packages/focus/package.json
@@ -24,7 +24,9 @@
"list": [
"autofocus",
"createAutofocus",
- "createFocusTrap"
+ "createFocusTrap",
+ "makeFocusListener",
+ "createFocusSignal"
],
"category": "Inputs"
},
@@ -66,6 +68,7 @@
"solid-js": "^2.0.0-beta.12"
},
"dependencies": {
+ "@solid-primitives/event-listener": "workspace:^",
"@solid-primitives/utils": "workspace:^"
},
"typesVersions": {},
diff --git a/packages/focus/src/focusSignal.ts b/packages/focus/src/focusSignal.ts
new file mode 100644
index 000000000..1b6e90426
--- /dev/null
+++ b/packages/focus/src/focusSignal.ts
@@ -0,0 +1,52 @@
+import { type Accessor } from "solid-js";
+import { isServer } from "@solidjs/web";
+import { type MaybeAccessor, createHydratableSignal } from "@solid-primitives/utils";
+import { makeEventListener, createEventListener } from "@solid-primitives/event-listener";
+
+/**
+ * Attaches "blur" and "focus" event listeners to the element.
+ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#makeFocusListener
+ * @param target element
+ * @param callback handle focus change
+ * @param useCapture activates capturing, which allows to listen on events at the root that don't support bubbling.
+ * @returns function for clearing event listeners
+ * @example
+ * const [isFocused, setIsFocused] = createSignal(false)
+ * const clear = makeFocusListener(el, focused => setIsFocused(focused));
+ * // remove listeners (happens also on cleanup)
+ * clear();
+ */
+export function makeFocusListener(
+ target: Element,
+ callback: (isActive: boolean) => void,
+ useCapture = true,
+): VoidFunction {
+ if (isServer) {
+ return () => void 0;
+ }
+ const clear1 = makeEventListener(target, "blur", callback.bind(void 0, false), useCapture);
+ const clear2 = makeEventListener(target, "focus", callback.bind(void 0, true), useCapture);
+ return () => (clear1(), clear2());
+}
+
+/**
+ * Provides a signal representing element's focus state.
+ * @param target element or a reactive function returning one
+ * @returns boolean signal representing element's focus state
+ * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/focus#createFocusSignal
+ * @example
+ * const isFocused = createFocusSignal(() => el)
+ * isFocused() // T: boolean
+ */
+export function createFocusSignal(target: MaybeAccessor): Accessor {
+ if (isServer) {
+ return () => false;
+ }
+ const [isActive, setIsActive] = createHydratableSignal(
+ false,
+ () => document.activeElement === target,
+ );
+ createEventListener(target, "blur", () => setIsActive(false), true);
+ createEventListener(target, "focus", () => setIsActive(true), true);
+ return isActive;
+}
diff --git a/packages/focus/src/index.ts b/packages/focus/src/index.ts
index bc812c568..678243b0b 100644
--- a/packages/focus/src/index.ts
+++ b/packages/focus/src/index.ts
@@ -2,3 +2,4 @@ export { autofocus, createAutofocus } from "./autofocus.js";
export type { E } from "./autofocus.js";
export { createFocusTrap } from "./focusTrap.js";
export type { CreateFocusTrapProps } from "./focusTrap.js";
+export { makeFocusListener, createFocusSignal } from "./focusSignal.js";
diff --git a/packages/focus/tsconfig.json b/packages/focus/tsconfig.json
index dc1970e16..b9b2b6782 100644
--- a/packages/focus/tsconfig.json
+++ b/packages/focus/tsconfig.json
@@ -6,6 +6,9 @@
"rootDir": "src"
},
"references": [
+ {
+ "path": "../event-listener"
+ },
{
"path": "../utils"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1623f9551..acb201640 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -334,6 +334,9 @@ importers:
packages/focus:
dependencies:
+ '@solid-primitives/event-listener':
+ specifier: workspace:^
+ version: link:../event-listener
'@solid-primitives/utils':
specifier: workspace:^
version: link:../utils
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index f621dcf45..92f944bbb 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,6 +2,10 @@ packages:
- packages/*
- site
+allowBuilds:
+ '@parcel/watcher': set this to true or false
+ esbuild: set this to true or false
+
onlyBuiltDependencies:
- "@parcel/watcher"
- esbuild
From 5a89eb75d3180e6c0be4050c3134a2e94a5600e9 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Wed, 13 May 2026 12:19:03 -0400
Subject: [PATCH 4/7] Bumped active-element to beta 12
---
packages/active-element/package.json | 6 ++++--
packages/active-element/src/index.ts | 4 ++--
pnpm-lock.yaml | 7 +++++--
3 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/packages/active-element/package.json b/packages/active-element/package.json
index 728c38941..2230c4aa4 100644
--- a/packages/active-element/package.json
+++ b/packages/active-element/package.json
@@ -54,10 +54,12 @@
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
- "solid-js": "^1.6.12"
+ "@solidjs/web": "^2.0.0-beta.12",
+ "solid-js": "^2.0.0-beta.12"
},
"typesVersions": {},
"devDependencies": {
- "solid-js": "^1.9.7"
+ "@solidjs/web": "2.0.0-beta.12",
+ "solid-js": "2.0.0-beta.12"
}
}
diff --git a/packages/active-element/src/index.ts b/packages/active-element/src/index.ts
index f32293dab..4afc9b0d2 100644
--- a/packages/active-element/src/index.ts
+++ b/packages/active-element/src/index.ts
@@ -1,5 +1,5 @@
import { type Accessor, type JSX } from "solid-js";
-import { isServer } from "solid-js/web";
+import { isServer } from "@solidjs/web";
import { type Directive, createHydratableSignal } from "@solid-primitives/utils";
import { makeEventListener } from "@solid-primitives/event-listener";
@@ -11,7 +11,7 @@ declare module "solid-js" {
}
}
// This ensures the `JSX` import won't fall victim to tree shaking
-export type E = JSX.Element;
+export type E = JSX.Directives;
const getActiveElement = () =>
document.activeElement === document.body ? null : document.activeElement;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index acb201640..717822004 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,9 +96,12 @@ importers:
specifier: workspace:^
version: link:../utils
devDependencies:
+ '@solidjs/web':
+ specifier: 2.0.0-beta.12
+ version: 2.0.0-beta.12(solid-js@2.0.0-beta.12)
solid-js:
- specifier: ^1.9.7
- version: 1.9.7
+ specifier: 2.0.0-beta.12
+ version: 2.0.0-beta.12
packages/analytics:
devDependencies:
From 1e9a524006c8197968bd81e3b7f7966f3a47a57b Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Wed, 13 May 2026 12:20:51 -0400
Subject: [PATCH 5/7] Added proper changesets
---
.changeset/active-element-solid2-migration.md | 18 ++++++++++++++++++
.changeset/focus-solid2-migration.md | 19 +++++++++++++++++--
2 files changed, 35 insertions(+), 2 deletions(-)
create mode 100644 .changeset/active-element-solid2-migration.md
diff --git a/.changeset/active-element-solid2-migration.md b/.changeset/active-element-solid2-migration.md
new file mode 100644
index 000000000..5f8139a51
--- /dev/null
+++ b/.changeset/active-element-solid2-migration.md
@@ -0,0 +1,18 @@
+---
+"@solid-primitives/active-element": major
+---
+
+Migrate to Solid.js v2.0 (beta.12)
+
+## Breaking Changes
+
+**Peer dependencies**: `solid-js@^2.0.0-beta.12` and `@solidjs/web@^2.0.0-beta.12` are now required.
+
+- `makeFocusListener` and `createFocusSignal` have moved to `@solid-primitives/focus`. Import them from there instead:
+ ```ts
+ // Before
+ import { makeFocusListener, createFocusSignal } from "@solid-primitives/active-element";
+ // After
+ import { makeFocusListener, createFocusSignal } from "@solid-primitives/focus";
+ ```
+- `isServer` is now sourced from `@solidjs/web` internally (no user-facing API change)
diff --git a/.changeset/focus-solid2-migration.md b/.changeset/focus-solid2-migration.md
index 8720ad291..fb95c2c84 100644
--- a/.changeset/focus-solid2-migration.md
+++ b/.changeset/focus-solid2-migration.md
@@ -2,11 +2,11 @@
"@solid-primitives/focus": major
---
-Migrate to Solid.js v2.0 (beta.10)
+Migrate to Solid.js v2.0 (beta.12)
## Breaking Changes
-**Peer dependencies**: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10` are now required.
+**Peer dependencies**: `solid-js@^2.0.0-beta.12` and `@solidjs/web@^2.0.0-beta.12` are now required.
- `autofocus` is now a **ref callback factory** (`use:autofocus` directive removed; Solid 2.0 no longer supports `use:` directives):
```tsx
@@ -23,3 +23,18 @@ Migrate to Solid.js v2.0 (beta.10)
- `JSX` type is now imported from `@solidjs/web` (was `solid-js`)
- `onMount` replaced by `onSettled` from `solid-js`
- `createAutofocus` uses split `createEffect(compute, apply)` form with proper timeout cleanup on re-focus
+
+## New Primitives
+
+`makeFocusListener` and `createFocusSignal` have moved here from `@solid-primitives/active-element`:
+
+- **`makeFocusListener(target, callback, useCapture?)`** — attaches `focus`/`blur` listeners to an element, calling `callback` with the new boolean focus state. Returns a cleanup function.
+ ```ts
+ const clear = makeFocusListener(el, isFocused => console.log(isFocused));
+ clear(); // remove listeners
+ ```
+- **`createFocusSignal(target)`** — reactive signal that tracks whether `target` is focused.
+ ```ts
+ const isFocused = createFocusSignal(() => el);
+ isFocused(); // boolean
+ ```
From def8ee04c213ed0eb20711cf9448ccba1da32109 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Thu, 14 May 2026 20:06:34 -0400
Subject: [PATCH 6/7] Better types, adapting to 2.0 and adding undefinedes
---
packages/event-listener/src/types.ts | 2 +-
packages/focus/package.json | 4 ++++
packages/focus/src/focusTrap.ts | 14 +++++++-------
packages/utils/src/index.ts | 13 +++++++------
4 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/packages/event-listener/src/types.ts b/packages/event-listener/src/types.ts
index 3394558df..46794c15d 100644
--- a/packages/event-listener/src/types.ts
+++ b/packages/event-listener/src/types.ts
@@ -1,4 +1,4 @@
-import type { JSX } from "solid-js";
+import type { JSX } from "@solidjs/web";
export type EventListenerOptions = boolean | AddEventListenerOptions;
diff --git a/packages/focus/package.json b/packages/focus/package.json
index 14b74bf26..c848eaa19 100644
--- a/packages/focus/package.json
+++ b/packages/focus/package.json
@@ -7,6 +7,10 @@
{
"name": "Jasmin Noetzli",
"url": "https://github.com/GiyoMoon"
+ },
+ {
+ "name": "David Di Biase",
+ "url": "https://github.com/davedbase"
}
],
"license": "MIT",
diff --git a/packages/focus/src/focusTrap.ts b/packages/focus/src/focusTrap.ts
index 669b0312b..06391279e 100644
--- a/packages/focus/src/focusTrap.ts
+++ b/packages/focus/src/focusTrap.ts
@@ -16,7 +16,7 @@ const EVENT_OPTIONS = { bubbles: false, cancelable: true } as const;
export type CreateFocusTrapProps = {
/** Element to trap focus within. */
- element: MaybeAccessor;
+ element: MaybeAccessor;
/** Whether the focus trap is active. Default: `true` */
enabled?: MaybeAccessor;
/**
@@ -28,7 +28,7 @@ export type CreateFocusTrapProps = {
* Element to focus when the trap activates.
* Default: the first focusable element inside `element`.
*/
- initialFocusElement?: MaybeAccessor;
+ initialFocusElement?: MaybeAccessor;
/**
* Restore focus to the element that was focused before the trap activated
* when the trap is deactivated. Default: `true`
@@ -38,7 +38,7 @@ export type CreateFocusTrapProps = {
* Element to focus when the trap deactivates.
* Default: the element that was focused before the trap activated.
*/
- finalFocusElement?: MaybeAccessor;
+ finalFocusElement?: MaybeAccessor;
/**
* Callback fired when focus moves into the trap.
* Call `event.preventDefault()` to suppress the focus move.
@@ -66,8 +66,8 @@ export type CreateFocusTrapProps = {
* ```
*/
export const createFocusTrap = (props: CreateFocusTrapProps): void => {
- const [focusableElements, setFocusableElements] = createSignal(
- null,
+ const [focusableElements, setFocusableElements] = createSignal(
+ undefined,
INTERNAL_OPTIONS,
);
@@ -176,7 +176,7 @@ export const createFocusTrap = (props: CreateFocusTrapProps): void => {
return () => {
if (observeChanges) observer.disconnect();
- setFocusableElements(null);
+ setFocusableElements(undefined);
triggerRestoreFocus(container);
};
},
@@ -186,7 +186,7 @@ export const createFocusTrap = (props: CreateFocusTrapProps): void => {
createEffect(
() => focusableElements(),
elements => {
- if (elements === null || elements.length !== 0) return;
+ if (!elements || elements.length !== 0) return;
document.addEventListener("keydown", preventTab);
return () => document.removeEventListener("keydown", preventTab);
},
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 4c0eab146..1556ee589 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -1,11 +1,11 @@
import {
getOwner,
onCleanup,
+ onSettled,
createSignal,
createStore,
type Accessor,
untrack,
- type AccessorArray,
type EffectFunction,
type ComputeFunction,
type NoInfer,
@@ -15,10 +15,11 @@ import {
type Store,
type StoreSetter,
sharedConfig,
- onMount,
DEV,
- equalFn,
+ isEqual,
} from "solid-js";
+
+type AccessorArray = Accessor[];
// isServer moved from solid-js/web (1.x) to @solidjs/web (2.x).
// typeof window is a universal fallback compatible with both versions.
const isServer = typeof window === "undefined";
@@ -47,11 +48,11 @@ export const noop = (() => void 0) as Noop;
export const trueFn: () => boolean = () => true;
export const falseFn: () => boolean = () => false;
-/** @deprecated use {@link equalFn} from "solid-js" */
-export const defaultEquals = equalFn;
+/** @deprecated use {@link isEqual} from "solid-js" */
+export const defaultEquals = isEqual;
export const EQUALS_FALSE_OPTIONS = { equals: false } as const satisfies SignalOptions;
-export const INTERNAL_OPTIONS = { internal: true } as const satisfies SignalOptions;
+export const INTERNAL_OPTIONS = {} as const satisfies SignalOptions;
/**
* Check if the value is an instance of ___
From 669039f76ee56217e85da7e4efc006c63994d502 Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Thu, 14 May 2026 20:56:10 -0400
Subject: [PATCH 7/7] =?UTF-8?q?Jeremy=20wants=20undefined=20instead=20of?=
=?UTF-8?q?=20void=200=20since=20it=E2=80=99s=20cursed=20:p?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/focus/src/focusSignal.ts | 6 +++---
packages/utils/src/index.ts | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/focus/src/focusSignal.ts b/packages/focus/src/focusSignal.ts
index 1b6e90426..c24e09e6c 100644
--- a/packages/focus/src/focusSignal.ts
+++ b/packages/focus/src/focusSignal.ts
@@ -22,10 +22,10 @@ export function makeFocusListener(
useCapture = true,
): VoidFunction {
if (isServer) {
- return () => void 0;
+ return () => {};
}
- const clear1 = makeEventListener(target, "blur", callback.bind(void 0, false), useCapture);
- const clear2 = makeEventListener(target, "focus", callback.bind(void 0, true), useCapture);
+ const clear1 = makeEventListener(target, "blur", callback.bind(undefined, false), useCapture);
+ const clear2 = makeEventListener(target, "focus", callback.bind(undefined, true), useCapture);
return () => (clear1(), clear2());
}
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 1556ee589..8213e539d 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -44,7 +44,7 @@ export const isDev = isClient && !!DEV;
export const isProd = !isDev;
/** no operation */
-export const noop = (() => void 0) as Noop;
+export const noop = (() => {}) as Noop;
export const trueFn: () => boolean = () => true;
export const falseFn: () => boolean = () => false;