Skip to content

fix(compiler): read [class=…] from the prop the class list arrives on - #448

Open
YevheniiKotyrlo wants to merge 3 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/class-attribute-selector
Open

YevheniiKotyrlo wants to merge 3 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/class-attribute-selector

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On main, [class…] never matches. React Native carries the class list as className, but the attribute query reads props.class, which no element has. So [class~="active"], [class*="act"], [class="a b"] and [class] answer false for every element, both on the element itself and on an ancestor.

Solution

The compiler built an attribute query in two places, the compound path and the :is()/:where() path, one of them with its own copy of the operator map. A first commit moves both onto one builder, with no change in behaviour. The second makes that builder read className for class.

Tests

Every case renders as Chromium 153, Firefox 155 and WebKit 26.6 do.

  • src/__tests__/compiler/selectors.test.tsx covers:
    • the query =, ~=, *= and presence compile to;
    • an ancestor's [class~=…] through the :is() path;
    • a data-* and a plain attribute, as controls.
  • src/__tests__/native/attributes.test.tsx renders each operator over a class list, and an ancestor's class list with and without the class.
  • On fix: match a dataSet value and a dir attribute the way the DOM does #447's head, 10 of the 55 cases in these files fail. Each of 3 mutations fails its own cases, including one that leaves only the :is() path unmapped.

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

Merge order

This builds on #447, which changes the same file and suites, so #447 lands first and this carries its commit until it does. #449 builds on this one.

It also shares lines with #462 (compiler/selector-builder.ts); whichever lands second rebases.

Base

Re-written on #447 (55253748), which sits on main (a5002c5). main names aria-* attributes as written, and the builder keeps that.

@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

No current device pair

Every build of my test app carries this change: the app's reading-direction code needs #459, which is built on #447, #448 and #449. So no build can show the before, and the description's tests are the evidence.

react-native-web writes a dataSet entry as a data-* attribute for every value
but null and undefined, so [data-x] matches data-x="false" in a browser. The
runtime treated false as absent there, as it rightly does for a boolean
prop, and compared an array value as no value, where the DOM holds its
comma-joined text.

HTML compares a dir value ASCII case-insensitively, so [dir="RTL"] matches
dir="rtl" unless the selector carries the s flag. The compiler emitted the
operand as written, and the direction condition never matched it. It now
folds the operand unless s is given.

Both folds share one ASCII lowercasing helper, which the runtime's i flag
also uses.
The compound path and the :is() / :where() path each built the same query
from an attribute selector, one with its own copy of the operator map. Both
now call one builder.
React Native carries the class list as className, so an attribute query
reading props.class answered false for every element, and [class~="x"],
[class*="x"] and [class] never matched. The attribute builder maps class to
className.

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