diff --git a/packages/react-dom/src/shared/CSSPropertyOperations.js b/packages/react-dom/src/shared/CSSPropertyOperations.js index 663d2cd958ab3..ad09c633e5cd9 100644 --- a/packages/react-dom/src/shared/CSSPropertyOperations.js +++ b/packages/react-dom/src/shared/CSSPropertyOperations.js @@ -5,11 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { - overlappingShorthandsInDev, - longhandToShorthandInDev, - shorthandToLonghandInDev, -} from './CSSShorthandProperty'; +import {shorthandToLonghand} from './CSSShorthandProperty'; import dangerousStyleValue from './dangerousStyleValue'; import hyphenateStyleName from './hyphenateStyleName'; @@ -90,6 +86,25 @@ function isValueEmpty(value) { return value == null || typeof value === 'boolean' || value === ''; } +/** + * Given {color: 'red', overflow: 'hidden'} returns { + * color: 'color', + * overflowX: 'overflow', + * overflowY: 'overflow', + * }. This can be read as "the overflowY property was set by the overflow + * shorthand". That is, the values are the property that each was derived from. + */ +function expandShorthandMap(styles) { + const expanded = {}; + for (const key in styles) { + const longhands = shorthandToLonghand[key] || [key]; + for (let i = 0; i < longhands.length; i++) { + expanded[longhands[i]] = key; + } + } + return expanded; +} + /** * When mixing shorthand and longhand property names, we warn during updates if * we expect an incorrect result to occur. In particular, we warn for: @@ -112,52 +127,18 @@ export function validateShorthandPropertyCollisionInDev( return; } - for (const key in styleUpdates) { - const isEmpty = isValueEmpty(styleUpdates[key]); - if (isEmpty) { - // Property removal; check if we're removing a longhand property - const shorthands = longhandToShorthandInDev[key]; - if (shorthands) { - const conflicting = shorthands.filter( - s => !isValueEmpty(nextStyles[s]), - ); - if (conflicting.length) { - warning( - false, - 'Removing a style property during rerender (%s) when a ' + - 'conflicting property is set (%s) can lead to styling bugs. To ' + - "avoid this, don't mix shorthand and non-shorthand properties " + - 'for the same value; instead, replace the shorthand with ' + - 'separate values.', - key, - conflicting.join(', '), - ); - } + const expandedUpdates = expandShorthandMap(styleUpdates); + const expandedStyles = expandShorthandMap(nextStyles); + const warnedAbout = {}; + for (const key in expandedUpdates) { + const originalKey = expandedUpdates[key]; + const correctOriginalKey = expandedStyles[key]; + if (correctOriginalKey && originalKey !== correctOriginalKey) { + const warningKey = originalKey + ',' + correctOriginalKey; + if (warnedAbout[warningKey]) { + continue; } - } - - // Updating or removing a property; check if it's a shorthand property - const longhands = shorthandToLonghandInDev[key]; - const overlapping = overlappingShorthandsInDev[key]; - // eslint-disable-next-line no-var - var conflicting = new Set(); - if (longhands) { - longhands.forEach(l => { - if (isValueEmpty(styleUpdates[l]) && !isValueEmpty(nextStyles[l])) { - // ex: key = 'font', l = 'fontStyle' - conflicting.add(l); - } - }); - } - if (overlapping) { - overlapping.forEach(l => { - if (isValueEmpty(styleUpdates[l]) && !isValueEmpty(nextStyles[l])) { - // ex: key = 'borderLeft', l = 'borderStyle' - conflicting.add(l); - } - }); - } - if (conflicting.size) { + warnedAbout[warningKey] = true; warning( false, '%s a style property during rerender (%s) when a ' + @@ -165,11 +146,9 @@ export function validateShorthandPropertyCollisionInDev( "avoid this, don't mix shorthand and non-shorthand properties " + 'for the same value; instead, replace the shorthand with ' + 'separate values.', - isEmpty ? 'Removing' : 'Updating', - key, - Array.from(conflicting) - .sort() - .join(', '), + isValueEmpty(styleUpdates[originalKey]) ? 'Removing' : 'Updating', + originalKey, + correctOriginalKey, ); } } diff --git a/packages/react-dom/src/shared/CSSShorthandProperty.js b/packages/react-dom/src/shared/CSSShorthandProperty.js index 11f9a9ecd643e..98c45cd7035f9 100644 --- a/packages/react-dom/src/shared/CSSShorthandProperty.js +++ b/packages/react-dom/src/shared/CSSShorthandProperty.js @@ -7,249 +7,186 @@ // List derived from Gecko source code: // https://github.com/mozilla/gecko-dev/blob/4e638efc71/layout/style/test/property_database.js -export const shorthandToLonghandInDev = __DEV__ - ? { - animation: [ - 'animationDelay', - 'animationDirection', - 'animationDuration', - 'animationFillMode', - 'animationIterationCount', - 'animationName', - 'animationPlayState', - 'animationTimingFunction', - ], - background: [ - 'backgroundAttachment', - 'backgroundClip', - 'backgroundColor', - 'backgroundImage', - 'backgroundOrigin', - 'backgroundPositionX', - 'backgroundPositionY', - 'backgroundRepeat', - 'backgroundSize', - ], - backgroundPosition: ['backgroundPositionX', 'backgroundPositionY'], - border: [ - 'borderBottomColor', - 'borderBottomStyle', - 'borderBottomWidth', - 'borderImageOutset', - 'borderImageRepeat', - 'borderImageSlice', - 'borderImageSource', - 'borderImageWidth', - 'borderLeftColor', - 'borderLeftStyle', - 'borderLeftWidth', - 'borderRightColor', - 'borderRightStyle', - 'borderRightWidth', - 'borderTopColor', - 'borderTopStyle', - 'borderTopWidth', - ], - borderBlockEnd: [ - 'borderBlockEndColor', - 'borderBlockEndStyle', - 'borderBlockEndWidth', - ], - borderBlockStart: [ - 'borderBlockStartColor', - 'borderBlockStartStyle', - 'borderBlockStartWidth', - ], - borderBottom: [ - 'borderBottomColor', - 'borderBottomStyle', - 'borderBottomWidth', - ], - borderColor: [ - 'borderBottomColor', - 'borderLeftColor', - 'borderRightColor', - 'borderTopColor', - ], - borderImage: [ - 'borderImageOutset', - 'borderImageRepeat', - 'borderImageSlice', - 'borderImageSource', - 'borderImageWidth', - ], - borderInlineEnd: [ - 'borderInlineEndColor', - 'borderInlineEndStyle', - 'borderInlineEndWidth', - ], - borderInlineStart: [ - 'borderInlineStartColor', - 'borderInlineStartStyle', - 'borderInlineStartWidth', - ], - borderLeft: ['borderLeftColor', 'borderLeftStyle', 'borderLeftWidth'], - borderRadius: [ - 'borderBottomLeftRadius', - 'borderBottomRightRadius', - 'borderTopLeftRadius', - 'borderTopRightRadius', - ], - borderRight: ['borderRightColor', 'borderRightStyle', 'borderRightWidth'], - borderStyle: [ - 'borderBottomStyle', - 'borderLeftStyle', - 'borderRightStyle', - 'borderTopStyle', - ], - borderTop: ['borderTopColor', 'borderTopStyle', 'borderTopWidth'], - borderWidth: [ - 'borderBottomWidth', - 'borderLeftWidth', - 'borderRightWidth', - 'borderTopWidth', - ], - columnRule: ['columnRuleColor', 'columnRuleStyle', 'columnRuleWidth'], - columns: ['columnCount', 'columnWidth'], - flex: ['flexBasis', 'flexGrow', 'flexShrink'], - flexFlow: ['flexDirection', 'flexWrap'], - font: [ - 'fontFamily', - 'fontFeatureSettings', - 'fontKerning', - 'fontLanguageOverride', - 'fontSize', - 'fontSizeAdjust', - 'fontStretch', - 'fontStyle', - 'fontVariant', - 'fontVariantAlternates', - 'fontVariantCaps', - 'fontVariantEastAsian', - 'fontVariantLigatures', - 'fontVariantNumeric', - 'fontVariantPosition', - 'fontWeight', - 'lineHeight', - ], - fontVariant: [ - 'fontVariantAlternates', - 'fontVariantCaps', - 'fontVariantEastAsian', - 'fontVariantLigatures', - 'fontVariantNumeric', - 'fontVariantPosition', - ], - gap: ['columnGap', 'rowGap'], - grid: [ - 'gridAutoColumns', - 'gridAutoFlow', - 'gridAutoRows', - 'gridTemplateAreas', - 'gridTemplateColumns', - 'gridTemplateRows', - ], - gridArea: [ - 'gridColumnEnd', - 'gridColumnStart', - 'gridRowEnd', - 'gridRowStart', - ], - gridColumn: ['gridColumnEnd', 'gridColumnStart'], - gridColumnGap: ['columnGap'], - gridGap: ['columnGap', 'rowGap'], - gridRow: ['gridRowEnd', 'gridRowStart'], - gridRowGap: ['rowGap'], - gridTemplate: [ - 'gridTemplateAreas', - 'gridTemplateColumns', - 'gridTemplateRows', - ], - listStyle: ['listStyleImage', 'listStylePosition', 'listStyleType'], - margin: ['marginBottom', 'marginLeft', 'marginRight', 'marginTop'], - marker: ['markerEnd', 'markerMid', 'markerStart'], - mask: [ - 'maskClip', - 'maskComposite', - 'maskImage', - 'maskMode', - 'maskOrigin', - 'maskPositionX', - 'maskPositionY', - 'maskRepeat', - 'maskSize', - ], - maskPosition: ['maskPositionX', 'maskPositionY'], - outline: ['outlineColor', 'outlineStyle', 'outlineWidth'], - overflow: ['overflowX', 'overflowY'], - padding: ['paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop'], - placeContent: ['alignContent', 'justifyContent'], - placeItems: ['alignItems', 'justifyItems'], - placeSelf: ['alignSelf', 'justifySelf'], - textDecoration: [ - 'textDecorationColor', - 'textDecorationLine', - 'textDecorationStyle', - ], - textEmphasis: ['textEmphasisColor', 'textEmphasisStyle'], - transition: [ - 'transitionDelay', - 'transitionDuration', - 'transitionProperty', - 'transitionTimingFunction', - ], - wordWrap: ['overflowWrap'], - } - : {}; - -export const longhandToShorthandInDev = {}; - -export const overlappingShorthandsInDev = {}; - -/** - * obj[key].push(val), handling new keys. - */ -function pushToValueArray(obj, key, val) { - let arr = obj[key]; - if (!arr) { - obj[key] = arr = []; - } - arr.push(val); -} - -if (__DEV__) { - // eslint-disable-next-line no-var - var shorthand; - - // We want to treat properties like 'backgroundPosition' as one of the - // properties that 'background' expands to, even though that's technically - // incorrect. - for (shorthand in shorthandToLonghandInDev) { - const longhands = shorthandToLonghandInDev[shorthand]; - for (const shorthand2 in shorthandToLonghandInDev) { - // eslint-disable-next-line no-var - var longhands2 = shorthandToLonghandInDev[shorthand2]; - if (shorthand <= shorthand2) { - continue; - } - if (longhands.every(l => l === shorthand || longhands2.includes(l))) { - // ex: shorthand = 'backgroundPosition', shorthand2 = 'background' - longhands2.push(shorthand); - } else if ( - longhands.some(l => l !== shorthand && longhands2.includes(l)) - ) { - // ex: shorthand = 'borderLeft', shorthand2 = 'borderStyle' - pushToValueArray(overlappingShorthandsInDev, shorthand, shorthand2); - pushToValueArray(overlappingShorthandsInDev, shorthand2, shorthand); - } - } - } - - // Flip into {maskPositionX: ['mask', 'maskPosition']} for reverse lookups - for (shorthand in shorthandToLonghandInDev) { - const longhands = shorthandToLonghandInDev[shorthand]; - longhands.forEach(longhand => { - pushToValueArray(longhandToShorthandInDev, longhand, shorthand); - }); - } -} +export const shorthandToLonghand = { + animation: [ + 'animationDelay', + 'animationDirection', + 'animationDuration', + 'animationFillMode', + 'animationIterationCount', + 'animationName', + 'animationPlayState', + 'animationTimingFunction', + ], + background: [ + 'backgroundAttachment', + 'backgroundClip', + 'backgroundColor', + 'backgroundImage', + 'backgroundOrigin', + 'backgroundPositionX', + 'backgroundPositionY', + 'backgroundRepeat', + 'backgroundSize', + ], + backgroundPosition: ['backgroundPositionX', 'backgroundPositionY'], + border: [ + 'borderBottomColor', + 'borderBottomStyle', + 'borderBottomWidth', + 'borderImageOutset', + 'borderImageRepeat', + 'borderImageSlice', + 'borderImageSource', + 'borderImageWidth', + 'borderLeftColor', + 'borderLeftStyle', + 'borderLeftWidth', + 'borderRightColor', + 'borderRightStyle', + 'borderRightWidth', + 'borderTopColor', + 'borderTopStyle', + 'borderTopWidth', + ], + borderBlockEnd: [ + 'borderBlockEndColor', + 'borderBlockEndStyle', + 'borderBlockEndWidth', + ], + borderBlockStart: [ + 'borderBlockStartColor', + 'borderBlockStartStyle', + 'borderBlockStartWidth', + ], + borderBottom: ['borderBottomColor', 'borderBottomStyle', 'borderBottomWidth'], + borderColor: [ + 'borderBottomColor', + 'borderLeftColor', + 'borderRightColor', + 'borderTopColor', + ], + borderImage: [ + 'borderImageOutset', + 'borderImageRepeat', + 'borderImageSlice', + 'borderImageSource', + 'borderImageWidth', + ], + borderInlineEnd: [ + 'borderInlineEndColor', + 'borderInlineEndStyle', + 'borderInlineEndWidth', + ], + borderInlineStart: [ + 'borderInlineStartColor', + 'borderInlineStartStyle', + 'borderInlineStartWidth', + ], + borderLeft: ['borderLeftColor', 'borderLeftStyle', 'borderLeftWidth'], + borderRadius: [ + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + ], + borderRight: ['borderRightColor', 'borderRightStyle', 'borderRightWidth'], + borderStyle: [ + 'borderBottomStyle', + 'borderLeftStyle', + 'borderRightStyle', + 'borderTopStyle', + ], + borderTop: ['borderTopColor', 'borderTopStyle', 'borderTopWidth'], + borderWidth: [ + 'borderBottomWidth', + 'borderLeftWidth', + 'borderRightWidth', + 'borderTopWidth', + ], + columnRule: ['columnRuleColor', 'columnRuleStyle', 'columnRuleWidth'], + columns: ['columnCount', 'columnWidth'], + flex: ['flexBasis', 'flexGrow', 'flexShrink'], + flexFlow: ['flexDirection', 'flexWrap'], + font: [ + 'fontFamily', + 'fontFeatureSettings', + 'fontKerning', + 'fontLanguageOverride', + 'fontSize', + 'fontSizeAdjust', + 'fontStretch', + 'fontStyle', + 'fontVariant', + 'fontVariantAlternates', + 'fontVariantCaps', + 'fontVariantEastAsian', + 'fontVariantLigatures', + 'fontVariantNumeric', + 'fontVariantPosition', + 'fontWeight', + 'lineHeight', + ], + fontVariant: [ + 'fontVariantAlternates', + 'fontVariantCaps', + 'fontVariantEastAsian', + 'fontVariantLigatures', + 'fontVariantNumeric', + 'fontVariantPosition', + ], + gap: ['columnGap', 'rowGap'], + grid: [ + 'gridAutoColumns', + 'gridAutoFlow', + 'gridAutoRows', + 'gridTemplateAreas', + 'gridTemplateColumns', + 'gridTemplateRows', + ], + gridArea: ['gridColumnEnd', 'gridColumnStart', 'gridRowEnd', 'gridRowStart'], + gridColumn: ['gridColumnEnd', 'gridColumnStart'], + gridColumnGap: ['columnGap'], + gridGap: ['columnGap', 'rowGap'], + gridRow: ['gridRowEnd', 'gridRowStart'], + gridRowGap: ['rowGap'], + gridTemplate: [ + 'gridTemplateAreas', + 'gridTemplateColumns', + 'gridTemplateRows', + ], + listStyle: ['listStyleImage', 'listStylePosition', 'listStyleType'], + margin: ['marginBottom', 'marginLeft', 'marginRight', 'marginTop'], + marker: ['markerEnd', 'markerMid', 'markerStart'], + mask: [ + 'maskClip', + 'maskComposite', + 'maskImage', + 'maskMode', + 'maskOrigin', + 'maskPositionX', + 'maskPositionY', + 'maskRepeat', + 'maskSize', + ], + maskPosition: ['maskPositionX', 'maskPositionY'], + outline: ['outlineColor', 'outlineStyle', 'outlineWidth'], + overflow: ['overflowX', 'overflowY'], + padding: ['paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop'], + placeContent: ['alignContent', 'justifyContent'], + placeItems: ['alignItems', 'justifyItems'], + placeSelf: ['alignSelf', 'justifySelf'], + textDecoration: [ + 'textDecorationColor', + 'textDecorationLine', + 'textDecorationStyle', + ], + textEmphasis: ['textEmphasisColor', 'textEmphasisStyle'], + transition: [ + 'transitionDelay', + 'transitionDuration', + 'transitionProperty', + 'transitionTimingFunction', + ], + wordWrap: ['overflowWrap'], +};