[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
2022-12-27 13:46:41 +00:00
parent 43b463bdbd
commit bc6e02e9d0
6 changed files with 184 additions and 179 deletions

View File

@@ -72,12 +72,6 @@ describe('compiler/createReactDOMStyle', () => {
`);
});
test('aspectRatio', () => {
expect(createReactDOMStyle({ aspectRatio: 9 / 16 })).toEqual({
aspectRatio: '0.5625'
});
});
describe('flexbox styles', () => {
test('flex: -1', () => {
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)'
});
});
});
});

View File

@@ -11,7 +11,7 @@ describe('StyleSheet/compile', () => {
describe('atomic', () => {
test('converts style to atomic CSS', () => {
const result = atomic({
animationDirection: ['alternate', 'alternate-reverse'],
animationDirection: 'alternate,alternate-reverse',
animationKeyframes: [
{ '0%': { top: 0 }, '50%': { top: 5 }, '100%': { top: 10 } },
{ from: { left: 0 }, to: { left: 10 } }
@@ -34,7 +34,7 @@ describe('StyleSheet/compile', () => {
{
"$$css": true,
"$$css$localize": true,
"animationDirection": "r-animationDirection-1kmv48j",
"animationDirection": "r-animationDirection-1wgwto7",
"animationKeyframes": "r-animationKeyframes-zacbmr",
"fontFamily": "r-fontFamily-1qd0xha",
"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,
],

View File

@@ -75,6 +75,76 @@ describe('StyleSheet/preprocess', () => {
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', () => {

View File

@@ -9,7 +9,6 @@
import normalizeValueWithProperty from './normalizeValueWithProperty';
import canUseDOM from '../../../modules/canUseDom';
import { warnOnce } from '../../../modules/warnOnce';
type Style = { [key: string]: any };
@@ -33,13 +32,6 @@ const supportsCSS3TextDecoration =
(window.CSS.supports('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 SYSTEM_FONT_STACK =
@@ -114,36 +106,6 @@ const STYLE_SHORT_FORM_EXPANSIONS = {
//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
*/
@@ -160,16 +122,12 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
if (
// Ignore everything with a null value
value == null ||
// Ignore some React Native styles
ignoredProps[prop]
value == null
) {
continue;
}
if (prop === 'aspectRatio') {
resolvedStyle[prop] = value.toString();
} else if (prop === 'backgroundClip') {
if (prop === 'backgroundClip') {
// TODO: remove once this issue is fixed
// https://github.com/rofrischmann/inline-style-prefixer/issues/159
if (value === 'text') {
@@ -196,24 +154,6 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
} else {
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') {
// use 'text-decoration' for browsers that only support CSS2
// text-decoration (e.g., IE, Edge)
@@ -222,8 +162,6 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
} else {
resolvedStyle.textDecorationLine = value;
}
} else if (prop === 'transform' || prop === 'transformMatrix') {
resolvedStyle.transform = createTransformValue(style);
} else if (prop === 'writingDirection') {
resolvedStyle.direction = value;
} else {
@@ -265,7 +203,7 @@ const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
}
});
} else {
resolvedStyle[prop] = Array.isArray(value) ? value.join(',') : value;
resolvedStyle[prop] = value;
}
}
}

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 } = {
borderBottomEndRadius: 'borderEndEndRadius',
borderBottomStartRadius: 'borderEndStartRadius',
@@ -79,6 +96,13 @@ const PROPERTIES_STANDARD: { [key: string]: string } = {
start: 'insetInlineStart'
};
const ignoredProps = {
elevation: true,
overlayColor: true,
resizeMode: true,
tintColor: true
};
/**
* Preprocess styles
*/
@@ -88,9 +112,64 @@ export const preprocess = <T: {| [key: string]: any |}>(
const style = originalStyle || emptyObject;
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) {
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];
let prop = PROPERTIES_STANDARD[originalProp] || originalProp;
const prop = PROPERTIES_STANDARD[originalProp] || originalProp;
let value = originalValue;
if (
@@ -100,42 +179,37 @@ export const preprocess = <T: {| [key: string]: any |}>(
continue;
}
// Convert shadow styles
if (
prop === 'shadowColor' ||
prop === 'shadowOffset' ||
prop === 'shadowOpacity' ||
prop === 'shadowRadius'
) {
const boxShadowValue = createBoxShadowValue(style);
if (boxShadowValue != null && nextStyle.boxShadow == null) {
const { boxShadow } = style;
prop = 'boxShadow';
value = boxShadow ? `${boxShadow}, ${boxShadowValue}` : boxShadowValue;
} else {
continue;
if (prop === 'aspectRatio') {
nextStyle[prop] = value.toString();
} else if (prop === 'fontVariant') {
if (Array.isArray(value) && value.length > 0) {
warnOnce(
'fontVariant',
'"fontVariant" style array value is deprecated. Use space-separated values.'
);
value = value.join(' ');
}
}
// Convert text shadow styles
if (
prop === 'textShadowColor' ||
prop === 'textShadowOffset' ||
prop === 'textShadowRadius'
) {
const textShadowValue = createTextShadowValue(style);
if (textShadowValue != null && nextStyle.textShadow == null) {
const { textShadow } = style;
prop = 'textShadow';
value = textShadow
? `${textShadow}, ${textShadowValue}`
: textShadowValue;
} else {
continue;
nextStyle[prop] = value;
} else if (prop === 'textAlignVertical') {
warnOnce(
'textAlignVertical',
'"textAlignVertical" style is deprecated. Use "verticalAlign".'
);
if (style.verticalAlign == null) {
nextStyle.verticalAlign = value === 'center' ? 'middle' : value;
}
} 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

View File

@@ -68,15 +68,10 @@ export function validate(obj: Object) {
let suggestion = '';
if (prop === 'animation' || prop === 'animationName') {
suggestion = 'Did you mean "animationKeyframes"?';
// } else if (prop === 'boxShadow') {
// suggestion = 'Did you mean "shadow{Color,Offset,Opacity,Radius}"?';
isInvalid = true;
} else if (prop === 'direction') {
suggestion = 'Did you mean "writingDirection"?';
isInvalid = true;
} else if (prop === 'verticalAlign') {
suggestion = 'Did you mean "textAlignVertical"?';
isInvalid = true;
} else if (invalidShortforms[prop]) {
suggestion = 'Please use long-form properties.';
isInvalid = true;