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/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/README.md b/packages/fullscreen/README.md index c224fb7b8..a39e5b5e3 100644 --- a/packages/fullscreen/README.md +++ b/packages/fullscreen/README.md @@ -8,59 +8,104 @@ [![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). + +- [**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 npm install @solid-primitives/fullscreen # or -yarn add @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); +``` -### createFullScreen +`enter(options?)` calls `element.requestFullscreen()`. Call-time options override creation-time options. `exit()` calls `document.exitFullscreen()`. ```ts -const MyComponent2: Component = () => { - const [fs, setFs] = createSignal(false); - let [ref, setRef] = createSignal(); - const active: Accessor = createFullscreen(ref, fs); - return ( -
setFs(fs => !fs)}> - {!active() ? "click to fullscreen" : "click to exit fullscreen"} -
- ); -}; +const [enter, exit] = makeFullscreen(videoEl, { navigationUI: "hide" }); +button.addEventListener("click", enter); ``` -This variant requires the ref to be a signal, otherwise the not-yet-filled ref will be captured in the closure of the primitive. +--- -You can either put the options into the second argument accessor output (useful for the directive use case) or as a third argument. +### `createFullscreen` -### Directive +Reactive primitive that binds `enter`/`exit` to an element and tracks fullscreen state via the `fullscreenchange` event. ```ts -const isActive: Accessor = createFullscreen( - ref: HTMLElement | undefined, - active?: Accessor, - options?: FullscreenOptions +const { enter, exit, isActive } = createFullscreen( + ref: HTMLElement | Accessor, + options?: FullscreenPrimitiveOptions, ); +``` + +`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. -// can be used as a directive +```tsx +const { enter, exit, isActive } = createFullscreen(ref); -const MyComponent: Component = () => { - const [fs, setFs] = createSignal(false); - return (
setFs(fs => !fs)}> - {!fs() ? 'click to fullscreen' : 'click to exit fullscreen'} -
); -} + + + + ``` -## Demo +`ref` can be a reactive accessor — `createFullscreen` rebinds when the element changes: + +```tsx +const [ref, setRef] = createSignal(); +const { enter, isActive } = createFullscreen(ref); + +
...
+``` + +#### `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 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 +// Simple toggle +
Click to go fullscreen
+ +// With options +
Click to go fullscreen
+``` -TODO +The click listener is removed automatically when the component unmounts. ## Changelog diff --git a/packages/fullscreen/package.json b/packages/fullscreen/package.json index 169c161ab..c713be708 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", @@ -13,7 +13,9 @@ "name": "fullscreen", "stage": 3, "list": [ - "createFullscreen" + "makeFullscreen", + "createFullscreen", + "fullscreen" ], "category": "Browser APIs" }, @@ -47,13 +49,16 @@ "primitives" ], "dependencies": { + "@solid-primitives/event-listener": "workspace:^", "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.14", + "solid-js": "^2.0.0-beta.14" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.14", + "solid-js": "2.0.0-beta.14" } } diff --git a/packages/fullscreen/src/index.ts b/packages/fullscreen/src/index.ts index 2a2f14d2c..b3e517e1b 100644 --- a/packages/fullscreen/src/index.ts +++ b/packages/fullscreen/src/index.ts @@ -1,72 +1,135 @@ -import { createEffect, createSignal, onCleanup, type JSX, getOwner } from "solid-js"; -import { isServer } from "solid-js/web"; -import type { Accessor } from "solid-js"; -import { access } from "@solid-primitives/utils"; +import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"; +import { isServer } from "@solidjs/web"; +import { makeEventListener } from "@solid-primitives/event-listener"; +import { access, INTERNAL_OPTIONS } from "@solid-primitives/utils"; -declare module "solid-js" { - namespace JSX { - interface Directives { - createFullscreen: ( - ref?: HTMLElement | Accessor, - active?: Accessor, - ) => Accessor; - } - } -} +/** + * 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 [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]; +}; -// only here so the `JSX` import won't be shaken off the tree: -export type E = JSX.Element; +export interface FullscreenPrimitiveOptions extends FullscreenOptions { + /** Exit fullscreen when the reactive scope is disposed. Default: `true`. */ + exitOnCleanup?: boolean; +} /** - * createFullscreen - reactively toggle fullscreen - * ```ts - * const [fs, setFs] = createSignal(false); + * 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. + * + * @param ref The element to fullscreen, or a reactive accessor returning one. + * @param options Extends `FullscreenOptions` with `exitOnCleanup` (default `true`). + * @returns `{ enter, exit, isActive }` * - * // via ref signal - * const [ref, setRef] = createSignal(); - * createFullscreen(ref, fs); - * return
!f)}>click me
+ * @example + * ```tsx + * const { enter, exit, isActive } = createFullscreen(ref); * - * // via directive: - * return
!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); - createEffect(() => { - const node = access(ref); - if (node) { - const activeOutput = active?.() ?? true; - if (!isActive() && activeOutput) { - node - .requestFullscreen(typeof activeOutput === "object" ? activeOutput : options) - .then(() => setActive(true)) - .catch(() => {}); - } else if (!activeOutput && isActive()) { - setActive(false); - document.exitFullscreen(); - } - } + const { exitOnCleanup = true, ...nativeOptions } = options ?? {}; + const initial = access(ref); + + let bound: ReturnType | null = initial + ? makeFullscreen(initial, nativeOptions) + : null; + + const [isActive, setActive] = createSignal( + document.fullscreenElement != null && document.fullscreenElement === initial, + INTERNAL_OPTIONS, + ); + + // 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(), + node => { + bound = node ? makeFullscreen(node, nativeOptions) : null; + setActive(document.fullscreenElement === node); + }, + ); + } + + onCleanup(() => { + if (exitOnCleanup && isActive()) document.exitFullscreen(); }); - const listener = () => setActive(document.fullscreenElement === access(ref)); - document.addEventListener("fullscreenchange", listener); - getOwner() && - onCleanup(() => { - document.removeEventListener("fullscreenchange", listener); - if (isActive()) { - document.exitFullscreen(); - } - }); - return isActive; + return { + enter: (callOptions?: FullscreenOptions): Promise => + bound?.[0](callOptions) ?? Promise.resolve(), + exit: (): Promise => bound?.[1]() ?? Promise.resolve(), + isActive, + }; +}; + +/** + * Ref directive that toggles fullscreen when the element is clicked. + * Attach to any element — clicking it enters fullscreen; clicking again exits. + * + * @example + * ```tsx + *
Click to go fullscreen
+ *
Click to go fullscreen
+ * ``` + */ +export const fullscreen = (options?: FullscreenOptions): ((el: HTMLElement) => void) => { + if (isServer) return () => {}; + + let removeListener: (() => void) | undefined; + onCleanup(() => removeListener?.()); + + return (el: HTMLElement) => { + removeListener?.(); + 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/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"} +
+
+ +
+
+ ); + }, +}); diff --git a/packages/fullscreen/test/index.test.ts b/packages/fullscreen/test/index.test.ts index 2426725d1..cbefb050c 100644 --- a/packages/fullscreen/test/index.test.ts +++ b/packages/fullscreen/test/index.test.ts @@ -1,78 +1,196 @@ +import "./setup.js"; import { setup_state } from "./setup.js"; -import { createRoot, createEffect, createSignal } from "solid-js"; -import { createFullscreen } from "../src/index.js"; -import { describe, it, expect } from "vitest"; +import { createRoot, flush } from "solid-js"; +import { makeFullscreen, createFullscreen, fullscreen } from "../src/index.js"; +import { describe, it, expect, beforeEach } from "vitest"; -describe("createFullscreen", () => { +beforeEach(() => { + setup_state.current_el = undefined; + setup_state.current_options = undefined; +}); + +describe("makeFullscreen", () => { const ref = document.createElement("div"); - it("will call the fullscreen method", async () => { - let captured: unknown; + 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); + }); +}); - const dispose = createRoot(dispose => { - const active = createFullscreen(ref); +describe("createFullscreen", () => { + const ref = document.createElement("div"); - createEffect(() => { - captured = active(); - }); + it("isActive starts false when element is not fullscreen", () => { + createRoot(dispose => { + const { isActive } = createFullscreen(ref); + expect(isActive()).toBe(false); + dispose(); + }); + }); - return dispose; + it("isActive starts true when element is already fullscreen", () => { + setup_state.current_el = ref; + + createRoot(dispose => { + const { isActive } = createFullscreen(ref); + expect(isActive()).toBe(true); + dispose(); }); + }); - expect(captured).toEqual(false); + it("enter() enters fullscreen and isActive becomes true", async () => { + await createRoot(async dispose => { + const { enter, isActive } = createFullscreen(ref); - await Promise.resolve(); - expect(captured).toEqual(true); + await enter(); + flush(); + expect(isActive()).toBe(true); - dispose(); + dispose(); + }); }); - it("will exit fullscreen on reactive change", async () => { - const [fs, setFs] = createSignal(true); - let captured: unknown; + 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(() => { - captured = active(); - }); + await exit(); + flush(); + expect(isActive()).toBe(false); - return dispose; + dispose(); }); + }); - expect(captured).toEqual(false); + it("isActive updates when OS dismisses fullscreen", async () => { + await createRoot(async dispose => { + const { enter, isActive } = createFullscreen(ref); - await Promise.resolve(); - expect(captured).toEqual(true); + await enter(); + flush(); + expect(isActive()).toBe(true); - setFs(false); - 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", async () => { - const options: FullscreenOptions = { navigationUI: "hide" }; - let captured: unknown; + 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); - const dispose = createRoot(dispose => { - const active = createFullscreen(ref, undefined, options); + // reset state + setup_state.current_el = undefined; + }); - createEffect(() => { - captured = active(); - }); + 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(); + }); + }); +}); - return dispose; +describe("fullscreen (directive)", () => { + const el = document.createElement("div"); + + it("clicking the element enters fullscreen", () => { + createRoot(dispose => { + const attach = fullscreen(); + attach(el); + el.click(); + expect(setup_state.current_el).toBe(el); + dispose(); }); + }); - expect(captured).toEqual(false); + 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(); + }); + }); - await Promise.resolve(); - 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/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/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 d7d328a0b..eb95b45cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,13 +384,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: