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..3e1121eea 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.10", + "solid-js": "^2.0.0-beta.10" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" } } 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/pnpm-lock.yaml b/pnpm-lock.yaml index f59c63a9c..294cc080d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -348,9 +348,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/geolocation: dependencies: @@ -2057,7 +2060,6 @@ packages: '@graphql-tools/prisma-loader@8.0.4': resolution: {integrity: sha512-hqKPlw8bOu/GRqtYr0+dINAI13HinTVYBDqhwGAPIFmLr5s+qKskzgCiwbsckdrb5LWVFmVZc+UXn80OGiyBzg==} engines: {node: '>=16.0.0'} - deprecated: 'This package was intended to be used with an older versions of Prisma.\nThe newer versions of Prisma has a different approach to GraphQL integration.\nTherefore, this package is no longer needed and has been deprecated and removed.\nLearn more: https://www.prisma.io/graphql' peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -3537,7 +3539,6 @@ packages: dax-sh@0.43.2: resolution: {integrity: sha512-uULa1sSIHgXKGCqJ/pA0zsnzbHlVnuq7g8O2fkHokWFNwEGIhh5lAJlxZa1POG5En5ba7AU4KcBAvGQWMMf8rg==} - deprecated: This package has moved to simply be 'dax' instead of 'dax-sh' db0@0.3.2: resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==} @@ -4189,12 +4190,11 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -6234,7 +6234,6 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -6750,7 +6749,6 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}