[change] Move all non-standard CSS transforms to 'preprocess' step

This patch reorganizes the style compiler so that the 'preprocess' step
is responsible for all the work needed to transform any non-standard CSS
from React Native into a form that can be 'compiled' to rules for the
CSSStyleSheet.

Over time the 'preprocess' step should eventually be unnecessary as
React Native aligns its APIs with CSS APIs. And any external style
compilers should be able to run the 'preprocess' function over the style
input to produce valid CSS as input for the compiler.
This commit is contained in:
Nicolas Gallagher
2023-03-20 12:25:50 -07:00
parent 43b463bdbd
commit bc6e02e9d0
6 changed files with 184 additions and 179 deletions
@@ -72,12 +72,6 @@ describe('compiler/createReactDOMStyle', () => {
`); `);
}); });
test('aspectRatio', () => {
expect(createReactDOMStyle({ aspectRatio: 9 / 16 })).toEqual({
aspectRatio: '0.5625'
});
});
describe('flexbox styles', () => { describe('flexbox styles', () => {
test('flex: -1', () => { test('flex: -1', () => {
expect(createReactDOMStyle({ flex: -1 })).toEqual({ expect(createReactDOMStyle({ flex: -1 })).toEqual({
@@ -190,70 +184,4 @@ describe('compiler/createReactDOMStyle', () => {
`); `);
}); });
}); });
test('fontVariant', () => {
expect(
createReactDOMStyle({ fontVariant: 'common-ligatures small-caps' })
).toEqual({
fontVariant: 'common-ligatures small-caps'
});
expect(
createReactDOMStyle({ fontVariant: ['common-ligatures', 'small-caps'] })
).toEqual({
fontVariant: 'common-ligatures small-caps'
});
});
test('textAlignVertical', () => {
expect(
createReactDOMStyle({
textAlignVertical: 'center'
})
).toEqual({
verticalAlign: 'middle'
});
});
test('verticalAlign', () => {
expect(
createReactDOMStyle({
verticalAlign: 'top',
textAlignVertical: 'center'
})
).toEqual({
verticalAlign: 'top'
});
});
describe('transform', () => {
// passthrough if transform value is ever a string
test('string', () => {
const transform =
'perspective(50px) scaleX(20) translateX(20px) rotate(20deg)';
const style = { transform };
const resolved = createReactDOMStyle(style);
expect(resolved).toEqual({ transform });
});
test('array', () => {
const style = {
transform: [
{ perspective: 50 },
{ scaleX: 20 },
{ translateX: 20 },
{ rotate: '20deg' },
{ matrix: [1, 2, 3, 4, 5, 6] },
{ matrix3d: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] }
]
};
const resolved = createReactDOMStyle(style);
expect(resolved).toEqual({
transform:
'perspective(50px) scaleX(20) translateX(20px) rotate(20deg) matrix(1,2,3,4,5,6) matrix3d(1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4)'
});
});
});
}); });
@@ -11,7 +11,7 @@ describe('StyleSheet/compile', () => {
describe('atomic', () => { describe('atomic', () => {
test('converts style to atomic CSS', () => { test('converts style to atomic CSS', () => {
const result = atomic({ const result = atomic({
animationDirection: ['alternate', 'alternate-reverse'], animationDirection: 'alternate,alternate-reverse',
animationKeyframes: [ animationKeyframes: [
{ '0%': { top: 0 }, '50%': { top: 5 }, '100%': { top: 10 } }, { '0%': { top: 0 }, '50%': { top: 5 }, '100%': { top: 10 } },
{ from: { left: 0 }, to: { left: 10 } } { from: { left: 0 }, to: { left: 10 } }
@@ -34,7 +34,7 @@ describe('StyleSheet/compile', () => {
{ {
"$$css": true, "$$css": true,
"$$css$localize": true, "$$css$localize": true,
"animationDirection": "r-animationDirection-1kmv48j", "animationDirection": "r-animationDirection-1wgwto7",
"animationKeyframes": "r-animationKeyframes-zacbmr", "animationKeyframes": "r-animationKeyframes-zacbmr",
"fontFamily": "r-fontFamily-1qd0xha", "fontFamily": "r-fontFamily-1qd0xha",
"insetInlineStart": [ "insetInlineStart": [
@@ -63,7 +63,7 @@ describe('StyleSheet/compile', () => {
[ [
[ [
[ [
".r-animationDirection-1kmv48j{animation-direction:alternate,alternate-reverse;}", ".r-animationDirection-1wgwto7{animation-direction:alternate,alternate-reverse;}",
], ],
3, 3,
], ],
@@ -75,6 +75,76 @@ describe('StyleSheet/preprocess', () => {
paddingInlineStart: 2 paddingInlineStart: 2
}); });
}); });
test('converts non-standard textAlignVertical', () => {
expect(
preprocess({
textAlignVertical: 'center'
})
).toEqual({
verticalAlign: 'middle'
});
expect(
preprocess({
verticalAlign: 'top',
textAlignVertical: 'center'
})
).toEqual({
verticalAlign: 'top'
});
});
test('aspectRatio', () => {
expect(preprocess({ aspectRatio: 9 / 16 })).toEqual({
aspectRatio: '0.5625'
});
});
test('fontVariant', () => {
expect(
preprocess({ fontVariant: 'common-ligatures small-caps' })
).toEqual({
fontVariant: 'common-ligatures small-caps'
});
expect(
preprocess({ fontVariant: ['common-ligatures', 'small-caps'] })
).toEqual({
fontVariant: 'common-ligatures small-caps'
});
});
describe('transform', () => {
// passthrough if transform value is ever a string
test('string', () => {
const transform =
'perspective(50px) scaleX(20) translateX(20px) rotate(20deg)';
const style = { transform };
const resolved = preprocess(style);
expect(resolved).toEqual({ transform });
});
test('array', () => {
const style = {
transform: [
{ perspective: 50 },
{ scaleX: 20 },
{ translateX: 20 },
{ rotate: '20deg' },
{ matrix: [1, 2, 3, 4, 5, 6] },
{ matrix3d: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] }
]
};
const resolved = preprocess(style);
expect(resolved).toEqual({
transform:
'perspective(50px) scaleX(20) translateX(20px) rotate(20deg) matrix(1,2,3,4,5,6) matrix3d(1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4)'
});
});
});
}); });
describe('preprocesses multiple shadow styles into a single declaration', () => { describe('preprocesses multiple shadow styles into a single declaration', () => {
@@ -9,7 +9,6 @@
import normalizeValueWithProperty from './normalizeValueWithProperty'; import normalizeValueWithProperty from './normalizeValueWithProperty';
import canUseDOM from '../../../modules/canUseDom'; import canUseDOM from '../../../modules/canUseDom';
import { warnOnce } from '../../../modules/warnOnce';
type Style = { [key: string]: any }; type Style = { [key: string]: any };
@@ -33,13 +32,6 @@ const supportsCSS3TextDecoration =
(window.CSS.supports('text-decoration-line', 'none') || (window.CSS.supports('text-decoration-line', 'none') ||
window.CSS.supports('-webkit-text-decoration-line', 'none'))); window.CSS.supports('-webkit-text-decoration-line', 'none')));
const ignoredProps = {
elevation: true,
overlayColor: true,
resizeMode: true,
tintColor: true
};
const MONOSPACE_FONT_STACK = 'monospace,monospace'; const MONOSPACE_FONT_STACK = 'monospace,monospace';
const SYSTEM_FONT_STACK = const SYSTEM_FONT_STACK =
@@ -114,36 +106,6 @@ const STYLE_SHORT_FORM_EXPANSIONS = {
//paddingInlineEnd: ['marginRight'], //paddingInlineEnd: ['marginRight'],
}; };
/**
* Transform
*/
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
// { matrix: [1,2,3,4,5,6] } => 'matrix(1,2,3,4,5,6)'
const mapTransform = (transform: Object): string => {
const type = Object.keys(transform)[0];
const value = transform[type];
if (type === 'matrix' || type === 'matrix3d') {
return `${type}(${value.join(',')})`;
} else {
const normalizedValue = normalizeValueWithProperty(value, type);
return `${type}(${normalizedValue})`;
}
};
export const createTransformValue = (style: Style): string => {
let transform = style.transform;
if (Array.isArray(style.transform)) {
warnOnce(
'transform',
'"transform" style array value is deprecated. Use space-separated string functions, e.g., "scaleX(2) rotateX(15deg)".'
);
transform = style.transform.map(mapTransform).join(' ');
}
return transform;
};
/** /**
* Reducer * Reducer
*/ */
@@ -160,16 +122,12 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
if ( if (
// Ignore everything with a null value // Ignore everything with a null value
value == null || value == null
// Ignore some React Native styles
ignoredProps[prop]
) { ) {
continue; continue;
} }
if (prop === 'aspectRatio') { if (prop === 'backgroundClip') {
resolvedStyle[prop] = value.toString();
} else if (prop === 'backgroundClip') {
// TODO: remove once this issue is fixed // TODO: remove once this issue is fixed
// https://github.com/rofrischmann/inline-style-prefixer/issues/159 // https://github.com/rofrischmann/inline-style-prefixer/issues/159
if (value === 'text') { if (value === 'text') {
@@ -196,24 +154,6 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
} else { } else {
resolvedStyle[prop] = value; resolvedStyle[prop] = value;
} }
} else if (prop === 'fontVariant') {
if (Array.isArray(value) && value.length > 0) {
warnOnce(
'fontVariant',
'"fontVariant" style array value is deprecated. Use space-separated values.'
);
resolvedStyle.fontVariant = value.join(' ');
} else {
resolvedStyle[prop] = value;
}
} else if (prop === 'textAlignVertical') {
warnOnce(
'textAlignVertical',
'"textAlignVertical" style is deprecated. Use "verticalAlign".'
);
if (resolvedStyle.verticalAlign == null) {
resolvedStyle.verticalAlign = value === 'center' ? 'middle' : value;
}
} else if (prop === 'textDecorationLine') { } else if (prop === 'textDecorationLine') {
// use 'text-decoration' for browsers that only support CSS2 // use 'text-decoration' for browsers that only support CSS2
// text-decoration (e.g., IE, Edge) // text-decoration (e.g., IE, Edge)
@@ -222,8 +162,6 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
} else { } else {
resolvedStyle.textDecorationLine = value; resolvedStyle.textDecorationLine = value;
} }
} else if (prop === 'transform' || prop === 'transformMatrix') {
resolvedStyle.transform = createTransformValue(style);
} else if (prop === 'writingDirection') { } else if (prop === 'writingDirection') {
resolvedStyle.direction = value; resolvedStyle.direction = value;
} else { } else {
@@ -265,7 +203,7 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
} }
}); });
} else { } else {
resolvedStyle[prop] = Array.isArray(value) ? value.join(',') : value; resolvedStyle[prop] = value;
} }
} }
} }
+108 -34
View File
@@ -56,6 +56,23 @@ export const createTextShadowValue = (style: Object): void | string => {
} }
}; };
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
// { matrix: [1,2,3,4,5,6] } => 'matrix(1,2,3,4,5,6)'
const mapTransform = (transform: Object): string => {
const type = Object.keys(transform)[0];
const value = transform[type];
if (type === 'matrix' || type === 'matrix3d') {
return `${type}(${value.join(',')})`;
} else {
const normalizedValue = normalizeValueWithProperty(value, type);
return `${type}(${normalizedValue})`;
}
};
export const createTransformValue = (value: Array<Object>): string => {
return value.map(mapTransform).join(' ');
};
const PROPERTIES_STANDARD: { [key: string]: string } = { const PROPERTIES_STANDARD: { [key: string]: string } = {
borderBottomEndRadius: 'borderEndEndRadius', borderBottomEndRadius: 'borderEndEndRadius',
borderBottomStartRadius: 'borderEndStartRadius', borderBottomStartRadius: 'borderEndStartRadius',
@@ -79,6 +96,13 @@ const PROPERTIES_STANDARD: { [key: string]: string } = {
start: 'insetInlineStart' start: 'insetInlineStart'
}; };
const ignoredProps = {
elevation: true,
overlayColor: true,
resizeMode: true,
tintColor: true
};
/** /**
* Preprocess styles * Preprocess styles
*/ */
@@ -88,9 +112,64 @@ export const preprocess = <T: {| [key: string]: any |}>(
const style = originalStyle || emptyObject; const style = originalStyle || emptyObject;
const nextStyle = {}; const nextStyle = {};
// Convert shadow styles
if (
style.shadowColor != null ||
style.shadowOffset != null ||
style.shadowOpacity != null ||
style.shadowRadius != null
) {
warnOnce(
'shadowStyles',
`"shadow*" style props are deprecated. Use "boxShadow".`
);
const boxShadowValue = createBoxShadowValue(style);
if (boxShadowValue != null && nextStyle.boxShadow == null) {
const { boxShadow } = style;
const value = boxShadow
? `${boxShadow}, ${boxShadowValue}`
: boxShadowValue;
nextStyle.boxShadow = value;
}
}
// Convert text shadow styles
if (
style.textShadowColor != null ||
style.textShadowOffset != null ||
style.textShadowRadius != null
) {
warnOnce(
'textShadowStyles',
`"textShadow*" style props are deprecated. Use "textShadow".`
);
const textShadowValue = createTextShadowValue(style);
if (textShadowValue != null && nextStyle.textShadow == null) {
const { textShadow } = style;
const value = textShadow
? `${textShadow}, ${textShadowValue}`
: textShadowValue;
nextStyle.textShadow = value;
}
}
for (const originalProp in style) { for (const originalProp in style) {
if (
// Ignore some React Native styles
ignoredProps[originalProp] != null ||
originalProp === 'shadowColor' ||
originalProp === 'shadowOffset' ||
originalProp === 'shadowOpacity' ||
originalProp === 'shadowRadius' ||
originalProp === 'textShadowColor' ||
originalProp === 'textShadowOffset' ||
originalProp === 'textShadowRadius'
) {
continue;
}
const originalValue = style[originalProp]; const originalValue = style[originalProp];
let prop = PROPERTIES_STANDARD[originalProp] || originalProp; const prop = PROPERTIES_STANDARD[originalProp] || originalProp;
let value = originalValue; let value = originalValue;
if ( if (
@@ -100,42 +179,37 @@ export const preprocess = <T: {| [key: string]: any |}>(
continue; continue;
} }
// Convert shadow styles if (prop === 'aspectRatio') {
if ( nextStyle[prop] = value.toString();
prop === 'shadowColor' || } else if (prop === 'fontVariant') {
prop === 'shadowOffset' || if (Array.isArray(value) && value.length > 0) {
prop === 'shadowOpacity' || warnOnce(
prop === 'shadowRadius' 'fontVariant',
) { '"fontVariant" style array value is deprecated. Use space-separated values.'
const boxShadowValue = createBoxShadowValue(style); );
if (boxShadowValue != null && nextStyle.boxShadow == null) { value = value.join(' ');
const { boxShadow } = style;
prop = 'boxShadow';
value = boxShadow ? `${boxShadow}, ${boxShadowValue}` : boxShadowValue;
} else {
continue;
} }
} nextStyle[prop] = value;
} else if (prop === 'textAlignVertical') {
// Convert text shadow styles warnOnce(
if ( 'textAlignVertical',
prop === 'textShadowColor' || '"textAlignVertical" style is deprecated. Use "verticalAlign".'
prop === 'textShadowOffset' || );
prop === 'textShadowRadius' if (style.verticalAlign == null) {
) { nextStyle.verticalAlign = value === 'center' ? 'middle' : value;
const textShadowValue = createTextShadowValue(style);
if (textShadowValue != null && nextStyle.textShadow == null) {
const { textShadow } = style;
prop = 'textShadow';
value = textShadow
? `${textShadow}, ${textShadowValue}`
: textShadowValue;
} else {
continue;
} }
} else if (prop === 'transform') {
if (Array.isArray(value)) {
warnOnce(
'transform',
'"transform" style array value is deprecated. Use space-separated string functions, e.g., "scaleX(2) rotateX(15deg)".'
);
value = createTransformValue(value);
}
nextStyle.transform = value;
} else {
nextStyle[prop] = value;
} }
nextStyle[prop] = value;
} }
// $FlowIgnore // $FlowIgnore
@@ -68,15 +68,10 @@ export function validate(obj: Object) {
let suggestion = ''; let suggestion = '';
if (prop === 'animation' || prop === 'animationName') { if (prop === 'animation' || prop === 'animationName') {
suggestion = 'Did you mean "animationKeyframes"?'; suggestion = 'Did you mean "animationKeyframes"?';
// } else if (prop === 'boxShadow') {
// suggestion = 'Did you mean "shadow{Color,Offset,Opacity,Radius}"?';
isInvalid = true; isInvalid = true;
} else if (prop === 'direction') { } else if (prop === 'direction') {
suggestion = 'Did you mean "writingDirection"?'; suggestion = 'Did you mean "writingDirection"?';
isInvalid = true; isInvalid = true;
} else if (prop === 'verticalAlign') {
suggestion = 'Did you mean "textAlignVertical"?';
isInvalid = true;
} else if (invalidShortforms[prop]) { } else if (invalidShortforms[prop]) {
suggestion = 'Please use long-form properties.'; suggestion = 'Please use long-form properties.';
isInvalid = true; isInvalid = true;