Compare commits

...

12 Commits

Author SHA1 Message Date
Nicolas Gallagher
03769f7d45 0.0.93 2017-04-29 19:53:45 -07:00
Nicolas Gallagher
eb43a8f3e7 [fix] setNativeProps with RTL layout
Ensure that 'setNativeProps' doesn't try to i18n flip styles that have
already been flipped. This is hacked into the current design.
Registering both RTL and LTR styles is not implemented yet either.
2017-04-29 19:49:59 -07:00
Nicolas Gallagher
cdf13b880d Reorganize 'createReactDOMStyle'
1. Rename 'expandStyle' to 'createReactDOMStyle'
2. Move use of 'i18nStyle' out of 'createReactDOMStyle' to decouple the
   two transformations.
3. Move the style property resolvers into 'createReactDOMStyle'
2017-04-29 19:03:48 -07:00
Nicolas Gallagher
47fad1ef58 [fix] setNativeProps 2017-04-29 18:58:15 -07:00
Nicolas Gallagher
5f69c8e8b8 0.0.92 2017-04-29 12:19:16 -07:00
Nicolas Gallagher
21550db5f2 [fix] stop propagation of ScrollView 'onScroll' event
Fix #440
2017-04-29 12:15:19 -07:00
Nicolas Gallagher
1cae5d55a1 [fix] setNativeProps DOM style copying
The 'style' object of an HTML node is a 'CSSStyleDeclaration'. Use the
'CSSStyleDeclaration' API to copy the inline styles, rather than
treating it like a plain object. This avoids errors that were resulting
from indices and property names being used a key-value pairs in the
resulting style copy.

Fix #460
Ref #454
2017-04-29 11:09:43 -07:00
Nicolas Gallagher
11d23f850a 0.0.91 2017-04-28 15:40:12 -07:00
Nicolas Gallagher
d994a25017 0.0.90 2017-04-28 15:17:44 -07:00
Nicolas Gallagher
756df70154 [fix] check 'transform' style is array before mapping 2017-04-28 15:15:57 -07:00
Nicolas Gallagher
f0b06419f9 Move prefixStyles module 2017-04-27 16:27:45 -07:00
Nicolas Gallagher
60ff75705e [fix] remove stray 'length' property from style object
Fix #454
2017-04-27 15:10:03 -07:00
22 changed files with 459 additions and 498 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-web",
"version": "0.0.89",
"version": "0.0.93",
"description": "React Native for Web",
"main": "dist/index.js",
"files": [

View File

@@ -6,13 +6,12 @@ import createReactDOMStyle from './createReactDOMStyle';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import I18nManager from '../I18nManager';
import i18nStyle from './i18nStyle';
import mapKeyValue from '../../modules/mapKeyValue';
import prefixInlineStyles from './prefixInlineStyles';
import { prefixInlineStyles } from '../../modules/prefixStyles';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
import StyleManager from './StyleManager';
const vendorPrefixPattern = /^(webkit|moz|ms)/;
const createCacheKey = id => {
const prefix = I18nManager.isRTL ? 'rtl' : 'ltr';
return `${prefix}-${id}`;
@@ -36,7 +35,7 @@ class StyleRegistry {
register(flatStyle) {
const id = ReactNativePropRegistry.register(flatStyle);
const key = createCacheKey(id);
const style = createReactDOMStyle(flatStyle);
const style = createReactDOMStyle(i18nStyle(flatStyle));
const classList = mapKeyValue(style, (prop, value) => {
if (value != null) {
return this.styleManager.setDeclaration(prop, value);
@@ -50,7 +49,7 @@ class StyleRegistry {
/**
* Resolves a React Native style object to DOM attributes
*/
resolve(reactNativeStyle) {
resolve(reactNativeStyle, options) {
if (!reactNativeStyle) {
return undefined;
}
@@ -58,12 +57,12 @@ class StyleRegistry {
// fast and cachable
if (typeof reactNativeStyle === 'number') {
const key = createCacheKey(reactNativeStyle);
return this._resolveStyleIfNeeded(key, reactNativeStyle);
return this._resolveStyleIfNeeded(reactNativeStyle, { key, ...options });
}
// resolve a plain RN style object
if (!Array.isArray(reactNativeStyle)) {
return this._resolveStyle(reactNativeStyle);
return this._resolveStyle(reactNativeStyle, options);
}
// flatten the style array
@@ -78,7 +77,7 @@ class StyleRegistry {
}
}
const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
return this._resolveStyleIfNeeded(key, flatArray);
return this._resolveStyleIfNeeded(flatArray, { key, ...options });
}
/**
@@ -88,10 +87,12 @@ class StyleRegistry {
* To determine the next style, some of the existing DOM state must be
* converted back into React Native styles.
*/
resolveStateful(rnStyleNext, { classList: domClassList, style: domStyle }) {
resolveStateful(rnStyleNext, domStyleProps, options) {
const { classList: rdomClassList, style: rdomStyle } = domStyleProps;
// Convert the DOM classList back into a React Native form
// Preserves unrecognized class names.
const { classList: rnClassList, style: rnStyle } = domClassList.reduce(
const { classList: rnClassList, style: rnStyle } = rdomClassList.reduce(
(styleProps, className) => {
const { prop, value } = this.styleManager.getDeclaration(className);
if (prop) {
@@ -104,24 +105,12 @@ class StyleRegistry {
{ classList: [], style: {} }
);
// DOM style may include vendor prefixes and properties set by other libraries.
// Preserve it but transform back into React DOM style.
const rdomStyle = Object.keys(domStyle).reduce((acc, styleName) => {
const value = domStyle[styleName];
if (value !== '') {
const reactStyleName = vendorPrefixPattern.test(styleName)
? styleName.charAt(0).toUpperCase() + styleName.slice(1)
: styleName;
acc[reactStyleName] = value;
}
return acc;
}, {});
// Create next DOM style props from current and next RN styles
const { classList: rdomClassListNext, style: rdomStyleNext } = this.resolve([
rnStyle,
rnStyleNext
]);
const { classList: rdomClassListNext, style: rdomStyleNext } = this.resolve(
[rnStyle, rnStyleNext],
options
);
// Next class names take priority over current inline styles
const style = { ...rdomStyle };
rdomClassListNext.forEach(className => {
@@ -130,8 +119,10 @@ class StyleRegistry {
style[prop] = '';
}
});
// Next inline styles take priority over current inline styles
Object.assign(style, rdomStyleNext);
// Add the current class names not managed by React Native
const className = classListToString(rdomClassListNext.concat(rnClassList));
@@ -141,8 +132,9 @@ class StyleRegistry {
/**
* Resolves a React Native style object
*/
_resolveStyle(reactNativeStyle) {
const domStyle = createReactDOMStyle(flattenStyle(reactNativeStyle));
_resolveStyle(reactNativeStyle, options) {
const flatStyle = flattenStyle(reactNativeStyle);
const domStyle = createReactDOMStyle(options.i18n === false ? flatStyle : i18nStyle(flatStyle));
const props = Object.keys(domStyle).reduce(
(props, styleProp) => {
@@ -174,15 +166,15 @@ class StyleRegistry {
/**
* Caching layer over 'resolveStyle'
*/
_resolveStyleIfNeeded(key, style) {
_resolveStyleIfNeeded(style, { key, ...rest }) {
if (key) {
if (!this.cache[key]) {
// slow: convert style object to props and cache
this.cache[key] = this._resolveStyle(style);
this.cache[key] = this._resolveStyle(style, rest);
}
return this.cache[key];
}
return this._resolveStyle(style);
return this._resolveStyle(style, rest);
}
}

View File

@@ -1,5 +1,6 @@
/* eslint-env jasmine, jest */
import I18nManager from '../../I18nManager';
import StyleRegistry from '../StyleRegistry';
let styleRegistry;
@@ -46,6 +47,16 @@ describe('apis/StyleSheet/StyleRegistry', () => {
testResolve(a, b, c);
});
test('with register before RTL, resolves to className', () => {
const a = styleRegistry.register({ left: '12.34%' });
const b = styleRegistry.register({ textAlign: 'left' });
const c = styleRegistry.register({ marginLeft: 10 });
I18nManager.forceRTL(true);
const resolved = styleRegistry.resolve([a, b, c]);
I18nManager.forceRTL(false);
expect(resolved).toMatchSnapshot();
});
test('with register, resolves to mixed', () => {
const a = styleA;
const b = styleRegistry.register(styleB);
@@ -82,7 +93,7 @@ describe('apis/StyleSheet/StyleRegistry', () => {
// note: this also checks for correctly uppercasing the first letter of DOM vendor prefixes
const domStyleProps = {
classList: [],
style: { opacity: 0.5, webkitTransform: 'scale(1)', transform: 'scale(1)' }
style: { opacity: 0.5, WebkitTransform: 'scale(1)', transform: 'scale(1)' }
};
const domStyleNextProps = styleRegistry.resolveStateful(
{ opacity: 1, transform: [{ scale: 2 }] },

View File

@@ -1,5 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apis/StyleSheet/StyleRegistry resolve with register before RTL, resolves to className 1`] = `
Object {
"classList": Array [],
"className": "",
"style": Object {
"marginRight": "10px",
"right": "12.34%",
"textAlign": "right",
},
}
`;
exports[`apis/StyleSheet/StyleRegistry resolve with register, resolves to className 1`] = `
Object {
"classList": Array [

View File

@@ -1,18 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apis/StyleSheet/createReactDOMStyle converts ReactNative style to ReactDOM style 1`] = `
exports[`apis/StyleSheet/createReactDOMStyle shortform -> longform 1`] = `
Object {
"borderBottomColor": "white",
"borderBottomStyle": "solid",
"borderBottomWidth": "1px",
"borderLeftWidth": "1px",
"borderRightWidth": "1px",
"borderTopWidth": "1px",
"borderWidthLeft": "2px",
"borderWidthRight": "3px",
"boxShadow": "1px 1px 1px 1px #000, 1px 2px 0px red",
"display": "flex",
"flexShrink": 0,
"marginBottom": "0px",
"marginTop": "0px",
"opacity": 0,
"borderLeftStyle": "solid",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightWidth": "0px",
"borderTopStyle": "solid",
"borderTopWidth": "0px",
"boxSizing": "border-box",
"marginBottom": "25px",
"marginLeft": "10px",
"marginRight": "10px",
"marginTop": "50px",
}
`;

View File

@@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apis/StyleSheet/expandStyle shortform -> longform 1`] = `
Object {
"borderBottomColor": "white",
"borderBottomStyle": "solid",
"borderBottomWidth": "1px",
"borderLeftStyle": "solid",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightWidth": "0px",
"borderTopStyle": "solid",
"borderTopWidth": "0px",
"boxSizing": "border-box",
"marginBottom": "25px",
"marginLeft": "10px",
"marginRight": "10px",
"marginTop": "50px",
}
`;

View File

@@ -16,13 +16,148 @@ const reactNativeStyle = {
};
describe('apis/StyleSheet/createReactDOMStyle', () => {
test('converts ReactNative style to ReactDOM style', () => {
expect(createReactDOMStyle(reactNativeStyle)).toMatchSnapshot();
});
test('noop on DOM styles', () => {
const firstStyle = createReactDOMStyle(reactNativeStyle);
const secondStyle = createReactDOMStyle(firstStyle);
expect(firstStyle).toEqual(secondStyle);
});
test('flex', () => {
expect(createReactDOMStyle({ display: 'flex' })).toEqual({
display: 'flex',
flexShrink: 0
});
expect(createReactDOMStyle({ display: 'flex', flex: 1 })).toEqual({
display: 'flex',
flexGrow: 1,
flexShrink: 1,
flexBasis: 'auto'
});
expect(createReactDOMStyle({ display: 'flex', flex: 10 })).toEqual({
display: 'flex',
flexGrow: 10,
flexShrink: 1,
flexBasis: 'auto'
});
expect(createReactDOMStyle({ display: 'flex', flexShrink: 1 })).toEqual({
display: 'flex',
flexShrink: 1
});
expect(createReactDOMStyle({ display: 'flex', flex: 1, flexShrink: 2 })).toEqual({
display: 'flex',
flexGrow: 1,
flexShrink: 2,
flexBasis: 'auto'
});
});
test('shortform -> longform', () => {
const style = {
borderStyle: 'solid',
boxSizing: 'border-box',
borderBottomColor: 'white',
borderBottomWidth: 1,
borderWidth: 0,
marginTop: 50,
marginVertical: 25,
margin: 10
};
expect(createReactDOMStyle(style)).toMatchSnapshot();
});
describe('shadow styles', () => {
test('shadowColor only', () => {
const style = { shadowColor: 'red' };
const resolved = createReactDOMStyle(style);
expect(resolved).toEqual({
boxShadow: '0px 0px 0px red'
});
});
test('shadowColor and shadowOpacity only', () => {
expect(createReactDOMStyle({ shadowColor: 'red', shadowOpacity: 0.5 })).toEqual({
boxShadow: '0px 0px 0px rgba(255,0,0,0.5)'
});
});
test('shadowOffset only', () => {
expect(createReactDOMStyle({ shadowOffset: { width: 1, height: 2 } })).toEqual({});
});
test('shadowRadius only', () => {
expect(createReactDOMStyle({ shadowRadius: 5 })).toEqual({});
});
test('shadowOffset, shadowRadius, shadowColor', () => {
expect(
createReactDOMStyle({
shadowColor: 'rgba(50,60,70,0.5)',
shadowOffset: { width: 1, height: 2 },
shadowOpacity: 0.5,
shadowRadius: 3
})
).toEqual({
boxShadow: '1px 2px 3px rgba(50,60,70,0.25)'
});
});
});
test('textAlignVertical', () => {
expect(
createReactDOMStyle({
textAlignVertical: 'center'
})
).toEqual({
verticalAlign: 'middle'
});
});
test('textShadowOffset', () => {
expect(
createReactDOMStyle({
textShadowColor: 'red',
textShadowOffset: { width: 1, height: 2 },
textShadowRadius: 5
})
).toEqual({
textShadow: '1px 2px 5px red'
});
});
describe('transform', () => {
// passthrough if transform value is ever a string
test('string', () => {
const transform = 'perspective(50px) scaleX(20) translateX(20px) rotate(20deg)';
const style = { transform };
const resolved = createReactDOMStyle(style);
expect(resolved).toEqual({ transform });
});
test('array', () => {
const style = {
transform: [{ perspective: 50 }, { scaleX: 20 }, { translateX: 20 }, { rotate: '20deg' }]
};
const resolved = createReactDOMStyle(style);
expect(resolved).toEqual({
transform: 'perspective(50px) scaleX(20) translateX(20px) rotate(20deg)'
});
});
test('transformMatrix', () => {
const style = { transformMatrix: [1, 2, 3, 4, 5, 6] };
const resolved = createReactDOMStyle(style);
expect(resolved).toEqual({
transform: 'matrix3d(1,2,3,4,5,6)'
});
});
});
});

View File

@@ -1,81 +0,0 @@
/* eslint-env jasmine, jest */
import expandStyle from '../expandStyle';
describe('apis/StyleSheet/expandStyle', () => {
test('shortform -> longform', () => {
const style = {
borderStyle: 'solid',
boxSizing: 'border-box',
borderBottomColor: 'white',
borderBottomWidth: 1,
borderWidth: 0,
marginTop: 50,
marginVertical: 25,
margin: 10
};
expect(expandStyle(style)).toMatchSnapshot();
});
test('textAlignVertical', () => {
const initial = {
textAlignVertical: 'center'
};
const expected = {
verticalAlign: 'middle'
};
expect(expandStyle(initial)).toEqual(expected);
});
test('flex', () => {
const value = 10;
const initial = {
flex: value
};
const expected = {
flexGrow: value,
flexShrink: 1,
flexBasis: 'auto'
};
expect(expandStyle(initial)).toEqual(expected);
});
test('flex', () => {
expect(expandStyle({ display: 'flex' })).toEqual({
display: 'flex',
flexShrink: 0
});
expect(expandStyle({ display: 'flex', flex: 1 })).toEqual({
display: 'flex',
flexGrow: 1,
flexShrink: 1,
flexBasis: 'auto'
});
expect(expandStyle({ display: 'flex', flex: 10 })).toEqual({
display: 'flex',
flexGrow: 10,
flexShrink: 1,
flexBasis: 'auto'
});
expect(expandStyle({ display: 'flex', flexShrink: 1 })).toEqual({
display: 'flex',
flexShrink: 1
});
expect(expandStyle({ display: 'flex', flex: 1, flexShrink: 2 })).toEqual({
display: 'flex',
flexGrow: 1,
flexShrink: 2,
flexBasis: 'auto'
});
});
});

View File

@@ -1,56 +0,0 @@
/* eslint-env jasmine, jest */
import resolveBoxShadow from '../resolveBoxShadow';
describe('apis/StyleSheet/resolveBoxShadow', () => {
test('shadowColor only', () => {
const resolvedStyle = {};
const style = { shadowColor: 'red' };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '0px 0px 0px red'
});
});
test('shadowColor and shadowOpacity only', () => {
const resolvedStyle = {};
const style = { shadowColor: 'red', shadowOpacity: 0.5 };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '0px 0px 0px rgba(255,0,0,0.5)'
});
});
test('shadowOffset only', () => {
const resolvedStyle = {};
const style = { shadowOffset: { width: 1, height: 2 } };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({});
});
test('shadowRadius only', () => {
const resolvedStyle = {};
const style = { shadowRadius: 5 };
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({});
});
test('shadowOffset, shadowRadius, shadowColor', () => {
const resolvedStyle = {};
const style = {
shadowColor: 'rgba(50,60,70,0.5)',
shadowOffset: { width: 1, height: 2 },
shadowOpacity: 0.5,
shadowRadius: 3
};
resolveBoxShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
boxShadow: '1px 2px 3px rgba(50,60,70,0.25)'
});
});
});

View File

@@ -1,19 +0,0 @@
/* eslint-env jasmine, jest */
import resolveTextShadow from '../resolveTextShadow';
describe('apis/StyleSheet/resolveTextShadow', () => {
test('textShadowOffset', () => {
const resolvedStyle = {};
const style = {
textShadowColor: 'red',
textShadowOffset: { width: 1, height: 2 },
textShadowRadius: 5
};
resolveTextShadow(resolvedStyle, style);
expect(resolvedStyle).toEqual({
textShadow: '1px 2px 5px red'
});
});
});

View File

@@ -1,27 +0,0 @@
/* eslint-env jasmine, jest */
import resolveTransform from '../resolveTransform';
describe('apis/StyleSheet/resolveTransform', () => {
test('transform', () => {
const resolvedStyle = {};
const style = {
transform: [{ perspective: 50 }, { scaleX: 20 }, { translateX: 20 }, { rotate: '20deg' }]
};
resolveTransform(resolvedStyle, style);
expect(resolvedStyle).toEqual({
transform: 'perspective(50px) scaleX(20) translateX(20px) rotate(20deg)'
});
});
test('transformMatrix', () => {
const resolvedStyle = {};
const style = { transformMatrix: [1, 2, 3, 4, 5, 6] };
resolveTransform(resolvedStyle, style);
expect(resolvedStyle).toEqual({
transform: 'matrix3d(1,2,3,4,5,6)'
});
});
});

View File

@@ -1,7 +1,226 @@
import expandStyle from './expandStyle';
import i18nStyle from './i18nStyle';
/**
* The browser implements the CSS cascade, where the order of properties is a
* factor in determining which styles to paint. React Native is different in
* giving precedence to the more specific styles. For example, the value of
* `paddingTop` takes precedence over that of `padding`.
*
* This module creates mutally exclusive style declarations by expanding all of
* React Native's supported shortform properties (e.g. `padding`) to their
* longfrom equivalents.
*/
const createReactDOMStyle = flattenedReactNativeStyle =>
expandStyle(i18nStyle(flattenedReactNativeStyle));
import normalizeValue from './normalizeValue';
import processColor from '../../modules/processColor';
const emptyObject = {};
const styleShortFormProperties = {
borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
borderRadius: [
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomRightRadius',
'borderBottomLeftRadius'
],
borderStyle: ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'],
borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
marginHorizontal: ['marginRight', 'marginLeft'],
marginVertical: ['marginTop', 'marginBottom'],
overflow: ['overflowX', 'overflowY'],
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
paddingHorizontal: ['paddingRight', 'paddingLeft'],
paddingVertical: ['paddingTop', 'paddingBottom'],
textDecorationLine: ['textDecoration'],
writingDirection: ['direction']
};
const colorProps = {
backgroundColor: true,
borderColor: true,
borderTopColor: true,
borderRightColor: true,
borderBottomColor: true,
borderLeftColor: true,
color: true
};
const alphaSortProps = propsArray =>
propsArray.sort((a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
const defaultOffset = { height: 0, width: 0 };
/**
* Shadow
*/
// TODO: add inset and spread support
const resolveShadow = (resolvedStyle, style) => {
const { height, width } = style.shadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, style.shadowRadius || 0);
const color = processColor(style.shadowColor, style.shadowOpacity);
if (color) {
const boxShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
resolvedStyle.boxShadow = style.boxShadow ? `${style.boxShadow}, ${boxShadow}` : boxShadow;
} else if (style.boxShadow) {
resolvedStyle.boxShadow = style.boxShadow;
}
};
/**
* Text Shadow
*/
const resolveTextShadow = (resolvedStyle, style) => {
const { height, width } = style.textShadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, style.textShadowRadius || 0);
const color = processColor(style.textShadowColor);
if (color) {
resolvedStyle.textShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
}
};
/**
* Transform
*/
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
const mapTransform = transform => {
const type = Object.keys(transform)[0];
const value = normalizeValue(type, transform[type]);
return `${type}(${value})`;
};
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
const convertTransformMatrix = transformMatrix => {
const matrix = transformMatrix.join(',');
return `matrix3d(${matrix})`;
};
const resolveTransform = (resolvedStyle, style) => {
let transform = style.transform;
if (Array.isArray(style.transform)) {
transform = style.transform.map(mapTransform).join(' ');
} else if (style.transformMatrix) {
transform = convertTransformMatrix(style.transformMatrix);
}
resolvedStyle.transform = transform;
};
/**
* Reducer
*/
const createReducer = (style, styleProps) => {
let hasResolvedShadow = false;
let hasResolvedTextShadow = false;
return (resolvedStyle, prop) => {
const value = normalizeValue(prop, style[prop]);
if (value == null) {
return resolvedStyle;
}
switch (prop) {
case 'display': {
resolvedStyle.display = value;
// default of 'flexShrink:0' has lowest precedence
if (style.display === 'flex' && style.flex == null && style.flexShrink == null) {
resolvedStyle.flexShrink = 0;
}
break;
}
// ignore React Native styles
case 'aspectRatio':
case 'elevation':
case 'overlayColor':
case 'resizeMode':
case 'tintColor': {
break;
}
case 'flex': {
resolvedStyle.flexGrow = value;
resolvedStyle.flexShrink = 1;
resolvedStyle.flexBasis = 'auto';
break;
}
case 'shadowColor':
case 'shadowOffset':
case 'shadowOpacity':
case 'shadowRadius': {
if (!hasResolvedShadow) {
resolveShadow(resolvedStyle, style);
}
hasResolvedShadow = true;
break;
}
case 'textAlignVertical': {
resolvedStyle.verticalAlign = value === 'center' ? 'middle' : value;
break;
}
case 'textShadowColor':
case 'textShadowOffset':
case 'textShadowRadius': {
if (!hasResolvedTextShadow) {
resolveTextShadow(resolvedStyle, style);
}
hasResolvedTextShadow = true;
break;
}
case 'transform':
case 'transformMatrix': {
resolveTransform(resolvedStyle, style);
break;
}
default: {
// normalize color values
let finalValue = value;
if (colorProps[prop]) {
finalValue = processColor(value);
}
const longFormProperties = styleShortFormProperties[prop];
if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// the value of any longform property in the original styles takes
// precedence over the shortform's value
if (styleProps.indexOf(longForm) === -1) {
resolvedStyle[longForm] = finalValue;
}
});
} else {
resolvedStyle[prop] = finalValue;
}
}
}
return resolvedStyle;
};
};
const createReactDOMStyle = 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;
};
module.exports = createReactDOMStyle;

View File

@@ -1,158 +0,0 @@
/**
* The browser implements the CSS cascade, where the order of properties is a
* factor in determining which styles to paint. React Native is different in
* giving precedence to the more specific styles. For example, the value of
* `paddingTop` takes precedence over that of `padding`.
*
* This module creates mutally exclusive style declarations by expanding all of
* React Native's supported shortform properties (e.g. `padding`) to their
* longfrom equivalents.
*/
import normalizeValue from './normalizeValue';
import processColor from '../../modules/processColor';
import resolveBoxShadow from './resolveBoxShadow';
import resolveTextShadow from './resolveTextShadow';
import resolveTransform from './resolveTransform';
const emptyObject = {};
const styleShortFormProperties = {
borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
borderRadius: [
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomRightRadius',
'borderBottomLeftRadius'
],
borderStyle: ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'],
borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
marginHorizontal: ['marginRight', 'marginLeft'],
marginVertical: ['marginTop', 'marginBottom'],
overflow: ['overflowX', 'overflowY'],
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
paddingHorizontal: ['paddingRight', 'paddingLeft'],
paddingVertical: ['paddingTop', 'paddingBottom'],
textDecorationLine: ['textDecoration'],
writingDirection: ['direction']
};
const colorProps = {
backgroundColor: true,
borderColor: true,
borderTopColor: true,
borderRightColor: true,
borderBottomColor: true,
borderLeftColor: true,
color: true
};
const alphaSortProps = propsArray =>
propsArray.sort((a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
const createReducer = (style, styleProps) => {
let hasResolvedBoxShadow = false;
let hasResolvedTextShadow = false;
return (resolvedStyle, prop) => {
const value = normalizeValue(prop, style[prop]);
if (value == null) {
return resolvedStyle;
}
switch (prop) {
case 'display': {
resolvedStyle.display = value;
// default of 'flexShrink:0' has lowest precedence
if (style.display === 'flex' && style.flex == null && style.flexShrink == null) {
resolvedStyle.flexShrink = 0;
}
break;
}
// ignore React Native styles
case 'aspectRatio':
case 'elevation':
case 'overlayColor':
case 'resizeMode':
case 'tintColor': {
break;
}
case 'flex': {
resolvedStyle.flexGrow = value;
resolvedStyle.flexShrink = 1;
resolvedStyle.flexBasis = 'auto';
break;
}
case 'shadowColor':
case 'shadowOffset':
case 'shadowOpacity':
case 'shadowRadius': {
if (!hasResolvedBoxShadow) {
resolveBoxShadow(resolvedStyle, style);
}
hasResolvedBoxShadow = true;
break;
}
case 'textAlignVertical': {
resolvedStyle.verticalAlign = value === 'center' ? 'middle' : value;
break;
}
case 'textShadowColor':
case 'textShadowOffset':
case 'textShadowRadius': {
if (!hasResolvedTextShadow) {
resolveTextShadow(resolvedStyle, style);
}
hasResolvedTextShadow = true;
break;
}
case 'transform': {
resolveTransform(resolvedStyle, style);
break;
}
default: {
// normalize color values
let finalValue = value;
if (colorProps[prop]) {
finalValue = processColor(value);
}
const longFormProperties = styleShortFormProperties[prop];
if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// the value of any longform property in the original styles takes
// precedence over the shortform's value
if (styleProps.indexOf(longForm) === -1) {
resolvedStyle[longForm] = finalValue;
}
});
} else {
resolvedStyle[prop] = finalValue;
}
}
}
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;
};
module.exports = expandStyle;

View File

@@ -1,7 +1,7 @@
import hyphenateStyleName from 'hyphenate-style-name';
import mapKeyValue from '../../modules/mapKeyValue';
import normalizeValue from './normalizeValue';
import prefixAll from 'inline-style-prefixer/static';
import prefixStyles from '../../modules/prefixStyles';
const createDeclarationString = (prop, val) => {
const name = hyphenateStyleName(prop);
@@ -19,6 +19,6 @@ const createDeclarationString = (prop, val) => {
* // => 'color:blue;width:20px'
*/
const generateCss = style =>
mapKeyValue(prefixAll(style), createDeclarationString).sort().join(';');
mapKeyValue(prefixStyles(style), createDeclarationString).sort().join(';');
module.exports = generateCss;

View File

@@ -71,15 +71,17 @@ const i18nStyle = originalStyle => {
continue;
}
const value = style[prop];
if (PROPERTIES_TO_SWAP[prop]) {
const newProp = flipProperty(prop);
nextStyle[newProp] = style[prop];
nextStyle[newProp] = value;
} else if (PROPERTIES_SWAP_LEFT_RIGHT[prop]) {
nextStyle[prop] = swapLeftRight(style[prop]);
nextStyle[prop] = swapLeftRight(value);
} else if (prop === 'textShadowOffset') {
nextStyle[prop] = style[prop];
nextStyle[prop].width = additiveInverse(style[prop].width);
} else if (prop === 'transform') {
nextStyle[prop] = value;
nextStyle[prop].width = additiveInverse(value.width);
} else if (prop === 'transform' && Array.isArray(value)) {
nextStyle[prop] = style[prop].map(flipTransform);
} else {
nextStyle[prop] = style[prop];

View File

@@ -1,22 +0,0 @@
import normalizeValue from './normalizeValue';
import processColor from '../../modules/processColor';
const defaultOffset = { height: 0, width: 0 };
// TODO: add inset and spread support
const resolveBoxShadow = (resolvedStyle, style) => {
const { height, width } = style.shadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, style.shadowRadius || 0);
const color = processColor(style.shadowColor, style.shadowOpacity);
if (color) {
const boxShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
resolvedStyle.boxShadow = style.boxShadow ? `${style.boxShadow}, ${boxShadow}` : boxShadow;
} else if (style.boxShadow) {
resolvedStyle.boxShadow = style.boxShadow;
}
};
module.exports = resolveBoxShadow;

View File

@@ -1,18 +0,0 @@
import normalizeValue from './normalizeValue';
import processColor from '../../modules/processColor';
const defaultOffset = { height: 0, width: 0 };
const resolveTextShadow = (resolvedStyle, style) => {
const { height, width } = style.textShadowOffset || defaultOffset;
const offsetX = normalizeValue(null, width);
const offsetY = normalizeValue(null, height);
const blurRadius = normalizeValue(null, style.textShadowRadius || 0);
const color = processColor(style.textShadowColor);
if (color) {
resolvedStyle.textShadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
}
};
module.exports = resolveTextShadow;

View File

@@ -1,27 +0,0 @@
import normalizeValue from './normalizeValue';
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
const mapTransform = transform => {
const type = Object.keys(transform)[0];
const value = normalizeValue(type, transform[type]);
return `${type}(${value})`;
};
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
const convertTransformMatrix = transformMatrix => {
const matrix = transformMatrix.join(',');
return `matrix3d(${matrix})`;
};
const resolveTransform = (resolvedStyle, style) => {
if (Array.isArray(style.transform)) {
const transform = style.transform.map(mapTransform).join(' ');
resolvedStyle.transform = transform;
} else if (style.transformMatrix) {
const transform = convertTransformMatrix(style.transformMatrix);
resolvedStyle.transform = transform;
}
};
module.exports = resolveTransform;

View File

@@ -87,6 +87,7 @@ export default class ScrollViewBase extends Component {
_handleScroll = e => {
e.persist();
e.stopPropagation();
const { scrollEventThrottle } = this.props;
// A scroll happened, so the scroll bumps the debounce.
this._debouncedOnScrollEnd(e);

View File

@@ -8,9 +8,13 @@
import createDOMProps from '../createDOMProps';
import findNodeHandle from '../findNodeHandle';
import i18nStyle from '../../apis/StyleSheet/i18nStyle';
import StyleRegistry from '../../apis/StyleSheet/registry';
import UIManager from '../../apis/UIManager';
const hyphenPattern = /-([a-z])/g;
const toCamelCase = str => str.replace(hyphenPattern, m => m[1].toUpperCase());
const NativeMethodsMixin = {
/**
* Removes focus from an input or view. This is the opposite of `focus()`.
@@ -69,13 +73,24 @@ const NativeMethodsMixin = {
setNativeProps(nativeProps: Object) {
// Copy of existing DOM state
const node = findNodeHandle(this);
const nodeStyle = node.style;
const classList = Array.prototype.slice.call(node.classList);
const style = { ...node.style };
const style = {};
// DOM style is a CSSStyleDeclaration
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration
for (let i = 0; i < node.style.length; i += 1) {
const property = nodeStyle.item(i);
if (property) {
// DOM style uses hyphenated prop names and may include vendor prefixes
// Transform back into React DOM style.
style[toCamelCase(property)] = nodeStyle.getPropertyValue(property);
}
}
const domStyleProps = { classList, style };
// Next DOM state
const domProps = createDOMProps(nativeProps, style =>
StyleRegistry.resolveStateful(style, domStyleProps)
const domProps = createDOMProps(i18nStyle(nativeProps), style =>
StyleRegistry.resolveStateful(style, domStyleProps, { i18n: false })
);
UIManager.updateView(node, domProps, this);
}

View File

@@ -1,9 +1,9 @@
/* eslint-env jasmine, jest */
import prefixInlineStyles from '../prefixInlineStyles';
import { prefixInlineStyles } from '..';
describe('apis/StyleSheet/prefixInlineStyles', () => {
test('handles array values', () => {
describe('modules/prefixStyles', () => {
test('handles array values for inline styles', () => {
const style = {
display: ['-webkit-flex', 'flex']
};

View File

@@ -1,6 +1,8 @@
import prefixAll from 'inline-style-prefixer/static';
const prefixInlineStyles = style => {
export default prefixAll;
export const prefixInlineStyles = style => {
const prefixedStyles = prefixAll(style);
// React@15 removed undocumented support for fallback values in
@@ -14,5 +16,3 @@ const prefixInlineStyles = style => {
return prefixedStyles;
};
module.exports = prefixInlineStyles;