diff --git a/CHANGELOG.md b/CHANGELOG.md index c149d888e886..39b41db40305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prepare for container queries setup ([#9526](https://github.com/tailwindlabs/tailwindcss/pull/9526)) - Add support for modifiers to `matchUtilities` ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541)) - Switch to positional argument + object for modifiers ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541)) +- Add new `min` and `max` variants ([#9558](https://github.com/tailwindlabs/tailwindcss/pull/9558)) ### Fixed diff --git a/src/corePlugins.js b/src/corePlugins.js index 0ae64d9b0762..0881a63dfc3e 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -12,7 +12,12 @@ 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 { + normalizeScreens, + isScreenSortable, + compareScreens, + toScreen, +} from './util/normalizeScreens' import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue' import { removeAlphaVariables } from './util/removeAlphaVariables' import { flagEnabled } from './featureFlags' @@ -220,12 +225,131 @@ export let variantPlugins = { addVariant('print', '@media print') }, - screenVariants: ({ theme, addVariant }) => { - for (let screen of normalizeScreens(theme('screens'))) { - let query = buildMediaQuery(screen) + screenVariants: ({ theme, addVariant, matchVariant }) => { + let rawScreens = theme('screens') ?? {} + let areSimpleScreens = Object.values(rawScreens).every((v) => typeof v === 'string') + let screens = normalizeScreens(theme('screens')) - addVariant(screen.name, `@media ${query}`) + /** @type {Set} */ + let unitCache = new Set([]) + + /** @param {string} value */ + function units(value) { + return value.match(/(\D+)$/)?.[1] ?? '(none)' + } + + /** @param {string} value */ + function recordUnits(value) { + if (value !== undefined) { + unitCache.add(units(value)) + } + } + + /** @param {string} value */ + function canUseUnits(value) { + recordUnits(value) + + // If the cache was empty it'll become 1 because we've just added the current unit + // If the cache was not empty and the units are the same the size doesn't change + // Otherwise, if the units are different from what is already known the size will always be > 1 + return unitCache.size === 1 + } + + for (const screen of screens) { + for (const value of screen.values) { + recordUnits(value.min) + recordUnits(value.max) + } + } + + let screensUseConsistentUnits = unitCache.size <= 1 + + /** + * @typedef {import('./util/normalizeScreens').Screen} Screen + */ + + /** + * @param {'min' | 'max'} type + * @returns {Record} + */ + function buildScreenValues(type) { + return Object.fromEntries( + screens + .filter((screen) => isScreenSortable(screen).result) + .map((screen) => { + let { min, max } = screen.values[0] + + if (type === 'min' && min !== undefined) { + return screen + } else if (type === 'min' && max !== undefined) { + return { ...screen, not: !screen.not } + } else if (type === 'max' && max !== undefined) { + return screen + } else if (type === 'max' && min !== undefined) { + return { ...screen, not: !screen.not } + } + }) + .map((screen) => [screen.name, screen]) + ) + } + + /** + * @param {'min' | 'max'} type + * @returns {(a: { value: string | Screen }, z: { value: string | Screen }) => number} + */ + function buildSort(type) { + return (a, z) => compareScreens(type, a.value, z.value) + } + + let maxSort = buildSort('max') + let minSort = buildSort('min') + + /** @param {'min'|'max'} type */ + function buildScreenVariant(type) { + return (value) => { + if (!areSimpleScreens) { + log.warn('complex-screen-config', [ + 'The min and max variants are not supported with a screen configuration containing objects.', + ]) + + return [] + } else if (!screensUseConsistentUnits) { + log.warn('mixed-screen-units', [ + 'The min and max variants are not supported with a screen configuration containing mixed units.', + ]) + + return [] + } else if (typeof value === 'string' && !canUseUnits(value)) { + log.warn('minmax-have-mixed-units', [ + 'The min and max variants are not supported with a screen configuration containing mixed units.', + ]) + + return [] + } + + return [`@media ${buildMediaQuery(toScreen(value, type))}`] + } } + + matchVariant('max', buildScreenVariant('max'), { + sort: maxSort, + values: areSimpleScreens ? buildScreenValues('max') : {}, + }) + + // screens and min-* are sorted together when they can be + let id = 'min-screens' + for (let screen of screens) { + addVariant(screen.name, `@media ${buildMediaQuery(screen)}`, { + id, + sort: areSimpleScreens && screensUseConsistentUnits ? minSort : undefined, + value: screen, + }) + } + + matchVariant('min', buildScreenVariant('min'), { + id, + sort: minSort, + }) }, supportsVariants: ({ matchVariant, theme }) => { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 2f0ca84084b5..47e4f6eb360e 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -560,7 +560,9 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs context.variantOptions.set(variantName, options) }, matchVariant(variant, variantFn, options) { - let id = ++variantIdentifier // A unique identifier that "groups" these variables together. + // A unique identifier that "groups" these variants together. + // This is for internal use only which is why it is not present in the types + let id = options?.id ?? ++variantIdentifier let isSpecial = variant === '@' let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js index ccfc63baf099..8489dd4bc9f4 100644 --- a/src/util/buildMediaQuery.js +++ b/src/util/buildMediaQuery.js @@ -2,8 +2,8 @@ export default function buildMediaQuery(screens) { screens = Array.isArray(screens) ? screens : [screens] return screens - .map((screen) => - screen.values.map((screen) => { + .map((screen) => { + let values = screen.values.map((screen) => { if (screen.raw !== undefined) { return screen.raw } @@ -15,6 +15,8 @@ export default function buildMediaQuery(screens) { .filter(Boolean) .join(' and ') }) - ) + + return screen.not ? `not all and ${values}` : values + }) .join(', ') } diff --git a/src/util/normalizeScreens.js b/src/util/normalizeScreens.js index 8f2ff709343f..559f7ccb6913 100644 --- a/src/util/normalizeScreens.js +++ b/src/util/normalizeScreens.js @@ -1,3 +1,17 @@ +/** + * @typedef {object} ScreenValue + * @property {number|undefined} min + * @property {number|undefined} max + * @property {string|undefined} raw + */ + +/** + * @typedef {object} Screen + * @property {string} name + * @property {boolean} not + * @property {ScreenValue[]} values + */ + /** * A function that normalizes the various forms that the screens object can be * provided in. @@ -10,6 +24,8 @@ * * Output(s): * - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values + * + * @returns {Screen[]} */ export function normalizeScreens(screens, root = true) { if (Array.isArray(screens)) { @@ -19,27 +35,106 @@ export function normalizeScreens(screens, root = true) { } if (typeof screen === 'string') { - return { name: screen.toString(), values: [{ min: screen, max: undefined }] } + return { name: screen.toString(), not: false, values: [{ min: screen, max: undefined }] } } let [name, options] = screen name = name.toString() if (typeof options === 'string') { - return { name, values: [{ min: options, max: undefined }] } + return { name, not: false, values: [{ min: options, max: undefined }] } } if (Array.isArray(options)) { - return { name, values: options.map((option) => resolveValue(option)) } + return { name, not: false, values: options.map((option) => resolveValue(option)) } } - return { name, values: [resolveValue(options)] } + return { name, not: false, values: [resolveValue(options)] } }) } return normalizeScreens(Object.entries(screens ?? {}), false) } +/** + * @param {Screen} screen + * @returns {{result: false, reason: string} | {result: true, reason: null}} + */ +export function isScreenSortable(screen) { + if (screen.values.length !== 1) { + return { result: false, reason: 'multiple-values' } + } else if (screen.values[0].raw !== undefined) { + return { result: false, reason: 'raw-values' } + } else if (screen.values[0].min !== undefined && screen.values[0].max !== undefined) { + return { result: false, reason: 'min-and-max' } + } + + return { result: true, reason: null } +} + +/** + * @param {'min' | 'max'} type + * @param {Screen | 'string'} a + * @param {Screen | 'string'} z + * @returns {number} + */ +export function compareScreens(type, a, z) { + let aScreen = toScreen(a, type) + let zScreen = toScreen(z, type) + + let aSorting = isScreenSortable(aScreen) + let bSorting = isScreenSortable(zScreen) + + // These cases should never happen and indicate a bug in Tailwind CSS itself + if (aSorting.reason === 'multiple-values' || bSorting.reason === 'multiple-values') { + throw new Error( + 'Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.' + ) + } else if (aSorting.reason === 'raw-values' || bSorting.reason === 'raw-values') { + throw new Error( + 'Attempted to sort a screen with raw values. This should never happen. Please open a bug report.' + ) + } else if (aSorting.reason === 'min-and-max' || bSorting.reason === 'min-and-max') { + throw new Error( + 'Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.' + ) + } + + // Let the sorting begin + let { min: aMin, max: aMax } = aScreen.values[0] + let { min: zMin, max: zMax } = zScreen.values[0] + + // Negating screens flip their behavior. Basically `not min-width` is `max-width` + if (a.not) [aMin, aMax] = [aMax, aMin] + if (z.not) [zMin, zMax] = [zMax, zMin] + + aMin = aMin === undefined ? aMin : parseFloat(aMin) + aMax = aMax === undefined ? aMax : parseFloat(aMax) + zMin = zMin === undefined ? zMin : parseFloat(zMin) + zMax = zMax === undefined ? zMax : parseFloat(zMax) + + let [aValue, zValue] = type === 'min' ? [aMin, zMin] : [zMax, aMax] + + return aValue - zValue +} + +/** + * + * @param {PartialScreen> | string} value + * @param {'min' | 'max'} type + * @returns {Screen} + */ +export function toScreen(value, type) { + if (typeof value === 'object') { + return value + } + + return { + name: 'arbitrary-screen', + values: [{ [type]: value }], + } +} + function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) { return { min, max, raw } } diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js index fad17193617d..7da1b42325df 100644 --- a/tests/match-variants.test.js +++ b/tests/match-variants.test.js @@ -235,7 +235,7 @@ it('should be possible to sort variants', () => { { raw: html`
-
+
`, }, @@ -243,7 +243,7 @@ it('should be possible to sort variants', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, @@ -259,13 +259,13 @@ it('should be possible to sort variants', () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` @media (min-width: 500px) { - .min-\[500px\]\:underline { + .testmin-\[500px\]\:underline { text-decoration-line: underline; } } @media (min-width: 700px) { - .min-\[700px\]\:italic { + .testmin-\[700px\]\:italic { font-style: italic; } } @@ -279,7 +279,7 @@ it('should be possible to compare arbitrary variants and hardcoded variants', () { raw: html`
-
+
`, }, @@ -287,7 +287,7 @@ it('should be possible to compare arbitrary variants and hardcoded variants', () corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { values: { example: '600px', }, @@ -306,19 +306,19 @@ it('should be possible to compare arbitrary variants and hardcoded variants', () return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` @media (min-width: 500px) { - .min-\[500px\]\:italic { + .testmin-\[500px\]\:italic { font-style: italic; } } @media (min-width: 600px) { - .min-example\:italic { + .testmin-example\:italic { font-style: italic; } } @media (min-width: 700px) { - .min-\[700px\]\:italic { + .testmin-\[700px\]\:italic { font-style: italic; } } @@ -333,13 +333,13 @@ it('should be possible to sort stacked arbitrary variants correctly', () => { raw: html`
-
+
-
+
-
+
-
+
`, }, @@ -347,13 +347,13 @@ it('should be possible to sort stacked arbitrary variants correctly', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', (value) => `@media (max-width: ${value})`, { + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -370,17 +370,17 @@ it('should be possible to sort stacked arbitrary variants correctly', () => { expect(result.css).toMatchFormattedCss(css` @media (min-width: 100px) { @media (max-width: 400px) { - .min-\[100px\]\:max-\[400px\]\:underline { + .testmin-\[100px\]\:testmax-\[400px\]\:underline { text-decoration-line: underline; } } @media (max-width: 350px) { - .min-\[100px\]\:max-\[350px\]\:underline { + .testmin-\[100px\]\:testmax-\[350px\]\:underline { text-decoration-line: underline; } } @media (max-width: 300px) { - .min-\[100px\]\:max-\[300px\]\:underline { + .testmin-\[100px\]\:testmax-\[300px\]\:underline { text-decoration-line: underline; } } @@ -388,7 +388,7 @@ it('should be possible to sort stacked arbitrary variants correctly', () => { @media (min-width: 150px) { @media (max-width: 400px) { - .min-\[150px\]\:max-\[400px\]\:underline { + .testmin-\[150px\]\:testmax-\[400px\]\:underline { text-decoration-line: underline; } } @@ -403,8 +403,8 @@ it('should maintain sort from other variants, if sort functions of arbitrary var { raw: html`
-
-
+
+
`, }, @@ -412,13 +412,13 @@ it('should maintain sort from other variants, if sort functions of arbitrary var corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', (value) => `@media (max-width: ${value})`, { + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -435,10 +435,10 @@ it('should maintain sort from other variants, if sort functions of arbitrary var expect(result.css).toMatchFormattedCss(css` @media (min-width: 100px) { @media (max-width: 200px) { - .min-\[100px\]\:max-\[200px\]\:hover\:underline:hover { + .testmin-\[100px\]\:testmax-\[200px\]\:hover\:underline:hover { text-decoration-line: underline; } - .min-\[100px\]\:max-\[200px\]\:focus\:underline:focus { + .testmin-\[100px\]\:testmax-\[200px\]\:focus\:underline:focus { text-decoration-line: underline; } } @@ -453,10 +453,10 @@ it('should sort arbitrary variants left to right (1)', () => { { raw: html`
-
-
-
-
+
+
+
+
`, }, @@ -464,12 +464,12 @@ it('should sort arbitrary variants left to right (1)', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', (value) => `@media (max-width: ${value})`, { + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -486,13 +486,13 @@ it('should sort arbitrary variants left to right (1)', () => { expect(result.css).toMatchFormattedCss(css` @media (min-width: 100px) { @media (max-width: 400px) { - .min-\[100px\]\:max-\[400px\]\:underline { + .testmin-\[100px\]\:testmax-\[400px\]\:underline { text-decoration-line: underline; } } @media (max-width: 300px) { - .min-\[100px\]\:max-\[300px\]\:underline { + .testmin-\[100px\]\:testmax-\[300px\]\:underline { text-decoration-line: underline; } } @@ -500,13 +500,13 @@ it('should sort arbitrary variants left to right (1)', () => { @media (min-width: 200px) { @media (max-width: 400px) { - .min-\[200px\]\:max-\[400px\]\:underline { + .testmin-\[200px\]\:testmax-\[400px\]\:underline { text-decoration-line: underline; } } @media (max-width: 300px) { - .min-\[200px\]\:max-\[300px\]\:underline { + .testmin-\[200px\]\:testmax-\[300px\]\:underline { text-decoration-line: underline; } } @@ -521,10 +521,10 @@ it('should sort arbitrary variants left to right (2)', () => { { raw: html`
-
-
-
-
+
+
+
+
`, }, @@ -532,12 +532,12 @@ it('should sort arbitrary variants left to right (2)', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', (value) => `@media (max-width: ${value})`, { + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -554,12 +554,12 @@ it('should sort arbitrary variants left to right (2)', () => { expect(result.css).toMatchFormattedCss(css` @media (max-width: 400px) { @media (min-width: 100px) { - .max-\[400px\]\:min-\[100px\]\:underline { + .testmax-\[400px\]\:testmin-\[100px\]\:underline { text-decoration-line: underline; } } @media (min-width: 200px) { - .max-\[400px\]\:min-\[200px\]\:underline { + .testmax-\[400px\]\:testmin-\[200px\]\:underline { text-decoration-line: underline; } } @@ -567,12 +567,12 @@ it('should sort arbitrary variants left to right (2)', () => { @media (max-width: 300px) { @media (min-width: 100px) { - .max-\[300px\]\:min-\[100px\]\:underline { + .testmax-\[300px\]\:testmin-\[100px\]\:underline { text-decoration-line: underline; } } @media (min-width: 200px) { - .max-\[300px\]\:min-\[200px\]\:underline { + .testmax-\[300px\]\:testmin-\[200px\]\:underline { text-decoration-line: underline; } } @@ -587,10 +587,10 @@ it('should guarantee that we are not passing values from other variants to the w { raw: html`
-
-
-
-
+
+
+
+
`, }, @@ -598,7 +598,7 @@ it('should guarantee that we are not passing values from other variants to the w corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', (value) => `@media (min-width: ${value})`, { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { sort(a, z) { let lookup = ['100px', '200px'] if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { @@ -607,7 +607,7 @@ it('should guarantee that we are not passing values from other variants to the w return lookup.indexOf(a.value) - lookup.indexOf(z.value) }, }) - matchVariant('max', (value) => `@media (max-width: ${value})`, { + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { sort(a, z) { let lookup = ['300px', '400px'] if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { @@ -628,13 +628,13 @@ it('should guarantee that we are not passing values from other variants to the w expect(result.css).toMatchFormattedCss(css` @media (min-width: 100px) { @media (max-width: 400px) { - .min-\[100px\]\:max-\[400px\]\:underline { + .testmin-\[100px\]\:testmax-\[400px\]\:underline { text-decoration-line: underline; } } @media (max-width: 300px) { - .min-\[100px\]\:max-\[300px\]\:underline { + .testmin-\[100px\]\:testmax-\[300px\]\:underline { text-decoration-line: underline; } } @@ -642,13 +642,13 @@ it('should guarantee that we are not passing values from other variants to the w @media (min-width: 200px) { @media (max-width: 400px) { - .min-\[200px\]\:max-\[400px\]\:underline { + .testmin-\[200px\]\:testmax-\[400px\]\:underline { text-decoration-line: underline; } } @media (max-width: 300px) { - .min-\[200px\]\:max-\[300px\]\:underline { + .testmin-\[200px\]\:testmax-\[300px\]\:underline { text-decoration-line: underline; } } diff --git a/tests/min-max-screen-variants.test.js b/tests/min-max-screen-variants.test.js new file mode 100644 index 000000000000..cb6a06293d15 --- /dev/null +++ b/tests/min-max-screen-variants.test.js @@ -0,0 +1,662 @@ +import { run, css, html } from './util/run' + +let warn + +beforeEach(() => { + let log = require('../src/util/log') + warn = jest.spyOn(log.default, 'warn') +}) + +afterEach(() => { + warn.mockClear() +}) + +let defaultScreens = { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', +} + +it('sorts min and max correctly relative to screens and each other', async () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: defaultScreens, + }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (max-width: 800px) { + .max-\[800px\]\:font-bold { + font-weight: 700; + } + } + @media (max-width: 700px) { + .max-\[700px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 700px) { + .min-\[700px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + @media (min-width: 800px) { + .min-\[800px\]\:font-bold { + font-weight: 700; + } + } + `) + }) +}) + +it('works when using min variants screens config is empty and variants all use the same unit', async () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: {}, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 700px) { + .min-\[700px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 800px) { + .min-\[800px\]\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(0) +}) + +it('works when using max variants screens config is empty and variants all use the same unit', () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: {}, + }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (max-width: 800px) { + .max-\[800px\]\:font-bold { + font-weight: 700; + } + } + @media (max-width: 700px) { + .max-\[700px\]\:font-bold { + font-weight: 700; + } + } + `) + }) +}) + +it('converts simple min-width screens for max variant', () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: defaultScreens, + }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media not all and (min-width: 1024px) { + .max-lg\:font-bold { + font-weight: 700; + } + } + @media (max-width: 700px) { + .max-\[700px\]\:font-bold { + font-weight: 700; + } + } + @media not all and (min-width: 640px) { + .max-sm\:font-bold { + font-weight: 700; + } + } + @media (max-width: 300px) { + .max-\[300px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + `) + }) +}) + +it('does not have keyed screens for min variant', () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: defaultScreens, + }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 300px) { + .min-\[300px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 700px) { + .min-\[700px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + `) + }) +}) + +it('warns when using min variants with complex screen configs', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + + // Any presence of an object makes it complex + yodawg: { min: '700px' }, + }, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['complex-screen-config']) +}) + +it('warns when using min variants with simple configs containing mixed units', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: { + sm: '640px', + md: '48rem', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 48rem) { + .md\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['mixed-screen-units']) +}) + +it('warns when using min variants with mixed units (with screens config)', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units']) +}) + +it('warns when using min variants with mixed units (with no screens config)', async () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: {}, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 700rem) { + .min-\[700rem\]\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units']) +}) + +it('warns when using max variants with complex screen configs', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + + // Any presence of an object makes it complex + yodawg: { min: '700px' }, + }, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['complex-screen-config']) +}) + +it('warns when using max variants with simple configs containing mixed units', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: { + sm: '640px', + md: '48rem', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 48rem) { + .md\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['mixed-screen-units']) +}) + +it('warns when using max variants with mixed units (with screens config)', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (min-width: 640px) { + .sm\:font-bold { + font-weight: 700; + } + } + @media (min-width: 768px) { + .md\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units']) +}) + +it('warns when using max variants with mixed units (with no screens config)', async () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: {}, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (max-width: 700rem) { + .max-\[700rem\]\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units']) +}) + +it('warns when using min and max variants with mixed units (with no screens config)', async () => { + let config = { + content: [ + { + raw: html` +
+ `, + }, + ], + corePlugins: { preflight: false }, + theme: { + screens: {}, + }, + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .font-bold { + font-weight: 700; + } + @media (max-width: 700rem) { + .max-\[700rem\]\:font-bold { + font-weight: 700; + } + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units']) +}) diff --git a/tests/normalize-screens.test.js b/tests/normalize-screens.test.js index 98fc22c57a91..da31c50d30d7 100644 --- a/tests/normalize-screens.test.js +++ b/tests/normalize-screens.test.js @@ -4,8 +4,8 @@ it('should normalize an array of string values', () => { let screens = ['768px', '1200px'] expect(normalizeScreens(screens)).toEqual([ - { name: '768px', values: [{ min: '768px', max: undefined }] }, - { name: '1200px', values: [{ min: '1200px', max: undefined }] }, + { name: '768px', not: false, values: [{ min: '768px', max: undefined }] }, + { name: '1200px', not: false, values: [{ min: '1200px', max: undefined }] }, ]) }) @@ -16,8 +16,8 @@ it('should normalize an object with string values', () => { } expect(normalizeScreens(screens)).toEqual([ - { name: 'a', values: [{ min: '768px', max: undefined }] }, - { name: 'b', values: [{ min: '1200px', max: undefined }] }, + { name: 'a', not: false, values: [{ min: '768px', max: undefined }] }, + { name: 'b', not: false, values: [{ min: '1200px', max: undefined }] }, ]) }) @@ -28,8 +28,8 @@ it('should normalize an object with object values', () => { } expect(normalizeScreens(screens)).toEqual([ - { name: 'a', values: [{ min: '768px', max: undefined }] }, - { name: 'b', values: [{ min: undefined, max: '1200px' }] }, + { name: 'a', not: false, values: [{ min: '768px', max: undefined }] }, + { name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] }, ]) }) @@ -41,6 +41,7 @@ it('should normalize an object with multiple object values', () => { expect(normalizeScreens(screens)).toEqual([ { name: 'a', + not: false, values: [ { max: undefined, min: '768px', raw: undefined }, { max: '1200px', min: undefined, raw: undefined }, @@ -56,7 +57,7 @@ it('should normalize an object with object values (min-width normalized to width } expect(normalizeScreens(screens)).toEqual([ - { name: 'a', values: [{ min: '768px', max: undefined }] }, - { name: 'b', values: [{ min: undefined, max: '1200px' }] }, + { name: 'a', not: false, values: [{ min: '768px', max: undefined }] }, + { name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] }, ]) })