fix(native): let an element's own custom property outrank an inherited one - #443
Open
YevheniiKotyrlo wants to merge 3 commits into
Open
fix(native): let an element's own custom property outrank an inherited one#443YevheniiKotyrlo wants to merge 3 commits into
YevheniiKotyrlo wants to merge 3 commits into
Conversation
…d one
`varResolver` read the inherited variable context before the element's own
record and returned from it, so a `vars()` declaration on the element was used
only when no ancestor declared the same name.
css-cascade-4 §7.2 makes inheritance a defaulting step: an element inherits a
property only when the cascade yields no declared value for it. A custom
property is an ordinary property (css-variables-1 §2), so an element declaring
`--x` uses its own value rather than its ancestor's.
Measured against a provider declaring `--my-var: red` around an element whose
`vars()` declares `--my-var: blue`, with the class reading `color: var(--my-var)`:
expected { color: "blue" }
actual { color: "red" }
Web is unaffected and already correct — it implements no precedence of its own.
`vars()` returns a plain `{"--x": v}` style object and `VariableContextProvider`
renders a `display: contents` div carrying the property, so the browser's cascade
decides.
The inherited read moves below the element's own record and keeps pushing the
RAW descriptor, which is what `testGuards` compares against the next render's
context. The second read of `variables[name]` that followed it is deleted: it
ran only when `name` was absent from `variables`, so it resolved `undefined` and
its guard never fired.
`inline-variables.ts` folds a custom property the sheet declares exactly once
into the declaration that reads it, so the compiled rule carries no variable and
the runtime performs no `var()` lookup.
That is a precondition every precedence test depends on and nothing stated:
`.own { --my-var: blue; color: var(--my-var) }` alone compiles to
`d: [{ color: "#00f" }]`, so a test written that way passes whatever the
resolver does. Pinning the fold means a change to it goes red here instead of
making those suites quietly vacuous.
A rule's `v` entries and a `vars()` object land in the same runtime record, so the resolution order governs plain stylesheet CSS too — and that half had no coverage. Five cases in `variables.test.tsx` and three more in `vars.test.tsx`. Six are red on main: an own rule against an ancestor's rule, against a provider, and across a re-render that changes the provider's value; `vars()` against the element's own class declaration; and two siblings under one provider. The rest are the regression half — an own rule still outranks `:root`, a declaring element still publishes to its descendants, and an inherited value still applies where the element declares nothing. Every sheet declares the name more than once, or the compiler folds it and the case measures nothing.
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
varResolverreads the inherited variable context before the element's own record and returns from it:So an element's own declaration of
--xis used only when no ancestor declared that name. css-cascade-4 §7.2 makes inheritance a defaulting step — reached only when the cascade yields no declared value — and a custom property is an ordinary property (css-variables-1 §2), so this is backwards.It is not specific to
vars(). A rule'sventries and avars()object land in the same runtime record (calculate-props.tswrites both intoinlineVariables), so the ordering governs plain stylesheet CSS, which is the half most consumers write. Measured onmain(f70c402), with--my-vardeclared more than once in each sheet so the compiler cannot fold it:main.ancestor { --my-var: red }›.own { --my-var: blue; color: var(--my-var) }{ color: "blue" }{ color: "red" }<Provider value={{"--my-var": "red"}}>›.own { --my-var: blue; color: var(--my-var) }{ color: "blue" }{ color: "red" }blue<Provider value={{"--my-var": "red"}}>›style={vars({"--my-var": "blue"})}{ color: "blue" }{ color: "red" }.my-class { --my-var: blue },vars()says green{ color: "green" }{ color: "red" }vars()red/bluered/redWeb is unaffected and already correct: it implements no precedence of its own —
vars()returns a plain{"--x": v}style object andVariableContextProviderrenders adisplay: contentsdiv carrying the property, so the browser's cascade decides. I measured the same tree in chromium. The compiler is unaffected too; it emitsv/vrtuples and never orders the two channels.Fix
The inherited read moves below the element's own record. It keeps pushing the raw descriptor rather than the resolved value, because
testGuardscompares the guard against the next render's context and a resolved value would never match.The second read of
variables[name]that followed the inline arm is deleted. It was unreachable-effective: it ran only whennamewas absent fromvariables, soresolve(variables[name])wasresolve(undefined)and itsvalue !== undefinedguard never fired.Net: 6 insertions, 12 deletions in one function.
Tests
Ten runtime cases across
variables.test.tsx(stylesheet channel) andvars.test.tsx(vars()channel). The six in the table above are red onmainand green after. The other four are the regression half — an own rule still outranks:root, a declaring element still publishes to its descendants, a parent'svars()still reaches one, and an inherited value still applies when the element declares nothing, so the fix cannot be satisfied by ignoring the context.Four compile-time cases in a new
src/__tests__/compiler/inline-variables.test.ts, because the runtime ones have a precondition that is easy to lose.inline-variables.tsfolds a custom property the sheet declares exactly once into the declaration that reads it, and a folded property performs no runtimevar()lookup at all —.own { --my-var: blue; color: var(--my-var) }alone compiles tod: [{ color: "#00f" }]and passes on unfixedmainwithout reachingvarResolver. Pinning the fold is what keeps every case above from going quietly vacuous if that rule ever changes.Mutation-proven against
mainwith the same test files in place:9 failed, 1056 passed, 1086 total— same total, so the delta is exactly the six cases.The 3 failures are the pre-existing Windows-only
babel-plugin-testermismatches over an unrewritten relativerequire("../View"), identical onmain(#390 fixes them; CI is green).What this does not fix, measured
An own declaration whose value is invalid at computed-value time still falls back to the inherited value —
.own { --my-var: var(--nothing-declares-this); color: var(--my-var) }under a provider paints the provider's value onmainand after this change, where css-variables-1 §3.2 makes the referencing declaration invalid instead.Keying the own read on presence (
name in inlineVariables) rather than on resolving to something would do it, but not on its own: the same function memoises resolved:rootand universal values back intooptions.inlineVariables, so a presence-keyed read would let a:rootvalue masquerade as the element's own on a second lookup in the same pass. That needs the memo moved into a record of its own, which is a larger change and composes with this one.Quality gates
Happy to reshape this — the tests are separately revertible commits, and the compiler file can land on its own if you would rather review the fold pin apart from the precedence cases.