Skip to content

Commit 266f029

Browse files
Use :is to make important selector option insensitive to DOM order (#10835)
* Use `:is` to make important selector option insensitive to DOM order * WIP * add `applyImportantSelector` helper * use new `applyImportantSelector` * update tests * remove unnecessary slice adjustment Not 100% sure. * update changelog --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent e4482c7 commit 266f029

9 files changed

+105
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- Add `line-clamp` utilities from `@tailwindcss/line-clamp` to core ([#10768](https://github.com/tailwindlabs/tailwindcss/pull/10768))
2323
- Support ESM and TypeScript config files ([#10785](https://github.com/tailwindlabs/tailwindcss/pull/10785))
2424
- Add `list-style-image` utilities ([#10817](https://github.com/tailwindlabs/tailwindcss/pull/10817))
25+
- Use `:is` to make important selector option insensitive to DOM order ([#10835](https://github.com/tailwindlabs/tailwindcss/pull/10835))
2526

2627
### Fixed
2728

src/lib/expandApplyAtRules.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import parser from 'postcss-selector-parser'
33

44
import { resolveMatches } from './generateRules'
55
import escapeClassName from '../util/escapeClassName'
6+
import { applyImportantSelector } from '../util/applyImportantSelector'
67

78
/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
89

@@ -555,7 +556,7 @@ function processApply(root, context, localCache) {
555556

556557
// And then re-add it if it was removed
557558
if (importantSelector && parentSelector !== parent.selector) {
558-
rule.selector = `${importantSelector} ${rule.selector}`
559+
rule.selector = applyImportantSelector(rule.selector, importantSelector)
559560
}
560561

561562
rule.walkDecls((d) => {

src/lib/generateRules.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
1717
import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
1818
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
1919
import { flagEnabled } from '../featureFlags'
20+
import { applyImportantSelector } from '../util/applyImportantSelector'
2021

2122
let classNameParser = selectorParser((selectors) => {
2223
return selectors.first.filter(({ type }) => type === 'class').pop().value
@@ -868,7 +869,7 @@ function getImportantStrategy(important) {
868869
}
869870

870871
rule.selectors = rule.selectors.map((selector) => {
871-
return `${important} ${selector}`
872+
return applyImportantSelector(selector, important)
872873
})
873874
}
874875
}

src/util/applyImportantSelector.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
2+
3+
export function applyImportantSelector(selector, important) {
4+
let matches = /^(.*?)(:before|:after|::[\w-]+)(\)*)$/g.exec(selector)
5+
if (!matches) return `${important} ${wrapWithIs(selector)}`
6+
7+
let [, before, pseudo, brackets] = matches
8+
return `${important} ${wrapWithIs(before + brackets)}${pseudo}`
9+
}
10+
11+
function wrapWithIs(selector) {
12+
let parts = splitAtTopLevelOnly(selector, ' ')
13+
14+
if (parts.length === 1 && parts[0].startsWith(':is(') && parts[0].endsWith(')')) {
15+
return selector
16+
}
17+
18+
return `:is(${selector})`
19+
}

tests/apply.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,10 +2120,10 @@ crosscheck(({ stable, oxide }) => {
21202120
let result = await run(input, config)
21212121

21222122
expect(result.css).toMatchFormattedCss(css`
2123-
#myselector .custom-utility {
2123+
#myselector :is(.custom-utility) {
21242124
font-weight: 400;
21252125
}
2126-
#myselector .group:hover .custom-utility {
2126+
#myselector :is(.group:hover .custom-utility) {
21272127
text-decoration-line: underline;
21282128
}
21292129
`)

tests/experimental.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,15 @@ crosscheck(() => {
176176
--tw-shadow: 0 0 #0000;
177177
--tw-shadow-colored: 0 0 #0000;
178178
}
179-
#app .resize {
179+
#app :is(.resize) {
180180
resize: both;
181181
}
182-
#app .divide-y > :not([hidden]) ~ :not([hidden]) {
182+
#app :is(.divide-y > :not([hidden]) ~ :not([hidden])) {
183183
--tw-divide-y-reverse: 0;
184184
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
185185
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
186186
}
187-
#app .shadow {
187+
#app :is(.shadow) {
188188
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
189189
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
190190
0 1px 2px -1px var(--tw-shadow-color);

tests/format-variant-selector.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ crosscheck(() => {
348348
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
349349
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
350350
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
351+
${'#app :is(.dark &::before)'} | ${'#app :is(.dark &)::before'}
352+
${'#app :is(:is(.dark &)::before)'} | ${'#app :is(:is(.dark &))::before'}
351353
`('should translate "$before" into "$after"', ({ before, after }) => {
352354
let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], {
353355
candidate: 'a',

tests/important-selector.test.js

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { crosscheck, run, html, css, defaults } from './util/run'
22

3-
crosscheck(() => {
3+
crosscheck(({ stable, oxide }) => {
44
test('important selector', () => {
55
let config = {
66
important: '#app',
@@ -20,6 +20,7 @@ crosscheck(() => {
2020
<div class="dark:focus:text-left"></div>
2121
<div class="group-hover:focus-within:text-left"></div>
2222
<div class="rtl:active:text-center"></div>
23+
<div class="dark:before:underline"></div>
2324
`,
2425
},
2526
],
@@ -122,35 +123,76 @@ crosscheck(() => {
122123
transform: rotate(360deg);
123124
}
124125
}
125-
#app .animate-spin {
126+
#app :is(.animate-spin) {
126127
animation: 1s linear infinite spin;
127128
}
128-
#app .font-bold {
129+
#app :is(.font-bold) {
129130
font-weight: 700;
130131
}
131132
.custom-util {
132133
button: no;
133134
}
134-
#app .group:hover .group-hover\:focus-within\:text-left:focus-within {
135+
#app :is(.group:hover .group-hover\:focus-within\:text-left:focus-within) {
135136
text-align: left;
136137
}
137-
#app :is([dir='rtl'] .rtl\:active\:text-center:active) {
138+
#app :is(:is([dir='rtl'] .rtl\:active\:text-center:active)) {
138139
text-align: center;
139140
}
140141
@media (prefers-reduced-motion: no-preference) {
141-
#app .motion-safe\:hover\:text-center:hover {
142+
#app :is(.motion-safe\:hover\:text-center:hover) {
142143
text-align: center;
143144
}
144145
}
145-
#app :is(.dark .dark\:focus\:text-left:focus) {
146+
#app :is(.dark .dark\:before\:underline):before {
147+
content: var(--tw-content);
148+
text-decoration-line: underline;
149+
}
150+
#app :is(:is(.dark .dark\:focus\:text-left:focus)) {
146151
text-align: left;
147152
}
148153
@media (min-width: 768px) {
149-
#app .md\:hover\:text-right:hover {
154+
#app :is(.md\:hover\:text-right:hover) {
150155
text-align: right;
151156
}
152157
}
153158
`)
154159
})
155160
})
161+
162+
test('pseudo-elements are appended after the `:is()`', () => {
163+
let config = {
164+
important: '#app',
165+
darkMode: 'class',
166+
content: [
167+
{
168+
raw: html` <div class="dark:before:bg-black"></div> `,
169+
},
170+
],
171+
corePlugins: { preflight: false },
172+
}
173+
174+
let input = css`
175+
@tailwind base;
176+
@tailwind components;
177+
@tailwind utilities;
178+
`
179+
180+
return run(input, config).then((result) => {
181+
stable.expect(result.css).toMatchFormattedCss(css`
182+
${defaults}
183+
#app :is(.dark .dark\:before\:bg-black)::before {
184+
content: var(--tw-content);
185+
--tw-bg-opacity: 1;
186+
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
187+
}
188+
`)
189+
oxide.expect(result.css).toMatchFormattedCss(css`
190+
${defaults}
191+
#app :is(.dark .dark\:before\:bg-black)::before {
192+
content: var(--tw-content);
193+
background-color: #000;
194+
}
195+
`)
196+
})
197+
})
156198
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { crosscheck } from '../util/run'
2+
import { applyImportantSelector } from '../../src/util/applyImportantSelector'
3+
4+
crosscheck(() => {
5+
it.each`
6+
before | after
7+
${'.foo'} | ${'#app :is(.foo)'}
8+
${'.foo .bar'} | ${'#app :is(.foo .bar)'}
9+
${'.foo:hover'} | ${'#app :is(.foo:hover)'}
10+
${'.foo .bar:hover'} | ${'#app :is(.foo .bar:hover)'}
11+
${'.foo::before'} | ${'#app :is(.foo)::before'}
12+
${'.foo::before'} | ${'#app :is(.foo)::before'}
13+
${'.foo::file-selector-button'} | ${'#app :is(.foo)::file-selector-button'}
14+
${'.foo::-webkit-progress-bar'} | ${'#app :is(.foo)::-webkit-progress-bar'}
15+
${'.foo:hover::before'} | ${'#app :is(.foo:hover)::before'}
16+
${':is(.dark :is([dir="rtl"] .foo::before))'} | ${'#app :is(.dark :is([dir="rtl"] .foo))::before'}
17+
${':is(.dark .foo) .bar'} | ${'#app :is(:is(.dark .foo) .bar)'}
18+
${':is(.foo) :is(.bar)'} | ${'#app :is(:is(.foo) :is(.bar))'}
19+
${':is(.foo)::before'} | ${'#app :is(.foo)::before'}
20+
${'.foo:before'} | ${'#app :is(.foo):before'}
21+
`('should generate "$after" from "$before"', ({ before, after }) => {
22+
expect(applyImportantSelector(before, '#app')).toEqual(after)
23+
})
24+
})

0 commit comments

Comments
 (0)