diff --git a/src/lib/resolveDefaultsAtRules.js b/src/lib/resolveDefaultsAtRules.js index 95887b359395..e8c1399b9769 100644 --- a/src/lib/resolveDefaultsAtRules.js +++ b/src/lib/resolveDefaultsAtRules.js @@ -3,7 +3,20 @@ import selectorParser from 'postcss-selector-parser' import { flagEnabled } from '../featureFlags' function minimumImpactSelector(nodes) { - let pseudos = nodes.filter((n) => n.type === 'pseudo') + let rest = nodes + // Keep all pseudo & combinator types (:not([hidden]) ~ :not([hidden])) + .filter((n) => n.type === 'pseudo' || n.type === 'combinator') + // Remove leading pseudo's (:hover, :focus, ...) + .filter((n, idx, all) => { + // Keep pseudo elements + if (n.type === 'pseudo' && n.value.startsWith('::')) return true + + if (idx === 0 && n.type === 'pseudo') return false + if (idx > 0 && n.type === 'pseudo' && all[idx - 1].type === 'pseudo') return false + + return true + }) + let [bestNode] = nodes for (let [type, getNode = (n) => n] of [ @@ -28,16 +41,12 @@ function minimumImpactSelector(nodes) { } } - return [bestNode, ...pseudos].join('').trim() + return [bestNode, ...rest].join('').trim() } let elementSelectorParser = selectorParser((selectors) => { return selectors.map((s) => { - let nodes = s - .split((n) => n.type === 'combinator') - .pop() - .filter((n) => n.type !== 'pseudo' || n.value.startsWith('::')) - + let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop() return minimumImpactSelector(nodes) }) }) diff --git a/tests/experimental.test.js b/tests/experimental.test.js new file mode 100644 index 000000000000..6404c0013c59 --- /dev/null +++ b/tests/experimental.test.js @@ -0,0 +1,209 @@ +import { run, html, css } from './util/run' + +test('experimental universal selector improvements (box-shadow)', () => { + let config = { + experimental: 'all', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(css` + .shadow { + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + } + + .resize { + resize: both; + } + + .shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), + var(--tw-shadow); + } + `) + }) +}) + +test('experimental universal selector improvements (pseudo hover)', () => { + let config = { + experimental: 'all', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(css` + .hover\\:shadow { + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + } + + .resize { + resize: both; + } + + .hover\\:shadow:hover { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), + var(--tw-shadow); + } + `) + }) +}) + +test('experimental universal selector improvements (multiple classes: group)', () => { + let config = { + experimental: 'all', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(css` + .group-hover\\:shadow { + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + } + + .resize { + resize: both; + } + + .group:hover .group-hover\\:shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), + var(--tw-shadow); + } + `) + }) +}) + +test('experimental universal selector improvements (child selectors: divide-y)', () => { + let config = { + experimental: 'all', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(css` + .divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + } + + .resize { + resize: both; + } + + .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)); + } + `) + }) +}) + +test('experimental universal selector improvements (hover:divide-y)', () => { + let config = { + experimental: 'all', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(css` + .hover\\:divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + } + + .resize { + resize: both; + } + + .hover\\:divide-y:hover > :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)); + } + `) + }) +}) + +test('experimental universal selector improvements (#app important)', () => { + let config = { + experimental: 'all', + important: '#app', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(css` + .divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + } + + .shadow { + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + } + + #app .resize { + resize: both; + } + + #app .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 { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), + var(--tw-shadow); + } + `) + }) +}) diff --git a/tests/resolve-defaults-at-rules.test.js b/tests/resolve-defaults-at-rules.test.js index 0a4d2af13a41..6f250bc6b177 100644 --- a/tests/resolve-defaults-at-rules.test.js +++ b/tests/resolve-defaults-at-rules.test.js @@ -527,6 +527,7 @@ test('when a utility uses defaults but they do not exist', async () => { test('selectors are reduced to the lowest possible specificity', async () => { let config = { + experimental: 'all', content: [{ raw: html`
` }], corePlugins: [], } @@ -576,9 +577,13 @@ test('selectors are reduced to the lowest possible specificity', async () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` - *, - ::before, - ::after { + .foo, + [id='app'], + [id='page'], + [id='other'], + [data-bar='baz'], + article, + [id='another']::before { --color: black; }