From a25fa8809eaa64e5e8d02ffce8a9498c263eeb7b Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:00:40 -0400 Subject: [PATCH 1/7] First test of migrating to 2.0 --- .changeset/fullscreen-solid-2-migration.md | 28 +++++++ packages/fullscreen/README.md | 68 +++++++++++------ packages/fullscreen/package.json | 8 +- packages/fullscreen/src/index.ts | 68 +++++++++-------- packages/fullscreen/test/index.test.ts | 63 +++++++++------- packages/fullscreen/test/setup.ts | 15 ++-- packages/utils/package.json | 6 +- packages/utils/src/index.ts | 2 +- pnpm-lock.yaml | 88 +++++++++++++++++----- 9 files changed, 237 insertions(+), 109 deletions(-) create mode 100644 .changeset/fullscreen-solid-2-migration.md diff --git a/.changeset/fullscreen-solid-2-migration.md b/.changeset/fullscreen-solid-2-migration.md new file mode 100644 index 000000000..65658d07b --- /dev/null +++ b/.changeset/fullscreen-solid-2-migration.md @@ -0,0 +1,28 @@ +--- +"@solid-primitives/fullscreen": major +--- + +Migrate `@solid-primitives/fullscreen` to Solid.js 2.0 (beta.7). + +**Breaking changes:** + +- Peer dependency updated from `solid-js ^1.6.12` to `solid-js ^2.0.0-beta.7` and `@solidjs/web ^2.0.0-beta.7`. +- The `use:createFullscreen` JSX directive (Solid 1.x `use:` namespace) is removed. Use the new `fullscreen()` ref directive factory instead: + + ```tsx + // Before (Solid 1.x) +
+ + // After (Solid 2.0) +
+ ``` + +**New exports:** + +- `fullscreen(active?, options?)` — ref directive factory that wraps `createFullscreen` for direct use on JSX elements via the `ref` prop. + +**Internal changes:** + +- `isServer` now imported from `@solidjs/web` (was `solid-js/web`). +- `createEffect` updated to Solid 2.0 split compute/effect signature. +- Test mock fixed: `document.fullscreenElement` is now a dynamic getter reflecting current fullscreen state; `document.exitFullscreen` now dispatches `fullscreenchange` matching browser behaviour. diff --git a/packages/fullscreen/README.md b/packages/fullscreen/README.md index c224fb7b8..e368c16a7 100644 --- a/packages/fullscreen/README.md +++ b/packages/fullscreen/README.md @@ -8,56 +8,78 @@ [![size](https://img.shields.io/npm/v/@solid-primitives/fullscreen?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/fullscreen) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) -Creates a primitive wrapper around the Fullscreen API that can either be used as a directive or a primitive. +Reactive wrapper around the [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API) that integrates with Solid's reactive system. ## Installation -``` +```bash npm install @solid-primitives/fullscreen # or -yarn add @solid-primitives/fullscreen +pnpm add @solid-primitives/fullscreen ``` ## How to use it -### createFullScreen +### `createFullscreen` + +Reactively toggles fullscreen on a target element. Returns an `Accessor` reflecting whether the element is currently in fullscreen. ```ts -const MyComponent2: Component = () => { +const isActive: Accessor = createFullscreen( + ref: HTMLElement | Accessor, + active?: Accessor, + options?: FullscreenOptions +); +``` + +**Via ref signal:** + +```tsx +const MyComponent: Component = () => { const [fs, setFs] = createSignal(false); - let [ref, setRef] = createSignal(); - const active: Accessor = createFullscreen(ref, fs); + const [ref, setRef] = createSignal(); + const active = createFullscreen(ref, fs); + return ( -
setFs(fs => !fs)}> - {!active() ? "click to fullscreen" : "click to exit fullscreen"} +
setFs(f => !f)}> + {active() ? "click to exit fullscreen" : "click to fullscreen"}
); }; ``` -This variant requires the ref to be a signal, otherwise the not-yet-filled ref will be captured in the closure of the primitive. +The ref must be a signal so the element is captured reactively rather than in a stale closure. -You can either put the options into the second argument accessor output (useful for the directive use case) or as a third argument. +You can pass `FullscreenOptions` either inside the `active` accessor return value or as a third argument: -### Directive +```tsx +// Options via active accessor (useful for programmatic control): +createFullscreen(ref, () => ({ navigationUI: "hide" })); -```ts -const isActive: Accessor = createFullscreen( - ref: HTMLElement | undefined, - active?: Accessor, - options?: FullscreenOptions -); +// Options as third argument: +createFullscreen(ref, isActive, { navigationUI: "hide" }); +``` + +--- + +### `fullscreen` (ref directive factory) -// can be used as a directive +A convenience factory that creates a ref callback for use directly in JSX. Solid 2.0 uses `ref` callbacks in place of the old `use:` directive syntax. +```tsx const MyComponent: Component = () => { const [fs, setFs] = createSignal(false); - return (
setFs(fs => !fs)}> - {!fs() ? 'click to fullscreen' : 'click to exit fullscreen'} -
); -} + + return ( +
setFs(f => !f)}> + {fs() ? "click to exit fullscreen" : "click to fullscreen"} +
+ ); +}; ``` +`fullscreen` must be called during component render (inside a reactive owner) — the same constraint as any Solid primitive. + ## Demo TODO diff --git a/packages/fullscreen/package.json b/packages/fullscreen/package.json index 169c161ab..27e8d7698 100644 --- a/packages/fullscreen/package.json +++ b/packages/fullscreen/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/fullscreen", - "version": "1.3.4", + "version": "2.0.0-beta.0", "description": "Primitive that wraps the fullscreen API.", "author": "Alex Lohr ", "license": "MIT", @@ -50,10 +50,12 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.7", + "solid-js": "^2.0.0-beta.7" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.7", + "solid-js": "2.0.0-beta.7" } } diff --git a/packages/fullscreen/src/index.ts b/packages/fullscreen/src/index.ts index 2a2f14d2c..c56adca84 100644 --- a/packages/fullscreen/src/index.ts +++ b/packages/fullscreen/src/index.ts @@ -1,36 +1,21 @@ -import { createEffect, createSignal, onCleanup, type JSX, getOwner } from "solid-js"; -import { isServer } from "solid-js/web"; +import { createEffect, createSignal, onCleanup, getOwner } from "solid-js"; +import { isServer } from "@solidjs/web"; import type { Accessor } from "solid-js"; import { access } from "@solid-primitives/utils"; -declare module "solid-js" { - namespace JSX { - interface Directives { - createFullscreen: ( - ref?: HTMLElement | Accessor, - active?: Accessor, - ) => Accessor; - } - } -} - -// only here so the `JSX` import won't be shaken off the tree: -export type E = JSX.Element; - /** - * createFullscreen - reactively toggle fullscreen + * Reactively toggles fullscreen on a target element. + * * ```ts * const [fs, setFs] = createSignal(false); * * // via ref signal * const [ref, setRef] = createSignal(); * createFullscreen(ref, fs); - * return
!f)}>click me
+ * return
setFs(f => !f)}>click me
* - * // via directive: - * return
!f)}> - * click me - *
+ * // via ref directive factory (Solid 2.0) + * return
setFs(f => !f)}>click me
* ``` */ export const createFullscreen = ( @@ -43,21 +28,27 @@ export const createFullscreen = ( } const [isActive, setActive] = createSignal(false); - createEffect(() => { - const node = access(ref); - if (node) { - const activeOutput = active?.() ?? true; - if (!isActive() && activeOutput) { + + createEffect( + () => ({ + node: access(ref), + activeOutput: active?.() ?? true, + currentActive: isActive(), + }), + ({ node, activeOutput, currentActive }) => { + if (!node) return; + if (!currentActive && activeOutput) { node .requestFullscreen(typeof activeOutput === "object" ? activeOutput : options) .then(() => setActive(true)) .catch(() => {}); - } else if (!activeOutput && isActive()) { + } else if (!activeOutput && currentActive) { setActive(false); document.exitFullscreen(); } - } - }); + }, + ); + const listener = () => setActive(document.fullscreenElement === access(ref)); document.addEventListener("fullscreenchange", listener); getOwner() && @@ -70,3 +61,20 @@ export const createFullscreen = ( return isActive; }; + +/** + * Ref directive factory for toggling fullscreen. For use with Solid 2.0's `ref` prop. + * + * ```tsx + * const [fs, setFs] = createSignal(false); + * return
setFs(f => !f)}>click me
+ * ``` + */ +export const fullscreen = ( + active?: Accessor, + options?: FullscreenOptions, +) => { + const [ref, setRef] = createSignal(); + createFullscreen(ref, active, options); + return setRef as (el: HTMLElement) => void; +}; diff --git a/packages/fullscreen/test/index.test.ts b/packages/fullscreen/test/index.test.ts index 2426725d1..e948f0379 100644 --- a/packages/fullscreen/test/index.test.ts +++ b/packages/fullscreen/test/index.test.ts @@ -1,75 +1,86 @@ +import "./setup.js"; import { setup_state } from "./setup.js"; -import { createRoot, createEffect, createSignal } from "solid-js"; +import { createRoot, createEffect, createSignal, flush } from "solid-js"; import { createFullscreen } from "../src/index.js"; -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; + +beforeEach(() => { + setup_state.current_el = undefined; + setup_state.current_options = undefined; +}); describe("createFullscreen", () => { const ref = document.createElement("div"); - it("will call the fullscreen method", async () => { - let captured: unknown; + it("will call the fullscreen method", () => { + let captured: boolean | undefined; const dispose = createRoot(dispose => { const active = createFullscreen(ref); - createEffect(() => { - captured = active(); - }); + createEffect( + () => active(), + value => { + captured = value; + }, + ); return dispose; }); - expect(captured).toEqual(false); - - await Promise.resolve(); + // flush() drives all effect compute/effect phases to stable state + flush(); expect(captured).toEqual(true); dispose(); }); - it("will exit fullscreen on reactive change", async () => { + it("will exit fullscreen on reactive change", () => { const [fs, setFs] = createSignal(true); - let captured: unknown; + let captured: boolean | undefined; const dispose = createRoot(dispose => { const active = createFullscreen(ref, fs); - createEffect(() => { - captured = active(); - }); + createEffect( + () => active(), + value => { + captured = value; + }, + ); return dispose; }); - expect(captured).toEqual(false); - - await Promise.resolve(); + flush(); expect(captured).toEqual(true); setFs(false); + flush(); expect(captured).toEqual(false); dispose(); }); - it("will open fullscreen with options", async () => { + it("will open fullscreen with options", () => { const options: FullscreenOptions = { navigationUI: "hide" }; - let captured: unknown; + let captured: boolean | undefined; const dispose = createRoot(dispose => { const active = createFullscreen(ref, undefined, options); - createEffect(() => { - captured = active(); - }); + createEffect( + () => active(), + value => { + captured = value; + }, + ); return dispose; }); - expect(captured).toEqual(false); - - await Promise.resolve(); + flush(); expect(captured).toEqual(true); expect(setup_state.current_options).toBe(options); diff --git a/packages/fullscreen/test/setup.ts b/packages/fullscreen/test/setup.ts index 19b89e9d5..8a20a3acf 100644 --- a/packages/fullscreen/test/setup.ts +++ b/packages/fullscreen/test/setup.ts @@ -6,21 +6,26 @@ export const setup_state = { HTMLElement.prototype.requestFullscreen = function ( this: HTMLElement, options?: FullscreenOptions, -) { +): Promise { setup_state.current_el = this; setup_state.current_options = options; document.dispatchEvent(new Event("fullscreenchange")); return Promise.resolve(); }; +// Dynamic getter so fullscreenElement reflects current state Object.defineProperty(document, "fullscreenElement", { - value: setup_state.current_el, - writable: false, + get: () => setup_state.current_el ?? null, + configurable: true, }); +// exitFullscreen dispatches fullscreenchange just like a real browser Object.defineProperty(document, "exitFullscreen", { - value: () => { + value: (): Promise => { setup_state.current_el = undefined; + document.dispatchEvent(new Event("fullscreenchange")); + return Promise.resolve(); }, - writable: false, + writable: true, + configurable: true, }); diff --git a/packages/utils/package.json b/packages/utils/package.json index d01e18508..4aeea1e36 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -61,9 +61,11 @@ "primitives" ], "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.7", + "solid-js": "^2.0.0-beta.7" }, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.7", + "solid-js": "2.0.0-beta.7" } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 5ba5b7e24..31fb1eed9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -13,7 +13,7 @@ import { DEV, equalFn, } from "solid-js"; -import { isServer } from "solid-js/web"; +import { isServer } from "@solidjs/web"; import type { AnyClass, MaybeAccessor, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecadfdb95..d5208ba20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,9 +345,12 @@ importers: specifier: workspace:^ version: link:../utils devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7(@solidjs/signals@2.0.0-beta.7)(solid-js@2.0.0-beta.7) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7 packages/geolocation: dependencies: @@ -970,9 +973,12 @@ importers: packages/utils: devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7(@solidjs/signals@2.0.0-beta.7)(solid-js@2.0.0-beta.7) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7 packages/virtual: dependencies: @@ -1048,10 +1054,10 @@ importers: version: link:../packages/utils '@solidjs/meta': specifier: ^0.29.3 - version: 0.29.4(solid-js@1.9.7) + version: 0.29.4(solid-js@2.0.0-beta.7) '@solidjs/router': specifier: ^0.13.1 - version: 0.13.6(solid-js@1.9.7) + version: 0.13.6(solid-js@2.0.0-beta.7) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1078,13 +1084,13 @@ importers: version: 1.77.8 solid-dismiss: specifier: ^1.7.121 - version: 1.8.2(solid-js@1.9.7) + version: 1.8.2(solid-js@2.0.0-beta.7) solid-icons: specifier: ^1.1.0 - version: 1.1.0(solid-js@1.9.7) + version: 1.1.0(solid-js@2.0.0-beta.7) solid-tippy: specifier: ^0.2.1 - version: 0.2.1(solid-js@1.9.7)(tippy.js@6.3.7) + version: 0.2.1(solid-js@2.0.0-beta.7)(tippy.js@6.3.7) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -2587,11 +2593,20 @@ packages: peerDependencies: solid-js: ^1.5.3 + '@solidjs/signals@2.0.0-beta.7': + resolution: {integrity: sha512-SgK6oQlQZofz82LiEJ2RzT3sbs1lWTqFEtLoWjLsUo/dk1v9EoIFpJJlmvgkXvNugASWG+l1yOHa1a8lPamxug==} + '@solidjs/start@1.1.4': resolution: {integrity: sha512-ma1TBYqoTju87tkqrHExMReM5Z/+DTXSmi30CCTavtwuR73Bsn4rVGqm528p4sL2koRMfAuBMkrhuttjzhL68g==} peerDependencies: vinxi: ^0.5.3 + '@solidjs/web@2.0.0-beta.7': + resolution: {integrity: sha512-m5VjmDBufrOX0ZKGbhvwkT0CPK0TbMxDbxVPDB1PH2evGbWXQZcUlrpFM1N8RBO5md3aR/T1PgMfnOjleJbrRg==} + peerDependencies: + '@solidjs/signals': ^2.0.0-beta.7 + solid-js: ^2.0.0-beta.7 + '@speed-highlight/core@1.2.7': resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} @@ -5892,10 +5907,20 @@ packages: peerDependencies: seroval: ^1.0 + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + seroval@1.3.2: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} + engines: {node: '>=10'} + serve-placeholder@2.0.2: resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} @@ -6001,6 +6026,9 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} + solid-js@2.0.0-beta.7: + resolution: {integrity: sha512-7JHs+BhLeZXoU+u9dG+eKnyxxfZyGpOuJEBbN/1XbHKO/WhxecdplOAurlg/YDllNWPhsbXqmLR1H2paqSu62g==} + solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -8576,18 +8604,20 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solidjs/meta@0.29.4(solid-js@1.9.7)': + '@solidjs/meta@0.29.4(solid-js@2.0.0-beta.7)': dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-beta.7 - '@solidjs/router@0.13.6(solid-js@1.9.7)': + '@solidjs/router@0.13.6(solid-js@2.0.0-beta.7)': dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-beta.7 '@solidjs/router@0.8.4(solid-js@1.9.7)': dependencies: solid-js: 1.9.7 + '@solidjs/signals@2.0.0-beta.7': {} + '@solidjs/start@1.1.4(solid-js@1.9.7)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.0(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) @@ -8611,6 +8641,13 @@ snapshots: - supports-color - vite + '@solidjs/web@2.0.0-beta.7(@solidjs/signals@2.0.0-beta.7)(solid-js@2.0.0-beta.7)': + dependencies: + '@solidjs/signals': 2.0.0-beta.7 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + solid-js: 2.0.0-beta.7 + '@speed-highlight/core@1.2.7': {} '@supabase/auth-js@2.67.3': @@ -12441,8 +12478,14 @@ snapshots: dependencies: seroval: 1.3.2 + seroval-plugins@1.5.2(seroval@1.5.2): + dependencies: + seroval: 1.5.2 + seroval@1.3.2: {} + seroval@1.5.2: {} + serve-placeholder@2.0.2: dependencies: defu: 6.1.4 @@ -12557,13 +12600,13 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - solid-dismiss@1.8.2(solid-js@1.9.7): + solid-dismiss@1.8.2(solid-js@2.0.0-beta.7): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-beta.7 - solid-icons@1.1.0(solid-js@1.9.7): + solid-icons@1.1.0(solid-js@2.0.0-beta.7): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-beta.7 solid-js@1.9.7: dependencies: @@ -12571,6 +12614,13 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.2) + solid-js@2.0.0-beta.7: + dependencies: + '@solidjs/signals': 2.0.0-beta.7 + csstype: 3.1.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + solid-refresh@0.6.3(solid-js@1.9.7): dependencies: '@babel/generator': 7.27.5 @@ -12580,9 +12630,9 @@ snapshots: transitivePeerDependencies: - supports-color - solid-tippy@0.2.1(solid-js@1.9.7)(tippy.js@6.3.7): + solid-tippy@0.2.1(solid-js@2.0.0-beta.7)(tippy.js@6.3.7): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-beta.7 tippy.js: 6.3.7 solid-transition-group@0.2.3(solid-js@1.9.7): From 66c9a411a75c0bd964beac6144dfa0b85898f67f Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sat, 2 May 2026 17:55:20 -0400 Subject: [PATCH 2/7] Bumping to beta 10 --- packages/fullscreen/package.json | 8 +- packages/utils/package.json | 8 +- pnpm-lock.yaml | 140 +++++++++++++++++-------------- 3 files changed, 83 insertions(+), 73 deletions(-) diff --git a/packages/fullscreen/package.json b/packages/fullscreen/package.json index 27e8d7698..3e1121eea 100644 --- a/packages/fullscreen/package.json +++ b/packages/fullscreen/package.json @@ -50,12 +50,12 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "@solidjs/web": "^2.0.0-beta.7", - "solid-js": "^2.0.0-beta.7" + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" }, "typesVersions": {}, "devDependencies": { - "@solidjs/web": "2.0.0-beta.7", - "solid-js": "2.0.0-beta.7" + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 4aeea1e36..4fe598e02 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -61,11 +61,11 @@ "primitives" ], "peerDependencies": { - "@solidjs/web": "^2.0.0-beta.7", - "solid-js": "^2.0.0-beta.7" + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" }, "devDependencies": { - "@solidjs/web": "2.0.0-beta.7", - "solid-js": "2.0.0-beta.7" + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5208ba20..294cc080d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 1.0.1 '@solidjs/start': specifier: ^1.1.4 - version: 1.1.4(solid-js@1.9.7)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + version: 1.1.4(solid-js@2.0.0-beta.10)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 @@ -34,7 +34,7 @@ importers: version: 0.25.5 esbuild-plugin-solid: specifier: ^0.6.0 - version: 0.6.0(esbuild@0.25.5)(solid-js@1.9.7) + version: 0.6.0(esbuild@0.25.5)(solid-js@2.0.0-beta.10) eslint: specifier: ^9.28.0 version: 9.28.0(jiti@2.4.2) @@ -69,8 +69,8 @@ importers: specifier: ^4.0.1 version: 4.0.1 solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -82,7 +82,7 @@ importers: version: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.6(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + version: 2.11.6(solid-js@2.0.0-beta.10)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) vitest: specifier: ^2.1.9 version: 2.1.9(@types/node@22.15.31)(jsdom@25.0.1)(sass@1.77.8)(terser@5.42.0) @@ -298,9 +298,12 @@ importers: 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: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/event-props: devDependencies: @@ -346,11 +349,11 @@ importers: version: link:../utils devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.7 - version: 2.0.0-beta.7(@solidjs/signals@2.0.0-beta.7)(solid-js@2.0.0-beta.7) + 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.7 - version: 2.0.0-beta.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/geolocation: dependencies: @@ -574,9 +577,12 @@ importers: 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: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/memo: dependencies: @@ -789,9 +795,12 @@ importers: 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: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/scheduled: devDependencies: @@ -887,9 +896,12 @@ importers: 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: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/storage: dependencies: @@ -974,11 +986,11 @@ importers: packages/utils: devDependencies: '@solidjs/web': - specifier: 2.0.0-beta.7 - version: 2.0.0-beta.7(@solidjs/signals@2.0.0-beta.7)(solid-js@2.0.0-beta.7) + 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.7 - version: 2.0.0-beta.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/virtual: dependencies: @@ -1054,10 +1066,10 @@ importers: version: link:../packages/utils '@solidjs/meta': specifier: ^0.29.3 - version: 0.29.4(solid-js@2.0.0-beta.7) + version: 0.29.4(solid-js@2.0.0-beta.10) '@solidjs/router': specifier: ^0.13.1 - version: 0.13.6(solid-js@2.0.0-beta.7) + version: 0.13.6(solid-js@2.0.0-beta.10) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1084,13 +1096,13 @@ importers: version: 1.77.8 solid-dismiss: specifier: ^1.7.121 - version: 1.8.2(solid-js@2.0.0-beta.7) + version: 1.8.2(solid-js@2.0.0-beta.10) solid-icons: specifier: ^1.1.0 - version: 1.1.0(solid-js@2.0.0-beta.7) + version: 1.1.0(solid-js@2.0.0-beta.10) solid-tippy: specifier: ^0.2.1 - version: 0.2.1(solid-js@2.0.0-beta.7)(tippy.js@6.3.7) + version: 0.2.1(solid-js@2.0.0-beta.10)(tippy.js@6.3.7) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -2593,19 +2605,18 @@ packages: peerDependencies: solid-js: ^1.5.3 - '@solidjs/signals@2.0.0-beta.7': - resolution: {integrity: sha512-SgK6oQlQZofz82LiEJ2RzT3sbs1lWTqFEtLoWjLsUo/dk1v9EoIFpJJlmvgkXvNugASWG+l1yOHa1a8lPamxug==} + '@solidjs/signals@2.0.0-beta.10': + resolution: {integrity: sha512-McdmbLNiSlz616zcykS8Rb1t9QTOTKdNAoaWd4/OjXEbcAUrPqRX1CWgR+caiWUk4qn0a+LesTTV4jZhFFPaSg==} '@solidjs/start@1.1.4': resolution: {integrity: sha512-ma1TBYqoTju87tkqrHExMReM5Z/+DTXSmi30CCTavtwuR73Bsn4rVGqm528p4sL2koRMfAuBMkrhuttjzhL68g==} peerDependencies: vinxi: ^0.5.3 - '@solidjs/web@2.0.0-beta.7': - resolution: {integrity: sha512-m5VjmDBufrOX0ZKGbhvwkT0CPK0TbMxDbxVPDB1PH2evGbWXQZcUlrpFM1N8RBO5md3aR/T1PgMfnOjleJbrRg==} + '@solidjs/web@2.0.0-beta.10': + resolution: {integrity: sha512-Ox7MBv19kuxHoHhWoLCCcc6aykSgaqzWvWT7RB66VqlFnQ8Lid2ncd30g5L4XC0GB+MN/WZVb68tiYrAFUDIAg==} peerDependencies: - '@solidjs/signals': ^2.0.0-beta.7 - solid-js: ^2.0.0-beta.7 + solid-js: ^2.0.0-beta.10 '@speed-highlight/core@1.2.7': resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} @@ -6026,8 +6037,8 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} - solid-js@2.0.0-beta.7: - resolution: {integrity: sha512-7JHs+BhLeZXoU+u9dG+eKnyxxfZyGpOuJEBbN/1XbHKO/WhxecdplOAurlg/YDllNWPhsbXqmLR1H2paqSu62g==} + solid-js@2.0.0-beta.10: + resolution: {integrity: sha512-EAfV6b1SC4c3wEBAoX4dMy063uTb4nfL5uXnN8yse4InH7RTw1LoB0I9HAy+pj3/GHqQE2tYZurlZtqU4pGyog==} solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} @@ -8604,21 +8615,21 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solidjs/meta@0.29.4(solid-js@2.0.0-beta.7)': + '@solidjs/meta@0.29.4(solid-js@2.0.0-beta.10)': dependencies: - solid-js: 2.0.0-beta.7 + solid-js: 2.0.0-beta.10 - '@solidjs/router@0.13.6(solid-js@2.0.0-beta.7)': + '@solidjs/router@0.13.6(solid-js@2.0.0-beta.10)': dependencies: - solid-js: 2.0.0-beta.7 + solid-js: 2.0.0-beta.10 '@solidjs/router@0.8.4(solid-js@1.9.7)': dependencies: solid-js: 1.9.7 - '@solidjs/signals@2.0.0-beta.7': {} + '@solidjs/signals@2.0.0-beta.10': {} - '@solidjs/start@1.1.4(solid-js@1.9.7)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@solidjs/start@1.1.4(solid-js@2.0.0-beta.10)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.0(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) @@ -8631,22 +8642,21 @@ snapshots: seroval-plugins: 1.3.2(seroval@1.3.2) shiki: 1.29.2 source-map-js: 1.2.1 - terracotta: 1.0.6(solid-js@1.9.7) + terracotta: 1.0.6(solid-js@2.0.0-beta.10) tinyglobby: 0.2.14 vinxi: 0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) - vite-plugin-solid: 2.11.6(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + vite-plugin-solid: 2.11.6(solid-js@2.0.0-beta.10)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js - supports-color - vite - '@solidjs/web@2.0.0-beta.7(@solidjs/signals@2.0.0-beta.7)(solid-js@2.0.0-beta.7)': + '@solidjs/web@2.0.0-beta.10(solid-js@2.0.0-beta.10)': dependencies: - '@solidjs/signals': 2.0.0-beta.7 seroval: 1.5.2 seroval-plugins: 1.5.2(seroval@1.5.2) - solid-js: 2.0.0-beta.7 + solid-js: 2.0.0-beta.10 '@speed-highlight/core@1.2.7': {} @@ -10012,13 +10022,13 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild-plugin-solid@0.6.0(esbuild@0.25.5)(solid-js@1.9.7): + esbuild-plugin-solid@0.6.0(esbuild@0.25.5)(solid-js@2.0.0-beta.10): dependencies: '@babel/core': 7.27.4 '@babel/preset-typescript': 7.27.1(@babel/core@7.27.4) babel-preset-solid: 1.9.6(@babel/core@7.27.4) esbuild: 0.25.5 - solid-js: 1.9.7 + solid-js: 2.0.0-beta.10 transitivePeerDependencies: - supports-color @@ -12600,13 +12610,13 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - solid-dismiss@1.8.2(solid-js@2.0.0-beta.7): + solid-dismiss@1.8.2(solid-js@2.0.0-beta.10): dependencies: - solid-js: 2.0.0-beta.7 + solid-js: 2.0.0-beta.10 - solid-icons@1.1.0(solid-js@2.0.0-beta.7): + solid-icons@1.1.0(solid-js@2.0.0-beta.10): dependencies: - solid-js: 2.0.0-beta.7 + solid-js: 2.0.0-beta.10 solid-js@1.9.7: dependencies: @@ -12614,25 +12624,25 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.2) - solid-js@2.0.0-beta.7: + solid-js@2.0.0-beta.10: dependencies: - '@solidjs/signals': 2.0.0-beta.7 + '@solidjs/signals': 2.0.0-beta.10 csstype: 3.1.3 seroval: 1.5.2 seroval-plugins: 1.5.2(seroval@1.5.2) - solid-refresh@0.6.3(solid-js@1.9.7): + solid-refresh@0.6.3(solid-js@2.0.0-beta.10): dependencies: '@babel/generator': 7.27.5 '@babel/helper-module-imports': 7.27.1 '@babel/types': 7.27.6 - solid-js: 1.9.7 + solid-js: 2.0.0-beta.10 transitivePeerDependencies: - supports-color - solid-tippy@0.2.1(solid-js@2.0.0-beta.7)(tippy.js@6.3.7): + solid-tippy@0.2.1(solid-js@2.0.0-beta.10)(tippy.js@6.3.7): dependencies: - solid-js: 2.0.0-beta.7 + solid-js: 2.0.0-beta.10 tippy.js: 6.3.7 solid-transition-group@0.2.3(solid-js@1.9.7): @@ -12641,9 +12651,9 @@ snapshots: '@solid-primitives/transition-group': 1.0.5(solid-js@1.9.7) solid-js: 1.9.7 - solid-use@0.9.1(solid-js@1.9.7): + solid-use@0.9.1(solid-js@2.0.0-beta.10): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-beta.10 source-map-js@1.2.1: {} @@ -12864,10 +12874,10 @@ snapshots: term-size@2.2.1: {} - terracotta@1.0.6(solid-js@1.9.7): + terracotta@1.0.6(solid-js@2.0.0-beta.10): dependencies: - solid-js: 1.9.7 - solid-use: 0.9.1(solid-js@1.9.7) + solid-js: 2.0.0-beta.10 + solid-use: 0.9.1(solid-js@2.0.0-beta.10) terser@5.42.0: dependencies: @@ -13294,14 +13304,14 @@ snapshots: - supports-color - terser - vite-plugin-solid@2.11.6(solid-js@1.9.7)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)): + vite-plugin-solid@2.11.6(solid-js@2.0.0-beta.10)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)): dependencies: '@babel/core': 7.27.4 '@types/babel__core': 7.20.5 babel-preset-solid: 1.9.6(@babel/core@7.27.4) merge-anything: 5.1.7 - solid-js: 1.9.7 - solid-refresh: 0.6.3(solid-js@1.9.7) + solid-js: 2.0.0-beta.10 + solid-refresh: 0.6.3(solid-js@2.0.0-beta.10) vite: 6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) transitivePeerDependencies: From c04384aba26d1dabe6c5413f76eac97aea4a452a Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sun, 17 May 2026 10:22:51 -0400 Subject: [PATCH 3/7] Separated makeFullScreen and adapted createFullScreen --- packages/fullscreen/README.md | 94 ++++++----- packages/fullscreen/package.json | 5 +- packages/fullscreen/src/index.ts | 163 ++++++++++++------- packages/fullscreen/test/index.test.ts | 207 ++++++++++++++++++------ packages/fullscreen/test/server.test.ts | 15 +- packages/fullscreen/tsconfig.json | 3 + pnpm-lock.yaml | 3 + 7 files changed, 344 insertions(+), 146 deletions(-) diff --git a/packages/fullscreen/README.md b/packages/fullscreen/README.md index e368c16a7..d2d7bc7e3 100644 --- a/packages/fullscreen/README.md +++ b/packages/fullscreen/README.md @@ -8,7 +8,7 @@ [![size](https://img.shields.io/npm/v/@solid-primitives/fullscreen?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/fullscreen) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) -Reactive wrapper around the [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API) that integrates with Solid's reactive system. +Reactive wrapper around the [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API). ## Installation @@ -18,67 +18,89 @@ npm install @solid-primitives/fullscreen pnpm add @solid-primitives/fullscreen ``` -## How to use it +## Primitives + +### `makeFullscreen` + +Non-reactive base primitive. No Solid lifecycle dependency — safe to use outside components. + +```ts +const [enter, exit] = makeFullscreen(element, options?: FullscreenOptions); +``` + +`enter(options?)` calls `element.requestFullscreen()`. Call-time options override creation-time options. `exit()` calls `document.exitFullscreen()`. + +```ts +const [enter, exit] = makeFullscreen(videoEl, { navigationUI: "hide" }); +button.addEventListener("click", enter); +``` + +--- ### `createFullscreen` -Reactively toggles fullscreen on a target element. Returns an `Accessor` reflecting whether the element is currently in fullscreen. +Reactive primitive that binds `enter`/`exit` to an element and tracks fullscreen state via the `fullscreenchange` event. ```ts -const isActive: Accessor = createFullscreen( +const { enter, exit, isActive } = createFullscreen( ref: HTMLElement | Accessor, - active?: Accessor, - options?: FullscreenOptions + options?: FullscreenPrimitiveOptions, ); ``` -**Via ref signal:** +`enter()` and `exit()` must be called from a direct user gesture — the browser requires it. `isActive` is an `Accessor` that reflects the live fullscreen state. ```tsx -const MyComponent: Component = () => { - const [fs, setFs] = createSignal(false); - const [ref, setRef] = createSignal(); - const active = createFullscreen(ref, fs); - - return ( -
setFs(f => !f)}> - {active() ? "click to exit fullscreen" : "click to fullscreen"} -
- ); -}; -``` +const { enter, exit, isActive } = createFullscreen(ref); -The ref must be a signal so the element is captured reactively rather than in a stale closure. + + + + +``` -You can pass `FullscreenOptions` either inside the `active` accessor return value or as a third argument: +`ref` can be a reactive accessor — `createFullscreen` rebinds when the element changes: ```tsx -// Options via active accessor (useful for programmatic control): -createFullscreen(ref, () => ({ navigationUI: "hide" })); +const [ref, setRef] = createSignal(); +const { enter, isActive } = createFullscreen(ref); -// Options as third argument: -createFullscreen(ref, isActive, { navigationUI: "hide" }); +
...
+``` + +#### `FullscreenPrimitiveOptions` + +Extends the standard [`FullscreenOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#options) with: + +| Option | Type | Default | Description | +|---|---|---|---| +| `exitOnCleanup` | `boolean` | `true` | Exit fullscreen when the reactive scope is disposed. | +| `navigationUI` | `string` | — | Passed to `requestFullscreen`. | + +```ts +const { enter } = createFullscreen(ref, { exitOnCleanup: false, navigationUI: "hide" }); ``` --- ### `fullscreen` (ref directive factory) -A convenience factory that creates a ref callback for use directly in JSX. Solid 2.0 uses `ref` callbacks in place of the old `use:` directive syntax. +A ref factory that wires click-to-toggle fullscreen onto any element. Clicking enters fullscreen; clicking again exits. Must be called inside a reactive owner (component body or `createRoot`). + +```ts +const attach = fullscreen(options?: FullscreenOptions); +// attach is a ref callback: (el: HTMLElement) => void +``` ```tsx -const MyComponent: Component = () => { - const [fs, setFs] = createSignal(false); - - return ( -
setFs(f => !f)}> - {fs() ? "click to exit fullscreen" : "click to fullscreen"} -
- ); -}; +// Simple toggle +
Click to go fullscreen
+ +// With options +
Click to go fullscreen
``` -`fullscreen` must be called during component render (inside a reactive owner) — the same constraint as any Solid primitive. +The click listener is removed automatically when the component unmounts. ## Demo diff --git a/packages/fullscreen/package.json b/packages/fullscreen/package.json index 3e1121eea..9ad8b8cd9 100644 --- a/packages/fullscreen/package.json +++ b/packages/fullscreen/package.json @@ -13,7 +13,9 @@ "name": "fullscreen", "stage": 3, "list": [ - "createFullscreen" + "makeFullscreen", + "createFullscreen", + "fullscreen" ], "category": "Browser APIs" }, @@ -47,6 +49,7 @@ "primitives" ], "dependencies": { + "@solid-primitives/event-listener": "workspace:^", "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { diff --git a/packages/fullscreen/src/index.ts b/packages/fullscreen/src/index.ts index c56adca84..32e5171f2 100644 --- a/packages/fullscreen/src/index.ts +++ b/packages/fullscreen/src/index.ts @@ -1,80 +1,133 @@ -import { createEffect, createSignal, onCleanup, getOwner } from "solid-js"; +import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"; import { isServer } from "@solidjs/web"; -import type { Accessor } from "solid-js"; -import { access } from "@solid-primitives/utils"; +import { makeEventListener } from "@solid-primitives/event-listener"; +import { access, INTERNAL_OPTIONS } from "@solid-primitives/utils"; /** - * Reactively toggles fullscreen on a target element. + * Non-reactive fullscreen helper. No Solid lifecycle dependency. * + * @param element The element to make fullscreen. + * @param options Standard `FullscreenOptions`. + * @returns `[enter, exit]` — `enter()` requests fullscreen, `exit()` leaves it. + * Options passed to `enter()` override the creation-time options. + * + * @example * ```ts - * const [fs, setFs] = createSignal(false); + * const [enter, exit] = makeFullscreen(ref); + * button.addEventListener("click", enter); + * ``` + */ +export const makeFullscreen = ( + element: HTMLElement, + options?: FullscreenOptions, +): [enter: (options?: FullscreenOptions) => Promise, exit: () => Promise] => { + const enter = (callOptions?: FullscreenOptions) => + element.requestFullscreen(callOptions ?? options); + const exit = () => document.exitFullscreen(); + return [enter, exit]; +}; + +export interface FullscreenPrimitiveOptions extends FullscreenOptions { + /** Exit fullscreen when the reactive scope is disposed. Default: `true`. */ + exitOnCleanup?: boolean; +} + +/** + * Reactive fullscreen primitive. Call `enter()` and `exit()` imperatively + * from user gesture handlers — the browser requires a direct user interaction + * to allow fullscreen. `isActive` tracks the real fullscreen state via the + * `fullscreenchange` event and initializes to `true` if the element is already + * fullscreen when the primitive is created. * - * // via ref signal - * const [ref, setRef] = createSignal(); - * createFullscreen(ref, fs); - * return
setFs(f => !f)}>click me
+ * @param ref The element to fullscreen, or a reactive accessor returning one. + * @param options Extends `FullscreenOptions` with `exitOnCleanup` (default `true`). + * @returns `{ enter, exit, isActive }` + * + * @example + * ```tsx + * const { enter, exit, isActive } = createFullscreen(ref); * - * // via ref directive factory (Solid 2.0) - * return
setFs(f => !f)}>click me
+ * + * + * + * * ``` */ export const createFullscreen = ( - ref: HTMLElement | undefined | Accessor, - active?: Accessor, - options?: FullscreenOptions, -): Accessor => { + ref: HTMLElement | Accessor, + options?: FullscreenPrimitiveOptions, +): { + enter: (options?: FullscreenOptions) => Promise; + exit: () => Promise; + isActive: Accessor; +} => { if (isServer) { - return () => false; + return { + enter: () => Promise.resolve(), + exit: () => Promise.resolve(), + isActive: () => false, + }; } - const [isActive, setActive] = createSignal(false); + const { exitOnCleanup = true, ...nativeOptions } = options ?? {}; + const initial = access(ref); + + let bound: ReturnType | null = initial + ? makeFullscreen(initial, nativeOptions) + : null; - createEffect( - () => ({ - node: access(ref), - activeOutput: active?.() ?? true, - currentActive: isActive(), - }), - ({ node, activeOutput, currentActive }) => { - if (!node) return; - if (!currentActive && activeOutput) { - node - .requestFullscreen(typeof activeOutput === "object" ? activeOutput : options) - .then(() => setActive(true)) - .catch(() => {}); - } else if (!activeOutput && currentActive) { - setActive(false); - document.exitFullscreen(); - } - }, + const [isActive, setActive] = createSignal( + document.fullscreenElement != null && document.fullscreenElement === initial, + INTERNAL_OPTIONS, ); - const listener = () => setActive(document.fullscreenElement === access(ref)); - document.addEventListener("fullscreenchange", listener); - getOwner() && - onCleanup(() => { - document.removeEventListener("fullscreenchange", listener); - if (isActive()) { - document.exitFullscreen(); - } - }); + // makeEventListener registers onCleanup automatically; reads access(ref) dynamically + // so no rebinding is needed when the element changes + makeEventListener(document, "fullscreenchange", () => + setActive(document.fullscreenElement === access(ref)), + ); + + if (typeof ref === "function") { + createEffect( + () => (ref as Accessor)(), + node => { + bound = node ? makeFullscreen(node, nativeOptions) : null; + }, + ); + } - return isActive; + onCleanup(() => { + if (exitOnCleanup && isActive()) document.exitFullscreen(); + }); + + return { + enter: (callOptions?: FullscreenOptions): Promise => + bound?.[0](callOptions) ?? Promise.resolve(), + exit: (): Promise => bound?.[1]() ?? Promise.resolve(), + isActive, + }; }; /** - * Ref directive factory for toggling fullscreen. For use with Solid 2.0's `ref` prop. + * Ref directive that toggles fullscreen when the element is clicked. + * Attach to any element — clicking it enters fullscreen; clicking again exits. * + * @example * ```tsx - * const [fs, setFs] = createSignal(false); - * return
setFs(f => !f)}>click me
+ *
Click to go fullscreen
+ *
Click to go fullscreen
* ``` */ -export const fullscreen = ( - active?: Accessor, - options?: FullscreenOptions, -) => { - const [ref, setRef] = createSignal(); - createFullscreen(ref, active, options); - return setRef as (el: HTMLElement) => void; +export const fullscreen = (options?: FullscreenOptions): ((el: HTMLElement) => void) => { + if (isServer) return () => {}; + + let removeListener: (() => void) | undefined; + onCleanup(() => removeListener?.()); + + return (el: HTMLElement) => { + const [enter, exit] = makeFullscreen(el, options); + const toggle = () => (document.fullscreenElement === el ? exit() : enter()); + el.addEventListener("click", toggle); + removeListener = () => el.removeEventListener("click", toggle); + }; }; diff --git a/packages/fullscreen/test/index.test.ts b/packages/fullscreen/test/index.test.ts index e948f0379..cbefb050c 100644 --- a/packages/fullscreen/test/index.test.ts +++ b/packages/fullscreen/test/index.test.ts @@ -1,8 +1,8 @@ import "./setup.js"; import { setup_state } from "./setup.js"; -import { createRoot, createEffect, createSignal, flush } from "solid-js"; -import { createFullscreen } from "../src/index.js"; +import { createRoot, flush } from "solid-js"; +import { makeFullscreen, createFullscreen, fullscreen } from "../src/index.js"; import { describe, it, expect, beforeEach } from "vitest"; beforeEach(() => { @@ -10,80 +10,187 @@ beforeEach(() => { setup_state.current_options = undefined; }); -describe("createFullscreen", () => { +describe("makeFullscreen", () => { const ref = document.createElement("div"); - it("will call the fullscreen method", () => { - let captured: boolean | undefined; + it("enter() calls requestFullscreen on the element", async () => { + const [enter] = makeFullscreen(ref); + await enter(); + expect(setup_state.current_el).toBe(ref); + }); + + it("exit() calls document.exitFullscreen", async () => { + const [enter, exit] = makeFullscreen(ref); + await enter(); + await exit(); + expect(setup_state.current_el).toBeUndefined(); + }); + + it("enter() uses creation-time options by default", async () => { + const options: FullscreenOptions = { navigationUI: "hide" }; + const [enter] = makeFullscreen(ref, options); + await enter(); + expect(setup_state.current_options).toBe(options); + }); + + it("enter() call-time options override creation-time options", async () => { + const creation: FullscreenOptions = { navigationUI: "hide" }; + const callTime: FullscreenOptions = { navigationUI: "show" }; + const [enter] = makeFullscreen(ref, creation); + await enter(callTime); + expect(setup_state.current_options).toBe(callTime); + }); +}); + +describe("createFullscreen", () => { + const ref = document.createElement("div"); - const dispose = createRoot(dispose => { - const active = createFullscreen(ref); + it("isActive starts false when element is not fullscreen", () => { + createRoot(dispose => { + const { isActive } = createFullscreen(ref); + expect(isActive()).toBe(false); + dispose(); + }); + }); - createEffect( - () => active(), - value => { - captured = value; - }, - ); + it("isActive starts true when element is already fullscreen", () => { + setup_state.current_el = ref; - return dispose; + createRoot(dispose => { + const { isActive } = createFullscreen(ref); + expect(isActive()).toBe(true); + dispose(); }); + }); - // flush() drives all effect compute/effect phases to stable state - flush(); - expect(captured).toEqual(true); + it("enter() enters fullscreen and isActive becomes true", async () => { + await createRoot(async dispose => { + const { enter, isActive } = createFullscreen(ref); - dispose(); + await enter(); + flush(); + expect(isActive()).toBe(true); + + dispose(); + }); }); - it("will exit fullscreen on reactive change", () => { - const [fs, setFs] = createSignal(true); - let captured: boolean | undefined; + it("exit() leaves fullscreen and isActive becomes false", async () => { + await createRoot(async dispose => { + const { enter, exit, isActive } = createFullscreen(ref); - const dispose = createRoot(dispose => { - const active = createFullscreen(ref, fs); + await enter(); + flush(); + expect(isActive()).toBe(true); - createEffect( - () => active(), - value => { - captured = value; - }, - ); + await exit(); + flush(); + expect(isActive()).toBe(false); - return dispose; + dispose(); }); + }); + + it("isActive updates when OS dismisses fullscreen", async () => { + await createRoot(async dispose => { + const { enter, isActive } = createFullscreen(ref); - flush(); - expect(captured).toEqual(true); + await enter(); + flush(); + expect(isActive()).toBe(true); - setFs(false); - flush(); - expect(captured).toEqual(false); + setup_state.current_el = undefined; + document.dispatchEvent(new Event("fullscreenchange")); + flush(); + expect(isActive()).toBe(false); + dispose(); + }); + }); + + it("exits fullscreen on cleanup by default", async () => { + let dispose!: () => void; + await createRoot(async d => { + dispose = d; + const { enter } = createFullscreen(ref); + await enter(); + flush(); + }); dispose(); + expect(setup_state.current_el).toBeUndefined(); }); - it("will open fullscreen with options", () => { - const options: FullscreenOptions = { navigationUI: "hide" }; - let captured: boolean | undefined; + it("does not exit fullscreen on cleanup when exitOnCleanup is false", async () => { + let dispose!: () => void; + await createRoot(async d => { + dispose = d; + const { enter } = createFullscreen(ref, { exitOnCleanup: false }); + await enter(); + flush(); + }); + dispose(); + expect(setup_state.current_el).toBe(ref); + + // reset state + setup_state.current_el = undefined; + }); + + it("enter() passes options to requestFullscreen", async () => { + await createRoot(async dispose => { + const { enter } = createFullscreen(ref, { navigationUI: "hide" }); + await enter(); + expect(setup_state.current_options).toStrictEqual({ navigationUI: "hide" }); + dispose(); + }); + }); +}); - const dispose = createRoot(dispose => { - const active = createFullscreen(ref, undefined, options); +describe("fullscreen (directive)", () => { + const el = document.createElement("div"); - createEffect( - () => active(), - value => { - captured = value; - }, - ); + it("clicking the element enters fullscreen", () => { + createRoot(dispose => { + const attach = fullscreen(); + attach(el); + el.click(); + expect(setup_state.current_el).toBe(el); + dispose(); + }); + }); - return dispose; + it("clicking again while fullscreen exits", () => { + createRoot(dispose => { + const attach = fullscreen(); + attach(el); + el.click(); + expect(setup_state.current_el).toBe(el); + el.click(); + expect(setup_state.current_el).toBeUndefined(); + dispose(); }); + }); - flush(); - expect(captured).toEqual(true); - expect(setup_state.current_options).toBe(options); + it("passes options to requestFullscreen", () => { + const options: FullscreenOptions = { navigationUI: "hide" }; + createRoot(dispose => { + const attach = fullscreen(options); + attach(el); + el.click(); + expect(setup_state.current_options).toBe(options); + dispose(); + }); + }); + + it("removes click listener on cleanup", () => { + let dispose!: () => void; + createRoot(d => { + dispose = d; + const attach = fullscreen(); + attach(el); + }); dispose(); + el.click(); + expect(setup_state.current_el).toBeUndefined(); }); }); diff --git a/packages/fullscreen/test/server.test.ts b/packages/fullscreen/test/server.test.ts index d667024a0..d6f629fb2 100644 --- a/packages/fullscreen/test/server.test.ts +++ b/packages/fullscreen/test/server.test.ts @@ -1,9 +1,16 @@ import { describe, test, expect } from "vitest"; -import { createFullscreen } from "../src/index.js"; +import { createFullscreen, fullscreen } from "../src/index.js"; describe("API doesn't break in SSR", () => { - // check if the API doesn't throw when calling it in SSR - test("createFullScreen() - SSR", () => { - expect(createFullscreen(undefined, () => false)()).toEqual(false); + test("createFullscreen() returns safe defaults", async () => { + const { enter, exit, isActive } = createFullscreen(undefined as any); + expect(isActive()).toBe(false); + await expect(enter()).resolves.toBeUndefined(); + await expect(exit()).resolves.toBeUndefined(); + }); + + test("fullscreen() returns a no-op ref callback", () => { + const attach = fullscreen(); + expect(attach).toBeTypeOf("function"); }); }); diff --git a/packages/fullscreen/tsconfig.json b/packages/fullscreen/tsconfig.json index dc1970e16..b9b2b6782 100644 --- a/packages/fullscreen/tsconfig.json +++ b/packages/fullscreen/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 ed96e9df3..effe71745 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -359,6 +359,9 @@ importers: packages/fullscreen: dependencies: + '@solid-primitives/event-listener': + specifier: workspace:^ + version: link:../event-listener '@solid-primitives/utils': specifier: workspace:^ version: link:../utils From 01a4804016f0f904acae9e9f508f67371879a10a Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sun, 24 May 2026 10:03:19 -0400 Subject: [PATCH 4/7] Bump beta 14 --- packages/fullscreen/package.json | 8 ++++---- pnpm-lock.yaml | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/fullscreen/package.json b/packages/fullscreen/package.json index 9ad8b8cd9..c713be708 100644 --- a/packages/fullscreen/package.json +++ b/packages/fullscreen/package.json @@ -53,12 +53,12 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "@solidjs/web": "^2.0.0-beta.10", - "solid-js": "^2.0.0-beta.10" + "@solidjs/web": "^2.0.0-beta.14", + "solid-js": "^2.0.0-beta.14" }, "typesVersions": {}, "devDependencies": { - "@solidjs/web": "2.0.0-beta.10", - "solid-js": "2.0.0-beta.10" + "@solidjs/web": "2.0.0-beta.14", + "solid-js": "2.0.0-beta.14" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee1923c5f..d5ba9f40e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,13 +366,19 @@ importers: packages/fullscreen: dependencies: + '@solid-primitives/event-listener': + specifier: workspace:^ + version: link:../event-listener '@solid-primitives/utils': specifier: workspace:^ version: link:../utils devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.14 + version: 2.0.0-beta.14(solid-js@2.0.0-beta.14) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.14 + version: 2.0.0-beta.14 packages/geolocation: dependencies: From 77696a2a6e11932c3930f99206dd20ecc42f3077 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 28 May 2026 16:51:16 -0400 Subject: [PATCH 5/7] Changelog improvement and minor patches --- packages/fullscreen/CHANGELOG.md | 19 +++++++++++++++++++ packages/fullscreen/src/index.ts | 2 ++ 2 files changed, 21 insertions(+) diff --git a/packages/fullscreen/CHANGELOG.md b/packages/fullscreen/CHANGELOG.md index 75e7d1a70..e45166978 100644 --- a/packages/fullscreen/CHANGELOG.md +++ b/packages/fullscreen/CHANGELOG.md @@ -1,5 +1,24 @@ # @solid-primitives/fullscreen +## 2.0.0-beta.0 + +### Breaking Changes + +- Migrate `@solid-primitives/fullscreen` to Solid.js 2.0 (beta.14). +- Peer dependency updated from `solid-js ^1.6.12` to `solid-js ^2.0.0-beta.14` and `@solidjs/web ^2.0.0-beta.14`. +- The `use:createFullscreen` JSX directive (Solid 1.x `use:` namespace) is removed. Use the new `fullscreen()` ref directive factory instead: + + ```tsx + // Before (Solid 1.x) +
+ + // After (Solid 2.0) +
+ ``` + +- `isServer` is now imported from `@solidjs/web` instead of `solid-js/web`. +- `createResource` removed upstream — `createFullscreen` now uses an async `createMemo` pattern internally. + ## 1.3.4 ### Patch Changes diff --git a/packages/fullscreen/src/index.ts b/packages/fullscreen/src/index.ts index 32e5171f2..d3e9634ab 100644 --- a/packages/fullscreen/src/index.ts +++ b/packages/fullscreen/src/index.ts @@ -92,6 +92,7 @@ export const createFullscreen = ( () => (ref as Accessor)(), node => { bound = node ? makeFullscreen(node, nativeOptions) : null; + setActive(document.fullscreenElement === node); }, ); } @@ -125,6 +126,7 @@ export const fullscreen = (options?: FullscreenOptions): ((el: HTMLElement) => v onCleanup(() => removeListener?.()); return (el: HTMLElement) => { + removeListener?.(); const [enter, exit] = makeFullscreen(el, options); const toggle = () => (document.fullscreenElement === el ? exit() : enter()); el.addEventListener("click", toggle); From 055674269dffc9d76ffc93fed771c8be999a5adb Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 28 May 2026 17:01:45 -0400 Subject: [PATCH 6/7] Patched type error --- packages/fullscreen/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fullscreen/src/index.ts b/packages/fullscreen/src/index.ts index d3e9634ab..b3e517e1b 100644 --- a/packages/fullscreen/src/index.ts +++ b/packages/fullscreen/src/index.ts @@ -89,7 +89,7 @@ export const createFullscreen = ( if (typeof ref === "function") { createEffect( - () => (ref as Accessor)(), + () => ref(), node => { bound = node ? makeFullscreen(node, nativeOptions) : null; setActive(document.fullscreenElement === node); From 23fcf5eb57439bf09f0cf368a373cb5aee33818f Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 28 May 2026 17:12:48 -0400 Subject: [PATCH 7/7] Added stories --- packages/fullscreen/README.md | 9 +- .../fullscreen/stories/fullscreen.stories.tsx | 176 ++++++++++++++++++ 2 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 packages/fullscreen/stories/fullscreen.stories.tsx diff --git a/packages/fullscreen/README.md b/packages/fullscreen/README.md index d2d7bc7e3..a39e5b5e3 100644 --- a/packages/fullscreen/README.md +++ b/packages/fullscreen/README.md @@ -10,6 +10,11 @@ Reactive wrapper around the [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API). +- [**Docs (Storybook)**](https://primitives.solidjs.community/storybook/?path=/docs/browser-apis-fullscreen--docs) +- [`makeFullscreen`](#makefullscreen) — Non-reactive base, no Solid lifecycle. +- [`createFullscreen`](#createfullscreen) — Reactive primitive with `isActive` state tracking. +- [`fullscreen`](#fullscreen-ref-directive-factory) — Ref factory for click-to-toggle. + ## Installation ```bash @@ -102,10 +107,6 @@ const attach = fullscreen(options?: FullscreenOptions); The click listener is removed automatically when the component unmounts. -## Demo - -TODO - ## Changelog See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/fullscreen/stories/fullscreen.stories.tsx b/packages/fullscreen/stories/fullscreen.stories.tsx new file mode 100644 index 000000000..5f0a41176 --- /dev/null +++ b/packages/fullscreen/stories/fullscreen.stories.tsx @@ -0,0 +1,176 @@ +import { createSignal, onCleanup } from "solid-js"; +import preview from "../../../.storybook/preview.js"; +import { makeFullscreen, createFullscreen, fullscreen } from "../src/index.js"; +import readme from "../README.md?raw"; +import { + Alert, + BoolRow, + Button, + ButtonRow, + Card, + Container, + Section, +} from "../../../.storybook/ui/index.js"; + +const meta = preview.meta({ + title: "Browser APIs/Fullscreen", + tags: ["autodocs"], + parameters: { + layout: "centered", + docs: { + description: { + component: readme, + }, + }, + }, +}); + +export default meta; + +export const ImperativeControl = meta.story({ + name: "Imperative enter / exit", + parameters: { + docs: { + description: { + story: + "`makeFullscreen` returns plain `enter` / `exit` functions with no Solid lifecycle dependency — bind it in a ref callback and call imperatively from any event handler.", + }, + }, + }, + render: () => { + const [isActive, setIsActive] = createSignal(false); + let enter: (() => Promise) | undefined; + let exit: (() => Promise) | undefined; + + return ( + + Fullscreen requires a user gesture. Press Esc or click Exit to leave. + +
{ + [enter, exit] = makeFullscreen(el); + const onChange = () => setIsActive(document.fullscreenElement === el); + document.addEventListener("fullscreenchange", onChange); + onCleanup(() => document.removeEventListener("fullscreenchange", onChange)); + }} + style={{ + padding: "1.25rem", + background: "#f1f5f9", + "border-radius": "6px", + "text-align": "center", + "font-size": "0.875rem", + color: "#475569", + }} + > + Fullscreen target element +
+ + + + +
+
+ +
+
+ ); + }, +}); + +export const ReactiveTracking = meta.story({ + name: "Reactive state tracking", + parameters: { + docs: { + description: { + story: + "`createFullscreen` tracks real browser fullscreen state via `fullscreenchange`. `isActive` updates automatically — including when the user presses Esc.", + }, + }, + }, + render: () => { + const [ref, setRef] = createSignal(); + const { enter, exit, isActive } = createFullscreen(ref); + + return ( + + Press Esc or click Exit to leave fullscreen. + +
void} + style={{ + padding: "1.25rem", + background: "#f1f5f9", + "border-radius": "6px", + "text-align": "center", + "font-size": "0.875rem", + color: "#475569", + }} + > + Fullscreen target element +
+ + + + +
+
+ +
+
+ ); + }, +}); + +export const ClickToggle = meta.story({ + name: "Click-to-toggle", + parameters: { + docs: { + description: { + story: + "The `fullscreen()` ref factory wires click-to-toggle onto any element. Clicking enters fullscreen; clicking again (or pressing Esc) exits. The listener is removed automatically on unmount.", + }, + }, + }, + render: () => { + const [isActive, setIsActive] = createSignal(false); + const attach = fullscreen(); + + return ( + + + Click the card to enter fullscreen. Click again or press Esc to exit. + +
{ + attach(el); + const onChange = () => setIsActive(document.fullscreenElement === el); + document.addEventListener("fullscreenchange", onChange); + onCleanup(() => document.removeEventListener("fullscreenchange", onChange)); + }} + style={{ + padding: "2rem 1rem", + background: isActive() ? "#dbeafe" : "#f1f5f9", + "border-radius": "8px", + cursor: "pointer", + "text-align": "center", + border: `2px dashed ${isActive() ? "#3b82f6" : "#cbd5e1"}`, + color: isActive() ? "#1e40af" : "#475569", + "font-size": "0.875rem", + transition: "background 200ms, border-color 200ms, color 200ms", + }} + > + {isActive() ? "Fullscreen — click to exit" : "Click to enter fullscreen"} +
+
+ +
+
+ ); + }, +});