[add] support for RTL layout

Add `I18nManager` API from React Native. This can be used to control
when the app displays in RTL mode.

Add `$noI18n` property suffix for properties that StyleSheet will
automatically flip. This can be used to opt-out of automatic flipping on
a per-declaration basis.
This commit is contained in:
Nicolas Gallagher
2016-07-27 15:06:41 -07:00
parent f1e221e51e
commit 6416166bc3
18 changed files with 432 additions and 47 deletions
+1
View File
@@ -115,6 +115,7 @@ Exported modules:
* [`AppState`](docs/apis/AppState.md)
* [`AsyncStorage`](docs/apis/AsyncStorage.md)
* [`Dimensions`](docs/apis/Dimensions.md)
* [`I18nManager`](docs/apis/I18nManager.md)
* [`NativeMethods`](docs/apis/NativeMethods.md)
* [`NetInfo`](docs/apis/NetInfo.md)
* [`PanResponder`](http://facebook.github.io/react-native/releases/0.20/docs/panresponder.html#content) (mirrors React Native)
+27
View File
@@ -0,0 +1,27 @@
# I18nManager
Control and set the layout and writing direction of the application. You must
set `dir="rtl"` (and should set `lang="${lang}"`) on the root element of your
app.
## Properties
**isRTL**: bool = false
Whether the application is currently in RTL mode.
## Methods
static **allowRTL**(allowRTL: bool)
Allow the application to display in RTL mode.
static **forceRTL**(allowRTL: bool)
Force the application to display in RTL mode.
static **setRTL**(allowRTL: bool)
Set the application to display in RTL mode. You will need to determine the
user's preferred locale and if it is an RTL language. (This is best done on the
server as it is notoriously inaccurate to deduce client-side.)
+1 -1
View File
@@ -29,7 +29,7 @@ static **removeEventListener**(eventName: ChangeEventName, handler: Function)
## Properties
**isConnected**
**isConnected**: bool = true
Available on all user agents. Asynchronously fetch a boolean to determine
internet connectivity.
+11 -3
View File
@@ -68,14 +68,22 @@ Lets the user select the text.
+ `fontWeight`
+ `letterSpacing`
+ `lineHeight`
+ `textAlign`
+ `textAlign`
+ `textAlignVertical`
+ `textDecorationLine`
+ `textShadow`
+ `textOverflow`
+ `textRendering`
+ `textShadowColor`
+ `textShadowOffset`
+ `textShadowRadius`
+ `textTransform`
+ `unicodeBidi`
+ `whiteSpace`
+ `wordWrap`
+ `writingDirection`
+ `writingDirection`
‡ This property can be suffixed with `$noI18n` to prevent automatic
bidi-flipping in RTL mode. This is only supported if `Platform.OS === 'web'`.
**testID**: string
+29 -10
View File
@@ -108,10 +108,26 @@ from `style`.
+ `backgroundPosition`
+ `backgroundRepeat`
+ `backgroundSize`
+ `borderColor`
+ `borderRadius`
+ `borderStyle`
+ `borderWidth`
+ `borderColor` (single value)
+ `borderTopColor`
+ `borderBottomColor`
+ `borderRightColor`
+ `borderLeftColor`
+ `borderRadius` (single value)
+ `borderTopLeftRadius`
+ `borderTopRightRadius`
+ `borderBottomLeftRadius`
+ `borderBottomRightRadius`
+ `borderStyle` (single value)
+ `borderTopStyle`
+ `borderRightStyle`
+ `borderBottomStyle`
+ `borderLeftStyle`
+ `borderWidth` (single value)
+ `borderBottomWidth`
+ `borderLeftWidth`
+ `borderRightWidth`
+ `borderTopWidth`
+ `bottom`
+ `boxShadow`
+ `boxSizing`
@@ -124,12 +140,12 @@ from `style`.
+ `flexWrap`
+ `height`
+ `justifyContent`
+ `left`
+ `left`
+ `margin` (single value)
+ `marginBottom`
+ `marginHorizontal`
+ `marginLeft`
+ `marginRight`
+ `marginLeft`
+ `marginRight`
+ `marginTop`
+ `marginVertical`
+ `maxHeight`
@@ -144,12 +160,12 @@ from `style`.
+ `padding` (single value)
+ `paddingBottom`
+ `paddingHorizontal`
+ `paddingLeft`
+ `paddingRight`
+ `paddingLeft`
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `position`
+ `right`
+ `right`
+ `top`
+ `transform`
+ `transformMatrix`
@@ -158,6 +174,9 @@ from `style`.
+ `width`
+ `zIndex`
‡ This property can be suffixed with `$noI18n` to prevent automatic
bidi-flipping in RTL mode. This is only supported if `Platform.OS === 'web'`.
Default:
```js
+1 -1
View File
@@ -271,7 +271,7 @@ const examples = [
<Text>
auto (default) - english LTR
</Text>
<Text style={{ writingDirection: 'rtl' }}>
<Text style={{ writingDirection$noI18n: 'rtl' }}>
أحب اللغة العربية auto (default) - arabic RTL
</Text>
<Text style={{textAlign: 'left'}}>
@@ -0,0 +1,38 @@
/* eslint-env mocha */
import assert from 'assert'
import I18nManager from '..'
suite('apis/I18nManager', () => {
suite('when RTL not enabled', () => {
setup(() => {
I18nManager.setRTL(false)
})
test('is "false" by default', () => {
assert.equal(I18nManager.isRTL, false)
})
test('is "true" when forced', () => {
I18nManager.forceRTL(true)
assert.equal(I18nManager.isRTL, true)
I18nManager.forceRTL(false)
})
})
suite('when RTL is enabled', () => {
setup(() => {
I18nManager.setRTL(true)
})
test('is "true" by default', () => {
assert.equal(I18nManager.isRTL, true)
})
test('is "false" when not allowed', () => {
I18nManager.allowRTL(false)
assert.equal(I18nManager.isRTL, false)
I18nManager.allowRTL(true)
})
})
})
+33
View File
@@ -0,0 +1,33 @@
type I18nManagerStatus = {
allowRTL: (allowRTL: boolean) => {},
forceRTL: (forceRTL: boolean) => {},
setRTL: (setRTL: boolean) => {},
isRTL: boolean
}
let isApplicationLanguageRTL = false
let isRTLAllowed = true
let isRTLForced = false
const I18nManager: I18nManagerStatus = {
allowRTL(bool) {
isRTLAllowed = bool
},
forceRTL(bool) {
isRTLForced = bool
},
setRTL(bool) {
isApplicationLanguageRTL = bool
},
get isRTL() {
if (isRTLForced) {
return true
}
if (isRTLAllowed && isApplicationLanguageRTL) {
return true
}
return false
}
}
module.exports = I18nManager
@@ -61,7 +61,6 @@ StyleSheetValidation.addValidStylePropTypes({
clear: PropTypes.string,
cursor: PropTypes.string,
display: PropTypes.string,
direction: PropTypes.string, /* @private */
float: PropTypes.oneOf([ 'left', 'none', 'right' ]),
font: PropTypes.string, /* @private */
listStyle: PropTypes.string
@@ -0,0 +1,91 @@
/* eslint-env mocha */
import assert from 'assert'
import I18nManager from '../../I18nManager'
import i18nStyle from '../i18nStyle'
const initial = {
borderLeftColor: 'red',
borderRightColor: 'blue',
borderTopLeftRadius: 10,
borderTopRightRadius: '1rem',
borderBottomLeftRadius: 20,
borderBottomRightRadius: '2rem',
borderLeftStyle: 'solid',
borderRightStyle: 'dotted',
borderLeftWidth: 5,
borderRightWidth: 6,
left: 1,
marginLeft: 7,
marginRight: 8,
paddingLeft: 9,
paddingRight: 10,
right: 2,
textAlign: 'left',
textShadowOffset: { width: '1rem', height: 10 },
writingDirection: 'ltr'
}
const initialNoI18n = Object.keys(initial).reduce((acc, prop) => {
const newProp = `${prop}$noI18n`
acc[newProp] = initial[prop]
return acc
}, {})
const expected = {
borderLeftColor: 'blue',
borderRightColor: 'red',
borderTopLeftRadius: '1rem',
borderTopRightRadius: 10,
borderBottomLeftRadius: '2rem',
borderBottomRightRadius: 20,
borderLeftStyle: 'dotted',
borderRightStyle: 'solid',
borderLeftWidth: 6,
borderRightWidth: 5,
left: 2,
marginLeft: 8,
marginRight: 7,
paddingLeft: 10,
paddingRight: 9,
right: 1,
textAlign: 'right',
textShadowOffset: { width: '-1rem', height: 10 },
writingDirection: 'rtl'
}
suite('apis/StyleSheet/i18nStyle', () => {
suite('LTR mode', () => {
setup(() => {
I18nManager.allowRTL(false)
})
teardown(() => {
I18nManager.allowRTL(true)
})
test('does not auto-flip', () => {
assert.deepEqual(i18nStyle(initial), initial)
})
test('normalizes properties', () => {
assert.deepEqual(i18nStyle(initialNoI18n), initial)
})
})
suite('RTL mode', () => {
setup(() => {
I18nManager.forceRTL(true)
})
teardown(() => {
I18nManager.forceRTL(false)
})
test('does auto-flip', () => {
assert.deepEqual(i18nStyle(initial), expected)
})
test('normalizes properties', () => {
assert.deepEqual(i18nStyle(initialNoI18n), initial)
})
})
})
@@ -1,5 +1,6 @@
import expandStyle from './expandStyle'
import flattenStyle from '../../modules/flattenStyle'
import i18nStyle from './i18nStyle'
import processTextShadow from './processTextShadow'
import processTransform from './processTransform'
import processVendorPrefixes from './processVendorPrefixes'
@@ -10,8 +11,10 @@ const plugins = [
processVendorPrefixes
]
const applyPlugins = (style) => plugins.reduce((style, plugin) => plugin(style), style)
const applyPlugins = (style) => {
return plugins.reduce((style, plugin) => plugin(style), style)
}
const createReactDOMStyleObject = (reactNativeStyle) => applyPlugins(expandStyle(flattenStyle(reactNativeStyle)))
const createReactDOMStyleObject = (reactNativeStyle) => applyPlugins(expandStyle(i18nStyle(flattenStyle(reactNativeStyle))))
module.exports = createReactDOMStyleObject
+124
View File
@@ -0,0 +1,124 @@
import I18nManager from '../I18nManager'
const CSS_UNIT_RE = /^[+-]?\d*(?:\.\d+)?(?:[Ee][+-]?\d+)?(\w*)/
/**
* 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 PROPERTIES_SWAP_LEFT_RIGHT = {
'clear': true,
'float': true,
'textAlign': true
}
const PROPERTIES_SWAP_LTR_RTL = {
'writingDirection': true
}
/**
* Invert the sign of a numeric-like value
*/
const additiveInverse = (value: String | Number) => {
if (typeof value === 'string') {
const number = parseFloat(value, 10) * -1
const unit = getUnit(value)
return `${number}${unit}`
} else if (isNumeric(value)) {
return value * -1
}
}
/**
* BiDi flip the given property.
*/
const flipProperty = (prop:String): String => {
return PROPERTIES_TO_SWAP.hasOwnProperty(prop) ? PROPERTIES_TO_SWAP[prop] : prop
}
/**
* BiDi flip translateX
*/
const flipTransform = (transform: Object): Object => {
const translateX = transform.translateX
if (translateX != null) {
transform.translateX = additiveInverse(translateX)
}
return transform
}
/**
* Get the CSS unit for string values
*/
const getUnit = (str) => str.match(CSS_UNIT_RE)[1]
const isNumeric = (n) => {
return !isNaN(parseFloat(n)) && isFinite(n)
}
const swapLeftRight = (value:String): String => {
return value === 'left' ? 'right' : value === 'right' ? 'left' : value
}
const swapLtrRtl = (value:String): String => {
return value === 'ltr' ? 'rtl' : value === 'rtl' ? 'ltr' : value
}
const i18nStyle = (style = {}) => {
const newStyle = {}
for (const prop in style) {
if (style.hasOwnProperty(prop)) {
const indexOfNoFlip = prop.indexOf('$noI18n')
if (I18nManager.isRTL) {
if (PROPERTIES_TO_SWAP[prop]) {
const newProp = flipProperty(prop)
newStyle[newProp] = style[prop]
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) {
newStyle[prop] = swapLeftRight(style[prop])
} else if (PROPERTIES_SWAP_LTR_RTL[prop]) {
newStyle[prop] = swapLtrRtl(style[prop])
} else if (prop === 'textShadowOffset') {
newStyle[prop] = style[prop]
newStyle[prop].width = additiveInverse(style[prop].width)
} else if (prop === 'transform') {
newStyle[prop] = style[prop].map(flipTransform)
} else if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip)
newStyle[newProp] = style[prop]
} else {
newStyle[prop] = style[prop]
}
} else {
if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip)
newStyle[newProp] = style[prop]
} else {
newStyle[prop] = style[prop]
}
}
}
}
return newStyle
}
module.exports = i18nStyle
+2 -27
View File
@@ -1,32 +1,7 @@
import { PropTypes } from 'react'
import ColorPropType from '../../propTypes/ColorPropType'
import TextPropTypes from '../../propTypes/TextPropTypes'
import ViewStylePropTypes from '../View/ViewStylePropTypes'
const { number, oneOf, oneOfType, shape, string } = PropTypes
const numberOrString = oneOfType([ number, string ])
module.exports = {
...ViewStylePropTypes,
color: ColorPropType,
fontFamily: string,
fontSize: numberOrString,
fontStyle: string,
fontWeight: string,
letterSpacing: numberOrString,
lineHeight: numberOrString,
textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]),
textAlignVertical: oneOf([ 'auto', 'bottom', 'center', 'top' ]),
textDecorationLine: string,
/* @platform web */
textOverflow: string,
textShadowColor: ColorPropType,
textShadowOffset: shape({ width: number, height: number }),
textShadowRadius: number,
/* @platform web */
textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]),
/* @platform web */
whiteSpace: string,
/* @platform web */
wordWrap: string,
writingDirection: oneOf([ 'auto', 'ltr', 'rtl' ])
...TextPropTypes
}
+2
View File
@@ -3,6 +3,7 @@ import './modules/injectResponderEventPlugin'
import findNodeHandle from './modules/findNodeHandle'
import ReactDOM from 'react-dom'
import ReactDOMServer from 'react-dom/server'
import I18nManager from './apis/I18nManager'
import StyleSheet from './apis/StyleSheet'
import Image from './components/Image'
import Text from './components/Text'
@@ -15,6 +16,7 @@ const ReactNativeCore = {
renderToStaticMarkup: ReactDOMServer.renderToStaticMarkup,
renderToString: ReactDOMServer.renderToString,
unmountComponentAtNode: ReactDOM.unmountComponentAtNode,
I18nManager,
StyleSheet,
Image,
Text,
+2
View File
@@ -11,6 +11,7 @@ import AppState from './apis/AppState'
import AsyncStorage from './apis/AsyncStorage'
import Dimensions from './apis/Dimensions'
import Easing from 'animated/lib/Easing'
import I18nManager from './apis/I18nManager'
import InteractionManager from './apis/InteractionManager'
import NetInfo from './apis/NetInfo'
import PanResponder from './apis/PanResponder'
@@ -59,6 +60,7 @@ const ReactNative = {
AsyncStorage,
Dimensions,
Easing,
I18nManager,
InteractionManager,
NetInfo,
PanResponder,
+10 -1
View File
@@ -19,7 +19,16 @@ const BorderPropTypes = {
borderTopStyle: BorderStylePropType,
borderRightStyle: BorderStylePropType,
borderBottomStyle: BorderStylePropType,
borderLeftStyle: BorderStylePropType
borderLeftStyle: BorderStylePropType,
/* Props to opt-out of RTL flipping */
borderLeftColor$noI18n: ColorPropType,
borderRightColor$noI18n: ColorPropType,
borderTopLeftRadius$noI18n: numberOrString,
borderTopRightRadius$noI18n: numberOrString,
borderBottomLeftRadius$noI18n: numberOrString,
borderBottomRightRadius$noI18n: numberOrString,
borderLeftStyle$noI18n: BorderStylePropType,
borderRightStyle$noI18n: BorderStylePropType
}
module.exports = BorderPropTypes
+10 -1
View File
@@ -48,7 +48,16 @@ const LayoutPropTypes = {
left: numberOrString,
position: oneOf([ 'absolute', 'fixed', 'relative', 'static' ]),
right: numberOrString,
top: numberOrString
top: numberOrString,
// opt-out of RTL flipping
borderLeftWidth$noI18n: numberOrString,
borderRightWidth$noI18n: numberOrString,
left$noI18n: numberOrString,
marginLeft$noI18n: numberOrString,
marginRight$noI18n: numberOrString,
paddingLeft$noI18n: numberOrString,
paddingRight$noI18n: numberOrString,
right$noI18n: numberOrString
}
module.exports = LayoutPropTypes
+45
View File
@@ -0,0 +1,45 @@
import ColorPropType from './ColorPropType'
import { PropTypes } from 'react'
const { number, oneOf, oneOfType, shape, string } = PropTypes
const numberOrString = oneOfType([ number, string ])
const ShadowOffsetPropType = shape({ width: number, height: number })
const TextAlignPropType = oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ])
const WritingDirectionPropType = oneOf([ 'auto', 'ltr', 'rtl' ])
const TextPropTypes = {
// box model
color: ColorPropType,
fontFamily: string,
fontSize: numberOrString,
fontStyle: string,
fontWeight: string,
letterSpacing: numberOrString,
lineHeight: numberOrString,
textAlign: TextAlignPropType,
textAlignVertical: oneOf([ 'auto', 'bottom', 'center', 'top' ]),
textDecorationLine: string,
/* @platform web */
textOverflow: string,
/* @platform web */
textRendering: oneOf([ 'auto', 'geometricPrecision', 'optimizeLegibility', 'optimizeSpeed' ]),
textShadowColor: ColorPropType,
textShadowOffset: ShadowOffsetPropType,
textShadowRadius: number,
/* @platform web */
textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]),
/* @platform web */
unicodeBidi: oneOf([ 'normal', 'bidi-override', 'embed', 'isolate', 'isolate-override', 'plaintext' ]),
/* @platform web */
whiteSpace: string,
/* @platform web */
wordWrap: string,
writingDirection: WritingDirectionPropType,
// opt-out of RTL flipping
textAlign$noI18n: TextAlignPropType,
textShadowOffset$noI18n: ShadowOffsetPropType,
writingDirection$noI18n: WritingDirectionPropType
}
module.exports = TextPropTypes