diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ddd9bbf099..13c37104d86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `line-clamp` utilities from `@tailwindcss/line-clamp` to core ([#10768](https://github.com/tailwindlabs/tailwindcss/pull/10768)) - Support ESM and TypeScript config files ([#10785](https://github.com/tailwindlabs/tailwindcss/pull/10785)) - Add `list-style-image` utilities ([#10817](https://github.com/tailwindlabs/tailwindcss/pull/10817)) +- Use `:is` to make important selector option insensitive to DOM order ([#10835](https://github.com/tailwindlabs/tailwindcss/pull/10835)) ### Fixed diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 8ca50fb99ae9..cdcd0b5688c9 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -3,6 +3,7 @@ import parser from 'postcss-selector-parser' import { resolveMatches } from './generateRules' import escapeClassName from '../util/escapeClassName' +import { applyImportantSelector } from '../util/applyImportantSelector' /** @typedef {Map} ApplyCache */ @@ -555,7 +556,7 @@ function processApply(root, context, localCache) { // And then re-add it if it was removed if (importantSelector && parentSelector !== parent.selector) { - rule.selector = `${importantSelector} ${rule.selector}` + rule.selector = applyImportantSelector(rule.selector, importantSelector) } rule.walkDecls((d) => { diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 0306893ad3ef..60da2323fc94 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -17,6 +17,7 @@ import { isValidVariantFormatString, parseVariant } from './setupContextUtils' import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue' import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js' import { flagEnabled } from '../featureFlags' +import { applyImportantSelector } from '../util/applyImportantSelector' let classNameParser = selectorParser((selectors) => { return selectors.first.filter(({ type }) => type === 'class').pop().value @@ -868,7 +869,7 @@ function getImportantStrategy(important) { } rule.selectors = rule.selectors.map((selector) => { - return `${important} ${selector}` + return applyImportantSelector(selector, important) }) } } diff --git a/src/util/applyImportantSelector.js b/src/util/applyImportantSelector.js new file mode 100644 index 000000000000..69de63325c79 --- /dev/null +++ b/src/util/applyImportantSelector.js @@ -0,0 +1,19 @@ +import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' + +export function applyImportantSelector(selector, important) { + let matches = /^(.*?)(:before|:after|::[\w-]+)(\)*)$/g.exec(selector) + if (!matches) return `${important} ${wrapWithIs(selector)}` + + let [, before, pseudo, brackets] = matches + return `${important} ${wrapWithIs(before + brackets)}${pseudo}` +} + +function wrapWithIs(selector) { + let parts = splitAtTopLevelOnly(selector, ' ') + + if (parts.length === 1 && parts[0].startsWith(':is(') && parts[0].endsWith(')')) { + return selector + } + + return `:is(${selector})` +} diff --git a/tests/apply.test.js b/tests/apply.test.js index 14b84550a8a8..f7fab7d70938 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -2120,10 +2120,10 @@ crosscheck(({ stable, oxide }) => { let result = await run(input, config) expect(result.css).toMatchFormattedCss(css` - #myselector .custom-utility { + #myselector :is(.custom-utility) { font-weight: 400; } - #myselector .group:hover .custom-utility { + #myselector :is(.group:hover .custom-utility) { text-decoration-line: underline; } `) diff --git a/tests/experimental.test.js b/tests/experimental.test.js index 5fb57d10c893..3497783f9c00 100644 --- a/tests/experimental.test.js +++ b/tests/experimental.test.js @@ -176,15 +176,15 @@ crosscheck(() => { --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; } - #app .resize { + #app :is(.resize) { resize: both; } - #app .divide-y > :not([hidden]) ~ :not([hidden]) { + #app :is(.divide-y > :not([hidden]) ~ :not([hidden])) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); } - #app .shadow { + #app :is(.shadow) { --tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a; --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); diff --git a/tests/format-variant-selector.test.js b/tests/format-variant-selector.test.js index b752a1cab406..e8318dab4a76 100644 --- a/tests/format-variant-selector.test.js +++ b/tests/format-variant-selector.test.js @@ -348,6 +348,8 @@ crosscheck(() => { ${'.parent::before &:hover'} | ${'.parent &:hover::before'} ${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'} ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} + ${'#app :is(.dark &::before)'} | ${'#app :is(.dark &)::before'} + ${'#app :is(:is(.dark &)::before)'} | ${'#app :is(:is(.dark &))::before'} `('should translate "$before" into "$after"', ({ before, after }) => { let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], { candidate: 'a', diff --git a/tests/important-selector.test.js b/tests/important-selector.test.js index 044037be1114..5c6515e43086 100644 --- a/tests/important-selector.test.js +++ b/tests/important-selector.test.js @@ -1,6 +1,6 @@ import { crosscheck, run, html, css, defaults } from './util/run' -crosscheck(() => { +crosscheck(({ stable, oxide }) => { test('important selector', () => { let config = { important: '#app', @@ -20,6 +20,7 @@ crosscheck(() => {
+
`, }, ], @@ -122,35 +123,76 @@ crosscheck(() => { transform: rotate(360deg); } } - #app .animate-spin { + #app :is(.animate-spin) { animation: 1s linear infinite spin; } - #app .font-bold { + #app :is(.font-bold) { font-weight: 700; } .custom-util { button: no; } - #app .group:hover .group-hover\:focus-within\:text-left:focus-within { + #app :is(.group:hover .group-hover\:focus-within\:text-left:focus-within) { text-align: left; } - #app :is([dir='rtl'] .rtl\:active\:text-center:active) { + #app :is(:is([dir='rtl'] .rtl\:active\:text-center:active)) { text-align: center; } @media (prefers-reduced-motion: no-preference) { - #app .motion-safe\:hover\:text-center:hover { + #app :is(.motion-safe\:hover\:text-center:hover) { text-align: center; } } - #app :is(.dark .dark\:focus\:text-left:focus) { + #app :is(.dark .dark\:before\:underline):before { + content: var(--tw-content); + text-decoration-line: underline; + } + #app :is(:is(.dark .dark\:focus\:text-left:focus)) { text-align: left; } @media (min-width: 768px) { - #app .md\:hover\:text-right:hover { + #app :is(.md\:hover\:text-right:hover) { text-align: right; } } `) }) }) + + test('pseudo-elements are appended after the `:is()`', () => { + let config = { + important: '#app', + darkMode: 'class', + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + stable.expect(result.css).toMatchFormattedCss(css` + ${defaults} + #app :is(.dark .dark\:before\:bg-black)::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); + } + `) + oxide.expect(result.css).toMatchFormattedCss(css` + ${defaults} + #app :is(.dark .dark\:before\:bg-black)::before { + content: var(--tw-content); + background-color: #000; + } + `) + }) + }) }) diff --git a/tests/util/apply-important-selector.test.js b/tests/util/apply-important-selector.test.js new file mode 100644 index 000000000000..0ec22792fe96 --- /dev/null +++ b/tests/util/apply-important-selector.test.js @@ -0,0 +1,24 @@ +import { crosscheck } from '../util/run' +import { applyImportantSelector } from '../../src/util/applyImportantSelector' + +crosscheck(() => { + it.each` + before | after + ${'.foo'} | ${'#app :is(.foo)'} + ${'.foo .bar'} | ${'#app :is(.foo .bar)'} + ${'.foo:hover'} | ${'#app :is(.foo:hover)'} + ${'.foo .bar:hover'} | ${'#app :is(.foo .bar:hover)'} + ${'.foo::before'} | ${'#app :is(.foo)::before'} + ${'.foo::before'} | ${'#app :is(.foo)::before'} + ${'.foo::file-selector-button'} | ${'#app :is(.foo)::file-selector-button'} + ${'.foo::-webkit-progress-bar'} | ${'#app :is(.foo)::-webkit-progress-bar'} + ${'.foo:hover::before'} | ${'#app :is(.foo:hover)::before'} + ${':is(.dark :is([dir="rtl"] .foo::before))'} | ${'#app :is(.dark :is([dir="rtl"] .foo))::before'} + ${':is(.dark .foo) .bar'} | ${'#app :is(:is(.dark .foo) .bar)'} + ${':is(.foo) :is(.bar)'} | ${'#app :is(:is(.foo) :is(.bar))'} + ${':is(.foo)::before'} | ${'#app :is(.foo)::before'} + ${'.foo:before'} | ${'#app :is(.foo):before'} + `('should generate "$after" from "$before"', ({ before, after }) => { + expect(applyImportantSelector(before, '#app')).toEqual(after) + }) +})