From a0f4a58386c4aef2be383916807031ce49e23b7e Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 9 Aug 2026 09:58:25 +0200 Subject: [PATCH] fix: smooth advanced focus entrance --- .../advanced-focus/advanced-focus.module.css | 6 ++ .../board/advanced-focus/entrance.spec.ts | 62 +++++++++++++++++++ .../board/advanced-focus/entrance.ts | 13 ++++ 3 files changed, 81 insertions(+) create mode 100644 apps/nextjs/src/components/board/advanced-focus/entrance.spec.ts create mode 100644 apps/nextjs/src/components/board/advanced-focus/entrance.ts diff --git a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css index 1293638ba..c1e05e899 100644 --- a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css +++ b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css @@ -16,9 +16,15 @@ box-shadow: var(--mantine-shadow-md); overscroll-behavior: contain; animation: focus-surface-in 180ms cubic-bezier(0.16, 1, 0.3, 1); + animation-fill-mode: backwards; + animation-play-state: paused; transform-origin: top left; } +.surfaceReady { + animation-play-state: running; +} + .manualSurface { position: fixed; max-width: none; diff --git a/apps/nextjs/src/components/board/advanced-focus/entrance.spec.ts b/apps/nextjs/src/components/board/advanced-focus/entrance.spec.ts new file mode 100644 index 000000000..fb952e07e --- /dev/null +++ b/apps/nextjs/src/components/board/advanced-focus/entrance.spec.ts @@ -0,0 +1,62 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, test, vi } from "vitest"; + +import { startAdvancedFocusEntrance } from "./entrance"; + +describe("startAdvancedFocusEntrance", () => { + afterEach(() => { + document.body.replaceChildren(); + vi.unstubAllGlobals(); + }); + + test("moves the persistent portal before releasing its source-frame animation", () => { + let releaseFrame: FrameRequestCallback | undefined; + vi.stubGlobal( + "requestAnimationFrame", + vi.fn((callback: FrameRequestCallback) => { + releaseFrame = callback; + return 7; + }), + ); + + const compactHost = document.createElement("div"); + const previewHost = document.createElement("div"); + const portalTarget = document.createElement("div"); + const surface = document.createElement("div"); + surface.classList.add("surface-ready"); + portalTarget.append(surface); + compactHost.append(portalTarget); + document.body.append(compactHost, previewHost); + + expect(startAdvancedFocusEntrance(portalTarget, previewHost, surface, "surface-ready")).toBe(7); + expect(portalTarget.parentElement).toBe(previewHost); + expect(surface.classList.contains("surface-ready")).toBe(false); + + releaseFrame?.(0); + expect(surface.classList.contains("surface-ready")).toBe(true); + }); + + test("does not release a detached surface", () => { + let releaseFrame: FrameRequestCallback | undefined; + vi.stubGlobal( + "requestAnimationFrame", + vi.fn((callback: FrameRequestCallback) => { + releaseFrame = callback; + return 8; + }), + ); + + const previewHost = document.createElement("div"); + const portalTarget = document.createElement("div"); + const surface = document.createElement("div"); + portalTarget.append(surface); + document.body.append(previewHost); + + startAdvancedFocusEntrance(portalTarget, previewHost, surface, "surface-ready"); + portalTarget.remove(); + releaseFrame?.(0); + + expect(surface.classList.contains("surface-ready")).toBe(false); + }); +}); diff --git a/apps/nextjs/src/components/board/advanced-focus/entrance.ts b/apps/nextjs/src/components/board/advanced-focus/entrance.ts new file mode 100644 index 000000000..998d01248 --- /dev/null +++ b/apps/nextjs/src/components/board/advanced-focus/entrance.ts @@ -0,0 +1,13 @@ +export const startAdvancedFocusEntrance = ( + portalTarget: HTMLElement, + host: HTMLElement, + surface: HTMLElement, + readyClass: string, +) => { + host.append(portalTarget); + surface.classList.remove(readyClass); + + return requestAnimationFrame(() => { + if (surface.isConnected) surface.classList.add(readyClass); + }); +};