Skip to content

Commit

Permalink
Add tuple syntax to guarantee screens order (#6104)
Browse files Browse the repository at this point in the history
* 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 <adam.wathan@gmail.com>
  • Loading branch information
RobinMalfait and adamwathan committed Nov 16, 2021
1 parent 6c4b86d commit ef325ea
Show file tree
Hide file tree
Showing 10 changed files with 531 additions and 194 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Expand Up @@ -7,15 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Nothing yet!

### Fixed

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

### Added

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

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

Expand Down
49 changes: 15 additions & 34 deletions src/corePlugins.js
Expand Up @@ -11,6 +11,7 @@ import isPlainObject from './util/isPlainObject'
import transformThemeValue from './util/transformThemeValue'
import { version as tailwindVersion } from '../package.json'
import log from './util/log'
import { normalizeScreens } from './util/normalizeScreens'
import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue'

export let variantPlugins = {
Expand Down Expand Up @@ -158,11 +159,10 @@ export let variantPlugins = {
},

screenVariants: ({ theme, addVariant }) => {
for (let screen in theme('screens')) {
let size = theme('screens')[screen]
let query = buildMediaQuery(size)
for (let screen of normalizeScreens(theme('screens'))) {
let query = buildMediaQuery(screen)

addVariant(screen, `@media ${query}`)
addVariant(screen.name, `@media ${query}`)
}
},
}
Expand All @@ -182,24 +182,10 @@ export let corePlugins = {
},

container: (() => {
function extractMinWidths(breakpoints) {
return Object.values(breakpoints ?? {}).flatMap((breakpoints) => {
if (typeof breakpoints === 'string') {
breakpoints = { min: breakpoints }
}

if (!Array.isArray(breakpoints)) {
breakpoints = [breakpoints]
}

return breakpoints
.filter((breakpoint) => {
return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width')
})
.map((breakpoint) => {
return breakpoint['min-width'] ?? breakpoint.min
})
})
function extractMinWidths(breakpoints = []) {
return breakpoints
.flatMap((breakpoint) => breakpoint.values.map((breakpoint) => breakpoint.min))
.filter((v) => v !== undefined)
}

function mapMinWidthsToPadding(minWidths, screens, paddings) {
Expand Down Expand Up @@ -228,16 +214,11 @@ export let corePlugins = {
}

for (let minWidth of minWidths) {
for (let [screen, value] of Object.entries(screens)) {
let screenMinWidth =
typeof value === 'object' && value !== null ? value.min || value['min-width'] : value

if (`${screenMinWidth}` === `${minWidth}`) {
mapping.push({
screen,
minWidth,
padding: paddings[screen],
})
for (let screen of screens) {
for (let { min } of screen.values) {
if (min === minWidth) {
mapping.push({ minWidth, padding: paddings[screen.name] })
}
}
}
}
Expand All @@ -246,12 +227,12 @@ export let corePlugins = {
}

return function ({ addComponents, theme }) {
let screens = theme('container.screens', theme('screens'))
let screens = normalizeScreens(theme('container.screens', theme('screens')))
let minWidths = extractMinWidths(screens)
let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))

let generatePaddingFor = (minWidth) => {
let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`)
let paddingConfig = paddings.find((padding) => padding.minWidth === minWidth)

if (!paddingConfig) {
return {}
Expand Down
7 changes: 5 additions & 2 deletions src/lib/evaluateTailwindFunctions.js
Expand Up @@ -2,6 +2,7 @@ import dlv from 'dlv'
import didYouMean from 'didyoumean'
import transformThemeValue from '../util/transformThemeValue'
import parseValue from 'postcss-value-parser'
import { normalizeScreens } from '../util/normalizeScreens'
import buildMediaQuery from '../util/buildMediaQuery'
import { toPath } from '../util/toPath'

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

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

return buildMediaQuery(config.theme.screens[screen])
return buildMediaQuery(screenDefinition)
},
}
return (root) => {
Expand Down
9 changes: 6 additions & 3 deletions src/lib/substituteScreenAtRules.js
@@ -1,16 +1,19 @@
import { normalizeScreens } from '../util/normalizeScreens'
import buildMediaQuery from '../util/buildMediaQuery'

export default function ({ tailwindConfig: { theme } }) {
return function (css) {
css.walkAtRules('screen', (atRule) => {
const screen = atRule.params
let screen = atRule.params
let screens = normalizeScreens(theme.screens)
let screenDefinition = screens.find(({ name }) => name === screen)

if (!theme.screens?.hasOwnProperty?.(screen)) {
if (!screenDefinition) {
throw atRule.error(`No \`${screen}\` screen found.`)
}

atRule.name = 'media'
atRule.params = buildMediaQuery(theme.screens[screen])
atRule.params = buildMediaQuery(screenDefinition)
})
}
}
32 changes: 14 additions & 18 deletions src/util/buildMediaQuery.js
@@ -1,24 +1,20 @@
export default function buildMediaQuery(screens) {
if (typeof screens === 'string') {
screens = { min: screens }
}

if (!Array.isArray(screens)) {
screens = [screens]
}
screens = Array.isArray(screens) ? screens : [screens]

return screens
.map((screen) => {
if (screen?.hasOwnProperty?.('raw')) {
return screen.raw
}
.map((screen) =>
screen.values.map((screen) => {
if (screen.raw !== undefined) {
return screen.raw
}

return Object.entries(screen)
.map(([feature, value]) => {
feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
return `(${feature}: ${value})`
})
.join(' and ')
})
return [
screen.min && `(min-width: ${screen.min})`,
screen.max && `(max-width: ${screen.max})`,
]
.filter(Boolean)
.join(' and ')
})
)
.join(', ')
}
42 changes: 42 additions & 0 deletions src/util/normalizeScreens.js
@@ -0,0 +1,42 @@
/**
* A function that normalizes the various forms that the screens object can be
* provided in.
*
* Input(s):
* - ['100px', '200px'] // Raw strings
* - { sm: '100px', md: '200px' } // Object with string values
* - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
* - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
* - [['sm', '100px'], ['md', '200px']] // Tuple object
*
* Output(s):
* - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
*/
export function normalizeScreens(screens) {
if (Array.isArray(screens)) {
return screens.map((screen) => {
if (typeof screen === 'string') {
return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
}

let [name, options] = screen
name = name.toString()

if (typeof options === 'string') {
return { name, values: [{ min: options, max: undefined }] }
}

if (Array.isArray(options)) {
return { name, values: options.map((option) => resolveValue(option)) }
}

return { name, values: [resolveValue(options)] }
})
}

return normalizeScreens(Object.entries(screens ?? {}))
}

function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
return { min, max, raw }
}
36 changes: 36 additions & 0 deletions tests/containerPlugin.test.js
Expand Up @@ -343,3 +343,39 @@ test('container can use variants', () => {
`)
})
})

test('container can use screens with tuple syntax', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: [
[1800, { min: '1800px' }],
[1200, { min: '1200px' }],
[768, { min: '768px' }],
],
},
}

return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
}
}
@media (min-width: 1800px) {
.container {
max-width: 1800px;
}
}
`)
})
})

0 comments on commit ef325ea

Please sign in to comment.