Skip to content

Commit a8836eb

Browse files
Improve experimental universal selector improvements (#5517)
* add dedicated tests for the experimenal universal selector improvements * Add failing test * keep pseudo elements * re-add logic for special types (class, id, attribute) Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
1 parent f9ee118 commit a8836eb

File tree

3 files changed

+233
-10
lines changed

3 files changed

+233
-10
lines changed

src/lib/resolveDefaultsAtRules.js

+16-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@ import selectorParser from 'postcss-selector-parser'
33
import { flagEnabled } from '../featureFlags'
44

55
function minimumImpactSelector(nodes) {
6-
let pseudos = nodes.filter((n) => n.type === 'pseudo')
6+
let rest = nodes
7+
// Keep all pseudo & combinator types (:not([hidden]) ~ :not([hidden]))
8+
.filter((n) => n.type === 'pseudo' || n.type === 'combinator')
9+
// Remove leading pseudo's (:hover, :focus, ...)
10+
.filter((n, idx, all) => {
11+
// Keep pseudo elements
12+
if (n.type === 'pseudo' && n.value.startsWith('::')) return true
13+
14+
if (idx === 0 && n.type === 'pseudo') return false
15+
if (idx > 0 && n.type === 'pseudo' && all[idx - 1].type === 'pseudo') return false
16+
17+
return true
18+
})
19+
720
let [bestNode] = nodes
821

922
for (let [type, getNode = (n) => n] of [
@@ -28,16 +41,12 @@ function minimumImpactSelector(nodes) {
2841
}
2942
}
3043

31-
return [bestNode, ...pseudos].join('').trim()
44+
return [bestNode, ...rest].join('').trim()
3245
}
3346

3447
let elementSelectorParser = selectorParser((selectors) => {
3548
return selectors.map((s) => {
36-
let nodes = s
37-
.split((n) => n.type === 'combinator')
38-
.pop()
39-
.filter((n) => n.type !== 'pseudo' || n.value.startsWith('::'))
40-
49+
let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop()
4150
return minimumImpactSelector(nodes)
4251
})
4352
})

tests/experimental.test.js

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { run, html, css } from './util/run'
2+
3+
test('experimental universal selector improvements (box-shadow)', () => {
4+
let config = {
5+
experimental: 'all',
6+
content: [{ raw: html`<div class="shadow resize"></div>` }],
7+
corePlugins: { preflight: false },
8+
}
9+
10+
let input = css`
11+
@tailwind base;
12+
@tailwind utilities;
13+
`
14+
15+
return run(input, config).then((result) => {
16+
expect(result.css).toMatchCss(css`
17+
.shadow {
18+
--tw-ring-offset-shadow: 0 0 #0000;
19+
--tw-ring-shadow: 0 0 #0000;
20+
--tw-shadow: 0 0 #0000;
21+
}
22+
23+
.resize {
24+
resize: both;
25+
}
26+
27+
.shadow {
28+
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
29+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
30+
var(--tw-shadow);
31+
}
32+
`)
33+
})
34+
})
35+
36+
test('experimental universal selector improvements (pseudo hover)', () => {
37+
let config = {
38+
experimental: 'all',
39+
content: [{ raw: html`<div class="hover:shadow resize"></div>` }],
40+
corePlugins: { preflight: false },
41+
}
42+
43+
let input = css`
44+
@tailwind base;
45+
@tailwind utilities;
46+
`
47+
48+
return run(input, config).then((result) => {
49+
expect(result.css).toMatchCss(css`
50+
.hover\\:shadow {
51+
--tw-ring-offset-shadow: 0 0 #0000;
52+
--tw-ring-shadow: 0 0 #0000;
53+
--tw-shadow: 0 0 #0000;
54+
}
55+
56+
.resize {
57+
resize: both;
58+
}
59+
60+
.hover\\:shadow:hover {
61+
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
62+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
63+
var(--tw-shadow);
64+
}
65+
`)
66+
})
67+
})
68+
69+
test('experimental universal selector improvements (multiple classes: group)', () => {
70+
let config = {
71+
experimental: 'all',
72+
content: [{ raw: html`<div class="group-hover:shadow resize"></div>` }],
73+
corePlugins: { preflight: false },
74+
}
75+
76+
let input = css`
77+
@tailwind base;
78+
@tailwind utilities;
79+
`
80+
81+
return run(input, config).then((result) => {
82+
expect(result.css).toMatchCss(css`
83+
.group-hover\\:shadow {
84+
--tw-ring-offset-shadow: 0 0 #0000;
85+
--tw-ring-shadow: 0 0 #0000;
86+
--tw-shadow: 0 0 #0000;
87+
}
88+
89+
.resize {
90+
resize: both;
91+
}
92+
93+
.group:hover .group-hover\\:shadow {
94+
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
95+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
96+
var(--tw-shadow);
97+
}
98+
`)
99+
})
100+
})
101+
102+
test('experimental universal selector improvements (child selectors: divide-y)', () => {
103+
let config = {
104+
experimental: 'all',
105+
content: [{ raw: html`<div class="divide-y resize"></div>` }],
106+
corePlugins: { preflight: false },
107+
}
108+
109+
let input = css`
110+
@tailwind base;
111+
@tailwind utilities;
112+
`
113+
114+
return run(input, config).then((result) => {
115+
expect(result.css).toMatchCss(css`
116+
.divide-y > :not([hidden]) ~ :not([hidden]) {
117+
--tw-border-opacity: 1;
118+
border-color: rgb(229 231 235 / var(--tw-border-opacity));
119+
}
120+
121+
.resize {
122+
resize: both;
123+
}
124+
125+
.divide-y > :not([hidden]) ~ :not([hidden]) {
126+
--tw-divide-y-reverse: 0;
127+
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
128+
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
129+
}
130+
`)
131+
})
132+
})
133+
134+
test('experimental universal selector improvements (hover:divide-y)', () => {
135+
let config = {
136+
experimental: 'all',
137+
content: [{ raw: html`<div class="hover:divide-y resize"></div>` }],
138+
corePlugins: { preflight: false },
139+
}
140+
141+
let input = css`
142+
@tailwind base;
143+
@tailwind utilities;
144+
`
145+
146+
return run(input, config).then((result) => {
147+
expect(result.css).toMatchCss(css`
148+
.hover\\:divide-y > :not([hidden]) ~ :not([hidden]) {
149+
--tw-border-opacity: 1;
150+
border-color: rgb(229 231 235 / var(--tw-border-opacity));
151+
}
152+
153+
.resize {
154+
resize: both;
155+
}
156+
157+
.hover\\:divide-y:hover > :not([hidden]) ~ :not([hidden]) {
158+
--tw-divide-y-reverse: 0;
159+
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
160+
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
161+
}
162+
`)
163+
})
164+
})
165+
166+
test('experimental universal selector improvements (#app important)', () => {
167+
let config = {
168+
experimental: 'all',
169+
important: '#app',
170+
content: [{ raw: html`<div class="shadow divide-y resize"></div>` }],
171+
corePlugins: { preflight: false },
172+
}
173+
174+
let input = css`
175+
@tailwind base;
176+
@tailwind utilities;
177+
`
178+
179+
return run(input, config).then((result) => {
180+
expect(result.css).toMatchCss(css`
181+
.divide-y > :not([hidden]) ~ :not([hidden]) {
182+
--tw-border-opacity: 1;
183+
border-color: rgb(229 231 235 / var(--tw-border-opacity));
184+
}
185+
186+
.shadow {
187+
--tw-ring-offset-shadow: 0 0 #0000;
188+
--tw-ring-shadow: 0 0 #0000;
189+
--tw-shadow: 0 0 #0000;
190+
}
191+
192+
#app .resize {
193+
resize: both;
194+
}
195+
196+
#app .divide-y > :not([hidden]) ~ :not([hidden]) {
197+
--tw-divide-y-reverse: 0;
198+
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
199+
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
200+
}
201+
202+
#app .shadow {
203+
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
204+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
205+
var(--tw-shadow);
206+
}
207+
`)
208+
})
209+
})

tests/resolve-defaults-at-rules.test.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ test('when a utility uses defaults but they do not exist', async () => {
527527

528528
test('selectors are reduced to the lowest possible specificity', async () => {
529529
let config = {
530+
experimental: 'all',
530531
content: [{ raw: html`<div class="foo"></div>` }],
531532
corePlugins: [],
532533
}
@@ -576,9 +577,13 @@ test('selectors are reduced to the lowest possible specificity', async () => {
576577

577578
return run(input, config).then((result) => {
578579
expect(result.css).toMatchFormattedCss(css`
579-
*,
580-
::before,
581-
::after {
580+
.foo,
581+
[id='app'],
582+
[id='page'],
583+
[id='other'],
584+
[data-bar='baz'],
585+
article,
586+
[id='another']::before {
582587
--color: black;
583588
}
584589

0 commit comments

Comments
 (0)