Skip to content

Commit

Permalink
[add] StyleSheet support for start/end properties and values
Browse files Browse the repository at this point in the history
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"`
  • Loading branch information
necolas committed Feb 16, 2018
1 parent 155b34e commit b754776
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import I18nManager from '../../I18nManager';
import i18nStyle from '../i18nStyle';

const style = {
const styleLeftRight = {
borderLeftColor: 'red',
borderRightColor: 'blue',
borderTopLeftRadius: 10,
Expand All @@ -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(() => {
Expand All @@ -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);
});
});

Expand All @@ -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);
});
});
});
151 changes: 106 additions & 45 deletions packages/react-native-web/src/exports/StyleSheet/i18nStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

// 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_SWAP_LEFT_RIGHT = {
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;
}
}

Expand Down
11 changes: 10 additions & 1 deletion packages/react-native-web/src/exports/Text/TextStylePropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit b754776

Please sign in to comment.