[fix] i18n styles

1. Fix auto-flipping of styles

The StyleRegistry didn't account for LTR/RTL when caching the results of
style resolution. The 'writingDirection' style is no longer flipped; no
clear use case for it.

2. Remove experimental '$noI18n' style prop suffix

This feature is essentially unused, and less likely to be used with the
introduction of 'dir=auto' on 'Text'. Removing also marginally improves
render performance.
This commit is contained in:
Nicolas Gallagher
2017-02-17 12:11:50 -08:00
parent 6d2ae4597e
commit 10ef2bfe20
13 changed files with 85 additions and 206 deletions

View File

@@ -68,22 +68,19 @@ Lets the user select the text.
+ `fontWeight`
+ `letterSpacing`
+ `lineHeight`
+ `textAlign`
+ `textAlign`
+ `textAlignVertical`
+ `textDecorationLine`
+ `textOverflow`
+ `textRendering`
+ `textShadowColor`
+ `textShadowOffset`
+ `textShadowOffset`
+ `textShadowRadius`
+ `textTransform`
+ `unicodeBidi`
+ `whiteSpace`
+ `wordWrap`
+ `writingDirection`
‡ This property can be suffixed with `$noI18n` to prevent automatic
bidi-flipping in RTL mode. This is only supported if `Platform.OS === 'web'`.
+ `writingDirection`
**testID**: string

View File

@@ -119,22 +119,22 @@ from `style`.
+ `borderColor` (single value)
+ `borderTopColor`
+ `borderBottomColor`
+ `borderRightColor`
+ `borderLeftColor`
+ `borderRightColor`
+ `borderLeftColor`
+ `borderRadius` (single value)
+ `borderTopLeftRadius`
+ `borderTopRightRadius`
+ `borderBottomLeftRadius`
+ `borderBottomRightRadius`
+ `borderTopLeftRadius`
+ `borderTopRightRadius`
+ `borderBottomLeftRadius`
+ `borderBottomRightRadius`
+ `borderStyle` (single value)
+ `borderTopStyle`
+ `borderRightStyle`
+ `borderRightStyle`
+ `borderBottomStyle`
+ `borderLeftStyle`
+ `borderLeftStyle`
+ `borderWidth` (single value)
+ `borderBottomWidth`
+ `borderLeftWidth`
+ `borderRightWidth`
+ `borderLeftWidth`
+ `borderRightWidth`
+ `borderTopWidth`
+ `bottom`
+ `boxShadow`
@@ -148,12 +148,12 @@ from `style`.
+ `flexWrap`
+ `height`
+ `justifyContent`
+ `left`
+ `left`
+ `margin` (single value)
+ `marginBottom`
+ `marginHorizontal`
+ `marginLeft`
+ `marginRight`
+ `marginLeft`
+ `marginRight`
+ `marginTop`
+ `marginVertical`
+ `maxHeight`
@@ -168,14 +168,14 @@ from `style`.
+ `padding` (single value)
+ `paddingBottom`
+ `paddingHorizontal`
+ `paddingLeft`
+ `paddingRight`
+ `paddingLeft`
+ `paddingRight`
+ `paddingTop`
+ `paddingVertical`
+ `perspective`
+ `perspectiveOrigin`
+ `position`
+ `right`
+ `right`
+ `top`
+ `transform`
+ `transformOrigin`
@@ -188,9 +188,6 @@ 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

View File

@@ -4,11 +4,6 @@ To support right-to-left languages, application layout can be automatically
flipped from LTR to RTL. The `I18nManager` API can be used to help with more
fine-grained control and testing of RTL layouts.
React Native for Web provides an experimental feature to support "true left"
and "true right" styles. For example, `left` will be flipped to `right` in RTL
mode, but `left$noI18n` will not. More information is available in the `Text`
and `View` documentation.
## Working with icons and images
Icons and images that must match the LTR or RTL layout of the app need to be manually flipped.

View File

@@ -14,14 +14,22 @@ class I18nManagerExample extends Component {
LTR/RTL layout example!
</Text>
<Text style={styles.text}>
This is sample text. The writing direction can be changed by pressing the button below.
</Text>
<Text style={[ styles.text, styles.ltrText ]}>
This is text that will always display LTR.
The writing direction of text is automatically determined by the browser, independent of the global writing direction of the app.
</Text>
<Text style={[ styles.text, styles.rtlText ]}>
This is text that will always display RTL.
أحب اللغة العربية
</Text>
<Text style={[ styles.text, styles.textAlign ]}>
textAlign toggles
</Text>
<View style={styles.horizontal}>
<View style={[ styles.box, { backgroundColor: 'lightblue' } ]}>
<Text>One</Text>
</View>
<View style={[ styles.box ]}>
<Text>Two</Text>
</View>
</View>
<TouchableHighlight
onPress={this._handleToggle}
style={styles.toggle}
@@ -34,8 +42,8 @@ class I18nManagerExample extends Component {
}
_handleToggle = () => {
this._isRTL = !this._isRTL
I18nManager.setPreferredLanguageRTL(this._isRTL)
I18nManager.setPreferredLanguageRTL(!I18nManager.isRTL)
this.forceUpdate();
}
}
@@ -55,13 +63,16 @@ const styles = StyleSheet.create({
fontSize: 18,
marginBottom: 5
},
ltrText: {
textAlign$noI18n: 'left',
writingDirection$noI18n: 'ltr'
textAlign: {
textAlign: 'left'
},
rtlText: {
textAlign$noI18n: 'right',
writingDirection$noI18n: 'rtl'
horizontal: {
flexDirection: 'row',
marginVertical: 10
},
box: {
borderWidth: 1,
flex: 1
},
toggle: {
alignSelf: 'center',

View File

@@ -271,7 +271,7 @@ const examples = [
<Text>
auto (default) - english LTR
</Text>
<Text style={{ writingDirection$noI18n: 'rtl' }}>
<Text>
أحب اللغة العربية auto (default) - arabic RTL
</Text>
<Text style={{textAlign: 'left'}}>

View File

@@ -25,33 +25,6 @@ Object {
}
`;
exports[`apis/StyleSheet/i18nStyle LTR mode normalizes properties 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",
},
"writingDirection": "ltr",
}
`;
exports[`apis/StyleSheet/i18nStyle RTL mode does auto-flip 1`] = `
Object {
"borderBottomLeftRadius": "2rem",
@@ -78,30 +51,3 @@ Object {
"writingDirection": "rtl",
}
`;
exports[`apis/StyleSheet/i18nStyle RTL mode normalizes properties 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",
},
"writingDirection": "ltr",
}
`;

View File

@@ -21,16 +21,9 @@ const style = {
paddingRight: 10,
right: 2,
textAlign: 'left',
textShadowOffset: { width: '1rem', height: 10 },
writingDirection: 'ltr'
textShadowOffset: { width: '1rem', height: 10 }
};
const styleNoI18n = Object.keys(style).reduce((acc, prop) => {
const newProp = `${prop}$noI18n`;
acc[newProp] = style[prop];
return acc;
}, {});
describe('apis/StyleSheet/i18nStyle', () => {
describe('LTR mode', () => {
beforeEach(() => {
@@ -44,9 +37,6 @@ describe('apis/StyleSheet/i18nStyle', () => {
test('does not auto-flip', () => {
expect(i18nStyle(style)).toMatchSnapshot();
});
test('normalizes properties', () => {
expect(i18nStyle(styleNoI18n)).toMatchSnapshot();
});
});
describe('RTL mode', () => {
@@ -61,8 +51,5 @@ describe('apis/StyleSheet/i18nStyle', () => {
test('does auto-flip', () => {
expect(i18nStyle(style)).toMatchSnapshot();
});
test('normalizes properties', () => {
expect(i18nStyle(styleNoI18n)).toMatchSnapshot();
});
});
});

View File

@@ -37,14 +37,11 @@ const alphaSortProps = (propsArray) => propsArray.sort((a, b) => {
return 0;
});
const expandStyle = (style) => {
if (!style) { return emptyObject; }
const styleProps = Object.keys(style);
const sortedStyleProps = alphaSortProps(styleProps);
const createReducer = (style, styleProps) => {
let hasResolvedBoxShadow = false;
let hasResolvedTextShadow = false;
const reducer = (resolvedStyle, prop) => {
return (resolvedStyle, prop) => {
const value = normalizeValue(prop, style[prop]);
if (value == null) { return resolvedStyle; }
@@ -105,7 +102,13 @@ const expandStyle = (style) => {
return resolvedStyle;
};
};
const expandStyle = (style) => {
if (!style) { return emptyObject; }
const styleProps = Object.keys(style);
const sortedStyleProps = alphaSortProps(styleProps);
const reducer = createReducer(style, styleProps);
const resolvedStyle = sortedStyleProps.reduce(reducer, {});
return resolvedStyle;
};

View File

@@ -31,10 +31,6 @@ const PROPERTIES_SWAP_LEFT_RIGHT = {
'textAlign': true
};
const PROPERTIES_SWAP_LTR_RTL = {
'writingDirection': true
};
/**
* Invert the sign of a numeric-like value
*/
@@ -43,7 +39,7 @@ const additiveInverse = (value: String | Number) => multiplyStyleLengthValue(val
/**
* BiDi flip the given property.
*/
const flipProperty = (prop:String): String => {
const flipProperty = (prop: String): String => {
return PROPERTIES_TO_SWAP.hasOwnProperty(prop) ? PROPERTIES_TO_SWAP[prop] : prop;
};
@@ -62,49 +58,35 @@ 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 = (originalStyle) => {
if (!I18nManager.isRTL) {
return originalStyle;
}
const style = originalStyle || emptyObject;
const nextStyle = {};
const i18nStyle = (style = emptyObject) => {
const newStyle = {};
for (const prop in style) {
if (!Object.prototype.hasOwnProperty.call(style, prop)) {
continue;
}
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];
}
if (PROPERTIES_TO_SWAP[prop]) {
const newProp = flipProperty(prop);
nextStyle[newProp] = style[prop];
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) {
nextStyle[prop] = swapLeftRight(style[prop]);
} else if (prop === 'textShadowOffset') {
nextStyle[prop] = style[prop];
nextStyle[prop].width = additiveInverse(style[prop].width);
} else if (prop === 'transform') {
nextStyle[prop] = style[prop].map(flipTransform);
} else {
if (indexOfNoFlip > -1) {
const newProp = prop.substring(0, indexOfNoFlip);
newStyle[newProp] = style[prop];
} else {
newStyle[prop] = style[prop];
}
nextStyle[prop] = style[prop];
}
}
return newStyle;
return nextStyle;
};
module.exports = i18nStyle;

View File

@@ -7,15 +7,20 @@ import createReactDOMStyle from './createReactDOMStyle';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import generateCss from './generateCss';
import I18nManager from '../I18nManager';
import injector from './injector';
import mapKeyValue from '../../modules/mapKeyValue';
import prefixInlineStyles from './prefixInlineStyles';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
const prefix = 'r-';
const SPACE_REGEXP = /\s/g;
const ESCAPE_SELECTOR_CHARS_REGEXP = /[(),":?.%\\$#*]/g;
const createCacheKey = (id) => {
const prefix = I18nManager.isRTL ? 'rtl' : 'ltr';
return `${prefix}-${id}`;
};
/**
* Creates an HTML class name for use on elements
*/
@@ -64,7 +69,7 @@ const registerStyle = (id, flatStyle) => {
}
});
const key = `${prefix}${id}`;
const key = createCacheKey(id);
resolvedPropsCache[key] = { className };
return id;
@@ -166,7 +171,7 @@ const StyleRegistry = {
// fast and cachable
if (typeof reactNativeStyle === 'number') {
const key = `${prefix}${reactNativeStyle}`;
const key = createCacheKey(reactNativeStyle);
return resolvePropsIfNeeded(key, reactNativeStyle);
}
@@ -187,30 +192,8 @@ const StyleRegistry = {
}
}
// TODO: determine when/if to cache unregistered styles. This produces 2x
// faster benchmark results for unregistered styles. However, the cache
// could be filled with props that are never used again.
//
// let hasValidKey = true;
// let key = flatArray.reduce((keyParts, element) => {
// if (typeof element === 'number') {
// keyParts.push(element);
// } else {
// if (element.transform) {
// hasValidKey = false;
// } else {
// const objectAsKey = Object.keys(element).map((prop) => `${prop}:${element[prop]}`).join(';');
// if (objectAsKey !== '') {
// keyParts.push(objectAsKey);
// }
// }
// }
// return keyParts;
// }, [ prefix ]).join('-');
// if (!hasValidKey) { key = null; }
// cache resolved props when all styles are registered
const key = isArrayOfNumbers ? `${prefix}${flatArray.join('-')}` : null;
const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
return resolvePropsIfNeeded(key, flatArray);
}

View File

@@ -32,11 +32,7 @@ const TextOnlyStylePropTypes = {
whiteSpace: string,
wordWrap: string,
MozOsxFontSmoothing: string,
WebkitFontSmoothing: string,
// opt-out of RTL flipping
textAlign$noI18n: TextAlignPropType,
textShadowOffset$noI18n: ShadowOffsetPropType,
writingDirection$noI18n: WritingDirectionPropType
WebkitFontSmoothing: string
};
module.exports = {

View File

@@ -19,16 +19,7 @@ const BorderPropTypes = {
borderTopStyle: BorderStylePropType,
borderRightStyle: BorderStylePropType,
borderBottomStyle: 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
borderLeftStyle: BorderStylePropType
};
module.exports = BorderPropTypes;

View File

@@ -48,16 +48,7 @@ const LayoutPropTypes = {
left: numberOrString,
position: oneOf([ 'absolute', 'fixed', 'relative', 'static' ]),
right: 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
top: numberOrString
};
module.exports = LayoutPropTypes;