Compare commits

...

42 Commits

Author SHA1 Message Date
Nicolas Gallagher
1c37a42566 0.0.94 2017-05-01 12:27:55 -07:00
Nicolas Gallagher
c38369ac0f [fix] RTL style registration and resolution
* Lazy-register RTL variants to generate class names
* Don't RTL-flip translateX
2017-05-01 12:27:38 -07:00
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
Nicolas Gallagher
5e8ad67296 0.0.89 2017-04-26 15:12:19 -07:00
Nathan Broadbent
ba24a882be Link to another starter kit example 2017-04-26 15:11:16 -07:00
Nicolas Gallagher
c7686209cd Update prettier 2017-04-26 15:07:44 -07:00
Nicolas Gallagher
f1b281ae32 Update debounce dependency 2017-04-26 15:06:41 -07:00
Nicolas Gallagher
b676fbd5e0 [fix] propTypes removal in production builds
Updates the relevant babel plugin, which now replaces component
propTypes with an empty object, avoiding the majority of potential
runtime errors related to this transform.

Fix #423
2017-04-26 11:05:33 -07:00
Nicolas Gallagher
51aef6c791 0.0.88 2017-04-24 13:28:38 -07:00
Nathan Leung
ae9a9cde5f Fix example webpack config in documentation 2017-04-24 13:21:46 -07:00
Nicolas Gallagher
beb907b180 Rename some variables in StyleRegistry 2017-04-24 13:21:27 -07:00
Nicolas Gallagher
a3362e1f38 [fix] setNativeProps inline styles
Inline styles are preserved when using 'setNativeProps'. Adds unit tests
for the resolution logic required by 'setNativeProps'/'resolveStateful'
in a DOM context.

Fix #439
2017-04-23 21:24:27 -07:00
Nicolas Gallagher
64d2d34367 0.0.87 2017-04-23 13:39:27 -07:00
Nicolas Gallagher
d83cd45b6f [fix] Clipboard browser support
Safari 10.3 supports copying (but apparently not from inputs)
2017-04-23 13:38:51 -07:00
Nicolas Gallagher
227971d22c [add] support for CSS grid properties (experimental)
Allow people to experiment with using CSS grid in react-native. (No
support for shorthand properties.)
2017-04-22 10:34:43 -07:00
Nicolas Gallagher
4822cf4620 [add] support for 'clip' and 'textIndent' styles 2017-04-22 10:06:27 -07:00
Nathan Broadbent
91e4528eac Allow filter property in CSS 2017-04-22 22:56:07 +07:00
Nicolas Gallagher
1ee64d8285 0.0.86 2017-04-21 19:04:23 -07:00
Nicolas Gallagher
66a4c13bf3 [fix] AppState.isSupported -> AppState.isAvailable
React Native exposes AppState support via 'isAvailable'.
2017-04-21 18:47:29 -07:00
Nicolas Gallagher
9012e98ba7 [fix] support 'mailto:' URLs in 'Linking' 2017-04-21 18:29:29 -07:00
Nicolas Gallagher
046e01dfa9 [fix] add AsyncStorage callbacks and tests
Add support for the callback interface and add test coverage.

Fix #399
Close #400
2017-04-21 18:13:14 -07:00
Nicolas Gallagher
6e71e1e058 [fix] attempt to avoid need for 'Array.from' polyfill
Fix #409
2017-04-20 18:04:09 -07:00
Nicolas Gallagher
d5a9f3e779 Add ES module export
Preparation for publishing an ES module build.
Move 'modality' into 'createDOMElement' to ensure it is always initialized.
2017-04-20 17:16:05 -07:00
Nicolas Gallagher
f16f5f21ce [add] WebkitMaskImage style prop
Undocumented supported. Commonly used in border-radius hacks.

Close #326
2017-04-20 15:12:07 -07:00
Nicolas Gallagher
0bb7e67e63 0.0.85 2017-04-20 15:07:56 -07:00
Nicolas Gallagher
c6b54930b6 [add] StatusBar stub component
Fix #425
2017-04-20 15:07:34 -07:00
Nicolas Gallagher
599f1fcaf5 Filter unsupported TextInput props
Fix #385
2017-04-20 14:58:48 -07:00
Nicolas Gallagher
3f7a4e455f [add] support 'outlineColor' style prop
Fix #435
2017-04-20 14:50:14 -07:00
Nicolas Gallagher
1f3e9cc6ee [change] ScrollView as new surface
Fix #405
2017-04-20 13:36:00 -07:00
Nicolas Gallagher
17ed63129f Add a note about accessibilityRole compat 2017-04-20 10:39:34 -07:00
Nicolas Gallagher
769334d04e Update benchmark results 2017-04-20 10:04:33 -07:00
52 changed files with 1237 additions and 820 deletions

View File

@@ -144,6 +144,7 @@ AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-ro
* [react-native-web-player](https://github.com/dabbott/react-native-web-player)
* [reactxp](https://github.com/microsoft/reactxp)
* [react-web](https://github.com/taobaofed/react-web)
* [react-native-web-webpack](https://github.com/ndbroadbent/react-native-web-webpack)
## License

View File

@@ -85,6 +85,7 @@ When `false`, the text is not selectable.
+ `textAlign`
+ `textAlignVertical`
+ `textDecorationLine`
+ `textIndent`
+ `textOverflow`
+ `textRendering`
+ `textShadowColor`

View File

@@ -186,16 +186,30 @@ Controls whether the View can be the target of touch events. The enhanced
+ `borderRightWidth`
+ `borderTopWidth`
+ `bottom`
+ `boxShadow`
+ `boxShadow`
+ `boxSizing`
+ `clip`
+ `cursor`
+ `display`
+ `filter`
+ `flex` (number)
+ `flexBasis`
+ `flexDirection`
+ `flexGrow`
+ `flexShrink`
+ `flexWrap`
+ `gridAutoColumns`
+ `gridAutoFlow`
+ `gridAutoRows`
+ `gridColumnEnd`
+ `gridColumnGap`
+ `gridColumnStart`
+ `gridRowEnd`
+ `gridRowGap`
+ `gridRowStart`
+ `gridTemplateColumns`
+ `gridTemplateRows`
+ `gridTemplateAreas`
+ `height`
+ `justifyContent`
+ `left`
@@ -213,6 +227,7 @@ Controls whether the View can be the target of touch events. The enhanced
+ `opacity`
+ `order`
+ `outline`
+ `outlineColor`
+ `overflow`
+ `overflowX`
+ `overflowY`
@@ -227,6 +242,10 @@ Controls whether the View can be the target of touch events. The enhanced
+ `perspectiveOrigin`
+ `position`
+ `right`
+ `shadowColor`
+ `shadowOffset`
+ `shadowOpacity`
+ `shadowRadius`
+ `top`
+ `transform`
+ `transformOrigin`

View File

@@ -39,7 +39,10 @@ using `aria-label`.
In some cases, we also want to alert the end user of the type of selected
component (i.e., that it is a “button”). To provide more context to screen
readers, you should specify the `accessibilityRole` property.
readers, you should specify the `accessibilityRole` property. (Note that React
Native for Web provides a compatibility mapping of equivalent
`accessibilityTraits` and `accessibilityComponentType` values to
`accessibilityRole`).
The `accessibilityRole` prop is used to infer an [analogous HTML
element][html-aria-url] to use in addition to the resulting ARIA `role`, where

View File

@@ -1,5 +1,8 @@
# Getting started
It is recommended that your application provide a `Promise` and `Array.from`
polyfill.
## Webpack and Babel
[Webpack](webpack.js.org) is a popular build tool for web apps. Below is an
@@ -58,7 +61,7 @@ module.exports = {
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
},
],
resolve: {
// Maps the 'react-native' import to 'react-native-web'.
@@ -73,6 +76,10 @@ module.exports = {
}
```
A more complex example setup for web apps can be found in various starter kits
(e.g., create-react-app and
[react-native-web-webpack](https://github.com/ndbroadbent/react-native-web-webpack))
Please refer to the Webpack documentation for more information.
## Jest

View File

@@ -27,7 +27,7 @@ module.exports = {
],
resolve: {
alias: {
'react-native': path.join(__dirname, '../../src')
'react-native': path.join(__dirname, '../../src/module')
}
}
}

View File

@@ -104,7 +104,6 @@ var styles = StyleSheet.create({
height: CIRCLE_SIZE,
borderRadius: CIRCLE_SIZE / 2,
position: 'absolute',
left: 0,
top: 0,
},
container: {

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-web",
"version": "0.0.84",
"version": "0.0.94",
"description": "React Native for Web",
"main": "dist/index.js",
"files": [
@@ -27,7 +27,7 @@
"array-find-index": "^1.0.2",
"babel-runtime": "^6.23.0",
"create-react-class": "^15.5.2",
"debounce": "^1.0.0",
"debounce": "1.0.2",
"deep-assign": "^2.0.0",
"fbjs": "^0.8.8",
"hyphenate-style-name": "^1.0.2",
@@ -42,19 +42,19 @@
"babel-core": "^6.24.1",
"babel-eslint": "^7.2.2",
"babel-loader": "^6.4.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.2",
"babel-preset-react-native": "^1.9.1",
"del-cli": "^0.2.1",
"enzyme": "^2.8.2",
"enzyme-to-json": "^1.5.1",
"eslint": "^3.19.0",
"eslint-config-prettier": "^1.6.0",
"eslint-config-prettier": "^1.7.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^6.10.3",
"file-loader": "^0.11.1",
"jest": "^19.0.2",
"node-libs-browser": "^0.5.3",
"prettier": "^1.1.0",
"prettier": "^1.2.2",
"react": "~15.4.1",
"react-addons-test-utils": "~15.4.1",
"react-dom": "~15.4.1",

View File

@@ -22,18 +22,19 @@ Typical render timings*: mean ± two standard deviations
| Implementation | Deep tree (ms) | Wide tree (ms) | Tweets (ms) |
| :--- | ---: | ---: | ---: |
| `css-modules` | `87.68` `±13.29` | `171.96` `±14.91` | |
| `react-native-web/stylesheet@0.0.81` | `90.59` `±12.03` | `190.37` `±19.65` | |
| `react-native-web@0.0.81` | `105.20` `±17.86` | `226.54` 29.50` | `12.12` 6.94ms` |
| `css-modules` | `87.67` `±15.22` | `170.85` `±16.87` | |
| `react-native-web/stylesheet@0.0.84` | `90.02` `±13.16` | `186.66` `±19.23` | |
| `react-native-web@0.0.84` | `102.72` `±19.26` | `222.35` 18.95` | `12.81` 5.45ms` |
Other libraries
| Implementation | Deep tree (ms) | Wide tree (ms) |
| :--- | ---: | ---: |
| `aphrodite@1.2.0` | `101.25` `±18.78` | `224.59` 22.28` |
| `glamor@3.0.0-1` | `143.39` `±23.05` | `275.21` 21.10` |
| `react-jss@5.4.1` | `142.27` `±16.62` | `318.62` `±26.19` |
| `reactxp@0.34.3` | `221.36` `±23.35` | `472.61` 40.86` |
| `styled-components@2.0.0-7` | `301.92` `±39.43` | `647.80` 102.1` |
| `styletron@2.5.1` | `88.48` `±12.00` | `171.89` 13.28` |
| `aphrodite@1.2.0` | `101.32` `±20.33` | `220.33` 31.41` |
| `glamor@3.0.0-1` | `129.00` `±14.92` | `264.57` `±28.54` |
| `react-jss@5.4.1` | `137.33` `±21.55` | `314.91` 29.03` |
| `reactxp@0.34.3` | `223.82` `±32.77` | `461.56` 34.43` |
| `styled-components@2.0.0-11` | `277.53` `±28.83` | `627.91` `±43.13` |
*MacBook Pro (13-inch, Early 2011); 2.7 GHz Intel Core i7; 16 GB 1600 MHz DDR3. Google Chrome 56.

View File

@@ -6,7 +6,9 @@ import React from 'react';
export const styletron = new Styletron();
const View = ({ style, ...other }) => <div {...other} className={classnames(viewStyle, ...style)} />;
const View = ({ style, ...other }) => (
<div {...other} className={classnames(viewStyle, ...style)} />
);
const viewStyle = injectStylePrefixed(styletron, {
alignItems: 'stretch',

View File

@@ -13,10 +13,10 @@ const AppStates = {
const listeners = [];
class AppState {
static isSupported = ExecutionEnvironment.canUseDOM && document.visibilityState;
static isAvailable = ExecutionEnvironment.canUseDOM && document.visibilityState;
static get currentState() {
if (!AppState.isSupported) {
if (!AppState.isAvailable) {
return AppState.ACTIVE;
}
@@ -31,7 +31,7 @@ class AppState {
}
static addEventListener(type: string, handler: Function) {
if (AppState.isSupported) {
if (AppState.isAvailable) {
invariant(
EVENT_TYPES.indexOf(type) !== -1,
'Trying to subscribe to unknown event: "%s"',
@@ -44,7 +44,7 @@ class AppState {
}
static removeEventListener(type: string, handler: Function) {
if (AppState.isSupported) {
if (AppState.isAvailable) {
invariant(
EVENT_TYPES.indexOf(type) !== -1,
'Trying to remove listener for unknown event: "%s"',

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apis/AsyncStorage mergeLocalStorageItem should have same behavior as react-native 1`] = `
exports[`apis/AsyncStorage mergeItem calls callback after setting item 1`] = `
Object {
"age": 31,
"name": "Chris",
@@ -11,3 +11,63 @@ Object {
},
}
`;
exports[`apis/AsyncStorage mergeItem promise after setting item 1`] = `
Object {
"age": 31,
"name": "Chris",
"traits": Object {
"eyes": "blue",
"hair": "brown",
"shoe_size": 10,
},
}
`;
exports[`apis/AsyncStorage multiMerge calls callback after setting items 1`] = `
Object {
"age": 31,
"name": "Chris",
"traits": Object {
"eyes": "blue",
"hair": "brown",
"shoe_size": 10,
},
}
`;
exports[`apis/AsyncStorage multiMerge calls callback after setting items 2`] = `
Object {
"age": 31,
"name": "Amy",
"traits": Object {
"eyes": "blue",
"hair": "black",
"shoe_size": 10,
},
}
`;
exports[`apis/AsyncStorage multiMerge promise after setting items 1`] = `
Object {
"age": 31,
"name": "Chris",
"traits": Object {
"eyes": "blue",
"hair": "brown",
"shoe_size": 10,
},
}
`;
exports[`apis/AsyncStorage multiMerge promise after setting items 2`] = `
Object {
"age": 31,
"name": "Amy",
"traits": Object {
"eyes": "blue",
"hair": "black",
"shoe_size": 10,
},
}
`;

View File

@@ -1,77 +1,275 @@
/* eslint-env jasmine, jest */
import AsyncStorage from '..';
const waterfall = (fns, cb) => {
const _waterfall = (...args) => {
const fn = (fns || []).shift();
if (typeof fn === 'function') {
fn(...args, (err, ...nextArgs) => {
if (err) {
return cb(err);
} else {
return _waterfall(...nextArgs);
}
});
} else {
cb(null, ...args);
}
};
_waterfall();
};
const originalLocalStorage = window.localStorage;
const obj = {};
let obj = {};
const mockLocalStorage = {
length: 0,
clear() {
obj = {};
mockLocalStorage.length = 0;
},
getItem(key) {
return obj[key];
},
key(index) {
return Object.keys(obj)[index];
},
removeItem(key) {
delete obj[key];
mockLocalStorage.length -= 1;
},
setItem(key, value) {
obj[key] = value;
mockLocalStorage.length += 1;
}
};
const originalLocalStorage = window.localStorage;
const uid123Object = {
name: 'Chris',
age: 30,
traits: { hair: 'brown', eyes: 'green' }
};
const uid123Delta = {
age: 31,
traits: { eyes: 'blue', shoe_size: 10 }
};
const uid124Object = {
name: 'Amy',
age: 28,
traits: { hair: 'black', eyes: 'brown' }
};
describe('apis/AsyncStorage', () => {
describe('mergeLocalStorageItem', () => {
test('should have same behavior as react-native', done => {
window.localStorage = mockLocalStorage;
// https://facebook.github.io/react-native/docs/asyncstorage.html
const UID123_object = {
name: 'Chris',
age: 30,
traits: { hair: 'brown', eyes: 'brown' }
};
const UID123_delta = {
age: 31,
traits: { eyes: 'blue', shoe_size: 10 }
};
beforeEach(() => {
mockLocalStorage.setItem('UID123', JSON.stringify(uid123Object));
mockLocalStorage.setItem('UID124', JSON.stringify(uid124Object));
window.localStorage = mockLocalStorage;
});
waterfall(
[
cb => {
AsyncStorage.setItem('UID123', JSON.stringify(UID123_object))
.then(() => cb(null))
.catch(cb);
},
cb => {
AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta))
.then(() => cb(null))
.catch(cb);
},
cb => {
AsyncStorage.getItem('UID123')
.then(result => {
cb(null, JSON.parse(result));
})
.catch(cb);
}
],
afterEach(() => {
mockLocalStorage.clear();
window.localStorage = originalLocalStorage;
});
describe('clear', () => {
const assertResult = () => {
expect(mockLocalStorage.length).toEqual(0);
};
test('promise of erased keys', () => {
expect(mockLocalStorage.length).toEqual(2);
return AsyncStorage.clear().then(() => {
assertResult();
});
});
test('calls callback after erasing keys', done => {
expect(mockLocalStorage.length).toEqual(2);
AsyncStorage.clear(err => {
expect(err).toEqual(null);
assertResult();
done();
});
});
});
describe('getAllKeys', () => {
const assertResult = result => {
expect(result).toEqual(['UID123', 'UID124']);
};
test('promise of keys', () => {
return AsyncStorage.getAllKeys().then(result => {
assertResult(result);
});
});
test('calls callback with keys', done => {
AsyncStorage.getAllKeys((err, result) => {
expect(err).toEqual(null);
assertResult(result);
done();
});
});
});
describe('getItem', () => {
const assertResult = result => {
expect(result).toEqual(JSON.stringify(uid123Object));
};
test('promise of item', () => {
return AsyncStorage.getItem('UID123').then(result => {
assertResult(result);
});
});
test('calls callback with item', done => {
AsyncStorage.getItem('UID123', (err, result) => {
expect(err).toEqual(null);
assertResult(result);
done();
});
});
});
describe('multiGet', () => {
const assertResult = result => {
expect(result).toEqual([
['UID123', JSON.stringify(uid123Object)],
['UID124', JSON.stringify(uid124Object)]
]);
};
test('promise of items', () => {
return AsyncStorage.multiGet(['UID123', 'UID124']).then(result => {
assertResult(result);
});
});
test('calls callback with items', done => {
AsyncStorage.multiGet(['UID123', 'UID124'], (err, result) => {
expect(err).toEqual(null);
assertResult(result);
done();
});
});
});
describe('setItem', () => {
const assertResult = () => {
expect(mockLocalStorage.getItem('UID123')).toEqual(JSON.stringify(uid123Object));
};
test('promise after setting item', () => {
return AsyncStorage.setItem('UID123', JSON.stringify(uid123Object)).then(() => {
assertResult();
});
});
test('calls callback after setting item', done => {
AsyncStorage.setItem('UID123', JSON.stringify(uid123Object), (err, result) => {
expect(err).toEqual(null);
assertResult();
done();
});
});
});
describe('multiSet', () => {
const assertResult = result => {
expect(mockLocalStorage.getItem('UID123')).toEqual(JSON.stringify(uid123Object));
expect(mockLocalStorage.getItem('UID124')).toEqual(JSON.stringify(uid124Object));
};
test('promise after setting items', () => {
return AsyncStorage.multiSet([
['UID123', JSON.stringify(uid123Object)],
['UID124', JSON.stringify(uid124Object)]
]).then(() => {
assertResult();
});
});
test('calls callback after setting items', done => {
AsyncStorage.multiSet(
[['UID123', JSON.stringify(uid123Object)], ['UID124', JSON.stringify(uid124Object)]],
(err, result) => {
expect(err).toEqual(null);
expect(result).toMatchSnapshot();
window.localStorage = originalLocalStorage;
assertResult();
done();
}
);
});
});
describe('mergeItem', () => {
const assertResult = () => {
expect(JSON.parse(mockLocalStorage.getItem('UID123'))).toMatchSnapshot();
};
test('promise after setting item', () => {
return AsyncStorage.mergeItem('UID123', JSON.stringify(uid123Delta)).then(() => {
assertResult();
});
});
test('calls callback after setting item', done => {
AsyncStorage.mergeItem('UID123', JSON.stringify(uid123Delta), (err, result) => {
expect(err).toEqual(null);
assertResult();
done();
});
});
});
describe('multiMerge', () => {
const assertResult = result => {
expect(JSON.parse(mockLocalStorage.getItem('UID123'))).toMatchSnapshot();
expect(JSON.parse(mockLocalStorage.getItem('UID124'))).toMatchSnapshot();
};
test('promise after setting items', () => {
return AsyncStorage.multiMerge([
['UID123', JSON.stringify(uid123Delta)],
['UID124', JSON.stringify(uid123Delta)]
]).then(() => {
assertResult();
});
});
test('calls callback after setting items', done => {
AsyncStorage.multiMerge(
[['UID123', JSON.stringify(uid123Delta)], ['UID124', JSON.stringify(uid123Delta)]],
(err, result) => {
expect(err).toEqual(null);
assertResult();
done();
}
);
});
});
describe('removeItem', () => {
const assertResult = () => {
expect(mockLocalStorage.getItem('UID123')).toBeUndefined();
};
test('promise after setting item', () => {
return AsyncStorage.removeItem('UID123').then(() => {
assertResult();
});
});
test('calls callback after setting item', done => {
AsyncStorage.removeItem('UID123', (err, result) => {
expect(err).toEqual(null);
assertResult();
done();
});
});
});
describe('multiRemove', () => {
const assertResult = result => {
expect(mockLocalStorage.getItem('UID123')).toBeUndefined();
expect(mockLocalStorage.getItem('UID124')).toBeUndefined();
};
test('promise after setting items', () => {
return AsyncStorage.multiRemove(['UID123', 'UID124']).then(() => {
assertResult();
});
});
test('calls callback after setting items', done => {
AsyncStorage.multiRemove(['UID123', 'UID124'], (err, result) => {
expect(err).toEqual(null);
assertResult();
done();
});
});
});
});

View File

@@ -13,66 +13,69 @@ const mergeLocalStorageItem = (key, value) => {
window.localStorage.setItem(key, nextValue);
};
const createPromise = (getValue, callback) => {
return new Promise((resolve, reject) => {
try {
const value = getValue();
if (callback) {
callback(null, value);
}
resolve(value);
} catch (err) {
if (callback) {
callback(err);
}
reject(err);
}
});
};
const createPromiseAll = (promises, callback, processResult) => {
return Promise.all(promises).then(
result => {
const value = processResult ? processResult(result) : null;
callback && callback(null, value);
return Promise.resolve(value);
},
errors => {
callback && callback(errors);
return Promise.reject(errors);
}
);
};
class AsyncStorage {
/**
* Erases *all* AsyncStorage for the domain.
*/
static clear() {
return new Promise((resolve, reject) => {
try {
window.localStorage.clear();
resolve(null);
} catch (err) {
reject(err);
}
});
static clear(callback) {
return createPromise(() => {
window.localStorage.clear();
}, callback);
}
/**
* Gets *all* keys known to the app, for all callers, libraries, etc.
*/
static getAllKeys() {
return new Promise((resolve, reject) => {
try {
const numberOfKeys = window.localStorage.length;
const keys = [];
for (let i = 0; i < numberOfKeys; i += 1) {
const key = window.localStorage.key(i);
keys.push(key);
}
resolve(keys);
} catch (err) {
reject(err);
static getAllKeys(callback) {
return createPromise(() => {
const numberOfKeys = window.localStorage.length;
const keys = [];
for (let i = 0; i < numberOfKeys; i += 1) {
const key = window.localStorage.key(i);
keys.push(key);
}
});
return keys;
}, callback);
}
/**
* Fetches `key` value.
*/
static getItem(key: string) {
return new Promise((resolve, reject) => {
try {
const value = window.localStorage.getItem(key);
resolve(value);
} catch (err) {
reject(err);
}
});
}
/**
* Merges existing value with input value, assuming they are stringified JSON.
*/
static mergeItem(key: string, value: string) {
return new Promise((resolve, reject) => {
try {
mergeLocalStorageItem(key, value);
resolve(null);
} catch (err) {
reject(err);
}
});
static getItem(key: string, callback) {
return createPromise(() => {
return window.localStorage.getItem(key);
}, callback);
}
/**
@@ -81,13 +84,37 @@ class AsyncStorage {
*
* multiGet(['k1', 'k2']) -> [['k1', 'val1'], ['k2', 'val2']]
*/
static multiGet(keys: Array<string>) {
static multiGet(keys: Array<string>, callback) {
const promises = keys.map(key => AsyncStorage.getItem(key));
const processResult = result => result.map((value, i) => [keys[i], value]);
return createPromiseAll(promises, callback, processResult);
}
return Promise.all(promises).then(
result => Promise.resolve(result.map((value, i) => [keys[i], value])),
error => Promise.reject(error)
);
/**
* Sets `value` for `key`.
*/
static setItem(key: string, value: string, callback) {
return createPromise(() => {
window.localStorage.setItem(key, value);
}, callback);
}
/**
* Takes an array of key-value array pairs.
* multiSet([['k1', 'val1'], ['k2', 'val2']])
*/
static multiSet(keyValuePairs: Array<Array<string>>, callback) {
const promises = keyValuePairs.map(item => AsyncStorage.setItem(item[0], item[1]));
return createPromiseAll(promises, callback);
}
/**
* Merges existing value with input value, assuming they are stringified JSON.
*/
static mergeItem(key: string, value: string, callback) {
return createPromise(() => {
mergeLocalStorageItem(key, value);
}, callback);
}
/**
@@ -96,57 +123,26 @@ class AsyncStorage {
*
* multiMerge([['k1', 'val1'], ['k2', 'val2']])
*/
static multiMerge(keyValuePairs: Array<Array<string>>) {
static multiMerge(keyValuePairs: Array<Array<string>>, callback) {
const promises = keyValuePairs.map(item => AsyncStorage.mergeItem(item[0], item[1]));
return Promise.all(promises).then(() => Promise.resolve(null), error => Promise.reject(error));
}
/**
* Delete all the keys in the `keys` array.
*/
static multiRemove(keys: Array<string>) {
const promises = keys.map(key => AsyncStorage.removeItem(key));
return Promise.all(promises).then(() => Promise.resolve(null), error => Promise.reject(error));
}
/**
* Takes an array of key-value array pairs.
* multiSet([['k1', 'val1'], ['k2', 'val2']])
*/
static multiSet(keyValuePairs: Array<Array<string>>) {
const promises = keyValuePairs.map(item => AsyncStorage.setItem(item[0], item[1]));
return Promise.all(promises).then(() => Promise.resolve(null), error => Promise.reject(error));
return createPromiseAll(promises, callback);
}
/**
* Removes a `key`
*/
static removeItem(key: string) {
return new Promise((resolve, reject) => {
try {
window.localStorage.removeItem(key);
resolve(null);
} catch (err) {
reject(err);
}
});
static removeItem(key: string, callback) {
return createPromise(() => {
return window.localStorage.removeItem(key);
}, callback);
}
/**
* Sets `value` for `key`.
* Delete all the keys in the `keys` array.
*/
static setItem(key: string, value: string) {
return new Promise((resolve, reject) => {
try {
window.localStorage.setItem(key, value);
resolve(null);
} catch (err) {
reject(err);
}
});
static multiRemove(keys: Array<string>, callback) {
const promises = keys.map(key => AsyncStorage.removeItem(key));
return createPromiseAll(promises, callback);
}
}

View File

@@ -1,19 +1,43 @@
/* global window */
class Clipboard {
static isSupported() {
return (
typeof document.queryCommandSupported === 'function' && document.queryCommandSupported('copy')
);
}
static getString() {
return Promise.resolve('');
}
static setString(text) {
let success = false;
const textField = document.createElement('textarea');
textField.innerText = text;
document.body.appendChild(textField);
textField.select();
// add the text to a hidden node
const node = document.createElement('span');
node.textContent = text;
node.style.position = 'absolute';
node.style.opacity = '0';
document.body.appendChild(node);
// select the text
const selection = window.getSelection();
selection.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(node);
selection.addRange(range);
// attempt to copy
try {
document.execCommand('copy');
success = true;
} catch (e) {}
document.body.removeChild(textField);
// remove selection and node
selection.removeAllRanges();
document.body.removeChild(node);
return success;
}
}

View File

@@ -28,6 +28,7 @@ const Linking = {
* https://mathiasbynens.github.io/rel-noopener/
*/
const iframeOpen = url => {
const noOpener = url.indexOf('mailto:') !== 0;
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
@@ -36,7 +37,7 @@ const iframeOpen = url => {
const script = iframeDoc.createElement('script');
script.text = `
window.parent = null; window.top = null; window.frameElement = null;
var child = window.open("${url}"); child.opener = null;
var child = window.open("${url}"); ${noOpener && 'child.opener = null'};
`;
iframeDoc.body.appendChild(script);
document.body.removeChild(iframe);

View File

@@ -299,7 +299,8 @@ var PanResponder = {
PanResponder._initializeGestureState(gestureState);
}
} else if (
e.nativeEvent.originalEvent && e.nativeEvent.originalEvent.type === 'mousedown'
e.nativeEvent.originalEvent &&
e.nativeEvent.originalEvent.type === 'mousedown'
) {
PanResponder._initializeGestureState(gestureState);
}

View File

@@ -6,13 +6,15 @@ import createReactDOMStyle from './createReactDOMStyle';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import I18nManager from '../I18nManager';
import mapKeyValue from '../../modules/mapKeyValue';
import prefixInlineStyles from './prefixInlineStyles';
import i18nStyle from './i18nStyle';
import { prefixInlineStyles } from '../../modules/prefixStyles';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
import StyleManager from './StyleManager';
const emptyObject = {};
const createCacheKey = id => {
const prefix = I18nManager.isRTL ? 'rtl' : 'ltr';
const prefix = 'rn';
return `${prefix}-${id}`;
};
@@ -20,7 +22,7 @@ const classListToString = list => list.join(' ').trim();
class StyleRegistry {
constructor() {
this.cache = {};
this.cache = { ltr: {}, rtl: {} };
this.styleManager = new StyleManager();
}
@@ -33,35 +35,43 @@ class StyleRegistry {
*/
register(flatStyle) {
const id = ReactNativePropRegistry.register(flatStyle);
const key = createCacheKey(id);
const style = createReactDOMStyle(flatStyle);
const classList = mapKeyValue(style, (prop, value) => {
if (value != null) {
return this.styleManager.setDeclaration(prop, value);
}
});
const className = classList.join(' ').trim();
this.cache[key] = { classList, className };
this._registerById(id);
return id;
}
_registerById(id) {
const dir = I18nManager.isRTL ? 'rtl' : 'ltr';
if (!this.cache[dir][id]) {
const style = flattenStyle(id);
const domStyle = createReactDOMStyle(i18nStyle(style));
Object.keys(domStyle).forEach(styleProp => {
const value = domStyle[styleProp];
if (value != null) {
this.styleManager.setDeclaration(styleProp, value);
}
});
this.cache[dir][id] = true;
}
}
/**
* Resolves a React Native style object to DOM attributes
*/
resolve(reactNativeStyle) {
resolve(reactNativeStyle, options = emptyObject) {
if (!reactNativeStyle) {
return undefined;
}
// fast and cachable
if (typeof reactNativeStyle === 'number') {
this._registerById(reactNativeStyle);
const key = createCacheKey(reactNativeStyle);
return this._resolveStyleIfNeeded(key, reactNativeStyle);
return this._resolveStyleIfNeeded(reactNativeStyle, options, key);
}
// resolve a plain RN style object
if (!Array.isArray(reactNativeStyle)) {
return this._resolveStyle(reactNativeStyle);
return this._resolveStyle(reactNativeStyle, options);
}
// flatten the style array
@@ -70,55 +80,72 @@ class StyleRegistry {
const flatArray = flattenArray(reactNativeStyle);
let isArrayOfNumbers = true;
for (let i = 0; i < flatArray.length; i++) {
if (typeof flatArray[i] !== 'number') {
const id = flatArray[i];
if (typeof id !== 'number') {
isArrayOfNumbers = false;
break;
} else {
this._registerById(id);
}
}
const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
return this._resolveStyleIfNeeded(key, flatArray);
return this._resolveStyleIfNeeded(flatArray, options, key);
}
/**
* Resolves a React Native style object to DOM attributes, accounting for
* the existing styles applied to the DOM node
* the existing styles applied to the DOM node.
*
* To determine the next style, some of the existing DOM state must be
* converted back into React Native styles.
*/
resolveStateful(reactNativeStyle, domClassList) {
const previousReactNativeStyle = {};
const preservedClassNames = [];
resolveStateful(rnStyleNext, domStyleProps, options) {
const { classList: rdomClassList, style: rdomStyle } = domStyleProps;
// Convert the existing classList to a React Native style and preserve any
// unrecognized classNames.
domClassList.forEach(className => {
const { prop, value } = this.styleManager.getDeclaration(className);
if (prop) {
previousReactNativeStyle[prop] = value;
} else {
preservedClassNames.push(className);
// Convert the DOM classList back into a React Native form
// Preserves unrecognized class names.
const { classList: rnClassList, style: rnStyle } = rdomClassList.reduce(
(styleProps, className) => {
const { prop, value } = this.styleManager.getDeclaration(className);
if (prop) {
styleProps.style[prop] = value;
} else {
styleProps.classList.push(className);
}
return styleProps;
},
{ classList: [], style: {} }
);
// Create next DOM style props from current and next RN styles
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 => {
const { prop } = this.styleManager.getDeclaration(className);
if (style[prop]) {
style[prop] = '';
}
});
// Resolve the two React Native styles.
const { classList, style = {} } = this.resolve([previousReactNativeStyle, reactNativeStyle]);
// Next inline styles take priority over current inline styles
Object.assign(style, rdomStyleNext);
// Because this is used in stateful operations we need to remove any
// existing inline styles that would override the classNames.
classList.forEach(className => {
const { prop } = this.styleManager.getDeclaration(className);
style[prop] = null;
});
// Add the current class names not managed by React Native
const className = classListToString(rdomClassListNext.concat(rnClassList));
classList.push(preservedClassNames);
const className = classListToString(classList);
return { className, style };
}
/**
* 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) => {
@@ -150,15 +177,16 @@ class StyleRegistry {
/**
* Caching layer over 'resolveStyle'
*/
_resolveStyleIfNeeded(key, style) {
_resolveStyleIfNeeded(style, options, key) {
const dir = I18nManager.isRTL ? 'rtl' : 'ltr';
if (key) {
if (!this.cache[key]) {
if (!this.cache[dir][key]) {
// slow: convert style object to props and cache
this.cache[key] = this._resolveStyle(style);
this.cache[dir][key] = this._resolveStyle(style, options);
}
return this.cache[key];
return this.cache[dir][key];
}
return this._resolveStyle(style);
return this._resolveStyle(style, options);
}
}

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);
@@ -58,19 +69,37 @@ describe('apis/StyleSheet/StyleRegistry', () => {
});
});
test('resolveStateful', () => {
// generate a classList to act as pre-existing DOM state
const mockStyle = styleRegistry.register({
borderWidth: 0,
borderColor: 'red',
width: 100
describe('resolveStateful', () => {
test('preserves unrelated class names', () => {
const domStyleProps = { classList: ['unknown-class-1', 'unknown-class-2'], style: {} };
const domStyleNextProps = styleRegistry.resolveStateful({}, domStyleProps);
expect(domStyleNextProps).toMatchSnapshot();
});
const { classList: domClassList } = styleRegistry.resolve(mockStyle);
domClassList.unshift('external-className');
expect(domClassList).toMatchSnapshot();
// test result
const result = styleRegistry.resolveStateful({ borderWidth: 1, opacity: 1 }, domClassList);
expect(result).toMatchSnapshot();
test('preserves unrelated inline styles', () => {
const domStyleProps = { classList: [], style: { fontSize: '20px' } };
const domStyleNextProps = styleRegistry.resolveStateful({ opacity: 1 }, domStyleProps);
expect(domStyleNextProps).toMatchSnapshot();
});
test('next class names have priority over current inline styles', () => {
const domStyleProps = { classList: [], style: { opacity: 0.5 } };
const nextStyle = styleRegistry.register({ opacity: 1 });
const domStyleNextProps = styleRegistry.resolveStateful(nextStyle, domStyleProps);
expect(domStyleNextProps).toMatchSnapshot();
});
test('next inline styles have priority over current inline styles', () => {
// 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)' }
};
const domStyleNextProps = styleRegistry.resolveStateful(
{ opacity: 1, transform: [{ scale: 2 }] },
domStyleProps
);
expect(domStyleNextProps).toMatchSnapshot();
});
});
});

View File

@@ -1,5 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apis/StyleSheet/StyleRegistry resolve with register before RTL, resolves to className 1`] = `
Object {
"classList": Array [
"rn-marginRight-zso239",
"rn-right-1bnbe1j",
"rn-textAlign-1ff274t",
],
"className": "rn-marginRight-zso239 rn-right-1bnbe1j rn-textAlign-1ff274t",
}
`;
exports[`apis/StyleSheet/StyleRegistry resolve with register, resolves to className 1`] = `
Object {
"classList": Array [
@@ -192,35 +203,39 @@ Object {
}
`;
exports[`apis/StyleSheet/StyleRegistry resolveStateful 1`] = `
Array [
"external-className",
"rn-borderTopColor-1gxhl28",
"rn-borderRightColor-knoah9",
"rn-borderBottomColor-1ani3fp",
"rn-borderLeftColor-ribj9x",
"rn-borderTopWidth-13yce4e",
"rn-borderRightWidth-fnigne",
"rn-borderBottomWidth-ndvcnb",
"rn-borderLeftWidth-gxnn5r",
"rn-width-b8lwoo",
]
`;
exports[`apis/StyleSheet/StyleRegistry resolveStateful 2`] = `
exports[`apis/StyleSheet/StyleRegistry resolveStateful next class names have priority over current inline styles 1`] = `
Object {
"className": "rn-borderBottomColor-1ani3fp rn-borderBottomWidth-ndvcnb rn-borderLeftColor-ribj9x rn-borderLeftWidth-gxnn5r rn-borderRightColor-knoah9 rn-borderRightWidth-fnigne rn-borderTopColor-1gxhl28 rn-borderTopWidth-13yce4e rn-width-b8lwoo external-className",
"className": "rn-opacity-6dt33c",
"style": Object {
"borderBottomColor": null,
"borderBottomWidth": null,
"borderLeftColor": null,
"borderLeftWidth": null,
"borderRightColor": null,
"borderRightWidth": null,
"borderTopColor": null,
"borderTopWidth": null,
"opacity": 1,
"width": null,
"opacity": "",
},
}
`;
exports[`apis/StyleSheet/StyleRegistry resolveStateful next inline styles have priority over current inline styles 1`] = `
Object {
"className": "",
"style": Object {
"WebkitTransform": "scale(2)",
"opacity": 1,
"transform": "scale(2)",
},
}
`;
exports[`apis/StyleSheet/StyleRegistry resolveStateful preserves unrelated class names 1`] = `
Object {
"className": "unknown-class-1 unknown-class-2",
"style": Object {},
}
`;
exports[`apis/StyleSheet/StyleRegistry resolveStateful preserves unrelated inline styles 1`] = `
Object {
"className": "",
"style": Object {
"fontSize": "20px",
"opacity": 1,
},
}
`;

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

@@ -43,17 +43,6 @@ 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;
};
const swapLeftRight = (value: String): String => {
return value === 'left' ? 'right' : value === 'right' ? 'left' : value;
};
@@ -71,16 +60,16 @@ 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] = style[prop].map(flipTransform);
nextStyle[prop] = value;
nextStyle[prop].width = additiveInverse(value.width);
} 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

@@ -206,7 +206,8 @@ class ListView extends Component {
totalIndex++;
if (
renderSeparator && (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)
renderSeparator &&
(rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)
) {
const adjacentRowHighlighted =
this.state.highlightedRow.sectionID === sectionID &&

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

@@ -143,11 +143,7 @@ const ScrollView = createReactClass({
children={this.props.children}
collapsable={false}
ref={this._setInnerViewRef}
style={[
styles.contentContainer,
horizontal && styles.contentContainerHorizontal,
contentContainerStyle
]}
style={[horizontal && styles.contentContainerHorizontal, contentContainerStyle]}
/>
);
@@ -232,16 +228,17 @@ const styles = StyleSheet.create({
flex: 1,
overflowX: 'hidden',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch'
WebkitOverflowScrolling: 'touch',
// Enable hardware compositing in modern browsers.
// Creates a new layer with its own backing surface that can significantly
// improve scroll performance.
transform: [{ translateZ: 0 }]
},
baseHorizontal: {
flexDirection: 'row',
overflowX: 'auto',
overflowY: 'hidden'
},
contentContainer: {
transform: [{ translateZ: 0 }]
},
contentContainerHorizontal: {
flexDirection: 'row'
}

View File

@@ -0,0 +1,14 @@
import { Component } from 'react';
class StatusBar extends Component {
static setBackgroundColor() {}
static setBarStyle() {}
static setHidden() {}
static setNetworkActivityIndicatorVisible() {}
static setTranslucent() {}
render() {
return null;
}
}
module.exports = StatusBar;

View File

@@ -25,6 +25,7 @@ const TextOnlyStylePropTypes = {
textShadowRadius: number,
writingDirection: WritingDirectionPropType,
/* @platform web */
textIndent: numberOrString,
textOverflow: string,
textRendering: oneOf(['auto', 'geometricPrecision', 'optimizeLegibility', 'optimizeSpeed']),
textTransform: oneOf(['capitalize', 'lowercase', 'none', 'uppercase']),

View File

@@ -147,6 +147,7 @@ class TextInput extends Component {
style,
/* eslint-disable */
blurOnSubmit,
caretHidden,
clearButtonMode,
clearTextOnFocus,
dataDetectorTypes,
@@ -163,6 +164,8 @@ class TextInput extends Component {
selection,
selectionColor,
selectTextOnFocus,
textBreakStrategy,
underlineColorAndroid,
/* eslint-enable */
...otherProps
} = this.props;

View File

@@ -18,7 +18,7 @@ module.exports = {
* @platform unsupported
*/
elevation: number,
/*
/**
* @platform web
*/
backgroundAttachment: string,
@@ -29,8 +29,11 @@ module.exports = {
backgroundRepeat: string,
backgroundSize: string,
boxShadow: string,
clip: string,
cursor: string,
filter: string,
outline: string,
outlineColor: ColorPropType,
perspective: oneOfType([number, string]),
perspectiveOrigin: string,
transitionDelay: string,
@@ -39,5 +42,6 @@ module.exports = {
transitionTimingFunction: string,
userSelect: string,
willChange: string,
WebkitMaskImage: string,
WebkitOverflowScrolling: oneOf(['auto', 'touch'])
};

View File

@@ -1,53 +1,57 @@
import createDOMElement from './modules/createDOMElement';
import findNodeHandle from './modules/findNodeHandle';
import modality from './modules/modality';
import NativeModules from './modules/NativeModules';
import processColor from './modules/processColor';
import { render, unmountComponentAtNode } from 'react-dom';
import {
// top-level API
findNodeHandle,
render,
unmountComponentAtNode,
// APIs
import Animated from './apis/Animated';
import AppRegistry from './apis/AppRegistry';
import AppState from './apis/AppState';
import AsyncStorage from './apis/AsyncStorage';
import BackAndroid from './apis/BackAndroid';
import Clipboard from './apis/Clipboard';
import Dimensions from './apis/Dimensions';
import Easing from 'animated/lib/Easing';
import I18nManager from './apis/I18nManager';
import InteractionManager from './apis/InteractionManager';
import Linking from './apis/Linking';
import NetInfo from './apis/NetInfo';
import PanResponder from './apis/PanResponder';
import PixelRatio from './apis/PixelRatio';
import Platform from './apis/Platform';
import StyleSheet from './apis/StyleSheet';
import UIManager from './apis/UIManager';
import Vibration from './apis/Vibration';
// modules
createDOMElement,
NativeModules,
processColor,
// components
import ActivityIndicator from './components/ActivityIndicator';
import Button from './components/Button';
import Image from './components/Image';
import ListView from './components/ListView';
import ProgressBar from './components/ProgressBar';
import ScrollView from './components/ScrollView';
import Switch from './components/Switch';
import Text from './components/Text';
import TextInput from './components/TextInput';
import Touchable from './components/Touchable/Touchable';
import TouchableHighlight from './components/Touchable/TouchableHighlight';
import TouchableOpacity from './components/Touchable/TouchableOpacity';
import TouchableWithoutFeedback from './components/Touchable/TouchableWithoutFeedback';
import View from './components/View';
// APIs
Animated,
AppRegistry,
AppState,
AsyncStorage,
BackAndroid,
Clipboard,
Dimensions,
Easing,
I18nManager,
InteractionManager,
Linking,
NetInfo,
PanResponder,
PixelRatio,
Platform,
StyleSheet,
UIManager,
Vibration,
// propTypes
import ColorPropType from './propTypes/ColorPropType';
import EdgeInsetsPropType from './propTypes/EdgeInsetsPropType';
import PointPropType from './propTypes/PointPropType';
import ViewPropTypes from './components/View/ViewPropTypes';
// components
ActivityIndicator,
Button,
Image,
ListView,
ProgressBar,
ScrollView,
StatusBar,
Switch,
Text,
TextInput,
Touchable,
TouchableHighlight,
TouchableOpacity,
TouchableWithoutFeedback,
View,
modality();
// propTypes
ColorPropType,
EdgeInsetsPropType,
PointPropType,
ViewPropTypes
} from './module';
const ReactNative = {
// top-level API
@@ -87,6 +91,7 @@ const ReactNative = {
ListView,
ProgressBar,
ScrollView,
StatusBar,
Switch,
Text,
TextInput,

50
src/module.js Normal file
View File

@@ -0,0 +1,50 @@
export { default as createDOMElement } from './modules/createDOMElement';
export { default as findNodeHandle } from './modules/findNodeHandle';
export { default as NativeModules } from './modules/NativeModules';
export { default as processColor } from './modules/processColor';
export { render, unmountComponentAtNode } from 'react-dom';
// APIs
export { default as Animated } from './apis/Animated';
export { default as AppRegistry } from './apis/AppRegistry';
export { default as AppState } from './apis/AppState';
export { default as AsyncStorage } from './apis/AsyncStorage';
export { default as BackAndroid } from './apis/BackAndroid';
export { default as Clipboard } from './apis/Clipboard';
export { default as Dimensions } from './apis/Dimensions';
export { default as Easing } from 'animated/lib/Easing';
export { default as I18nManager } from './apis/I18nManager';
export { default as InteractionManager } from './apis/InteractionManager';
export { default as Linking } from './apis/Linking';
export { default as NetInfo } from './apis/NetInfo';
export { default as PanResponder } from './apis/PanResponder';
export { default as PixelRatio } from './apis/PixelRatio';
export { default as Platform } from './apis/Platform';
export { default as StyleSheet } from './apis/StyleSheet';
export { default as UIManager } from './apis/UIManager';
export { default as Vibration } from './apis/Vibration';
// components
export { default as ActivityIndicator } from './components/ActivityIndicator';
export { default as Button } from './components/Button';
export { default as Image } from './components/Image';
export { default as ListView } from './components/ListView';
export { default as ProgressBar } from './components/ProgressBar';
export { default as ScrollView } from './components/ScrollView';
export { default as StatusBar } from './components/StatusBar';
export { default as Switch } from './components/Switch';
export { default as Text } from './components/Text';
export { default as TextInput } from './components/TextInput';
export { default as Touchable } from './components/Touchable/Touchable';
export { default as TouchableHighlight } from './components/Touchable/TouchableHighlight';
export { default as TouchableOpacity } from './components/Touchable/TouchableOpacity';
export {
default as TouchableWithoutFeedback
} from './components/Touchable/TouchableWithoutFeedback';
export { default as View } from './components/View';
// propTypes
export { default as ColorPropType } from './propTypes/ColorPropType';
export { default as EdgeInsetsPropType } from './propTypes/EdgeInsetsPropType';
export { default as PointPropType } from './propTypes/PointPropType';
export { default as ViewPropTypes } from './components/View/ViewPropTypes';

View File

@@ -6,6 +6,7 @@ const accessibilityComponentTypeToRole = {
const accessibilityTraitsToRole = {
adjustable: 'slider',
button: 'button',
header: 'heading',
image: 'img',
link: 'link',
none: 'presentation',

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()`.
@@ -67,12 +71,26 @@ const NativeMethodsMixin = {
* the initial styles from the DOM node and merge them with incoming props.
*/
setNativeProps(nativeProps: Object) {
// DOM state
// Copy of existing DOM state
const node = findNodeHandle(this);
const classList = [...node.classList];
const nodeStyle = node.style;
const classList = Array.prototype.slice.call(node.classList);
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 };
const domProps = createDOMProps(nativeProps, style =>
StyleRegistry.resolveStateful(style, classList)
// Next DOM state
const domProps = createDOMProps(i18nStyle(nativeProps), style =>
StyleRegistry.resolveStateful(style, domStyleProps, { i18n: false })
);
UIManager.updateView(node, domProps, this);
}

View File

@@ -75,7 +75,10 @@ const applyLayout = Component => {
if (!this._isMounted) return;
if (
layout.x !== x || layout.y !== y || layout.width !== width || layout.height !== height
layout.x !== x ||
layout.y !== y ||
layout.width !== width ||
layout.height !== height
) {
this._layoutState = { x, y, width, height };
const nativeEvent = { layout: this._layoutState };

View File

@@ -2,9 +2,12 @@ import '../injectResponderEventPlugin';
import AccessibilityUtil from '../AccessibilityUtil';
import createDOMProps from '../createDOMProps';
import modality from '../modality';
import normalizeNativeEvent from '../normalizeNativeEvent';
import React from 'react';
modality();
const eventHandlerNames = {
onClick: true,
onClickCapture: true,

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;

View File

@@ -14,7 +14,6 @@ const LayoutPropTypes = {
]),
alignItems: oneOf(['baseline', 'center', 'flex-end', 'flex-start', 'stretch']),
alignSelf: oneOf(['auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch']),
aspectRatio: number,
backfaceVisibility: hiddenOrVisible,
borderWidth: numberOrString,
borderBottomWidth: numberOrString,
@@ -61,7 +60,26 @@ const LayoutPropTypes = {
top: numberOrString,
visibility: hiddenOrVisible,
width: numberOrString,
zIndex: number
zIndex: number,
/**
* @platform unsupported
*/
aspectRatio: number,
/**
* @platform web
*/
gridAutoColumns: string,
gridAutoFlow: string,
gridAutoRows: string,
gridColumnEnd: string,
gridColumnGap: string,
gridColumnStart: string,
gridRowEnd: string,
gridRowGap: string,
gridRowStart: string,
gridTemplateColumns: string,
gridTemplateRows: string,
gridTemplateAreas: string
};
module.exports = LayoutPropTypes;

View File

@@ -158,7 +158,9 @@ function dangerousStyleValue(name, value, component) {
var isNonNumeric = isNaN(value);
if (
isNonNumeric || value === 0 || (unitlessNumbers.hasOwnProperty(name) && unitlessNumbers[name])
isNonNumeric ||
value === 0 ||
(unitlessNumbers.hasOwnProperty(name) && unitlessNumbers[name])
) {
return '' + value; // cast to string
}

View File

@@ -931,9 +931,9 @@ babel-plugin-transform-react-jsx@^6.3.13, babel-plugin-transform-react-jsx@^6.5.
babel-plugin-syntax-jsx "^6.8.0"
babel-runtime "^6.0.0"
babel-plugin-transform-react-remove-prop-types@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.0.tgz#f63840e7953563d661be8c647b094d74d7363f17"
babel-plugin-transform-react-remove-prop-types@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.2.tgz#97e3cab4d05938df91e16c0473737ef6c575a2e6"
babel-plugin-transform-regenerator@6.16.1:
version "6.16.1"
@@ -1917,19 +1917,13 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
date-now@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-1.0.1.tgz#bb7d086438debe4182a485fb3df3fbfb99d6153c"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
debounce:
version "1.0.0"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.0.0.tgz#0948af513d2e4ce407916f8506a423d3f9cf72d8"
dependencies:
date-now "1.0.1"
debounce@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.0.2.tgz#503cc674d8d7f737099664fb75ddbd36b9626dc6"
debug@2.6.1:
version "2.6.1"
@@ -2330,9 +2324,9 @@ escope@^3.6.0:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-config-prettier@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-1.6.0.tgz#56e53a8eb461c06eced20cec40d765c185100fd5"
eslint-config-prettier@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-1.7.0.tgz#cda3ce22df1e852daa9370f1f3446e8b8a02ce44"
dependencies:
get-stdin "^5.0.1"
@@ -5012,9 +5006,9 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.1.0.tgz#9d6ad005703efefa66b6999b8916bfc6afeaf9f8"
prettier@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.2.2.tgz#22d17c1132faaaea1f1d4faea31f19f7a1959f3e"
dependencies:
ast-types "0.9.8"
babel-code-frame "6.22.0"