diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index 2715ff5a9c..ad991aac9c 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -214,6 +214,48 @@ describe("GSAP rules", () => { expect(finding).toBeUndefined(); }); + it("does not error when a full-frame overlay uses a GSAP fromTo entrance at frame zero", async () => { + const html = ` + +
+

Scene 1

+ + +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find( + (f) => f.code === "gsap_fullscreen_overlay_starts_visible", + ); + expect(finding).toBeUndefined(); + }); + + it("does not error when a full-frame overlay's GSAP fromTo entrance at frame zero is later hidden again", async () => { + const html = ` + +
+

Scene 1

+

Scene 2

+ + +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find( + (f) => f.code === "gsap_fullscreen_overlay_starts_visible", + ); + expect(finding).toBeUndefined(); + }); + it("reports one full-frame transition flash finding when multiple scripts touch it", async () => { const html = ` diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index b54b7c8732..f5600892a9 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -42,6 +42,10 @@ type GsapWindow = { end: number; properties: string[]; propertyValues: Record; + // fromTo()'s first ("from") vars object — the true state at the tween's start, + // as opposed to propertyValues which is always the destination (toVars). Only + // set for method === "fromTo"; other methods have no separate from-state. + fromPropertyValues?: Record; overwriteAuto: boolean; method: string; raw: string; @@ -141,6 +145,7 @@ async function extractGsapWindows(script: string): Promise { end: animation.position + effectiveDuration, properties: Object.keys(animation.properties), propertyValues: animation.properties, + fromPropertyValues: animation.fromProperties, overwriteAuto: unwrapRaw(animation.extras?.overwrite) === "auto", method: animation.method, raw: synthesizeWindowRaw(parsed.timelineVar, animation), @@ -209,6 +214,16 @@ function isVisibleGsapState(values: Record): boolean { return false; } +// The property values a window's element actually holds at the window's own +// start (position). For fromTo() this is the authored fromVars, not the +// destination toVars captured in propertyValues — a fade-in via +// `fromTo(el, {opacity:0}, {opacity:1}, 0)` genuinely starts hidden even +// though propertyValues (the toVars) says opacity: 1. +function windowStartStateValues(win: GsapWindow): Record { + if (win.method === "fromTo") return win.fromPropertyValues ?? win.propertyValues; + return win.propertyValues; +} + function makesOverlayVisible(win: GsapWindow): boolean { if (win.method === "from" && isHiddenGsapState(win.propertyValues)) return true; return isVisibleGsapState(win.propertyValues); @@ -680,7 +695,8 @@ export const gsapRules: LintRule[] = [ .sort((a, b) => a.position - b.position); const startsHiddenAtZero = visibilityWindows.some( (win) => - win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues), + win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && + isHiddenGsapState(windowStartStateValues(win)), ); if (startsHiddenAtZero) continue; const firstVisible = visibilityWindows.find((win) => makesOverlayVisible(win));