fix(compiler): scope ::selection / ::placeholder declarations to the pseudo-element - #411
Open
YevheniiKotyrlo wants to merge 2 commits into
Open
Conversation
A `::selection` / `::placeholder` rule maps ONE declaration onto a React
Native prop and returned every other declaration unchanged — so an unmapped
one was applied to the real element. `::selection { background-color: blue }`
tinted the whole control rather than the selection, silently.
The leak was on the mapping path too: `::selection { color: red;
background-color: blue }` emitted the blue background AND the mapped prop.
Three changes:
- Unmapped declarations are DROPPED. `[]` is the correct answer for a
declaration the platform cannot express — applying it to the element
instead is strictly worse than not applying it.
- `::selection` maps `background-color`, not `color`. `selectionColor` is the
band painted BEHIND the selected text, which is `background-color` in CSS;
`color` there is the selected TEXT's colour, which React Native has no prop
for. The old mapping inverted the meaning — a stylesheet asking for white
selected text got a white band and unchanged text sitting on it.
- `::placeholder` keeps `color` -> `placeholderTextColor`, which is correct,
and drops the rest.
The second and third are BREAKING for anyone relying on 3.0.7's inverted
`color` mapping. `vendor/tailwind/states.test.tsx`'s `selection` case pinned
it and is updated to `selection:bg-black`, with a second case asserting that
`selection:text-black` no longer reaches the element.
7 new compiler tests, including the control an over-broad fix would break: a
plain `.a { background-color }` on the same class is untouched.
pseudo-elements.ts carries no comments upstream, so the multi-paragraph blocks stood out. What is left is the two facts the code cannot state: that ::selection maps background-color rather than color, and that an unmapped declaration is dropped instead of returned.
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
A pseudo-element's declarations are scoped to the pseudo-element.
react-native-csscompiles a::selection/::placeholderrule by mapping ONE declaration onto a React Native prop and returning every other declaration unchanged — so an unmapped one is applied to the real element.Two consequences, both silent:
::selection { background-color }paints the element. It compiles to exactly what a plainbackground-coloron the same class compiles to, so a rule intended to tint a selection tints the whole control.::selection { color }maps toselectionColor, which is its opposite. In CSS,colorinside::selectionis the colour of the selected text; in React NativeselectionColoris the band painted behind it. A stylesheet asking for white selected text gets a white band, and the text it meant to lighten is unchanged — now sitting on a light band.::placeholderhas the same leak on its unmapped side; itscolor→placeholderTextColormapping is correct and is unchanged here.Reproduction
No framework, no device, no bundler — the compiler alone.
Actual, on 3.0.7:
Expected:
Read
.aagainst.e. They are byte-identical output for two selectors that mean different things — one asks to tint a selection, the other to paint a box, and the compiler cannot tell them apart afterwards..dshows a third, minor artifact present on both mapping paths: the empty{}alongside the mapped declaration is the...restremainder being returned even when nothing is left in it.What a real app sees
Written together, which is how every web stylesheet styles a selection:
3.0.7 renders a near-black background across the whole field plus a white selection band. Neither declaration does what it says, and nothing warns.
Root cause
modifyStyleDeclarationreturns unmatched declarations rather than dropping them, on every path — including the mapped one, whererestleaks alongside the mapped prop. The second defect is the caller's argument rather than this function's logic:modifyRuleForSelectionpasses"color"as the source property, when the property meaning "the selection band" on both platforms isbackground-color.Fix
::selectionmapsbackground-colorand drops everything else;::placeholderkeepscolor→placeholderTextColorand drops everything else.Dropping — rather than passing through — is the whole change.
[]is the correct answer for a declaration the target platform cannot express: the pseudo-element asked for something React Native has no prop for, and applying it to the element instead is strictly worse than not applying it.Submission shape — three changes, and you may want only the first
background-color→selectionColor— a new capability, making::selection { background-color }mean on React Native what it means on the web.colordropped from::selection— breaking, and the one to flag. Anyone relying on 3.0.7's behaviour is relying on an inverted mapping, but they are relying on it. If that is contentious, 1 and 2 stand alone: withbackground-colormapped,colorcould keep its current target and merely stop being the only route to the band.This branch carries all three. Happy to split it.
Tests
7 new compiler tests covering every row of the table above, including the
.econtrol an over-broad fix would break, plus the one existing expectation this legitimately changes (vendor/tailwind/states.test.tsx'sselection, updated with the reason at the site).