From b75477637385e46037014cbec5b364f085821c3b Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sun, 11 Feb 2018 10:27:45 -0800 Subject: [PATCH] [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"` --- .../StyleSheet/StyleSheetValidation.js | 2 +- .../__snapshots__/i18nStyle-test.js.snap | 56 ++++++- .../StyleSheet/__tests__/i18nStyle-test.js | 73 ++++++++- .../src/exports/StyleSheet/i18nStyle.js | 151 ++++++++++++------ .../src/exports/Text/TextStylePropTypes.js | 11 +- .../src/modules/LayoutPropTypes/index.js | 8 + .../storybook/1-components/Text/TextScreen.js | 2 +- .../storybook/1-components/View/ViewScreen.js | 100 +++++++++--- 8 files changed, 330 insertions(+), 73 deletions(-) diff --git a/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js b/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js index d2466f9f..4896a5a2 100644 --- a/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js +++ b/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js @@ -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, diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap index 43eda988..4af7401a 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap @@ -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, diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js b/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js index d49e4316..19e0659a 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js @@ -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); }); }); }); diff --git a/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js b/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js index e7e5df45..6006311c 100644 --- a/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js +++ b/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js @@ -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; } } diff --git a/packages/react-native-web/src/exports/Text/TextStylePropTypes.js b/packages/react-native-web/src/exports/Text/TextStylePropTypes.js index 5caa4ef8..f56c4278 100644 --- a/packages/react-native-web/src/exports/Text/TextStylePropTypes.js +++ b/packages/react-native-web/src/exports/Text/TextStylePropTypes.js @@ -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 = { diff --git a/packages/react-native-web/src/modules/LayoutPropTypes/index.js b/packages/react-native-web/src/modules/LayoutPropTypes/index.js index a687458d..3a256524 100644 --- a/packages/react-native-web/src/modules/LayoutPropTypes/index.js +++ b/packages/react-native-web/src/modules/LayoutPropTypes/index.js @@ -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, diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index f507f115..1a32f2f1 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -206,7 +206,7 @@ const stylePropTypes = [ }, { name: 'textAlign', - typeInfo: 'string' + typeInfo: 'enum("center", "end", "inherit", "justify", "justify-all", "left", "right", "start")' }, { name: 'textAlignVertical', diff --git a/website/storybook/1-components/View/ViewScreen.js b/website/storybook/1-components/View/ViewScreen.js index 0308e645..8c242fa7 100644 --- a/website/storybook/1-components/View/ViewScreen.js +++ b/website/storybook/1-components/View/ViewScreen.js @@ -403,32 +403,36 @@ const stylePropTypes = [ name: 'borderColor', typeInfo: 'color' }, - { - name: 'borderTopColor', - typeInfo: 'color' - }, { name: 'borderBottomColor', typeInfo: 'color' }, { - name: 'borderRightColor', + name: 'borderEndColor', typeInfo: 'color' }, { name: 'borderLeftColor', typeInfo: 'color' }, + { + name: 'borderRightColor', + typeInfo: 'color' + }, + { + name: 'borderStartColor', + typeInfo: 'color' + }, + { + name: 'borderTopColor', + typeInfo: 'color' + }, { name: 'borderRadius', typeInfo: 'number | string' }, { - name: 'borderTopLeftRadius', - typeInfo: 'number | string' - }, - { - name: 'borderTopRightRadius', + name: 'borderBottomEndRadius', typeInfo: 'number | string' }, { @@ -439,26 +443,54 @@ const stylePropTypes = [ name: 'borderBottomRightRadius', 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', typeInfo: 'string' }, - { - name: 'borderTopStyle', - typeInfo: 'string' - }, - { - name: 'borderRightStyle', - typeInfo: 'string' - }, { name: 'borderBottomStyle', typeInfo: 'string' }, + { + name: 'borderEndStyle', + typeInfo: 'string' + }, { name: 'borderLeftStyle', typeInfo: 'string' }, + { + name: 'borderRightStyle', + typeInfo: 'string' + }, + { + name: 'borderStartStyle', + typeInfo: 'string' + }, + { + name: 'borderTopStyle', + typeInfo: 'string' + }, { name: 'borderWidth', typeInfo: 'number | string' @@ -467,6 +499,10 @@ const stylePropTypes = [ name: 'borderBottomWidth', typeInfo: 'number | string' }, + { + name: 'borderEndWidth', + typeInfo: 'number | string' + }, { name: 'borderLeftWidth', typeInfo: 'number | string' @@ -475,6 +511,10 @@ const stylePropTypes = [ name: 'borderRightWidth', typeInfo: 'number | string' }, + { + name: 'borderStartWidth', + typeInfo: 'number | string' + }, { name: 'borderTopWidth', typeInfo: 'number | string' @@ -511,6 +551,10 @@ const stylePropTypes = [ name: 'display', typeInfo: 'string' }, + { + name: 'end', + typeInfo: 'number | string' + }, { label: 'web', name: 'filter', @@ -620,6 +664,10 @@ const stylePropTypes = [ name: 'marginBottom', typeInfo: 'number | string' }, + { + name: 'marginEnd', + typeInfo: 'number | string' + }, { name: 'marginHorizontal', typeInfo: 'number | string' @@ -632,6 +680,10 @@ const stylePropTypes = [ name: 'marginRight', typeInfo: 'number | string' }, + { + name: 'marginStart', + typeInfo: 'number | string' + }, { name: 'marginTop', typeInfo: 'number | string' @@ -711,6 +763,10 @@ const stylePropTypes = [ name: 'paddingBottom', typeInfo: 'number | string' }, + { + name: 'paddingEnd', + typeInfo: 'number | string' + }, { name: 'paddingHorizontal', typeInfo: 'number | string' @@ -723,6 +779,10 @@ const stylePropTypes = [ name: 'paddingRight', typeInfo: 'number | string' }, + { + name: 'paddingStart', + typeInfo: 'number | string' + }, { name: 'paddingTop', typeInfo: 'number | string' @@ -765,6 +825,10 @@ const stylePropTypes = [ name: 'shadowRadius', typeInfo: 'number | string' }, + { + name: 'start', + typeInfo: 'number | string' + }, { label: 'web', name: 'touchAction',