mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +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"]');
|
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", () => {
|
it("wraps scripts with authored root id normalization for #id GSAP selectors", () => {
|
||||||
const { document } = parseHTML(`
|
const { document } = parseHTML(`
|
||||||
<div data-composition-id="intro">
|
<div data-composition-id="intro">
|
||||||
|
|||||||
@@ -200,6 +200,24 @@ function isInsideGlobalAtRule(rule: Rule): boolean {
|
|||||||
return false;
|
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(
|
export function scopeCssToComposition(
|
||||||
css: string,
|
css: string,
|
||||||
compositionId: string,
|
compositionId: string,
|
||||||
@@ -216,6 +234,7 @@ export function scopeCssToComposition(
|
|||||||
|
|
||||||
root.walkRules((rule) => {
|
root.walkRules((rule) => {
|
||||||
if (isInsideGlobalAtRule(rule)) return;
|
if (isInsideGlobalAtRule(rule)) return;
|
||||||
|
if (isNestedInsideAnotherRule(rule)) return;
|
||||||
rule.selectors = rule.selectors.map((selector) =>
|
rule.selectors = rule.selectors.map((selector) =>
|
||||||
scopeSelector(
|
scopeSelector(
|
||||||
selector,
|
selector,
|
||||||
|
|||||||
Reference in New Issue
Block a user