[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, clear: string,
cursor: string, cursor: string,
fill: string, fill: string,
float: oneOf(['left', 'none', 'right']), float: oneOf(['end', 'left', 'none', 'right', 'start']),
listStyle: string, listStyle: string,
pointerEvents: string, pointerEvents: string,
tableLayout: string, tableLayout: string,
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 { Object {
"borderBottomLeftRadius": 20, "borderBottomLeftRadius": 20,
"borderBottomRightRadius": "2rem", "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 { Object {
"borderBottomLeftRadius": "2rem", "borderBottomLeftRadius": "2rem",
"borderBottomRightRadius": 20, "borderBottomRightRadius": 20,
@@ -3,7 +3,7 @@
import I18nManager from '../../I18nManager'; import I18nManager from '../../I18nManager';
import i18nStyle from '../i18nStyle'; import i18nStyle from '../i18nStyle';
const style = { const styleLeftRight = {
borderLeftColor: 'red', borderLeftColor: 'red',
borderRightColor: 'blue', borderRightColor: 'blue',
borderTopLeftRadius: 10, borderTopLeftRadius: 10,
@@ -24,6 +24,27 @@ const style = {
textShadowOffset: { width: '1rem', height: 10 } 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('StyleSheet/i18nStyle', () => {
describe('LTR mode', () => { describe('LTR mode', () => {
beforeEach(() => { beforeEach(() => {
@@ -34,8 +55,29 @@ describe('StyleSheet/i18nStyle', () => {
I18nManager.allowRTL(true); I18nManager.allowRTL(true);
}); });
test('does not auto-flip', () => { test("doesn't flip left/right", () => {
expect(i18nStyle(style)).toMatchSnapshot(); 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); I18nManager.forceRTL(false);
}); });
test('does auto-flip', () => { test('flips left/right', () => {
expect(i18nStyle(style)).toMatchSnapshot(); 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 = {}; const emptyObject = {};
/** const borderTopLeftRadius = 'borderTopLeftRadius';
* Map of property names to their BiDi equivalent. const borderTopRightRadius = 'borderTopRightRadius';
*/ const borderBottomLeftRadius = 'borderBottomLeftRadius';
const PROPERTIES_TO_SWAP = { const borderBottomRightRadius = 'borderBottomRightRadius';
borderTopLeftRadius: 'borderTopRightRadius', const borderLeftColor = 'borderLeftColor';
borderTopRightRadius: 'borderTopLeftRadius', const borderLeftStyle = 'borderLeftStyle';
borderBottomLeftRadius: 'borderBottomRightRadius', const borderLeftWidth = 'borderLeftWidth';
borderBottomRightRadius: 'borderBottomLeftRadius', const borderRightColor = 'borderRightColor';
borderLeftColor: 'borderRightColor', const borderRightStyle = 'borderRightStyle';
borderLeftStyle: 'borderRightStyle', const borderRightWidth = 'borderRightWidth';
borderLeftWidth: 'borderRightWidth', const right = 'right';
borderRightColor: 'borderLeftColor', const marginLeft = 'marginLeft';
borderRightWidth: 'borderLeftWidth', const marginRight = 'marginRight';
borderRightStyle: 'borderLeftStyle', const paddingLeft = 'paddingLeft';
left: 'right', const paddingRight = 'paddingRight';
marginLeft: 'marginRight', const left = 'left';
marginRight: 'marginLeft',
paddingLeft: 'paddingRight', // Map of LTR property names to their BiDi equivalent.
paddingRight: 'paddingLeft', const PROPERTIES_FLIP = {
right: 'left' 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, clear: true,
float: true, float: true,
textAlign: 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); const additiveInverse = (value: String | Number) => multiplyStyleLengthValue(value, -1);
/** // Convert I18N properties and values
* BiDi flip the given property. const convertProperty = (prop: String): String => {
*/ return PROPERTIES_I18N.hasOwnProperty(prop) ? PROPERTIES_I18N[prop] : prop;
const flipProperty = (prop: String): String => { };
return PROPERTIES_TO_SWAP.hasOwnProperty(prop) ? PROPERTIES_TO_SWAP[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; return value === 'left' ? 'right' : value === 'right' ? 'left' : value;
}; };
const i18nStyle = originalStyle => { const i18nStyle = originalStyle => {
if (!I18nManager.isRTL) { const isRTL = I18nManager.isRTL;
return originalStyle;
}
const style = originalStyle || emptyObject; const style = originalStyle || emptyObject;
const nextStyle = {}; const nextStyle = {};
const frozenProps = {};
for (const prop in style) { for (const originalProp in style) {
if (!Object.prototype.hasOwnProperty.call(style, prop)) { if (!Object.prototype.hasOwnProperty.call(style, originalProp)) {
continue; continue;
} }
const value = style[prop]; let prop = originalProp;
let value = style[originalProp];
let shouldFreezeProp = false;
if (PROPERTIES_TO_SWAP[prop]) { // Process I18N properties and values
const newProp = flipProperty(prop); if (PROPERTIES_I18N[prop]) {
nextStyle[newProp] = value; prop = convertProperty(prop);
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) { // I18N properties takes precendence over left/right
nextStyle[prop] = swapLeftRight(value); shouldFreezeProp = true;
} else if (prop === 'textShadowOffset') { } else if (PROPERTIES_VALUE[prop]) {
nextStyle[prop] = value; value = convertValue(value);
nextStyle[prop].width = additiveInverse(value.width); }
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 { } 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 numberOrString = oneOfType([number, string]);
const ShadowOffsetPropType = shape({ width: number, height: number }); 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 WritingDirectionPropType = oneOf(['auto', 'ltr', 'rtl']);
const TextStylePropTypes = { const TextStylePropTypes = {
@@ -27,13 +27,16 @@ const LayoutPropTypes = {
backfaceVisibility: hiddenOrVisible, backfaceVisibility: hiddenOrVisible,
borderWidth: numberOrString, borderWidth: numberOrString,
borderBottomWidth: numberOrString, borderBottomWidth: numberOrString,
borderEndWidth: numberOrString,
borderLeftWidth: numberOrString, borderLeftWidth: numberOrString,
borderRightWidth: numberOrString, borderRightWidth: numberOrString,
borderStartWidth: numberOrString,
borderTopWidth: numberOrString, borderTopWidth: numberOrString,
bottom: numberOrString, bottom: numberOrString,
boxSizing: string, boxSizing: string,
direction: oneOf(['inherit', 'ltr', 'rtl']), direction: oneOf(['inherit', 'ltr', 'rtl']),
display: string, display: string,
end: numberOrString,
flex: number, flex: number,
flexBasis: numberOrString, flexBasis: numberOrString,
flexDirection: oneOf(['column', 'column-reverse', 'row', 'row-reverse']), flexDirection: oneOf(['column', 'column-reverse', 'row', 'row-reverse']),
@@ -53,8 +56,10 @@ const LayoutPropTypes = {
margin: numberOrString, margin: numberOrString,
marginBottom: numberOrString, marginBottom: numberOrString,
marginHorizontal: numberOrString, marginHorizontal: numberOrString,
marginEnd: numberOrString,
marginLeft: numberOrString, marginLeft: numberOrString,
marginRight: numberOrString, marginRight: numberOrString,
marginStart: numberOrString,
marginTop: numberOrString, marginTop: numberOrString,
marginVertical: numberOrString, marginVertical: numberOrString,
maxHeight: numberOrString, maxHeight: numberOrString,
@@ -68,12 +73,15 @@ const LayoutPropTypes = {
padding: numberOrString, padding: numberOrString,
paddingBottom: numberOrString, paddingBottom: numberOrString,
paddingHorizontal: numberOrString, paddingHorizontal: numberOrString,
paddingEnd: numberOrString,
paddingLeft: numberOrString, paddingLeft: numberOrString,
paddingRight: numberOrString, paddingRight: numberOrString,
paddingStart: numberOrString,
paddingTop: numberOrString, paddingTop: numberOrString,
paddingVertical: numberOrString, paddingVertical: numberOrString,
position: oneOf(['absolute', 'fixed', 'relative', 'static', 'sticky']), position: oneOf(['absolute', 'fixed', 'relative', 'static', 'sticky']),
right: numberOrString, right: numberOrString,
start: numberOrString,
top: numberOrString, top: numberOrString,
visibility: hiddenOrVisible, visibility: hiddenOrVisible,
width: numberOrString, width: numberOrString,
@@ -206,7 +206,7 @@ const stylePropTypes = [
}, },
{ {
name: 'textAlign', name: 'textAlign',
typeInfo: 'string' typeInfo: 'enum("center", "end", "inherit", "justify", "justify-all", "left", "right", "start")'
}, },
{ {
name: 'textAlignVertical', name: 'textAlignVertical',
@@ -403,32 +403,36 @@ const stylePropTypes = [
name: 'borderColor', name: 'borderColor',
typeInfo: 'color' typeInfo: 'color'
}, },
{
name: 'borderTopColor',
typeInfo: 'color'
},
{ {
name: 'borderBottomColor', name: 'borderBottomColor',
typeInfo: 'color' typeInfo: 'color'
}, },
{ {
name: 'borderRightColor', name: 'borderEndColor',
typeInfo: 'color' typeInfo: 'color'
}, },
{ {
name: 'borderLeftColor', name: 'borderLeftColor',
typeInfo: 'color' typeInfo: 'color'
}, },
{
name: 'borderRightColor',
typeInfo: 'color'
},
{
name: 'borderStartColor',
typeInfo: 'color'
},
{
name: 'borderTopColor',
typeInfo: 'color'
},
{ {
name: 'borderRadius', name: 'borderRadius',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{ {
name: 'borderTopLeftRadius', name: 'borderBottomEndRadius',
typeInfo: 'number | string'
},
{
name: 'borderTopRightRadius',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{ {
@@ -439,26 +443,54 @@ const stylePropTypes = [
name: 'borderBottomRightRadius', name: 'borderBottomRightRadius',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'borderBottomStartRadius',
typeInfo: 'number | string'
},
{
name: 'borderTopEndRadius',
typeInfo: 'number | string'
},
{
name: 'borderTopLeftRadius',
typeInfo: 'number | string'
},
{
name: 'borderTopRightRadius',
typeInfo: 'number | string'
},
{
name: 'borderTopStartRadius',
typeInfo: 'number | string'
},
{ {
name: 'borderStyle', name: 'borderStyle',
typeInfo: 'string' typeInfo: 'string'
}, },
{
name: 'borderTopStyle',
typeInfo: 'string'
},
{
name: 'borderRightStyle',
typeInfo: 'string'
},
{ {
name: 'borderBottomStyle', name: 'borderBottomStyle',
typeInfo: 'string' typeInfo: 'string'
}, },
{
name: 'borderEndStyle',
typeInfo: 'string'
},
{ {
name: 'borderLeftStyle', name: 'borderLeftStyle',
typeInfo: 'string' typeInfo: 'string'
}, },
{
name: 'borderRightStyle',
typeInfo: 'string'
},
{
name: 'borderStartStyle',
typeInfo: 'string'
},
{
name: 'borderTopStyle',
typeInfo: 'string'
},
{ {
name: 'borderWidth', name: 'borderWidth',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -467,6 +499,10 @@ const stylePropTypes = [
name: 'borderBottomWidth', name: 'borderBottomWidth',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'borderEndWidth',
typeInfo: 'number | string'
},
{ {
name: 'borderLeftWidth', name: 'borderLeftWidth',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -475,6 +511,10 @@ const stylePropTypes = [
name: 'borderRightWidth', name: 'borderRightWidth',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'borderStartWidth',
typeInfo: 'number | string'
},
{ {
name: 'borderTopWidth', name: 'borderTopWidth',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -511,6 +551,10 @@ const stylePropTypes = [
name: 'display', name: 'display',
typeInfo: 'string' typeInfo: 'string'
}, },
{
name: 'end',
typeInfo: 'number | string'
},
{ {
label: 'web', label: 'web',
name: 'filter', name: 'filter',
@@ -620,6 +664,10 @@ const stylePropTypes = [
name: 'marginBottom', name: 'marginBottom',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'marginEnd',
typeInfo: 'number | string'
},
{ {
name: 'marginHorizontal', name: 'marginHorizontal',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -632,6 +680,10 @@ const stylePropTypes = [
name: 'marginRight', name: 'marginRight',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'marginStart',
typeInfo: 'number | string'
},
{ {
name: 'marginTop', name: 'marginTop',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -711,6 +763,10 @@ const stylePropTypes = [
name: 'paddingBottom', name: 'paddingBottom',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'paddingEnd',
typeInfo: 'number | string'
},
{ {
name: 'paddingHorizontal', name: 'paddingHorizontal',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -723,6 +779,10 @@ const stylePropTypes = [
name: 'paddingRight', name: 'paddingRight',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'paddingStart',
typeInfo: 'number | string'
},
{ {
name: 'paddingTop', name: 'paddingTop',
typeInfo: 'number | string' typeInfo: 'number | string'
@@ -765,6 +825,10 @@ const stylePropTypes = [
name: 'shadowRadius', name: 'shadowRadius',
typeInfo: 'number | string' typeInfo: 'number | string'
}, },
{
name: 'start',
typeInfo: 'number | string'
},
{ {
label: 'web', label: 'web',
name: 'touchAction', name: 'touchAction',