mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
Chrome 112+ / Firefox 117+ / Safari 16.5+ support native CSS Nesting.
A nested rule like '.title { … }' inside '[data-composition-id="intro"]
{ … }' resolves at match time to '<parent> .title' via the implicit
'&' prefix.
'scopeCssToComposition' walks every rule via 'root.walkRules' and re-
scopes selectors, but it did so for nested rules too — producing
'[…scope…] .title' inside '[…scope…] { … }', which nesting then
prepends AGAIN to '[…scope…] […scope…] .title'. Since the composition
root only appears once in the DOM, the doubly-scoped selector never
matches — the nested rule appears 'just ignored' as the reporter
described (#2721).
Reproduced on 0.7.66 with the reporter's exact composition. Fix: add
'isNestedInsideAnotherRule' predicate — mirrors the existing
'isInsideGlobalAtRule' — and skip nested rules in the walkRules
callback. Top-level rules still get scoped; their nested descendants
inherit scope naturally via CSS Nesting at match time.
Added two focused tests:
- 'preserves nested-rule selectors so CSS Nesting inheritance works
(#2721)' — asserts nested '.title' and 'h2' selectors stay verbatim
while top-level rules keep scoping.
- 'preserves deeply-nested CSS Nesting rules (#2721)' — same rule at
depth 3.
All 37 existing scopeCssToComposition tests still pass.
Fixes #2721.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a637f394ee
commit
1e2c7d673f
@@ -800,6 +800,51 @@ window.__afterTimeline = window.__timelines.scene;
|
||||
expect(result).toContain('[id="intro"]');
|
||||
});
|
||||
|
||||
it("preserves nested-rule selectors so CSS Nesting inheritance works (#2721)", () => {
|
||||
// Chrome 112+ / Firefox 117+ / Safari 16.5+ support native CSS Nesting.
|
||||
// A nested rule like `.title { … }` inside `[data-composition-id="intro"] { … }`
|
||||
// resolves at match time to `<parent> .title` via the implicit `&` prefix.
|
||||
// The scoper must NOT re-apply the composition scope to the nested selector —
|
||||
// that produces `<scope> <scope> .title`, which matches nothing because the
|
||||
// composition root only appears once in the DOM.
|
||||
const scoped = scopeCssToComposition(
|
||||
`
|
||||
[data-composition-id="intro"] h1 { color: red; }
|
||||
[data-composition-id="intro"] {
|
||||
.title { color: brown; }
|
||||
h2 { color: blue; }
|
||||
}
|
||||
`,
|
||||
"intro",
|
||||
);
|
||||
// Top-level rules still get scoped.
|
||||
expect(scoped).toContain('[data-composition-id="intro"] h1');
|
||||
// Nested rules keep their author-original selectors verbatim so CSS Nesting
|
||||
// can prepend the parent's `&` at match time.
|
||||
expect(scoped).toMatch(/\.title\s*\{/);
|
||||
expect(scoped).toMatch(/h2\s*\{/);
|
||||
// The pre-fix bug re-scoped nested rules to `[…] .title`, which never matched.
|
||||
expect(scoped).not.toContain('[data-composition-id="intro"] .title');
|
||||
expect(scoped).not.toContain('[data-composition-id="intro"] h2');
|
||||
});
|
||||
|
||||
it("preserves deeply-nested CSS Nesting rules (#2721)", () => {
|
||||
const scoped = scopeCssToComposition(
|
||||
`
|
||||
[data-composition-id="intro"] {
|
||||
.card {
|
||||
.header { font-weight: bold; }
|
||||
}
|
||||
}
|
||||
`,
|
||||
"intro",
|
||||
);
|
||||
expect(scoped).toMatch(/\.card\s*\{/);
|
||||
expect(scoped).toMatch(/\.header\s*\{/);
|
||||
expect(scoped).not.toContain('[data-composition-id="intro"] .card');
|
||||
expect(scoped).not.toContain('[data-composition-id="intro"] .header');
|
||||
});
|
||||
|
||||
it("wraps scripts with authored root id normalization for #id GSAP selectors", () => {
|
||||
const { document } = parseHTML(`
|
||||
<div data-composition-id="intro">
|
||||
|
||||
@@ -200,6 +200,24 @@ function isInsideGlobalAtRule(rule: Rule): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Rule nested inside another Rule (CSS Nesting Module Level 1) already
|
||||
* inherits scope from its parent's `&` prefix at match time — re-applying
|
||||
* the composition scope to the nested selector produces
|
||||
* `<scope> <scope> .child`, which matches nothing when the composition
|
||||
* root only appears once in the DOM. Only top-level rules get scoped;
|
||||
* their nested descendants inherit the scope naturally via CSS nesting.
|
||||
* See #2721 for the reproducer that motivated this.
|
||||
*/
|
||||
function isNestedInsideAnotherRule(rule: Rule): boolean {
|
||||
let current: Node["parent"] = rule.parent;
|
||||
while (current) {
|
||||
if (current.type === "rule") return true;
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function scopeCssToComposition(
|
||||
css: string,
|
||||
compositionId: string,
|
||||
@@ -216,6 +234,7 @@ export function scopeCssToComposition(
|
||||
|
||||
root.walkRules((rule) => {
|
||||
if (isInsideGlobalAtRule(rule)) return;
|
||||
if (isNestedInsideAnotherRule(rule)) return;
|
||||
rule.selectors = rule.selectors.map((selector) =>
|
||||
scopeSelector(
|
||||
selector,
|
||||
|
||||
Reference in New Issue
Block a user