[add] StyleSheet support for start/end properties and values

Add support for new style properties and values that automatically
account for the writing direction (as introduced in React Native
0.51.0). The start/end variants are automatically resolved to match the
global writing direction, as defined by I18nManager.isRTL. Start/End
take precedence over Left/Right.

Adds support for the following properties:

* `borderTop{End,Start}Radius`
* `borderBottom{End,Start}Radius`
* `border{End,Start}Color`
* `border{End,Start}Style`
* `border{End,Start}Width`
* `end`
* `margin{End,Start}`
* `padding{End,Start}`
* `start`

And values:

* `clear: "end" | "start"`
* `float: "end" | "start"`
* `textAlign: "end" | "start"`
This commit is contained in:
Nicolas Gallagher
2018-02-11 10:27:45 -08:00
parent 155b34e495
commit b754776373
8 changed files with 330 additions and 73 deletions
@@ -92,7 +92,7 @@ StyleSheetValidation.addValidStylePropTypes({
clear: string,
cursor: string,
fill: string,
float: oneOf(['left', 'none', 'right']),
float: oneOf(['end', 'left', 'none', 'right', 'start']),
listStyle: string,
pointerEvents: string,
tableLayout: string,
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StyleSheet/i18nStyle LTR mode does not auto-flip 1`] = `
exports[`StyleSheet/i18nStyle LTR mode converts and doesn't flip start/end 1`] = `
Object {
"borderBottomLeftRadius": 20,
"borderBottomRightRadius": "2rem",
@@ -26,7 +26,59 @@ Object {
}
`;
exports[`StyleSheet/i18nStyle RTL mode does auto-flip 1`] = `
exports[`StyleSheet/i18nStyle LTR mode doesn't flip left/right 1`] = `
Object {
"borderBottomLeftRadius": 20,
"borderBottomRightRadius": "2rem",
"borderLeftColor": "red",
"borderLeftStyle": "solid",
"borderLeftWidth": 5,
"borderRightColor": "blue",
"borderRightStyle": "dotted",
"borderRightWidth": 6,
"borderTopLeftRadius": 10,
"borderTopRightRadius": "1rem",
"left": 1,
"marginLeft": 7,
"marginRight": 8,
"paddingLeft": 9,
"paddingRight": 10,
"right": 2,
"textAlign": "left",
"textShadowOffset": Object {
"height": 10,
"width": "1rem",
},
}
`;
exports[`StyleSheet/i18nStyle RTL mode converts and flips start/end 1`] = `
Object {
"borderBottomLeftRadius": "2rem",
"borderBottomRightRadius": 20,
"borderLeftColor": "blue",
"borderLeftStyle": "dotted",
"borderLeftWidth": 6,
"borderRightColor": "red",
"borderRightStyle": "solid",
"borderRightWidth": 5,
"borderTopLeftRadius": "1rem",
"borderTopRightRadius": 10,
"left": 2,
"marginLeft": 8,
"marginRight": 7,
"paddingLeft": 10,
"paddingRight": 9,
"right": 1,
"textAlign": "right",
"textShadowOffset": Object {
"height": 10,
"width": "-1rem",
},
}
`;
exports[`StyleSheet/i18nStyle RTL mode flips left/right 1`] = `
Object {
"borderBottomLeftRadius": "2rem",
"borderBottomRightRadius": 20,
@@ -3,7 +3,7 @@
import I18nManager from '../../I18nManager';
import i18nStyle from '../i18nStyle';
const style = {
const styleLeftRight = {
borderLeftColor: 'red',
borderRightColor: 'blue',
borderTopLeftRadius: 10,
@@ -24,6 +24,27 @@ const style = {
textShadowOffset: { width: '1rem', height: 10 }
};
const styleStartEnd = {
borderStartColor: 'red',
borderEndColor: 'blue',
borderTopStartRadius: 10,
borderTopEndRadius: '1rem',
borderBottomStartRadius: 20,
borderBottomEndRadius: '2rem',
borderStartStyle: 'solid',
borderEndStyle: 'dotted',
borderStartWidth: 5,
borderEndWidth: 6,
start: 1,
marginStart: 7,
marginEnd: 8,
paddingStart: 9,
paddingEnd: 10,
end: 2,
textAlign: 'start',
textShadowOffset: { width: '1rem', height: 10 }
};
describe('StyleSheet/i18nStyle', () => {
describe('LTR mode', () => {
beforeEach(() => {
@@ -34,8 +55,29 @@ describe('StyleSheet/i18nStyle', () => {
I18nManager.allowRTL(true);
});
test('does not auto-flip', () => {
expect(i18nStyle(style)).toMatchSnapshot();
test("doesn't flip left/right", () => {
expect(i18nStyle(styleLeftRight)).toMatchSnapshot();
});
test("converts and doesn't flip start/end", () => {
expect(i18nStyle(styleStartEnd)).toMatchSnapshot();
});
test('start/end takes precedence over left/right', () => {
const style = {
borderTopStartRadius: 10,
borderTopLeftRadius: 0,
end: 10,
right: 0,
marginStart: 10,
marginLeft: 0
};
const expected = {
borderTopLeftRadius: 10,
marginLeft: 10,
right: 10
};
expect(i18nStyle(style)).toEqual(expected);
});
});
@@ -48,8 +90,29 @@ describe('StyleSheet/i18nStyle', () => {
I18nManager.forceRTL(false);
});
test('does auto-flip', () => {
expect(i18nStyle(style)).toMatchSnapshot();
test('flips left/right', () => {
expect(i18nStyle(styleLeftRight)).toMatchSnapshot();
});
test('converts and flips start/end', () => {
expect(i18nStyle(styleStartEnd)).toMatchSnapshot();
});
test('start/end takes precedence over left/right', () => {
const style = {
borderTopStartRadius: 10,
borderTopLeftRadius: 0,
end: 10,
right: 0,
marginStart: 10,
marginLeft: 0
};
const expected = {
borderTopRightRadius: 10,
marginRight: 10,
left: 10
};
expect(i18nStyle(style)).toEqual(expected);
});
});
});
+106 -45
View File
@@ -13,75 +13,136 @@ import multiplyStyleLengthValue from '../../modules/multiplyStyleLengthValue';
const emptyObject = {};
/**
* Map of property names to their BiDi equivalent.
*/
const PROPERTIES_TO_SWAP = {
borderTopLeftRadius: 'borderTopRightRadius',
borderTopRightRadius: 'borderTopLeftRadius',
borderBottomLeftRadius: 'borderBottomRightRadius',
borderBottomRightRadius: 'borderBottomLeftRadius',
borderLeftColor: 'borderRightColor',
borderLeftStyle: 'borderRightStyle',
borderLeftWidth: 'borderRightWidth',
borderRightColor: 'borderLeftColor',
borderRightWidth: 'borderLeftWidth',
borderRightStyle: 'borderLeftStyle',
left: 'right',
marginLeft: 'marginRight',
marginRight: 'marginLeft',
paddingLeft: 'paddingRight',
paddingRight: 'paddingLeft',
right: 'left'
const borderTopLeftRadius = 'borderTopLeftRadius';
const borderTopRightRadius = 'borderTopRightRadius';
const borderBottomLeftRadius = 'borderBottomLeftRadius';
const borderBottomRightRadius = 'borderBottomRightRadius';
const borderLeftColor = 'borderLeftColor';
const borderLeftStyle = 'borderLeftStyle';
const borderLeftWidth = 'borderLeftWidth';
const borderRightColor = 'borderRightColor';
const borderRightStyle = 'borderRightStyle';
const borderRightWidth = 'borderRightWidth';
const right = 'right';
const marginLeft = 'marginLeft';
const marginRight = 'marginRight';
const paddingLeft = 'paddingLeft';
const paddingRight = 'paddingRight';
const left = 'left';
// Map of LTR property names to their BiDi equivalent.
const PROPERTIES_FLIP = {
borderTopLeftRadius: borderTopRightRadius,
borderTopRightRadius: borderTopLeftRadius,
borderBottomLeftRadius: borderBottomRightRadius,
borderBottomRightRadius: borderBottomLeftRadius,
borderLeftColor: borderRightColor,
borderLeftStyle: borderRightStyle,
borderLeftWidth: borderRightWidth,
borderRightColor: borderLeftColor,
borderRightStyle: borderLeftStyle,
borderRightWidth: borderLeftWidth,
left: right,
marginLeft: marginRight,
marginRight: marginLeft,
paddingLeft: paddingRight,
paddingRight: paddingLeft,
right: left
};
const PROPERTIES_SWAP_LEFT_RIGHT = {
// Map of I18N property names to their LTR equivalent.
const PROPERTIES_I18N = {
borderTopStartRadius: borderTopLeftRadius,
borderTopEndRadius: borderTopRightRadius,
borderBottomStartRadius: borderBottomLeftRadius,
borderBottomEndRadius: borderBottomRightRadius,
borderStartColor: borderLeftColor,
borderStartStyle: borderLeftStyle,
borderStartWidth: borderLeftWidth,
borderEndColor: borderRightColor,
borderEndStyle: borderRightStyle,
borderEndWidth: borderRightWidth,
end: right,
marginStart: marginLeft,
marginEnd: marginRight,
paddingStart: paddingLeft,
paddingEnd: paddingRight,
start: left
};
const PROPERTIES_VALUE = {
clear: true,
float: true,
textAlign: true
};
/**
* Invert the sign of a numeric-like value
*/
// Invert the sign of a numeric-like value
const additiveInverse = (value: String | Number) => multiplyStyleLengthValue(value, -1);
/**
* BiDi flip the given property.
*/
const flipProperty = (prop: String): String => {
return PROPERTIES_TO_SWAP.hasOwnProperty(prop) ? PROPERTIES_TO_SWAP[prop] : prop;
// Convert I18N properties and values
const convertProperty = (prop: String): String => {
return PROPERTIES_I18N.hasOwnProperty(prop) ? PROPERTIES_I18N[prop] : prop;
};
const convertValue = (value: String): String => {
return value === 'start' ? 'left' : value === 'end' ? 'right' : value;
};
const swapLeftRight = (value: String): String => {
// BiDi flip properties and values
const flipProperty = (prop: String): String => {
return PROPERTIES_FLIP.hasOwnProperty(prop) ? PROPERTIES_FLIP[prop] : prop;
};
const flipValue = (value: String): String => {
return value === 'left' ? 'right' : value === 'right' ? 'left' : value;
};
const i18nStyle = originalStyle => {
if (!I18nManager.isRTL) {
return originalStyle;
}
const isRTL = I18nManager.isRTL;
const style = originalStyle || emptyObject;
const nextStyle = {};
const frozenProps = {};
for (const prop in style) {
if (!Object.prototype.hasOwnProperty.call(style, prop)) {
for (const originalProp in style) {
if (!Object.prototype.hasOwnProperty.call(style, originalProp)) {
continue;
}
const value = style[prop];
let prop = originalProp;
let value = style[originalProp];
let shouldFreezeProp = false;
if (PROPERTIES_TO_SWAP[prop]) {
const newProp = flipProperty(prop);
nextStyle[newProp] = value;
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) {
nextStyle[prop] = swapLeftRight(value);
} else if (prop === 'textShadowOffset') {
nextStyle[prop] = value;
nextStyle[prop].width = additiveInverse(value.width);
// Process I18N properties and values
if (PROPERTIES_I18N[prop]) {
prop = convertProperty(prop);
// I18N properties takes precendence over left/right
shouldFreezeProp = true;
} else if (PROPERTIES_VALUE[prop]) {
value = convertValue(value);
}
if (isRTL) {
if (PROPERTIES_FLIP[prop]) {
const newProp = flipProperty(prop);
if (!frozenProps[prop]) {
nextStyle[newProp] = value;
}
} else if (PROPERTIES_VALUE[prop]) {
nextStyle[prop] = flipValue(value);
} else if (prop === 'textShadowOffset') {
nextStyle[prop] = value;
nextStyle[prop].width = additiveInverse(value.width);
} else {
nextStyle[prop] = style[prop];
}
} else {
nextStyle[prop] = style[prop];
if (!frozenProps[prop]) {
nextStyle[prop] = value;
}
}
// Mark the style prop as frozen
if (shouldFreezeProp) {
frozenProps[prop] = true;
}
}
@@ -16,7 +16,16 @@ import { number, oneOf, oneOfType, shape, string } from 'prop-types';
const numberOrString = oneOfType([number, string]);
const ShadowOffsetPropType = shape({ width: number, height: number });
const TextAlignPropType = oneOf(['center', 'inherit', 'justify', 'justify-all', 'left', 'right']);
const TextAlignPropType = oneOf([
'center',
'end',
'inherit',
'justify',
'justify-all',
'left',
'right',
'start'
]);
const WritingDirectionPropType = oneOf(['auto', 'ltr', 'rtl']);
const TextStylePropTypes = {
@@ -27,13 +27,16 @@ const LayoutPropTypes = {
backfaceVisibility: hiddenOrVisible,
borderWidth: numberOrString,
borderBottomWidth: numberOrString,
borderEndWidth: numberOrString,
borderLeftWidth: numberOrString,
borderRightWidth: numberOrString,
borderStartWidth: numberOrString,
borderTopWidth: numberOrString,
bottom: numberOrString,
boxSizing: string,
direction: oneOf(['inherit', 'ltr', 'rtl']),
display: string,
end: numberOrString,
flex: number,
flexBasis: numberOrString,
flexDirection: oneOf(['column', 'column-reverse', 'row', 'row-reverse']),
@@ -53,8 +56,10 @@ const LayoutPropTypes = {
margin: numberOrString,
marginBottom: numberOrString,
marginHorizontal: numberOrString,
marginEnd: numberOrString,
marginLeft: numberOrString,
marginRight: numberOrString,
marginStart: numberOrString,
marginTop: numberOrString,
marginVertical: numberOrString,
maxHeight: numberOrString,
@@ -68,12 +73,15 @@ const LayoutPropTypes = {
padding: numberOrString,
paddingBottom: numberOrString,
paddingHorizontal: numberOrString,
paddingEnd: numberOrString,
paddingLeft: numberOrString,
paddingRight: numberOrString,
paddingStart: numberOrString,
paddingTop: numberOrString,
paddingVertical: numberOrString,
position: oneOf(['absolute', 'fixed', 'relative', 'static', 'sticky']),
right: numberOrString,
start: numberOrString,
top: numberOrString,
visibility: hiddenOrVisible,
width: numberOrString,