Skip to content

fix(native): answer an ancestor chain as a path, not as a set of names - #463

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/ancestor-chain-order
Open

YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/ancestor-chain-order

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A container is registered by name, and every compound of an ancestor chain resolves in the element's own scope. So .outer .inner .subject asks whether both names sit somewhere above the element. A descendant combinator asks for an .inner ancestor that itself has an .outer ancestor (Selectors 4 §16.1). Measured on main with .outer .inner .subject { color: red }:

nesting should main
outer › inner › subject match {"color":"#f00"}
inner › outer › subject not match {"color":"#f00"}
one element with both classes › subject not match {"color":"#f00"}
inner beside outer › subject not match undefined

The last row is why this is a defect rather than a missing feature: the set of names is answered correctly, and only the order is lost. Stacked group variants are what Tailwind emits.

Solution

A registration keeps the scope it registered in, beside the element and props it already carries. testContainerQueries walks the chain innermost first, each compound resolving in the scope of the one inside it, so a single compound resolves exactly as before.

The registration a container publishes on every render takes the scope it renders in, so the walk never reads a stale one. Guards stay on the element's own scope, the only one it can re-read, and still compare the registration rather than the element, so a container whose props change re-derives its descendants.

Tests

src/__tests__/native/grouping.test.tsx gains seven cases:

  • the compiled shape the walk reads, outermost compound first, each class registering a container;
  • the order the selector names, with elements between, applying;
  • the reversed nesting, siblings and one element carrying both classes not applying;
  • a three-deep chain with one pair swapped;
  • a hover on either compound reading its own container;
  • an outer container's [data-state] read from that container, and re-derived when it changes;
  • an idempotent re-render.

On main, 3 of the 11 cases in the file fail. Each of 6 mutations fails its own cases, among them comparing a guard on the element alone, which misses a container whose props change.

Verification

On Windows with Node 26:

  • yarn lint clean
  • yarn typecheck clean
  • yarn test --coverage: every failure also fails on main on this machine (the four babel cases)
  • yarn build clean
  • yarn example expo export --platform web exported

Device evidence, before and after: #463 (comment)

Known limits

An unnamed query still resolves the nearest container that registered, not any ancestor. #462 states the same limit for a classless ancestor.

Merge order

#427 also edits src/native/reactivity.ts, so whichever lands second rebases. Independent of #462, which decides a classless ancestor's condition in the compiler; they touch no common file.

Base

Re-written on main (a5002c5).

@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — The first two bars are red. A chain asks only whether both ancestor classes are somewhere above the subject, so .outer .inner .probe matches the reversed nesting too.

FIXED — Only the first bar is red. The chain resolves innermost first, each compound in the scope of the one inside it.

before — 3.1.0-rc.0 after — with this PR

Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. before is the published 3.1.0-rc.0 plus what my test app needs from my other patches to run: the native and native-internal subpath exports, PR 459, cascade-layer ranking, and a shim that compiles a class-qualified :root inert. after adds the rest of my open PRs, this one included. One of them shows on every screen: PR 417 narrows a var()-valued font-family to one family, so the after sets its text in the theme's face where the before falls back to the platform's.

A container is registered by name, and every compound of an ancestor
chain was resolved in the element's own scope. So `.outer .inner .x`
asked whether both names sit somewhere above the element, and it matched
the reversed nesting too. A descendant combinator asks for an `.inner`
ancestor that itself has an `.outer` ancestor (Selectors 4 §16.1).

Each registration now carries the scope it registered in, beside its
element and props. The chain is walked innermost first, each compound
resolving in the scope of the one inside it. A single compound resolves
as before. Guards stay on the element's own scope, the one it can
re-read.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant