diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js index 8489dd4bc9f4..e14f558d0fbd 100644 --- a/src/util/buildMediaQuery.js +++ b/src/util/buildMediaQuery.js @@ -1,3 +1,5 @@ +import { splitDimensionPrefix } from './splitDimensionPrefix' + export default function buildMediaQuery(screens) { screens = Array.isArray(screens) ? screens : [screens] @@ -8,9 +10,12 @@ export default function buildMediaQuery(screens) { return screen.raw } + let [minDimension, minValue] = splitDimensionPrefix(screen.min) + let [maxDimension, maxValue] = splitDimensionPrefix(screen.max) + return [ - screen.min && `(min-width: ${screen.min})`, - screen.max && `(max-width: ${screen.max})`, + minValue && `(min-${minDimension}: ${minValue})`, + maxValue && `(max-${maxDimension}: ${maxValue})`, ] .filter(Boolean) .join(' and ') diff --git a/src/util/normalizeScreens.js b/src/util/normalizeScreens.js index 559f7ccb6913..386760deda41 100644 --- a/src/util/normalizeScreens.js +++ b/src/util/normalizeScreens.js @@ -1,3 +1,5 @@ +import { splitDimensionPrefix } from './splitDimensionPrefix' + /** * @typedef {object} ScreenValue * @property {number|undefined} min @@ -108,14 +110,27 @@ export function compareScreens(type, a, z) { 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) + // Split each mediq query value into its respective dimension and value (e.g. `width` and `100px`) + let [aMinDimension, aMinValue] = splitDimensionPrefix(aMin) + let [aMaxDimension, aMaxValue] = splitDimensionPrefix(aMax) + let [zMinDimension, zMinValue] = splitDimensionPrefix(zMin) + let [zMaxDimension, zMaxValue] = splitDimensionPrefix(zMax) + + // Determine which dimension to compare (e.g. `min-width/height` vs. `max-width/height`) + let [aDimension, zDimension] = + type === 'min' ? [aMinDimension, zMinDimension] : [aMaxDimension, zMaxDimension] + + // Invert the values if we're comparing max values to use descending order + let [aValue, zValue] = type === 'min' ? [aMinValue, zMinValue] : [zMaxValue, aMaxValue] + + // Compare dimensions (e.g. "height" or "width" alphabetically) + const dimensionComparison = aDimension.localeCompare(zDimension) - let [aValue, zValue] = type === 'min' ? [aMin, zMin] : [zMax, aMax] + // Compare dimensions (e.g. "100px" and "200px" -> `100 - 200`) + const valueComparison = parseFloat(aValue) - parseFloat(zValue) - return aValue - zValue + // Sort by dimension first, then by value (if dimensions are the same) + return dimensionComparison || valueComparison } /** diff --git a/src/util/splitDimensionPrefix.js b/src/util/splitDimensionPrefix.js new file mode 100644 index 000000000000..4d8f0e2acfc3 --- /dev/null +++ b/src/util/splitDimensionPrefix.js @@ -0,0 +1,11 @@ +import { splitVariantPrefix } from './splitVariantPrefix' + +/** + * @param {string} value + * @returns {['height' | 'width', string]} + */ +export function splitDimensionPrefix(value) { + const [prefix, extractedValue] = splitVariantPrefix(value) + const dimension = prefix === 'h' ? 'height' : 'width' + return [dimension, extractedValue] +} diff --git a/src/util/splitVariantPrefix.js b/src/util/splitVariantPrefix.js new file mode 100644 index 000000000000..1b9b478a8c40 --- /dev/null +++ b/src/util/splitVariantPrefix.js @@ -0,0 +1,9 @@ +/** + * @param {string} value + * @returns {[string, string]} + */ +export function splitVariantPrefix(value) { + if (typeof value !== 'string') return ['', value] + let parts = value.split(':') + return ['', ...parts].slice(-2) +} diff --git a/tests/min-max-screen-variants.test.js b/tests/min-max-screen-variants.test.js index a86c6f6cd201..81b9fbc8fe5e 100644 --- a/tests/min-max-screen-variants.test.js +++ b/tests/min-max-screen-variants.test.js @@ -261,6 +261,200 @@ crosscheck(() => { }) }) + it('supports min-* and max-* variants with or without arbitrary dimension prefixes', () => { + 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-height: 100px) { + .max-\[h\:100px\]\:font-bold { + font-weight: 700; + } + } + @media (max-width: 100px) { + .max-\[100px\]\:font-bold, + .max-\[w\:100px\]\:font-bold { + font-weight: 700; + } + } + @media (min-height: 100px) { + .min-\[h\:100px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 100px) { + .min-\[100px\]\:font-bold, + .min-\[w\:100px\]\:font-bold { + font-weight: 700; + } + } + `) + }) + }) + + it('supports min-* and max-* variants being chained together with or without arbitrary dimension prefixes', () => { + 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` + @media (min-width: 100px) { + @media (min-width: 100px) { + @media (min-height: 100px) { + @media (max-width: 100px) { + @media (max-width: 100px) { + @media (max-height: 100px) { + .min-\[100px\]\:min-\[w\:100px\]\:min-\[h\:100px\]\:max-\[100px\]\:max-\[w\:100px\]\:max-\[h\:100px\]\:font-bold { + font-weight: 700; + } + } + } + } + } + } + } + `) + }) + }) + + it('supports proper sorting of min-* and max-* variants with arbitrary dimension prefixes', () => { + 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` + @media (max-height: 3px) { + .max-\[h\:3px\]\:font-bold { + font-weight: 700; + } + } + @media (max-height: 2px) { + .max-\[h\:2px\]\:font-bold { + font-weight: 700; + } + } + @media (max-height: 1px) { + .max-\[h\:1px\]\:font-bold { + font-weight: 700; + } + } + @media (max-width: 3px) { + .max-\[3px\]\:font-bold, + .max-\[w\:3px\]\:font-bold { + font-weight: 700; + } + } + @media (max-width: 2px) { + .max-\[2px\]\:font-bold, + .max-\[w\:2px\]\:font-bold { + font-weight: 700; + } + } + @media (max-width: 1px) { + .max-\[1px\]\:font-bold, + .max-\[w\:1px\]\:font-bold { + font-weight: 700; + } + } + @media (min-height: 1px) { + .min-\[h\:1px\]\:font-bold { + font-weight: 700; + } + } + @media (min-height: 2px) { + .min-\[h\:2px\]\:font-bold { + font-weight: 700; + } + } + @media (min-height: 3px) { + .min-\[h\:3px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 1px) { + .min-\[1px\]\:font-bold, + .min-\[w\:1px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 2px) { + .min-\[2px\]\:font-bold, + .min-\[w\:2px\]\:font-bold { + font-weight: 700; + } + } + @media (min-width: 3px) { + .min-\[3px\]\:font-bold, + .min-\[w\:3px\]\:font-bold { + font-weight: 700; + } + } + `) + }) + }) + it('warns when using min variants with complex screen configs', async () => { let config = { content: [