Skip to content

Commit ef325ea

Browse files
Add tuple syntax to guarantee screens order (#6104)
* add normalizeScreens function This will allow us to normalize the various kinds of inputs to a stable version that is consistent regardless of the input. * use normalized screens * add dedicated test for new tuple syntax * make test consistent with other tests While working on the normalizeScreens feature, some tests started failing (the one with multiple screens), while looking at them I made them consistent with the rest of the codebase. * add test to ensure consistent order in screens output * update changelog * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Adam Wathan <[email protected]>
1 parent 6c4b86d commit ef325ea

10 files changed

+531
-194
lines changed

CHANGELOG.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
Nothing yet!
11-
1210
### Fixed
1311

1412
- Enforce the order of some variants (like `before` and `after`) ([#6018](https://github.com/tailwindlabs/tailwindcss/pull/6018))
1513

1614
### Added
1715

1816
- Add `placeholder` variant ([#6106](https://github.com/tailwindlabs/tailwindcss/pull/6106))
17+
- Add tuple syntax for configuring screens while guaranteeing order ([#5956](https://github.com/tailwindlabs/tailwindcss/pull/5956))
1918

2019
## [3.0.0-alpha.2] - 2021-11-08
2120

src/corePlugins.js

+15-34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import isPlainObject from './util/isPlainObject'
1111
import transformThemeValue from './util/transformThemeValue'
1212
import { version as tailwindVersion } from '../package.json'
1313
import log from './util/log'
14+
import { normalizeScreens } from './util/normalizeScreens'
1415
import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue'
1516

1617
export let variantPlugins = {
@@ -158,11 +159,10 @@ export let variantPlugins = {
158159
},
159160

160161
screenVariants: ({ theme, addVariant }) => {
161-
for (let screen in theme('screens')) {
162-
let size = theme('screens')[screen]
163-
let query = buildMediaQuery(size)
162+
for (let screen of normalizeScreens(theme('screens'))) {
163+
let query = buildMediaQuery(screen)
164164

165-
addVariant(screen, `@media ${query}`)
165+
addVariant(screen.name, `@media ${query}`)
166166
}
167167
},
168168
}
@@ -182,24 +182,10 @@ export let corePlugins = {
182182
},
183183

184184
container: (() => {
185-
function extractMinWidths(breakpoints) {
186-
return Object.values(breakpoints ?? {}).flatMap((breakpoints) => {
187-
if (typeof breakpoints === 'string') {
188-
breakpoints = { min: breakpoints }
189-
}
190-
191-
if (!Array.isArray(breakpoints)) {
192-
breakpoints = [breakpoints]
193-
}
194-
195-
return breakpoints
196-
.filter((breakpoint) => {
197-
return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width')
198-
})
199-
.map((breakpoint) => {
200-
return breakpoint['min-width'] ?? breakpoint.min
201-
})
202-
})
185+
function extractMinWidths(breakpoints = []) {
186+
return breakpoints
187+
.flatMap((breakpoint) => breakpoint.values.map((breakpoint) => breakpoint.min))
188+
.filter((v) => v !== undefined)
203189
}
204190

205191
function mapMinWidthsToPadding(minWidths, screens, paddings) {
@@ -228,16 +214,11 @@ export let corePlugins = {
228214
}
229215

230216
for (let minWidth of minWidths) {
231-
for (let [screen, value] of Object.entries(screens)) {
232-
let screenMinWidth =
233-
typeof value === 'object' && value !== null ? value.min || value['min-width'] : value
234-
235-
if (`${screenMinWidth}` === `${minWidth}`) {
236-
mapping.push({
237-
screen,
238-
minWidth,
239-
padding: paddings[screen],
240-
})
217+
for (let screen of screens) {
218+
for (let { min } of screen.values) {
219+
if (min === minWidth) {
220+
mapping.push({ minWidth, padding: paddings[screen.name] })
221+
}
241222
}
242223
}
243224
}
@@ -246,12 +227,12 @@ export let corePlugins = {
246227
}
247228

248229
return function ({ addComponents, theme }) {
249-
let screens = theme('container.screens', theme('screens'))
230+
let screens = normalizeScreens(theme('container.screens', theme('screens')))
250231
let minWidths = extractMinWidths(screens)
251232
let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
252233

253234
let generatePaddingFor = (minWidth) => {
254-
let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`)
235+
let paddingConfig = paddings.find((padding) => padding.minWidth === minWidth)
255236

256237
if (!paddingConfig) {
257238
return {}

src/lib/evaluateTailwindFunctions.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import dlv from 'dlv'
22
import didYouMean from 'didyoumean'
33
import transformThemeValue from '../util/transformThemeValue'
44
import parseValue from 'postcss-value-parser'
5+
import { normalizeScreens } from '../util/normalizeScreens'
56
import buildMediaQuery from '../util/buildMediaQuery'
67
import { toPath } from '../util/toPath'
78

@@ -173,12 +174,14 @@ export default function ({ tailwindConfig: config }) {
173174
},
174175
screen: (node, screen) => {
175176
screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
177+
let screens = normalizeScreens(config.theme.screens)
178+
let screenDefinition = screens.find(({ name }) => name === screen)
176179

177-
if (config.theme.screens[screen] === undefined) {
180+
if (!screenDefinition) {
178181
throw node.error(`The '${screen}' screen does not exist in your theme.`)
179182
}
180183

181-
return buildMediaQuery(config.theme.screens[screen])
184+
return buildMediaQuery(screenDefinition)
182185
},
183186
}
184187
return (root) => {

src/lib/substituteScreenAtRules.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import { normalizeScreens } from '../util/normalizeScreens'
12
import buildMediaQuery from '../util/buildMediaQuery'
23

34
export default function ({ tailwindConfig: { theme } }) {
45
return function (css) {
56
css.walkAtRules('screen', (atRule) => {
6-
const screen = atRule.params
7+
let screen = atRule.params
8+
let screens = normalizeScreens(theme.screens)
9+
let screenDefinition = screens.find(({ name }) => name === screen)
710

8-
if (!theme.screens?.hasOwnProperty?.(screen)) {
11+
if (!screenDefinition) {
912
throw atRule.error(`No \`${screen}\` screen found.`)
1013
}
1114

1215
atRule.name = 'media'
13-
atRule.params = buildMediaQuery(theme.screens[screen])
16+
atRule.params = buildMediaQuery(screenDefinition)
1417
})
1518
}
1619
}

src/util/buildMediaQuery.js

+14-18
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
export default function buildMediaQuery(screens) {
2-
if (typeof screens === 'string') {
3-
screens = { min: screens }
4-
}
5-
6-
if (!Array.isArray(screens)) {
7-
screens = [screens]
8-
}
2+
screens = Array.isArray(screens) ? screens : [screens]
93

104
return screens
11-
.map((screen) => {
12-
if (screen?.hasOwnProperty?.('raw')) {
13-
return screen.raw
14-
}
5+
.map((screen) =>
6+
screen.values.map((screen) => {
7+
if (screen.raw !== undefined) {
8+
return screen.raw
9+
}
1510

16-
return Object.entries(screen)
17-
.map(([feature, value]) => {
18-
feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
19-
return `(${feature}: ${value})`
20-
})
21-
.join(' and ')
22-
})
11+
return [
12+
screen.min && `(min-width: ${screen.min})`,
13+
screen.max && `(max-width: ${screen.max})`,
14+
]
15+
.filter(Boolean)
16+
.join(' and ')
17+
})
18+
)
2319
.join(', ')
2420
}

src/util/normalizeScreens.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* A function that normalizes the various forms that the screens object can be
3+
* provided in.
4+
*
5+
* Input(s):
6+
* - ['100px', '200px'] // Raw strings
7+
* - { sm: '100px', md: '200px' } // Object with string values
8+
* - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
9+
* - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
10+
* - [['sm', '100px'], ['md', '200px']] // Tuple object
11+
*
12+
* Output(s):
13+
* - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
14+
*/
15+
export function normalizeScreens(screens) {
16+
if (Array.isArray(screens)) {
17+
return screens.map((screen) => {
18+
if (typeof screen === 'string') {
19+
return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
20+
}
21+
22+
let [name, options] = screen
23+
name = name.toString()
24+
25+
if (typeof options === 'string') {
26+
return { name, values: [{ min: options, max: undefined }] }
27+
}
28+
29+
if (Array.isArray(options)) {
30+
return { name, values: options.map((option) => resolveValue(option)) }
31+
}
32+
33+
return { name, values: [resolveValue(options)] }
34+
})
35+
}
36+
37+
return normalizeScreens(Object.entries(screens ?? {}))
38+
}
39+
40+
function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
41+
return { min, max, raw }
42+
}

tests/containerPlugin.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,39 @@ test('container can use variants', () => {
343343
`)
344344
})
345345
})
346+
347+
test('container can use screens with tuple syntax', () => {
348+
let config = {
349+
content: [{ raw: html`<div class="container"></div>` }],
350+
theme: {
351+
screens: [
352+
[1800, { min: '1800px' }],
353+
[1200, { min: '1200px' }],
354+
[768, { min: '768px' }],
355+
],
356+
},
357+
}
358+
359+
return run('@tailwind components', config).then((result) => {
360+
expect(result.css).toMatchFormattedCss(css`
361+
.container {
362+
width: 100%;
363+
}
364+
@media (min-width: 768px) {
365+
.container {
366+
max-width: 768px;
367+
}
368+
}
369+
@media (min-width: 1200px) {
370+
.container {
371+
max-width: 1200px;
372+
}
373+
}
374+
@media (min-width: 1800px) {
375+
.container {
376+
max-width: 1800px;
377+
}
378+
}
379+
`)
380+
})
381+
})

0 commit comments

Comments
 (0)