diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts
index 17779e5435..e51353c21d 100644
--- a/packages/lint/src/rules/gsap.test.ts
+++ b/packages/lint/src/rules/gsap.test.ts
@@ -3721,6 +3721,43 @@ describe("SVG draw-on rules", () => {
expect(finding).toBeUndefined();
});
+ describe("gsap_fullscreen_overlay_starts_visible — a scene child hidden through a scoped query", () => {
+ const scene = (hide: string) => `
+
+
+
+`;
+ const flagged = async (html: string) =>
+ (await lintHyperframeHtml(html)).findings.some(
+ (f) => f.code === "gsap_fullscreen_overlay_starts_visible",
+ );
+
+ it("accepts gsap.set on a querySelector call with a literal selector", async () => {
+ expect(await flagged(scene('gsap.set(root.querySelector(".ov"), { opacity: 0 });'))).toBe(
+ false,
+ );
+ });
+
+ it("accepts gsap.set on a variable bound to a querySelector call", async () => {
+ expect(
+ await flagged(scene('const ov = root.querySelector(".ov"); gsap.set(ov, { opacity: 0 });')),
+ ).toBe(false);
+ });
+
+ it("still flags an overlay nothing hides at zero", async () => {
+ expect(await flagged(scene(""))).toBe(true);
+ });
+ });
+
describe("gsap_fullscreen_overlay_starts_visible — the from() shape is not a defect", () => {
const overlay = (style: string, script: string) => `
diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts
index d0611c9413..2471f4de36 100644
--- a/packages/lint/src/rules/gsap.ts
+++ b/packages/lint/src/rules/gsap.ts
@@ -253,7 +253,10 @@ function hiddenSetTargetSelectors(target: string, aliases: Map):
return parts
.flatMap((part) => {
const trimmed = part.trim();
- const resolved = /^(["'`])([^"'`]+)\1$/.exec(trimmed)?.[2] ?? aliases.get(trimmed);
+ const resolved =
+ /^(["'`])([^"'`]+)\1$/.exec(trimmed)?.[2] ??
+ /^[\w$.]+\.querySelector(?:All)?\(\s*(["'`])([^"'`]+)\1\s*\)$/.exec(trimmed)?.[2] ??
+ aliases.get(trimmed);
return resolved === undefined ? [] : resolved.split(",");
})
.map((selector) => selector.trim())
@@ -270,8 +273,13 @@ function extractStandaloneHiddenSelectors(script: string): Set {
)) {
aliases.set(match[1] ?? "", match[3] ?? "");
}
+ for (const match of source.matchAll(
+ /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[\w$.]+\.querySelector(?:All)?\(\s*(["'`])([^"'`]+)\2\s*\)\s*;/g,
+ )) {
+ aliases.set(match[1] ?? "", match[3] ?? "");
+ }
const pattern =
- /gsap\.set\s*\(\s*(\[[^[\]]*\]|"[^"]*"|'[^']*'|`[^`]*`|[^,()[\]]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g;
+ /gsap\.set\s*\(\s*(\[[^[\]]*\]|"[^"]*"|'[^']*'|`[^`]*`|[\w$.]+\.querySelector(?:All)?\(\s*(?:"[^"]*"|'[^']*'|`[^`]*`)\s*\)|[^,()[\]]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g;
let match: RegExpExecArray | null;
while ((match = pattern.exec(source)) !== null) {
// Skip callback/handler bodies; keep IIFEs (they run at parse time).