Repository navigation
fix(native): answer :dir() per element, and compile every spelling of it - #459
Open
YevheniiKotyrlo wants to merge 5 commits into
Open
YevheniiKotyrlo wants to merge 5 commits into
YevheniiKotyrlo wants to merge 5 commits into
Conversation
Contributor
Author
No current device pairEvery build of my test app carries this change, because the app's reading-direction code needs it, so no build can show the |
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.
[ns|att] represents only an attribute in the namespace ns (Selectors 4 §6.4), and a React Native prop is in no namespace, so the selector can match nothing. The compiler ignored the namespace and read the prop, so [ns|data-x="a"] matched where every browser matches nothing. An undeclared prefix is an invalid selector, with the same outcome. The attribute builder now answers no query for a namespace-qualified attribute, and both of its callers drop the selector; [|att] and [*|att] are unchanged.
:dir() matches an element's own directionality (Selectors 4 §7.1): its dir,
or else its nearest ancestor's. React Native had no per-element direction,
so the runtime answered every direction condition from I18nManager for the
whole app, and a View with dir="ltr" inside a right-to-left app still
matched :dir(rtl).
View and Text take a dir prop. An element publishes the directions declared
on it and its ancestors, nearest first, and takes the user-agent rule
[dir] { direction }. The platform's direction is the root's directionality
and no element's dir, as for a document whose html carries none.
The compiler emits each spelling as the question it asks. :dir() on the
element is its directionality and [dir] on the element is its own prop.
[dir] on an ancestor asks whether any ancestor declares it, :dir() on an
ancestor adds the root's directionality, and html:dir() is the root's.
Tailwind's rtl: and ltr: arms become one rule each, so rtl: reaches into
an ltr island as it does in a browser. A directionality the runtime cannot
place on its ancestor compiles to nothing.
YevheniiKotyrlo
force-pushed
the
fix/per-element-directionality
branch
from
October 11, 2026 12:36
45e86e2 to
f74e7ad
Compare
This was referenced Oct 11, 2026
Open
This branch has not been deployed
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
:dir()matches an element's own directionality (Selectors 4 §7.1): itsdir, else the nearest ancestor's. Onmainnothing has a direction of its own. Every direction condition readsI18nManager.isRTLfor the whole app, and the compiler reads the spellings of one question differently:main.x:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *), Tailwind'srtl:.x:dir(rtl)·.x:where([dir="rtl"])·:dir(rtl) .x[dir="rtl"] .x·html[dir="rtl"] .x.x[dir="rtl"]dirSolution
ViewandTexttake adirprop. An element publishes the directions declared on it and its ancestors, nearest first, through the variable context its descendants already read. A declaring element takes the user-agent rule[dir] { direction }, beneath every author rule. Its directionality is its owndir, else the nearest declared one, else the platform's.The platform's direction is the root's directionality and no element's
dir. That is the document a browser renders when<html>carries nodir, so adiron a rootViewreads the same on web and native.The compiler emits each spelling as the question it asks:
.x:dir(rtl)·@media (dir: rtl)["=", "dir", "rtl"], the element's directionality.x[dir="rtl"]·.x:where([dir="rtl"])["a", "dir", "=", "rtl", "i"], its owndir[dir="rtl"] .x·.x:where([dir="rtl"] *)["=", "ancestor-dir", "rtl"], an ancestor declares it:dir(rtl) .x·.x:where(:dir(rtl) *)root-dirorancestor-dirhtml:dir(rtl) .x["=", "root-dir", "rtl"]html[dir="rtl"] .xTailwind's
rtl:becomes one rule per arm. As in every browser, it therefore reaches into anltrisland inside anrtltree. A value folds ASCII case-insensitively unless the selector carriess.A directionality the runtime cannot place on its ancestor compiles to nothing:
:dir();[dir]or:dir();[dir]more than one compound up.A container's own
[dir], as in.group[dir="rtl"] .item, stays an attribute of that container.Tests
Every expected value is what Firefox 155 and WebKit 26.6 render. Chromium 153 agrees except on the
s-flag rows: it does not implements, so it drops those selectors. An RTL platform is measured as an<html dir="auto">whose text resolves it tortl.src/__tests__/native/directionality.test.tsxrenders 19 spellings over 9 trees on both platform directions, 342 cases. The trees include nested opposite declarations, a declaration under an opposite ancestor, and an undeclared element in between. Further cases cover:direction;autoand values outside the enumeration, and inline variables;dirthat changes, comes or goes, keeping children mounted;Text, the@media (dir)operators,:rootvariables, and an idempotent re-render.src/__tests__/compiler/directionality.test.tshas 31 cases overcompile().[dir]rows incompiler/selectors.test.tsxandnative/direction-media.test.tsxnow read the element's owndir.Verification
On Windows with Node 26:
yarn lintcleanyarn typecheckcleanyarn test --coverage: every failure also fails onmainon this machine (the four babel cases)yarn buildcleanyarn example expo export --platform webexportedKnown limits
rtl:andltr:reach this code once fix(metro): compile the native stylesheet from the authored CSS, never its web build #461 lands. Until then the Metro transformer hands the compiler CSS lowered for browsers, where:dir()has become:lang()(rtl:/ltr: variants silently dropped — lightningcss downcompiles :dir() to :lang() which is unhandled #453).:not(:dir()), Tailwind'snot-rtl:, still compiles to nothing, since the builder negates prop questions only.[dir="rtl"] > .xcompiles to nothing.dirkey itself changes its render shape unless its rules already provide a scope. The existing re-mount log reports it.Merge order
diris answered here before fix(compiler): answer an ancestor condition that names no class #462 builds a container query.selector-builder.tsandspecificity.ts, and fix(types): accept an explicit undefined on every prop this package adds #418 editstypes.d.ts; each rebases on whichever lands first.It also shares lines with #475 (
compiler/selector-builder.ts); whichever lands second rebases.Base
Re-written on #449 (
590d395e), which sits on #448, #447 andmain(a5002c5).