fix(native): answer an attribute selector the way Selectors §6 defines it - #447
Open
YevheniiKotyrlo wants to merge 1 commit into
Open
fix(native): answer an attribute selector the way Selectors §6 defines it#447YevheniiKotyrlo wants to merge 1 commit into
YevheniiKotyrlo wants to merge 1 commit into
Conversation
…s it `testAttribute` disagreed with the specification on six clauses, and with the DOM on the same element a `dataSet` object projects onto. §6.1 equality compared with `==` against an operand lightningcss always hands over as a string. Loose equality between a string and a non-string coerces BOTH to numbers, so `true` never matched `[data-x="true"]` while `false` matched `[data-x="0"]` and `[data-x=""]`, and `0` matched `[data-x=""]` — wrong in both directions, and the second needs no boolean at all. Every other operator already coerced with `.toString()`, so equality was the one spelling that disagreed with its five siblings about one element. §6.1 presence excluded `false` for both query types. react-native-web writes a `data-*` attribute for every non-nullish value, `false` included, so `[data-x]` matches on the web; a plain prop is the boolean-attribute case and keeps `!== false`, which is what `:disabled` depends on. §6.1 dash-match implemented only its prefix half, so `[lang|="en"]` did not match `lang="en"` — the language it names — while matching every subtag of it. It is also the one operator §6 gives NO empty-operand exclusion: a browser matches `[att|=""]` against `""`, `"-"` and `"-foo"`. §6.1 includes-match split on the space character. The separator set is INFRA's ASCII whitespace, which §6.1 links to — TAB, LF, FF, CR and SPACE. `\s` is a different set, honouring twenty further code points including NBSP and the BOM, so a value holding one was read as two words. §6.3's case flags never left the compiler. `AttributeQuery` already declared the fifth tuple element and lightningcss already reported `caseSensitivity`, so `[att=val i]` compiled to a case-sensitive query with no warning. Folding is ASCII-only: `toLowerCase` applies the full Unicode mapping and folds U+212A KELVIN SIGN onto `k`. Both compiler call sites now build through one `attributeQueryFor`, which is what let `[dir=…]` become a third construction site with none of the operator handling; its operand is folded, because `dir` is on HTML's ASCII-case-insensitive list. `s` is not emitted — nothing reads a fifth element unless it is `i`. The value-to-string conversion runs AFTER the negation arm returns. `:empty` compiles to that arm and its prop is `children`, so coercing first threw on a null-prototype child and walked every element of the ordinary array case on every render. Tests go from 4 to 106. The specifications' own examples are encoded in their original spelling — h1[title], the Cleveland/Columbus conjunction, a[rel~="copyright"], *[lang|="en"] over en / en-US / en-cockney, object[type^="image/"], a[href$=".html"], p[title*="hello"] and [frame=hsides i] — and the operator censuses are keyed by `AttrSelectorOperator` and bound with `satisfies`, so a seventh operator is a compile error rather than an assertion that silently stays at six.
This was referenced Sep 10, 2026
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.


Summary
testAttributedisagrees with Selectors §6 on six clauses, and with the DOM about the samedataSetobject. This fixes all six and adds 102 tests.Problem
A component writes one
dataSetobject and one class string; react-native-web projects it ontodata-*attributes for the browser, and this package answers the same selector on native. Measured — web through RN-Web's ownView+ React's renderer answered by Chromium, native by drivingtestRule:dataSet.x[data-x][data-x="true"][data-x="1"][data-x=""]truedata-x="true"falsedata-x="false"0data-x="0"web / native; bold is a divergence.=compares with==against an operand lightningcss always hands over as a string, and loose equality between a string and a non-string coerces both to numbers —Number("true")isNaN. Sotruenever matches[data-x="true"], whilefalsematches[data-x="0"]and0matches[data-x=""]. Every other operator already coerces with.toString(), so=is the one spelling that disagrees with its five siblings about one element.Five more, independent of the DOM:
[attr]presence excludesfalse.createDOMPropsskips only a nullishdataSetvalue, sodata-x="false"is written and the browser matches.[att|=val]implements only its prefix half —[lang|="en"]does not matchlang="en", the language it names.[att|=""]represents nothing. §6 gives this operator no empty-operand exclusion; Chromium matches"","-"and"-foo".[att~=val]splits on the space character. §6.1's whitespace links to INFRA — TAB, LF, FF, CR, SPACE — so a newline-separated pair reads as one word.[att=val i]is case-sensitive.AttributeQueryalready declares the fifth tuple element and lightningcss already reportscaseSensitivity; neither call site pushed one.Solution
=compares strings. Presence splits by query type — nullish-ness fordataSet,!== falsefor a plain prop, which is what:disableddepends on.|=gains its exact-match arm and loses an exclusion §6 does not give it.~=splits on INFRA's five code points,\sbeing a different set of twenty-five.attributeQueryFor— which is how[dir=…]became a third construction site with none of the operator handling. Its operand is now folded,dirbeing on HTML's ASCII-case-insensitive list. Onlyiis emitted;sasks for the default, so a query carrying it is a fifth element the runtime ignores. Folding is ASCII-only —toLowerCasefolds U+212A KELVIN SIGN ontok.!arm returns.:emptyis that arm withchildrenas its prop, so coercing first threw on a null-prototype child and walked every element of the ordinary array case on every render.Test plan
src/__tests__/native/attributes.test.tsx, 4 → 106 cases. Each renders a real element and reads back the width the matched rule sets.The specs' own examples are encoded in their original spelling —
span[hello="Cleveland"][goodbye="Columbus"],a[rel~="copyright"],*[lang|="en"]overen/en-US/en-cockney,object[type^="image/"],a[href$=".html"],p[title*="hello"],[frame=hsides i]. The operator censuses are keyed byAttrSelectorOperatorand closed withsatisfies, so a seventh operator is a compile error.Every fix was mutation-proved: reverting one arm at a time turns 1–17 named cases red, and the four pre-existing cases (
:disabled,:empty,truthy,equals) stay green in all fifteen runs.yarn typecheckandyarn lintclean;yarn test1150 passed, 3 failed, 21 skipped. Two babel suites fail identically on an untouchedf70c402worktree (Windows-only module-specifier rewrites) — this touches no babel file.Out of scope
Each answers identically before and after this change, measured on
main. The first two are now filed:[class=…]/[class~=…]never match: the query readsprops.class, and the compound-class path already knows to mapclassName.:is()/:where()is stored as a container query and never matched. A single-compound:is()is unaffected — lightningcss flattens it first.component.namespaceis discarded, so[ns|att]matches the unqualified set and an undeclared prefix matches rather than being dropped.falseas absent while the operators compare it as"false", a true boolean attribute is""in the DOM, and React removes an attribute whose value is a function or symbol.