Compare commits

..

47 Commits

Author SHA1 Message Date
Nicolas Gallagher
e473031321 Add new Text render benchmark 2019-01-10 13:12:47 -08:00
Nicolas Gallagher
97e2fb53a1 Add a note about referrer tracking 2019-01-10 11:41:45 -08:00
atp
7b79c27bd0 [add] Text support for wordBreak style
Close #1200
2019-01-10 11:41:45 -08:00
Nicolas Gallagher
18248f1229 [change] attribute/string prefix to 'r-' 2019-01-10 11:41:44 -08:00
Nicolas Gallagher
45ccd9517f Use default array sort function 2019-01-10 11:41:44 -08:00
Nicolas Gallagher
550eca672f [change] Introduce static CSS base rules for core primitives
This patch addresses 2 related issues:

1) Browser layout times in Chrome increase when elements use a lot of CSS class
names. This begins to add up for larger trees.

2) React Native supports passing 'null' values for styles. This can remove base
styles defined using 'StyleSheet' in the implementation of components like View
and Text.

Both of these issues can be avoided, and some runtime logic and computation
removed, by moving the base styles to static CSS rules. Comparisons of the
"benchmark" UI tests (which only render View) indicate that total times in
Chrome are reduced by ~20% with almost all of those savings coming from a
~33% reduction in layout-related timings.

To avoid style conflicts, static CSS rules are inserted before atomic CSS rules. The modality-related focus style is now inserted into the in-memory style sheet, making it available in SSR output.

Ref #1136
Close #1165
2019-01-10 11:41:44 -08:00
Nicolas Gallagher
020c06d3f2 Don't print AppRegistry message in unit tests 2019-01-01 13:46:20 -08:00
Nicolas Gallagher
93eb3f041f [fix] use getBoundingClientRect to measure layout
Fix #1037
Fix #1151
2019-01-01 13:46:20 -08:00
Nicolas Gallagher
77c778a9bf [add] support backdropFilter style
Close #1070
2019-01-01 13:46:19 -08:00
Nicolas Gallagher
05e87ab3f1 Update webpack for examples 2019-01-01 13:46:19 -08:00
Nicolas Gallagher
e7108fd414 Update eslint 2019-01-01 13:46:19 -08:00
Nicolas Gallagher
1183cf36b8 [change] implement TextInput without e.which
e.which is considered deprecated and should be replaced with e.key.

Fix #1190
Close #1193
2019-01-01 13:46:18 -08:00
Nicolas Gallagher
b65743fa66 [change] Compile using Babel 7
Fix #1170
Close #1205
Close #1191
2019-01-01 13:46:18 -08:00
Nicolas Gallagher
b8995c20eb [add] support for accessibilityRole and accessibilityStates
React Native 0.57 introduced 'accessibilityRole' and
'accessibilityStates' as cross-platform accessibility APIs to replace
'accessibilityComponentType' and 'accessibilityTraits' for Android and
iOS.

React Native for Web has supported the 'accessibilityRole' for a while.
This patch maps some of the values defined in React Native to web
equivalents, and continues to allow a larger selection of roles for web
apps. It also adds support for 'accessibilityStates', mapping values to
ARIA states with boolean values and expanding support beyond 'disabled'
and 'selected'.

Fix #1112
Close #1113
2019-01-01 11:58:41 -08:00
Nicolas Gallagher
000b92e707 0.9.13 2018-12-31 17:31:46 -08:00
Nicolas Gallagher
d5f5dbccdb [fix] inline-style-prefixer API update
Fix #1217
2018-12-31 17:22:05 -08:00
Nicolas Gallagher
79456d5920 0.9.12 2018-12-31 10:34:04 -08:00
Nicolas Gallagher
2d1e303a6a [fix] ScrollView with stickyHeaderIndices regression
A ScrollView with stickyHeaderIndices would not render children that
weren't sticky.

Fixes 1e202b6bd5
2018-12-31 10:26:33 -08:00
Nicolas Gallagher
209ff1fa40 0.9.11 2018-12-31 08:23:58 -08:00
Nicolas Gallagher
34d8160a43 [fix] CSS animation insertion for Android 5.1 HuaWei browser
Inserting unprefixed CSS keyframes rules causes a `SYNTAX_ERR: DOM Exception
12` error in Android 5.1. A similar issue with inserting rules containing
vendor-prefixed pseudo-selectors was patched by wrapping rule in `@media all
{}` blocks. This patch removes the media query wrapper from keyframe animations
(as it doesn't prevent the error) and relies on `CSSStyleSheet::insertRule`
being called within a try-catch block.

Fix #1199
Close #1210
2018-12-31 07:47:52 -08:00
Nicolas Gallagher
ada5651be2 Don't minify local benchmarks build 2018-12-24 13:35:21 +00:00
Nicolas Gallagher
9fe9d3c68a Update react-native-web dependencies 2018-12-24 13:03:49 +00:00
krister
1e202b6bd5 [add] ScrollView support for pagingEnabled
Available in browsers that support CSS snappoints.

Fix #1057
Close #1212

Co-authored-by: Nicolas Gallagher <nicolasgallagher@gmail.com>
2018-12-23 18:05:29 +00:00
Robert Sayre
2b77bfd853 [fix] improve style resolver performance
Script time in the benchmark was profiled by adding `console.profile` around
the timings for script time. The call to Array.join in the resolve function
stood out. Since the code already iterates over the array it can run slightly
faster by building the cache key in that loop instead.

Close #1213
2018-12-23 15:07:48 +00:00
Nicolas Gallagher
d0ac55aa4f Update benchmarks dependencies 2018-12-21 21:33:43 +00:00
Nicolas Gallagher
66dd1bd9ef Remove glamor and radium from benchmarks
Glamor is unmaintained. Radium is extremely slow. There is no need to compare
against these libraries.
2018-12-21 21:20:30 +00:00
Nicolas Gallagher
6add18c6f0 0.9.10 2018-12-21 21:04:26 +00:00
krister
30d7c31b65 [fix] ScrollView smooth scrolling
Rely on the `element.scroll()` programmatic API when available (or polyfilled).

Fix #1203
Fix #1173
Close #1208
2018-12-21 20:57:25 +00:00
Raibima Putra
f7e6b43422 Fix docs typo
Close #1209
2018-12-21 20:46:29 +00:00
Nicolas Gallagher
4b3f6efb21 Support style inspection in production 2018-12-10 17:01:23 -08:00
Nicolas Gallagher
85e098deec 0.9.9 2018-12-05 14:46:11 -08:00
Nicolas Gallagher
c949b0145a [fix] TextInput onKeyPress supports Escape key
Fix #1189
2018-11-27 12:15:59 -08:00
Charlie Croom
86b4cf5a51 [add] scaleZ and scale3d style transforms
Web-specific additions similar to the web-specific additions to `translate` styles.

Close #1184
2018-11-27 11:53:04 -08:00
Giuseppe
1b7ce4eec6 [fix] Fix regression in modality refactor
Fixes https://github.com/necolas/react-native-web/pull/1169#issuecomment-440590544

And removes the focus ring for press-after-keyboard edge cases.

Close #1186

Co-authored-by: Nicolas Gallagher <nicolasgallagher@gmail.com>
2018-11-27 11:52:46 -08:00
Nicolas Gallagher
8c8978ff76 0.9.8 2018-11-15 21:40:18 -08:00
Nicolas Gallagher
513b5de881 Partially revert d841db2337
The modification to VirtualizedList is not required now that ScrollView wraps
sticky header items in a View.
2018-11-15 10:26:49 -08:00
Julian Hundeloh
145f80409d [fix] ScrollView dependency on 'style' forwarding for sticky headers
Not every item that may be rendered by a ScrollView will forward 'style', so cloning the item element is not safe. Instead, we can wrap the item in a 'View' and apply the styles for the sticky header to this element.

Close #1175
2018-11-15 10:24:54 -08:00
Julian Hundeloh
6d92cc5ec3 [fix] ListView section error when missing renderHeader
Close #1174
2018-11-15 10:11:54 -08:00
Nicolas Gallagher
ec6458c09d Update some links in README 2018-11-15 10:09:14 -08:00
James Munro
a84c2ac95e Add DataCamp to companies using react-native-web in production
Close #1176
2018-11-15 10:07:26 -08:00
Nicolas Gallagher
75db0e9183 0.9.7 2018-11-12 17:53:16 -08:00
Giuseppe Gurgone
4b9a5fd8b4 [fix] modality performance
Inserting and deleting the CSS ':focus' rule triggers styles recalculation for the
entire DOM tree which results in performance degradation on focus and makes the
keyboard very slow to open in Safari iOS.

This commit fixes the issue by picking up the upstream changes to the W3C
focus-visible polyfill. A class name is applied to elements when they receive
focus via keyboard. The related CSS rule is inserted only once into the style
sheet. This performs much better since the browser needs to recalculate styles
only for the focused element and its small subtree.

Fix #1155
Close #1169
2018-11-12 15:54:07 -08:00
Nicolas Gallagher
b6be677db9 [fix] ref.focus() should focus any element type
Ensure that programmatic focus can be moved to any element. Each
instance of a primitive component type (e.g., `View`, `Text`, etc.)
includes a `focus` method. However, on the web only certain elements can
receive programmatic focus by default: those that can also receive
keyboard focus, e.g., `a`, `button`, `input`, etc. All other element
types must set `tabIndex="-1"` in order to be programmatically focusable
without also being focusable via keyboard or mouse.

Fix #1099
2018-11-10 12:03:44 -08:00
Nicolas Gallagher
91c9457392 Remove reference to create-react-native-app
Close #1167
2018-11-10 11:23:04 -08:00
Nicolas Gallagher
006d315a1a Add example Next.js recipes 2018-11-10 11:07:54 -08:00
Nicolas Gallagher
220eb79357 Fix UIExplorer headings accessibility 2018-11-10 11:05:49 -08:00
Murtaza Raja
5db9a765b0 Fix README install command typo
Close #1163
2018-11-04 18:39:23 -08:00
94 changed files with 4363 additions and 4009 deletions

View File

@@ -1,5 +0,0 @@
{
"presets": [
"./scripts/babel/preset"
]
}

View File

@@ -1,4 +1,11 @@
{
"settings": {
"react": {
"pragma": "React",
"version": "16.6",
"flowVersion": "0.78" // Flow version
}
},
// babel parser to support ES6/7 features
"parser": "babel-eslint",
"parserOptions": {

View File

@@ -1,7 +1,7 @@
language: node_js
node_js:
- "8"
- "10"
before_install:
# Install Yarn

View File

@@ -23,10 +23,11 @@ React Native for Web can also render to HTML and critical CSS on the server
using Node.js.
Who is using React Native in production web apps?
[Twitter](https://mobile.twitter.com), [Major League
Soccer](https://matchcenter.mlssoccer.com),
[Flipkart](https://www.flipkart.com/), Uber, [The
Times](https://github.com/newsuk/times-components).
[Twitter](https://mobile.twitter.com),
[Major League Soccer](https://matchcenter.mlssoccer.com),
[Flipkart](https://twitter.com/naqvitalha/status/969577892991549440),
[Uber](https://www.youtube.com/watch?v=RV9rxrNIxnY),
[The Times](https://github.com/newsuk/times-components), [DataCamp](https://www.datacamp.com/community/tech/porting-practice-to-web-part1).
Browser support: Chrome, Firefox, Edge, Safari 7+, IE 10+.
@@ -75,7 +76,7 @@ Examples of using React Native for Web with other web tools:
* [Docz](https://github.com/pedronauck/docz-plugin-react-native)
* [Gatsby](https://github.com/slorber/gatsby-plugin-react-native-web)
* [Next.js](https://github.com/zeit/next.js/tree/master/examples/with-react-native-web)
* [Next.js](https://github.com/zeit/next.js/tree/master/examples/with-react-native-web) (and [example recipes](https://gist.github.com/necolas/f9034091723f1b279be86c7429eb0c96))
* [Phenomic](https://github.com/phenomic/phenomic/tree/master/examples/react-native-web-app)
* [Razzle](https://github.com/jaredpalmer/razzle/tree/master/examples/with-react-native-web)
* [Storybook](https://github.com/necolas/react-native-web/tree/master/packages/website/storybook/.storybook)
@@ -139,7 +140,7 @@ React Native v0.55
| Picker | ✓ | |
| RefreshControl | ✘ | Not started ([#1027](https://github.com/necolas/react-native-web/issues/1027)). |
| SafeAreaView | ✓ | |
| ScrollView | ✓ | Missing momentum scroll events ([#1021](https://github.com/necolas/react-native-web/issues/1021)) and `pagingEnabled` ([#1057](https://github.com/necolas/react-native-web/issues/1057)). |
| ScrollView | ✓ | Missing momentum scroll events ([#1021](https://github.com/necolas/react-native-web/issues/1021)). |
| SectionList | ✓ | |
| Slider | ✘ | Not started ([#1022](https://github.com/necolas/react-native-web/issues/1022)). |
| StatusBar | (✓) | Mock. No equivalent web APIs. |

7
babel.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['./scripts/babel/preset']
};
};

View File

@@ -35,6 +35,34 @@ using `aria-label`.
</TouchableOpacity>
```
### accessibilityLiveRegion
When components dynamically change we may need to inform the user. The
`accessibilityLiveRegion` property serves this purpose and can be set to
`none`, `polite` and `assertive`. On web, `accessibilityLiveRegion` is
implemented using `aria-live`.
* `none`: Accessibility services should not announce changes to this view.
* `polite`: Accessibility services should announce changes to this view.
* `assertive`: Accessibility services should interrupt ongoing speech to immediately announce changes to this view.
```
<TouchableWithoutFeedback onPress={this._addOne}>
<View style={styles.embedded}>
<Text>Click me</Text>
</View>
</TouchableWithoutFeedback>
<Text accessibilityLiveRegion="polite">
Clicked {this.state.count} times
</Text>
```
In the above example, method `_addOne` changes the `state.count` variable. As
soon as an end user clicks the `TouchableWithoutFeedback`, screen readers
announce text in the `Text` view because of its
`accessibilityLiveRegion="polite"` property.
### accessibilityRole
In some cases, we also want to alert the end user of the type of selected
@@ -49,7 +77,8 @@ element][html-aria-url] and ARIA `role`, where possible. In most cases, both
the element and ARIA `role` are rendered. While this may contradict some ARIA
recommendations, it also helps avoid certain browser bugs, HTML5 conformance
errors, and accessibility anti-patterns (e.g., giving a `heading` role to a
`button` element).
`button` element). On the Web, `accessibilityRole` supports more values than
React Native does for [Andriod and iOS](https://facebook.github.io/react-native/docs/accessibility#accessibilityrole-ios-android).
Straight-forward examples:
@@ -85,33 +114,12 @@ Note: Avoid changing `accessibilityRole` values over time or after user
actions. Generally, accessibility APIs do not provide a means of notifying
assistive technologies of a `role` value change.
### accessibilityLiveRegion
### accessibilityStates
When components dynamically change we may need to inform the user. The
`accessibilityLiveRegion` property serves this purpose and can be set to
`none`, `polite` and `assertive`. On web, `accessibilityLiveRegion` is
implemented using `aria-live`.
* `none`: Accessibility services should not announce changes to this view.
* `polite`: Accessibility services should announce changes to this view.
* `assertive`: Accessibility services should interrupt ongoing speech to immediately announce changes to this view.
```
<TouchableWithoutFeedback onPress={this._addOne}>
<View style={styles.embedded}>
<Text>Click me</Text>
</View>
</TouchableWithoutFeedback>
<Text accessibilityLiveRegion="polite">
Clicked {this.state.count} times
</Text>
```
In the above example, method `_addOne` changes the `state.count` variable. As
soon as an end user clicks the `TouchableWithoutFeedback`, screen readers
announce text in the `Text` view because of its
`accessibilityLiveRegion="polite"` property.
The `accessibilityStates` prop is an array of values used to infer the
analogous ARIA states, e.g., `aria-disabled`, `aria-pressed`, `aria-selected`.
On the Web, `accessibilityStates` supports more values than React Native does
for [Andriod and iOS](https://facebook.github.io/react-native/docs/accessibility#accessibilitystate-ios-android).
### importantForAccessibility

View File

@@ -109,5 +109,5 @@ If you're using `skpm`, you can rely on an [undocumented
feature](https://github.com/sketch-pm/skpm/blob/master/lib/utils/webpackConfig.js)
which will merge your `webpack.config.js`, `.babelrc`, or `package.json` Babel
config into its internal webpack config. The simplest option may be to use the
[babel-plugin-module-alias](https://www.npmjs.com/package/babel-plugin-module-alias)
[babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver)
and configure it in your `package.json`.

View File

@@ -24,20 +24,13 @@ yarn add react-art
## Starter kits
Web: [create-react-app](https://github.com/facebookincubator/create-react-app)
[create-react-app](https://github.com/facebookincubator/create-react-app)
includes built-in support for aliasing `react-native-web` to `react-native`.
```
create-react-app my-app
```
Multi-platform: [create-react-native-app](https://github.com/react-community/create-react-native-app)
includes experimental support for Web.
```
create-react-native-app my-app --with-web-support
```
## Configuring a module bundler
If you have a custom setup, you may choose to configure your module bundler to

View File

@@ -36,7 +36,7 @@ target platform.
What follows is merely an _example_ of one basic way to package a web app using
[Webpack](https://webpack.js.org) and [Babel](https://babeljs.io/). (You can
also the React Native bundler, [Metro](https://github.com/facebook/metro), to
also use the React Native bundler, [Metro](https://github.com/facebook/metro), to
build web apps.)
Packaging web apps is subtly different to packaging React Native apps and is
@@ -54,7 +54,7 @@ from your web app build. To help with this, you can install the following Babel
plugin:
```
yarn install --dev babel-plugin-react-native-web
yarn add --dev babel-plugin-react-native-web
```
Create a `web/webpack.config.js` file:

View File

@@ -148,8 +148,8 @@ styles defined using `StyleSheet.create` will ultimately be rendered using CSS
class names.
React Native for Web introduced a novel strategy to achieve this. Each rule is
broken down into declarations, properties are expanded to their long-form, and
the resulting key-value pairs are mapped to unique "atomic CSS" class names.
broken down into declarations and the resulting key-value pairs are mapped to
unique "atomic CSS" class names.
Input:
@@ -158,7 +158,9 @@ const Box = () => <View style={styles.box} />
const styles = StyleSheet.create({
box: {
margin: 0
backgroundColor: 'red',
height: 100,
width: 100,
}
});
```
@@ -167,13 +169,12 @@ Output:
```html
<style>
.rn-1mnahxq { margin-top: 0px; }
.rn-61z16t { margin-right: 0px; }
.rn-p1pxzi { margin-bottom: 0px; }
.rn-11wrixw { margin-left: 0px; }
.r-1mnahxq { background-color: red; }
.r-p1pxzi { height: 100px; }
.r-61z16t { width: 100px; }
</style>
<div class="rn-156q2ks rn-61z16t rn-p1pxzi rn-11wrixw"></div>
<div class="rn-156q2ks rn-61z16t rn-p1pxzi"></div>
```
This ensures that CSS order doesn't impact rendering and CSS rules are
@@ -226,6 +227,6 @@ handled at the component-level.
### What about using Dev Tools?
React Dev Tools supports inspecting and editing of React Native styles. It's
React Dev Tools supports inspecting of React Native styles. It's
recommended that you rely more on React Dev Tools and live/hot-reloading rather
than inspecting and editing the DOM directly.

View File

@@ -1,12 +1,12 @@
{
"private": true,
"version": "0.9.6",
"version": "0.9.13",
"name": "react-native-web-monorepo",
"scripts": {
"clean": "del ./packages/*/dist",
"compile": "npm-run-all clean -p \"compile:* -- {@}\" --",
"compile:commonjs": "cd packages/react-native-web && BABEL_ENV=commonjs babel src --out-dir dist/cjs --ignore \"**/__tests__\"",
"compile:es": "cd packages/react-native-web && babel src --out-dir dist --ignore \"**/__tests__\"",
"compile:commonjs": "cd packages/react-native-web && BABEL_ENV=commonjs babel --root-mode upward src --out-dir dist/cjs --ignore \"**/__tests__\"",
"compile:es": "cd packages/react-native-web && babel --root-mode upward src --out-dir dist --ignore \"**/__tests__\"",
"benchmarks": "cd packages/benchmarks && yarn build",
"benchmarks:release": "cd packages/benchmarks && yarn release",
"examples": "cd packages/examples && yarn build",
@@ -15,7 +15,7 @@
"website:release": "cd packages/website && yarn release",
"flow": "flow",
"fmt": "prettier --write \"**/*.js\"",
"jest": "BABEL_ENV=commonjs jest --config ./scripts/jest/config.js",
"jest": "jest --config ./scripts/jest/config.js",
"lint": "yarn lint:check --fix",
"lint:check": "eslint packages scripts",
"precommit": "lint-staged",
@@ -25,39 +25,41 @@
"test": "yarn flow && yarn lint:check && yarn jest --runInBand"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.2",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.10",
"babel-preset-env": "^1.6.1",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"babel-preset-react-native": "^4.0.0",
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.2.3",
"@babel/plugin-proposal-object-rest-spread": "^7.2.0",
"@babel/preset-env": "^7.2.3",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^10.0.0",
"babel-jest": "24.0.0-alpha.9",
"babel-loader": "^8.0.0",
"babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.21",
"caniuse-api": "^2.0.0",
"del-cli": "^1.1.0",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.3",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-promise": "^3.7.0",
"eslint": "^5.11.1",
"eslint-config-prettier": "^3.3.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.7.0",
"flow-bin": "^0.63.1",
"glob": "^7.1.2",
"husky": "^0.14.3",
"jest": "^22.4.3",
"inline-style-prefixer": "^5.0.3",
"jest": "24.0.0-alpha.9",
"jest-canvas-mock": "^1.0.2",
"lint-staged": "^7.1.0",
"metro-react-native-babel-preset": "^0.51.0",
"npm-run-all": "^4.1.3",
"prettier": "^1.12.1",
"react": "^16.5.1",
"react-art": "^16.5.1",
"react-dom": "^16.5.1",
"react-test-renderer": "^16.5.1"
"react": "^16.7.0",
"react-art": "^16.7.0",
"react-dom": "^16.7.0",
"react-test-renderer": "^16.7.0"
},
"workspaces": [
"packages/*"

View File

@@ -1,9 +1,10 @@
{
"name": "babel-plugin-react-native-web",
"version": "0.9.6",
"version": "0.9.13",
"description": "Babel plugin for React Native for Web",
"main": "index.js",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-plugin-tester": "^5.0.0"
},
"author": "Nicolas Gallagher",

View File

@@ -1,37 +1,35 @@
{
"private": true,
"name": "benchmarks",
"version": "0.9.6",
"version": "0.9.13",
"scripts": {
"build": "mkdir -p dist && cp -f index.html dist/index.html && ./node_modules/.bin/webpack-cli --config ./webpack.config.js",
"release": "yarn build && git checkout gh-pages && rm -rf ../../benchmarks && mv dist ../../benchmarks && git add -A && git commit -m \"Benchmarks deploy\" && git push origin gh-pages && git checkout -"
"release": "NODE_ENV=production yarn build && git checkout gh-pages && rm -rf ../../benchmarks && mv dist ../../benchmarks && git add -A && git commit -m \"Benchmarks deploy\" && git push origin gh-pages && git checkout -"
},
"dependencies": {
"aphrodite": "^2.2.2",
"aphrodite": "^2.2.3",
"classnames": "^2.2.6",
"d3-scale-chromatic": "^1.3.0",
"emotion": "^9.2.4",
"fela": "^6.1.9",
"glamor": "2.20.40",
"radium": "^0.24.0",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react-fela": "^7.3.1",
"d3-scale-chromatic": "^1.3.3",
"emotion": "^10.0.5",
"fela": "^10.0.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-fela": "^10.0.2",
"react-jss": "^8.6.1",
"react-native-web": "0.9.6",
"reactxp": "^1.3.0",
"styled-components": "^3.3.3",
"styled-jsx": "^2.2.7",
"styletron-engine-atomic": "^1.0.5",
"styletron-react": "^4.3.1"
"react-native-web": "0.9.13",
"reactxp": "^1.5.0",
"styled-components": "^4.1.3",
"styled-jsx": "^3.1.2",
"styletron-engine-atomic": "^1.0.13",
"styletron-react": "^4.4.4"
},
"devDependencies": {
"babel-plugin-react-native-web": "0.9.6",
"css-loader": "^1.0.0",
"style-loader": "^0.21.0",
"url-loader": "^1.0.1",
"webpack": "^4.15.1",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-cli": "^3.0.8"
"babel-plugin-react-native-web": "0.9.13",
"css-loader": "^2.0.2",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.28.1",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2"
}
}

View File

@@ -2,12 +2,12 @@
* The MIT License (MIT)
* Copyright (c) 2017 Paul Armstrong
* https://github.com/paularmstrong/react-component-benchmark
* @flow
*/
/* global $Values */
/**
* @flow
*/
/* eslint-disable react/prop-types */
import * as Timing from './timing';
import React, { Component } from 'react';
import { getMean, getMedian, getStdDev } from './math';

View File

@@ -0,0 +1,45 @@
import { BenchmarkType } from '../app/Benchmark';
import { number, object } from 'prop-types';
import React, { Component } from 'react';
class TextTree extends Component {
static displayName = 'TextTree';
static benchmarkType = BenchmarkType.MOUNT;
static propTypes = {
breadth: number.isRequired,
components: object,
depth: number.isRequired,
id: number.isRequired,
wrap: number.isRequired
};
render() {
const { breadth, components, depth, id, wrap } = this.props;
const { TextBox } = components;
let result = (
<TextBox children={'TextBox ${id % 3}'} color={id % 3} outer>
{depth === 0 && <TextBox children={'Depth 0'} color={(id % 3) + 3} />}
{depth !== 0 &&
Array.from({ length: breadth }).map((el, i) => (
<TextTree
breadth={breadth}
components={components}
depth={depth - 1}
id={i}
key={i}
wrap={wrap}
/>
))}
</TextBox>
);
for (let i = 0; i < wrap; i++) {
result = <TextBox>{result}</TextBox>;
}
return result;
}
}
export default TextTree;

View File

@@ -8,6 +8,7 @@ type ComponentsType = {
Box: Component,
Dot: Component,
Provider: Component,
TextBox: Component,
View: Component
};

View File

@@ -1,49 +0,0 @@
/* eslint-disable react/prop-types */
import React from 'react';
import View from './View';
const Box = ({ color, fixed = false, layout = 'column', outer = false, ...other }) => (
<View
{...other}
style={[
styles[`color${color}`],
fixed && styles.fixed,
layout === 'row' && styles.row,
outer && styles.outer
]}
/>
);
const styles = {
outer: {
alignSelf: 'flex-start',
padding: 4
},
row: {
flexDirection: 'row'
},
color0: {
backgroundColor: '#14171A'
},
color1: {
backgroundColor: '#AAB8C2'
},
color2: {
backgroundColor: '#E6ECF0'
},
color3: {
backgroundColor: '#FFAD1F'
},
color4: {
backgroundColor: '#F45D22'
},
color5: {
backgroundColor: '#E0245E'
},
fixed: {
width: 6,
height: 6
}
};
export default Box;

View File

@@ -1,33 +0,0 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { css } from 'glamor';
const Dot = ({ size, x, y, children, color }) => (
<div
className={css(styles.root, {
borderBottomColor: color,
borderRightWidth: `${size / 2}px`,
borderBottomWidth: `${size / 2}px`,
borderLeftWidth: `${size / 2}px`,
marginLeft: `${x}px`,
marginTop: `${y}px`
})}
>
{children}
</div>
);
const styles = {
root: {
position: 'absolute',
cursor: 'pointer',
width: 0,
height: 0,
borderColor: 'transparent',
borderStyle: 'solid',
borderTopWidth: 0,
transform: 'translate(50%, 50%)'
}
};
export default Dot;

View File

@@ -1,2 +0,0 @@
import View from './View';
export default View;

View File

@@ -1,29 +0,0 @@
/* eslint-disable react/prop-types */
import { css } from 'glamor';
import React from 'react';
class View extends React.Component {
render() {
const { style, ...other } = this.props;
return <div {...other} className={css(viewStyle, ...style)} />;
}
}
const viewStyle = {
alignItems: 'stretch',
borderWidth: 0,
borderStyle: 'solid',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
margin: 0,
padding: 0,
position: 'relative',
// fix flexbox bugs
minHeight: 0,
minWidth: 0
};
export default View;

View File

@@ -1,11 +0,0 @@
import Box from './Box';
import Dot from './Dot';
import Provider from './Provider';
import View from './View';
export default {
Box,
Dot,
Provider,
View
};

View File

@@ -1,50 +0,0 @@
/* eslint-disable react/prop-types */
import Radium from 'radium';
import React from 'react';
import View from './View';
const Box = ({ color, fixed = false, layout = 'column', outer = false, ...other }) => (
<View
{...other}
style={[
styles[`color${color}`],
fixed && styles.fixed,
layout === 'row' && styles.row,
outer && styles.outer
]}
/>
);
const styles = {
outer: {
alignSelf: 'flex-start',
padding: 4
},
row: {
flexDirection: 'row'
},
color0: {
backgroundColor: '#14171A'
},
color1: {
backgroundColor: '#AAB8C2'
},
color2: {
backgroundColor: '#E6ECF0'
},
color3: {
backgroundColor: '#FFAD1F'
},
color4: {
backgroundColor: '#F45D22'
},
color5: {
backgroundColor: '#E0245E'
},
fixed: {
width: 6,
height: 6
}
};
export default Radium(Box);

View File

@@ -1,36 +0,0 @@
/* eslint-disable react/prop-types */
import Radium from 'radium';
import React from 'react';
const Dot = ({ size, x, y, children, color }) => (
<div
style={[
styles.root,
{
borderBottomColor: color,
borderRightWidth: `${size / 2}px`,
borderBottomWidth: `${size / 2}px`,
borderLeftWidth: `${size / 2}px`,
marginLeft: `${x}px`,
marginTop: `${y}px`
}
]}
>
{children}
</div>
);
const styles = {
root: {
position: 'absolute',
cursor: 'pointer',
width: 0,
height: 0,
borderColor: 'transparent',
borderStyle: 'solid',
borderTopWidth: 0,
transform: 'translate(50%, 50%)'
}
};
export default Radium(Dot);

View File

@@ -1,2 +0,0 @@
import View from './View';
export default View;

View File

@@ -1,31 +0,0 @@
/* eslint-disable react/prop-types */
import Radium from 'radium';
import React from 'react';
class View extends React.Component {
render() {
const { style, ...other } = this.props;
return <div {...other} style={[styles.root, style]} />;
}
}
const styles = {
root: {
alignItems: 'stretch',
borderWidth: 0,
borderStyle: 'solid',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
margin: 0,
padding: 0,
position: 'relative',
// fix flexbox bugs
minHeight: 0,
minWidth: 0
}
};
export default Radium(View);

View File

@@ -1,11 +0,0 @@
import Box from './Box';
import Dot from './Dot';
import Provider from './Provider';
import View from './View';
export default {
Box,
Dot,
Provider,
View
};

View File

@@ -0,0 +1,39 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { StyleSheet, Text } from 'react-native';
const TextBox = ({ color, outer = false, ...other }) => (
<Text {...other} style={[styles.root, styles[`color${color}`], outer && styles.outer]} />
);
const styles = StyleSheet.create({
root: {
color: 'white'
},
outer: {
fontStyle: 'italic'
},
row: {
flexDirection: 'row'
},
color0: {
color: '#14171A'
},
color1: {
color: '#AAB8C2'
},
color2: {
color: '#E6ECF0'
},
color3: {
color: '#FFAD1F'
},
color4: {
color: '#F45D22'
},
color5: {
color: '#E0245E'
}
});
export default TextBox;

View File

@@ -1,11 +1,13 @@
import Box from './Box';
import Dot from './Dot';
import Provider from './Provider';
import TextBox from './TextBox';
import { View } from 'react-native';
export default {
Box,
Dot,
Provider,
TextBox,
View
};

View File

@@ -1,5 +1,6 @@
import App from './app/App';
import impl from './impl';
import TextTree from './cases/TextTree';
import Tree from './cases/Tree';
import SierpinskiTriangle from './cases/SierpinskiTriangle';
@@ -50,6 +51,13 @@ const tests = {
},
Provider: components.Provider,
sampleCount: 100
})),
'Mount text tree': createTestBlock(components => ({
benchmarkType: 'mount',
Component: TextTree,
getComponentProps: () => ({ breadth: 6, components, depth: 3, id: 0, wrap: 2 }),
Provider: components.Provider,
sampleCount: 50
}))
};

View File

@@ -12,6 +12,9 @@ module.exports = {
path: path.resolve(appDirectory, 'dist'),
filename: 'bundle.js'
},
optimization: {
minimize: process.env.NODE_ENV === 'production'
},
module: {
rules: [
{
@@ -31,7 +34,7 @@ module.exports = {
loader: 'babel-loader',
options: {
cacheDirectory: false,
presets: babelPreset,
presets: [babelPreset],
plugins: ['styled-jsx/babel']
}
}

View File

@@ -1,23 +1,24 @@
{
"private": true,
"name": "react-native-examples",
"version": "0.9.6",
"version": "0.9.13",
"scripts": {
"build": "mkdir -p dist && cp -f src/index.html dist/index.html && ./node_modules/.bin/webpack-cli --config ./webpack.config.js",
"release": "yarn build && git checkout gh-pages && rm -rf ../../examples && mv dist ../../examples && git add -A && git commit -m \"Examples deploy\" && git push origin gh-pages && git checkout -"
},
"dependencies": {
"babel-runtime": "^6.26.0",
"@babel/runtime": "^7.2.0",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react-native-web": "0.9.6"
"react-native-web": "0.9.13"
},
"devDependencies": {
"babel-plugin-react-native-web": "0.9.6",
"@babel/plugin-transform-runtime": "^7.2.0",
"babel-plugin-react-native-web": "0.9.13",
"babel-plugin-transform-runtime": "^6.23.0",
"file-loader": "^1.1.11",
"webpack": "^4.8.1",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-cli": "^2.1.3"
"webpack": "^4.28.3",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2"
}
}

View File

@@ -34,10 +34,10 @@ module.exports = {
loader: 'babel-loader',
options: {
cacheDirectory: false,
presets: ['react-native'],
presets: ['module:metro-react-native-babel-preset'],
plugins: [
// needed to support async/await
'transform-runtime'
'@babel/plugin-transform-runtime'
]
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-web",
"version": "0.9.6",
"version": "0.9.13",
"description": "React Native for Web",
"module": "dist/index.js",
"main": "dist/cjs/index.js",
@@ -15,14 +15,14 @@
"dependencies": {
"array-find-index": "^1.0.2",
"create-react-class": "^15.6.2",
"debounce": "^1.1.0",
"deep-assign": "^2.0.0",
"fbjs": "^0.8.16",
"debounce": "^1.2.0",
"deep-assign": "^3.0.0",
"fbjs": "^1.0.0",
"hyphenate-style-name": "^1.0.2",
"inline-style-prefixer": "^4.0.2",
"inline-style-prefixer": "^5.0.3",
"normalize-css-color": "^1.0.2",
"prop-types": "^15.6.0",
"react-timer-mixin": "^0.13.3"
"react-timer-mixin": "^0.13.4"
},
"peerDependencies": {
"react": ">=16.5.1",

View File

@@ -2,56 +2,32 @@
exports[`AppRegistry getApplication "getStyleElement" produces styles that are a function of rendering "element": Additional CSS for styled app 1`] = `
"
.rn-backgroundColor-aot4c7{background-color:rgba(128,0,128,1.00)}
.rn-borderTopWidth-10pzpfo{border-top-width:1234px}
.rn-borderRightWidth-1y24uml{border-right-width:1234px}
.rn-borderBottomWidth-98wxn4{border-bottom-width:1234px}
.rn-borderLeftWidth-150mub4{border-left-width:1234px}"
.r-backgroundColor-aot4c7{background-color:rgba(128,0,128,1.00)}
.r-borderTopWidth-10pzpfo{border-top-width:1234px}
.r-borderRightWidth-1y24uml{border-right-width:1234px}
.r-borderBottomWidth-98wxn4{border-bottom-width:1234px}
.r-borderLeftWidth-150mub4{border-left-width:1234px}"
`;
exports[`AppRegistry getApplication "getStyleElement" produces styles that are a function of rendering "element": CSS for an unstyled app 1`] = `
"@media all{
":focus:not([data-r-focusvisible-x92cna]){outline: none;}
@media all{
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
.r-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;}
.r-pointer{cursor:pointer;}
}
.rn-pointerEvents-12vffkv > *{pointer-events:auto}
.rn-pointerEvents-12vffkv{pointer-events:none !important}
.rn-alignItems-1oszu61{-ms-flex-align:stretch;-webkit-align-items:stretch;-webkit-box-align:stretch;align-items:stretch}
.rn-borderTopStyle-1efd50x{border-top-style:solid}
.rn-borderRightStyle-14skgim{border-right-style:solid}
.rn-borderBottomStyle-rull8r{border-bottom-style:solid}
.rn-borderLeftStyle-mm0ijv{border-left-style:solid}
.rn-borderTopWidth-13yce4e{border-top-width:0px}
.rn-borderRightWidth-fnigne{border-right-width:0px}
.rn-borderBottomWidth-ndvcnb{border-bottom-width:0px}
.rn-borderLeftWidth-gxnn5r{border-left-width:0px}
.rn-boxSizing-deolkf{box-sizing:border-box}
.rn-display-6koalj{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}
.rn-flexShrink-1qe8dj5{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0}
.rn-flexBasis-1mlwlqe{-ms-flex-preferred-size:auto;-webkit-flex-basis:auto;flex-basis:auto}
.rn-flexDirection-eqz5dr{-ms-flex-direction:column;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}
.rn-marginTop-1mnahxq{margin-top:0px}
.rn-marginRight-61z16t{margin-right:0px}
.rn-marginBottom-p1pxzi{margin-bottom:0px}
.rn-marginLeft-11wrixw{margin-left:0px}
.rn-minHeight-ifefl9{min-height:0px}
.rn-minWidth-bcqeeo{min-width:0px}
.rn-paddingTop-wk8lta{padding-top:0px}
.rn-paddingRight-9aemit{padding-right:0px}
.rn-paddingBottom-1mdbw0j{padding-bottom:0px}
.rn-paddingLeft-gy4na3{padding-left:0px}
.rn-position-bnwqim{position:relative}
.rn-zIndex-1lgpqti{z-index:0}
.rn-flexGrow-16y2uox{-ms-flex-positive:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}
.rn-flexShrink-1wbh5a2{-ms-flex-negative:1;-webkit-flex-shrink:1;flex-shrink:1}
.rn-flexBasis-1ro0kt6{-ms-flex-preferred-size:0%;-webkit-flex-basis:0%;flex-basis:0%}"
.r-pointerEvents-12vffkv > *{pointer-events:auto}
.r-pointerEvents-12vffkv{pointer-events:none !important}
.r-flexGrow-16y2uox{-ms-flex-positive:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}
.r-flexShrink-1wbh5a2{-ms-flex-negative:1;-webkit-flex-shrink:1;flex-shrink:1}
.r-flexBasis-1ro0kt6{-ms-flex-preferred-size:0%;-webkit-flex-basis:0%;flex-basis:0%}"
`;
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`] = `
<AppContainer
WrapperComponent={undefined}
rootTag={Object {}}
>
<RootComponent />
@@ -59,10 +35,20 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
`;
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
"<style id=\\"react-native-stylesheet\\">@media all{
"<style id=\\"react-native-stylesheet\\">:focus:not([data-r-focusvisible-x92cna]){outline: none;}
@media all{
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
}</style>"
.r-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;}
.r-pointer{cursor:pointer;}
}
.r-ui-textinput-14mbsun{-moz-appearance:textfield;-webkit-appearance:none;background-color:transparent;border-radius:0px;border:0 solid black;box-sizing:border-box;font-family:14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif;padding:0px;resize:none}
.r-ui-textSingleLine-1xjj19i{max-width:100%;overflow:hidden !important;text-overflow:ellipsis !important;white-space:nowrap !important}
.r-ui-textHasAncestor-z2plr{color:inherit;font:inherit;text-decoration:inherit;white-space:inherit}
.r-ui-textIsRoot-gw3a6r{color:black;font:normal 14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif;text-decoration:none;white-space:pre-wrap}
.r-ui-text-1ntzlq4{background-color:transparent;border-width:0px;box-sizing:border-box;display:inline;margin:0px;padding:0px;text-align:inherit;word-wrap:break-word}
.r-ui-hitSlop-14nrb4u{bottom:0px;left:0px;position:absolute;right:0px;top:0px;z-index:-1}
.r-ui-view-15pvbv0{-ms-flex-align:stretch;-ms-flex-direction:column;-ms-flex-negative:0;-ms-flex-preferred-size:auto;-webkit-align-items:stretch;-webkit-box-align:stretch;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-basis:auto;-webkit-flex-direction:column;-webkit-flex-shrink:0;align-items:stretch;background-color:transparent;border:0 solid black;box-sizing:border-box;color:inherit;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;font:inherit;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-align:inherit;text-decoration:none;z-index:0}</style>"
`;

View File

@@ -90,7 +90,7 @@ export default class AppRegistry {
}
static runApplication(appKey: string, appParameters: Object): void {
const isDevelopment = process.env.NODE_ENV !== 'production';
const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
if (isDevelopment) {
const params = { ...appParameters };
params.rootTag = `#${params.rootTag.id}`;

View File

@@ -10,7 +10,7 @@ exports[`components/Picker prop "children" items 1`] = `
exports[`components/Picker prop "children" renders items 1`] = `
<select
className="rn-fontFamily-14xgk7a rn-fontSize-7cikom rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw"
className="r-fontFamily-14xgk7a r-fontSize-7cikom r-marginTop-1mnahxq r-marginRight-61z16t r-marginBottom-p1pxzi r-marginLeft-11wrixw"
data-focusable={true}
onChange={[Function]}
>

View File

@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ScrollView "pagingEnabled" prop 1`] = `undefined`;
exports[`components/ScrollView "pagingEnabled" prop 2`] = `"y mandatory"`;
exports[`components/ScrollView "pagingEnabled" prop 3`] = `"start"`;

View File

@@ -2,7 +2,9 @@
import React from 'react';
import ScrollView from '..';
import { mount } from 'enzyme';
import StyleSheet from '../../StyleSheet';
import View from '../../View';
import { mount, shallow } from 'enzyme';
describe('components/ScrollView', () => {
test('instance method setNativeProps', () => {
@@ -11,4 +13,32 @@ describe('components/ScrollView', () => {
instance.setNativeProps();
}).not.toThrow();
});
test('"children" prop', () => {
const component = shallow(
<ScrollView>
<View testID="child" />
</ScrollView>
);
expect(component.find({ testID: 'child' }).length).toBe(1);
component.setProps({ stickyHeaderIndices: [4] });
expect(component.find({ testID: 'child' }).length).toBe(1);
component.setProps({ pagingEnabled: true });
expect(component.find({ testID: 'child' }).length).toBe(1);
});
test('"pagingEnabled" prop', () => {
const getStyleProp = (component, prop) => StyleSheet.flatten(component.prop('style'))[prop];
// false
const component = shallow(<ScrollView children={'Child'} />);
expect(getStyleProp(component, 'scrollSnapType')).toMatchSnapshot();
// true
component.setProps({ pagingEnabled: true });
expect(getStyleProp(component, 'scrollSnapType')).toMatchSnapshot();
expect(getStyleProp(component.children().childAt(0), 'scrollSnapAlign')).toMatchSnapshot();
});
});

View File

@@ -137,10 +137,10 @@ const ScrollView = createReactClass({
onContentSizeChange,
refreshControl,
stickyHeaderIndices,
pagingEnabled,
/* eslint-disable */
keyboardDismissMode,
onScroll,
pagingEnabled,
/* eslint-enable */
...other
} = this.props;
@@ -164,11 +164,22 @@ const ScrollView = createReactClass({
};
}
const hasStickyHeaderIndices = !horizontal && Array.isArray(stickyHeaderIndices);
const children =
!horizontal && Array.isArray(stickyHeaderIndices)
hasStickyHeaderIndices || pagingEnabled
? React.Children.map(this.props.children, (child, i) => {
if (stickyHeaderIndices.indexOf(i) > -1) {
return React.cloneElement(child, { style: [child.props.style, styles.stickyHeader] });
const isSticky = hasStickyHeaderIndices && stickyHeaderIndices.indexOf(i) > -1;
if (child != null && (isSticky || pagingEnabled)) {
return (
<View
style={StyleSheet.compose(
isSticky && styles.stickyHeader,
pagingEnabled && styles.pagingEnabledChild
)}
>
{child}
</View>
);
} else {
return child;
}
@@ -181,15 +192,21 @@ const ScrollView = createReactClass({
children={children}
collapsable={false}
ref={this._setInnerViewRef}
style={[horizontal && styles.contentContainerHorizontal, contentContainerStyle]}
style={StyleSheet.compose(
horizontal && styles.contentContainerHorizontal,
contentContainerStyle
)}
/>
);
const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical;
const pagingEnabledStyle = horizontal
? styles.pagingEnabledHorizontal
: styles.pagingEnabledVertical;
const props = {
...other,
style: [baseStyle, this.props.style],
style: [baseStyle, pagingEnabled && pagingEnabledStyle, this.props.style],
onTouchStart: this.scrollResponderHandleTouchStart,
onTouchMove: this.scrollResponderHandleTouchMove,
onTouchEnd: this.scrollResponderHandleTouchEnd,
@@ -223,7 +240,7 @@ const ScrollView = createReactClass({
}
return (
<ScrollViewClass {...props} ref={this._setScrollViewRef} style={props.style}>
<ScrollViewClass {...props} ref={this._setScrollViewRef}>
{contentContainer}
</ScrollViewClass>
);
@@ -294,6 +311,15 @@ const styles = StyleSheet.create({
position: 'sticky',
top: 0,
zIndex: 10
},
pagingEnabledHorizontal: {
scrollSnapType: 'x mandatory'
},
pagingEnabledVertical: {
scrollSnapType: 'y mandatory'
},
pagingEnabledChild: {
scrollSnapAlign: 'start'
}
});

View File

@@ -84,15 +84,19 @@ export default class ReactNativeStyleResolver {
// otherwise fallback to resolving
const flatArray = flattenArray(style);
let isArrayOfNumbers = true;
let cacheKey = '';
for (let i = 0; i < flatArray.length; i++) {
const id = flatArray[i];
if (typeof id !== 'number') {
isArrayOfNumbers = false;
} else {
if (isArrayOfNumbers) {
cacheKey += (id + '-');
}
this._injectRegisteredStyle(id);
}
}
const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
const key = isArrayOfNumbers ? createCacheKey(cacheKey) : null;
return this._resolveStyleIfNeeded(flatArray, key);
}

View File

@@ -15,9 +15,9 @@ import WebStyleSheet from './WebStyleSheet';
const emptyObject = {};
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
const createClassName = (prop, value) => {
const hashed = hash(prop + normalizeValue(value));
return process.env.NODE_ENV !== 'production' ? `rn-${prop}-${hashed}` : `rn-${hashed}`;
const createClassName = (name, value) => {
const hashed = hash(name + normalizeValue(value));
return process.env.NODE_ENV !== 'production' ? `r-${name}-${hashed}` : `r-${hashed}`;
};
const normalizeValue = value => (typeof value === 'object' ? JSON.stringify(value) : value);
@@ -69,6 +69,14 @@ export default class StyleSheetManager {
return className;
}
injectRule(name, body: string): void {
const className = createClassName(`ui-${name}`, body);
const rule = `.${className}{${body}}`;
// insert after the reset + modality but before atomic css
this._sheet.insertRuleOnce(rule, 2);
return className;
}
_addToCache(className, prop, value) {
const cache = this._cache;
if (!cache.byProp[prop]) {

View File

@@ -103,10 +103,7 @@ StyleSheetValidation.addValidStylePropTypes({
objectFit: oneOf(['fill', 'contain', 'cover', 'none', 'scale-down']),
objectPosition: string,
pointerEvents: string,
tableLayout: string,
/* @private */
MozAppearance: string,
WebkitAppearance: string
tableLayout: string
});
export default StyleSheetValidation;

View File

@@ -30,12 +30,13 @@ export default class WebStyleSheet {
}
if (domStyleElement) {
modality(domStyleElement);
// $FlowFixMe
this._sheet = domStyleElement.sheet;
this._textContent = domStyleElement.textContent;
}
}
modality((rule) => this.insertRuleOnce(rule, 0));
}
containsRule(rule: string): boolean {
@@ -49,13 +50,25 @@ export default class WebStyleSheet {
insertRuleOnce(rule: string, position: ?number) {
// Reduce chance of duplicate rules
if (!this.containsRule(rule)) {
this._cssRules.push(rule);
if (position != null) {
this._cssRules.splice(position, 0, rule);
} else {
this._cssRules.push(rule);
}
// Check whether a rule was part of any prerendered styles (textContent
// doesn't include styles injected via 'insertRule')
if (this._textContent.indexOf(rule) === -1 && this._sheet) {
const pos = position || this._sheet.cssRules.length;
this._sheet.insertRule(rule, pos);
try {
this._sheet.insertRule(rule, pos);
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Failed to inject CSS: "${rule}". The browser may have rejecting unrecognized vendor prefixes. (This should have no user-facing impact.)`
);
}
}
}
}
}

View File

@@ -3,102 +3,102 @@
exports[`StyleSheet/ReactNativeStyleResolver resolve resolves inline-style pointerEvents to classname 1`] = `
Object {
"classList": Array [
"rn-pointerEvents-12vffkv",
"r-pointerEvents-12vffkv",
],
"className": "rn-pointerEvents-12vffkv",
"className": "r-pointerEvents-12vffkv",
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to correct className 1`] = `
Object {
"classList": Array [
"rn-marginRight-zso239",
"rn-right-1bnbe1j",
"rn-textAlign-1ff274t",
"r-marginRight-zso239",
"r-right-1bnbe1j",
"r-textAlign-1ff274t",
],
"className": "rn-marginRight-zso239 rn-right-1bnbe1j rn-textAlign-1ff274t",
"className": "r-marginRight-zso239 r-right-1bnbe1j r-textAlign-1ff274t",
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to correct className 2`] = `
Object {
"classList": Array [
"rn-left-2s0hu9",
"rn-marginLeft-1n0xq6e",
"rn-textAlign-fdjqy7",
"r-left-2s0hu9",
"r-marginLeft-1n0xq6e",
"r-textAlign-fdjqy7",
],
"className": "rn-left-2s0hu9 rn-marginLeft-1n0xq6e rn-textAlign-fdjqy7",
"className": "r-left-2s0hu9 r-marginLeft-1n0xq6e r-textAlign-fdjqy7",
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 1`] = `
Object {
"classList": Array [
"rn-borderTopColor-3vzq9n",
"rn-borderRightColor-ycnuvz",
"rn-borderBottomColor-wqks8h",
"rn-borderLeftColor-3se2kx",
"rn-borderTopWidth-13yce4e",
"rn-borderRightWidth-fnigne",
"rn-borderBottomWidth-ndvcnb",
"rn-borderLeftWidth-gxnn5r",
"rn-left-1tsx3h3",
"rn-opacity-icoktb",
"rn-position-u8s1d",
"rn-width-b8lwoo",
"r-borderTopColor-3vzq9n",
"r-borderRightColor-ycnuvz",
"r-borderBottomColor-wqks8h",
"r-borderLeftColor-3se2kx",
"r-borderTopWidth-13yce4e",
"r-borderRightWidth-fnigne",
"r-borderBottomWidth-ndvcnb",
"r-borderLeftWidth-gxnn5r",
"r-left-1tsx3h3",
"r-opacity-icoktb",
"r-position-u8s1d",
"r-width-b8lwoo",
],
"className": "rn-borderTopColor-3vzq9n rn-borderRightColor-ycnuvz rn-borderBottomColor-wqks8h rn-borderLeftColor-3se2kx rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-left-1tsx3h3 rn-opacity-icoktb rn-position-u8s1d rn-width-b8lwoo",
"className": "r-borderTopColor-3vzq9n r-borderRightColor-ycnuvz r-borderBottomColor-wqks8h r-borderLeftColor-3se2kx r-borderTopWidth-13yce4e r-borderRightWidth-fnigne r-borderBottomWidth-ndvcnb r-borderLeftWidth-gxnn5r r-left-1tsx3h3 r-opacity-icoktb r-position-u8s1d r-width-b8lwoo",
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 2`] = `
Object {
"classList": Array [
"rn-borderTopColor-3vzq9n",
"rn-borderRightColor-ycnuvz",
"rn-borderBottomColor-wqks8h",
"rn-borderLeftColor-3se2kx",
"rn-borderTopWidth-13yce4e",
"rn-borderRightWidth-fnigne",
"rn-borderBottomWidth-ndvcnb",
"rn-borderLeftWidth-gxnn5r",
"rn-left-1tsx3h3",
"rn-opacity-icoktb",
"rn-position-u8s1d",
"rn-width-l0gwng",
"r-borderTopColor-3vzq9n",
"r-borderRightColor-ycnuvz",
"r-borderBottomColor-wqks8h",
"r-borderLeftColor-3se2kx",
"r-borderTopWidth-13yce4e",
"r-borderRightWidth-fnigne",
"r-borderBottomWidth-ndvcnb",
"r-borderLeftWidth-gxnn5r",
"r-left-1tsx3h3",
"r-opacity-icoktb",
"r-position-u8s1d",
"r-width-l0gwng",
],
"className": "rn-borderTopColor-3vzq9n rn-borderRightColor-ycnuvz rn-borderBottomColor-wqks8h rn-borderLeftColor-3se2kx rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-left-1tsx3h3 rn-opacity-icoktb rn-position-u8s1d rn-width-l0gwng",
"className": "r-borderTopColor-3vzq9n r-borderRightColor-ycnuvz r-borderBottomColor-wqks8h r-borderLeftColor-3se2kx r-borderTopWidth-13yce4e r-borderRightWidth-fnigne r-borderBottomWidth-ndvcnb r-borderLeftWidth-gxnn5r r-left-1tsx3h3 r-opacity-icoktb r-position-u8s1d r-width-l0gwng",
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 3`] = `
Object {
"classList": Array [
"rn-borderTopColor-3vzq9n",
"rn-borderRightColor-ycnuvz",
"rn-borderBottomColor-wqks8h",
"rn-borderLeftColor-3se2kx",
"rn-borderTopWidth-13yce4e",
"rn-borderRightWidth-fnigne",
"rn-borderBottomWidth-ndvcnb",
"rn-borderLeftWidth-gxnn5r",
"rn-left-1tsx3h3",
"rn-opacity-icoktb",
"rn-position-u8s1d",
"rn-width-b8lwoo",
"r-borderTopColor-3vzq9n",
"r-borderRightColor-ycnuvz",
"r-borderBottomColor-wqks8h",
"r-borderLeftColor-3se2kx",
"r-borderTopWidth-13yce4e",
"r-borderRightWidth-fnigne",
"r-borderBottomWidth-ndvcnb",
"r-borderLeftWidth-gxnn5r",
"r-left-1tsx3h3",
"r-opacity-icoktb",
"r-position-u8s1d",
"r-width-b8lwoo",
],
"className": "rn-borderTopColor-3vzq9n rn-borderRightColor-ycnuvz rn-borderBottomColor-wqks8h rn-borderLeftColor-3se2kx rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-left-1tsx3h3 rn-opacity-icoktb rn-position-u8s1d rn-width-b8lwoo",
"className": "r-borderTopColor-3vzq9n r-borderRightColor-ycnuvz r-borderBottomColor-wqks8h r-borderLeftColor-3se2kx r-borderTopWidth-13yce4e r-borderRightWidth-fnigne r-borderBottomWidth-ndvcnb r-borderLeftWidth-gxnn5r r-left-1tsx3h3 r-opacity-icoktb r-position-u8s1d r-width-b8lwoo",
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to mixed 1`] = `
Object {
"classList": Array [
"rn-left-1tsx3h3",
"rn-opacity-icoktb",
"rn-position-u8s1d",
"r-left-1tsx3h3",
"r-opacity-icoktb",
"r-position-u8s1d",
],
"className": "rn-left-1tsx3h3 rn-opacity-icoktb rn-position-u8s1d",
"className": "r-left-1tsx3h3 r-opacity-icoktb r-position-u8s1d",
"style": Object {
"borderBottomColor": "rgba(255,0,0,1.00)",
"borderBottomWidth": "0px",
@@ -116,12 +116,12 @@ Object {
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to mixed 2`] = `
Object {
"classList": Array [
"rn-left-1tsx3h3",
"rn-opacity-icoktb",
"rn-position-u8s1d",
"rn-width-l0gwng",
"r-left-1tsx3h3",
"r-opacity-icoktb",
"r-position-u8s1d",
"r-width-l0gwng",
],
"className": "rn-left-1tsx3h3 rn-opacity-icoktb rn-position-u8s1d rn-width-l0gwng",
"className": "r-left-1tsx3h3 r-opacity-icoktb r-position-u8s1d r-width-l0gwng",
"style": Object {
"borderBottomColor": "rgba(255,0,0,1.00)",
"borderBottomWidth": "0px",
@@ -138,11 +138,11 @@ Object {
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to mixed 3`] = `
Object {
"classList": Array [
"rn-left-1tsx3h3",
"rn-opacity-icoktb",
"rn-position-u8s1d",
"r-left-1tsx3h3",
"r-opacity-icoktb",
"r-position-u8s1d",
],
"className": "rn-left-1tsx3h3 rn-opacity-icoktb rn-position-u8s1d",
"className": "r-left-1tsx3h3 r-opacity-icoktb r-position-u8s1d",
"style": Object {
"borderBottomColor": "rgba(255,0,0,1.00)",
"borderBottomWidth": "0px",
@@ -222,7 +222,7 @@ Object {
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode next class names have priority over current inline styles 1`] = `
Object {
"className": "rn-opacity-6dt33c",
"className": "r-opacity-6dt33c",
"style": Object {
"opacity": "",
},
@@ -270,7 +270,7 @@ Object {
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true, resolves to flipped classNames 1`] = `
Object {
"className": "rn-left-1u10d71 rn-marginRight-zso239",
"className": "r-left-1u10d71 r-marginRight-zso239",
"style": Object {
"marginRight": "",
"right": "5px",

View File

@@ -5,12 +5,15 @@ exports[`StyleSheet/StyleSheetManager getClassName 1`] = `undefined`;
exports[`StyleSheet/StyleSheetManager getStyleSheet 1`] = `
Object {
"id": "react-native-stylesheet",
"textContent": "@media all{
"textContent": ":focus:not([data-r-focusvisible-x92cna]){outline: none;}
@media all{
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
.r-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;}
.r-pointer{cursor:pointer;}
}
.rn---test-property-ax3bxi{--test-property:test-value}",
.r---test-property-ax3bxi{--test-property:test-value}",
}
`;

View File

@@ -2,11 +2,11 @@
exports[`StyleSheet/createAtomicRules transforms custom "animationName" declaration 1`] = `
Array [
"@media all {@-webkit-keyframes rn-anim-2k74q5{0%{top:0px}50%{top:5px}100%{top:10px}}}",
"@media all {@keyframes rn-anim-2k74q5{0%{top:0px}50%{top:5px}100%{top:10px}}}",
"@media all {@-webkit-keyframes rn-anim-zc91cv{from{left:0px}to{left:10px}}}",
"@media all {@keyframes rn-anim-zc91cv{from{left:0px}to{left:10px}}}",
".test{-webkit-animation-name:rn-anim-2k74q5,rn-anim-zc91cv;animation-name:rn-anim-2k74q5,rn-anim-zc91cv}",
"@-webkit-keyframes r-anim-2k74q5{0%{top:0px}50%{top:5px}100%{top:10px}}",
"@keyframes r-anim-2k74q5{0%{top:0px}50%{top:5px}100%{top:10px}}",
"@-webkit-keyframes r-anim-zc91cv{from{left:0px}to{left:10px}}",
"@keyframes r-anim-zc91cv{from{left:0px}to{left:10px}}",
".test{-webkit-animation-name:r-anim-2k74q5,r-anim-zc91cv;animation-name:r-anim-2k74q5,r-anim-zc91cv}",
]
`;

View File

@@ -39,44 +39,17 @@ describe('StyleSheet/createReactDOMStyle', () => {
expect(createReactDOMStyle(style)).toMatchSnapshot();
});
describe('borderWidth styles', () => {
test('defaults to 0 when "null"', () => {
expect(createReactDOMStyle({ borderWidth: null })).toEqual({
borderTopWidth: '0px',
borderRightWidth: '0px',
borderBottomWidth: '0px',
borderLeftWidth: '0px'
});
expect(createReactDOMStyle({ borderWidth: 2, borderRightWidth: null })).toEqual({
borderTopWidth: '2px',
borderRightWidth: '0px',
borderBottomWidth: '2px',
borderLeftWidth: '2px'
});
});
});
describe('flexbox styles', () => {
test('flex defaults', () => {
expect(createReactDOMStyle({ display: 'flex' })).toEqual({
display: 'flex',
flexShrink: 0,
flexBasis: 'auto'
});
});
test('flex: -1', () => {
expect(createReactDOMStyle({ display: 'flex', flex: -1 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: -1 })).toEqual({
flexBasis: 'auto',
flexGrow: 0,
flexShrink: 1,
flexBasis: 'auto'
flexShrink: 1
});
});
test('flex: 0', () => {
expect(createReactDOMStyle({ display: 'flex', flex: 0 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 0 })).toEqual({
flexGrow: 0,
flexShrink: 0,
flexBasis: '0%'
@@ -84,8 +57,7 @@ describe('StyleSheet/createReactDOMStyle', () => {
});
test('flex: 1', () => {
expect(createReactDOMStyle({ display: 'flex', flex: 1 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 1 })).toEqual({
flexGrow: 1,
flexShrink: 1,
flexBasis: '0%'
@@ -93,8 +65,7 @@ describe('StyleSheet/createReactDOMStyle', () => {
});
test('flex: 10', () => {
expect(createReactDOMStyle({ display: 'flex', flex: 10 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 10 })).toEqual({
flexGrow: 10,
flexShrink: 1,
flexBasis: '0%'
@@ -103,15 +74,12 @@ describe('StyleSheet/createReactDOMStyle', () => {
test('flexBasis overrides', () => {
// is flex-basis applied?
expect(createReactDOMStyle({ display: 'flex', flexBasis: '25%' })).toEqual({
display: 'flex',
flexShrink: 0,
expect(createReactDOMStyle({ flexBasis: '25%' })).toEqual({
flexBasis: '25%'
});
// can flex-basis override the 'flex' expansion?
expect(createReactDOMStyle({ display: 'flex', flex: 1, flexBasis: '25%' })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 1, flexBasis: '25%' })).toEqual({
flexGrow: 1,
flexShrink: 1,
flexBasis: '25%'
@@ -120,15 +88,12 @@ describe('StyleSheet/createReactDOMStyle', () => {
test('flexShrink overrides', () => {
// is flex-shrink applied?
expect(createReactDOMStyle({ display: 'flex', flexShrink: 1 })).toEqual({
display: 'flex',
flexShrink: 1,
flexBasis: 'auto'
expect(createReactDOMStyle({ flexShrink: 1 })).toEqual({
flexShrink: 1
});
// can flex-shrink override the 'flex' expansion?
expect(createReactDOMStyle({ display: 'flex', flex: 1, flexShrink: 2 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 1, flexShrink: 2 })).toEqual({
flexGrow: 1,
flexShrink: 2,
flexBasis: '0%'

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
export const monospaceFontStack = 'monospace, monospace';
export const systemFontStack =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif';

View File

@@ -7,7 +7,7 @@ const hashObject = obj => hash(JSON.stringify(obj));
const createIdentifier = obj => {
const hashed = hashObject(obj);
return process.env.NODE_ENV !== 'production' ? `rn-anim-${hashed}` : `rn-${hashed}`;
return process.env.NODE_ENV !== 'production' ? `r-anim-${hashed}` : `r-${hashed}`;
};
const prefixes = ['-webkit-', ''];
@@ -29,7 +29,7 @@ const makeSteps = keyframes =>
const createKeyframesRules = (keyframes: Object): Array<String> => {
const identifier = createIdentifier(keyframes);
const rules = prefixes.map(prefix => {
return `@media all {@${prefix}keyframes ${identifier}{${makeSteps(keyframes)}}}`;
return `@${prefix}keyframes ${identifier}{${makeSteps(keyframes)}}`;
});
return { identifier, rules };
};

View File

@@ -7,6 +7,7 @@
* @noflow
*/
import { monospaceFontStack, systemFontStack } from './constants';
import normalizeColor from '../../modules/normalizeColor';
import normalizeValue from './normalizeValue';
import resolveShadowValue from './resolveShadowValue';
@@ -54,29 +55,6 @@ const colorProps = {
color: true
};
const borderWidthProps = {
borderWidth: true,
borderTopWidth: true,
borderRightWidth: true,
borderBottomWidth: true,
borderLeftWidth: true
};
const monospaceFontStack = 'monospace, monospace';
const systemFontStack =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif';
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 };
/**
@@ -167,12 +145,6 @@ const createReducer = (style, styleProps) => {
return (resolvedStyle, prop) => {
let value = normalizeValue(prop, style[prop]);
// Make sure the default border width is explicitly set to '0' to avoid
// falling back to any unwanted user-agent styles.
if (borderWidthProps[prop]) {
value = value == null ? normalizeValue(null, 0) : value;
}
// Normalize color values
if (colorProps[prop]) {
value = normalizeColor(value);
@@ -203,21 +175,6 @@ const createReducer = (style, styleProps) => {
break;
}
case 'display': {
resolvedStyle.display = value;
// A flex container in React Native has these defaults which should be
// set only if there is no otherwise supplied flex style.
if (style.display === 'flex' && style.flex == null) {
if (style.flexShrink == null) {
resolvedStyle.flexShrink = 0;
}
if (style.flexBasis == null) {
resolvedStyle.flexBasis = 'auto';
}
}
break;
}
// The 'flex' property value in React Native must be a positive integer,
// 0, or -1.
case 'flex': {
@@ -324,7 +281,7 @@ const createReactDOMStyle = style => {
return emptyObject;
}
const styleProps = Object.keys(style);
const sortedStyleProps = alphaSortProps(styleProps);
const sortedStyleProps = styleProps.sort();
const reducer = createReducer(style, styleProps);
const resolvedStyle = sortedStyleProps.reduce(reducer, {});
return resolvedStyle;

View File

@@ -8,7 +8,6 @@
*/
import hyphenateStyleName from 'hyphenate-style-name';
import mapKeyValue from '../../modules/mapKeyValue';
import normalizeValue from './normalizeValue';
import prefixStyles from '../../modules/prefixStyles';
@@ -27,9 +26,15 @@ const createDeclarationString = (prop, val) => {
* createRuleBlock({ width: 20, color: 'blue' });
* // => 'color:blue;width:20px'
*/
const createRuleBlock = style =>
mapKeyValue(prefixStyles(style), createDeclarationString)
.sort()
.join(';');
const createRuleBlock = style => {
const prefixedStyle = prefixStyles(style);
return (
Object.keys(prefixedStyle)
.map(prop => createDeclarationString(prop, prefixedStyle[prop]))
// put short-form and vendor prefixed properties first
.sort()
.join(';')
);
};
export default createRuleBlock;

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
import createRuleBlock from './createRuleBlock';
import styleResolver from './styleResolver';
import { systemFontStack } from './constants';
const fontFamilyProperties = ['font', 'fontFamily'];
/**
* A simple (and dangerous) CSS system.
* The order of CSS rule insertion is not guaranteed.
* Avoiding combining 2 or more classes that modify the same property.
*/
const css = {
/**
* const classes = css.create({ base: {}, extra: {} })
*/
create(rules) {
const result = {};
Object.keys(rules).forEach(key => {
const rule = rules[key];
fontFamilyProperties.forEach(prop => {
const value = rule[prop];
if (value && value.indexOf('System') > -1) {
rule[prop] = value.replace('System', systemFontStack);
}
});
const cssRule = createRuleBlock(rule);
const className = styleResolver.styleSheetManager.injectRule(key, cssRule);
result[key] = className;
});
return result;
},
/**
* css.combine(classes.base, classes.extra)
*/
combine(...args) {
return args.reduce((className, value) => {
if (value) {
className += className.length > 0 ? ' ' + value : value;
}
return className;
}, '');
}
};
export default css;

View File

@@ -1,11 +1,9 @@
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import StyleSheet from './StyleSheet';
// allow component styles to be editable in React Dev Tools
if (process.env.NODE_ENV !== 'production') {
if (canUseDOM && window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.resolveRNStyle = StyleSheet.flatten;
}
// allow original component styles to be inspected in React Dev Tools
if (canUseDOM && window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.resolveRNStyle = StyleSheet.flatten;
}
export default StyleSheet;

View File

@@ -12,13 +12,34 @@ const safeRule = rule => `@media all{\n${rule}\n}`;
const resets = [
// minimal top-level reset
'html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}',
'html{' +
'-ms-text-size-adjust:100%;' +
'-webkit-text-size-adjust:100%;' +
'-webkit-tap-highlight-color:rgba(0,0,0,0);' +
'}',
'body{margin:0;}',
// minimal form pseudo-element reset
'button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}',
'input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,' +
'input::-webkit-search-cancel-button,input::-webkit-search-decoration,' +
'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}'
'input::-webkit-inner-spin-button,' +
'input::-webkit-outer-spin-button,' +
'input::-webkit-search-cancel-button,' +
'input::-webkit-search-decoration,' +
'input::-webkit-search-results-button,' +
'input::-webkit-search-results-decoration{' +
'display:none;' +
'}',
// Reset styles for heading, link, and list DOM elements
'.r-reset{' +
'background-color:transparent;' +
'color:inherit;' +
'font:inherit;' +
'list-style:none;' +
'margin:0;' +
'text-align:inherit;' +
'text-decoration:none;' +
'}',
// For pressable elements
'.r-pointer{cursor:pointer;}'
];
const reset = [safeRule(resets.join('\n'))];

View File

@@ -11,125 +11,267 @@
* 1. a keydown event occurred immediately before a focus event;
* 2. a focus event happened on an element which requires keyboard interaction (e.g., a text field);
*
* Based on https://github.com/WICG/focus-ring
* This software or document includes material copied from or derived from https://github.com/WICG/focus-visible.
* Copyright © 2018 W3C® (MIT, ERCIM, Keio, Beihang).
* W3C Software Notice and License: https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* @noflow
*/
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import hash from '../../vendor/hash';
const rule = ':focus { outline: none; }';
let ruleExists = false;
const focusVisibleAttributeName =
'data-r-' + (process.env.NODE_ENV !== 'production' ? 'focusvisible-' : '') + hash('focusvisible');
const rule = `:focus:not([${focusVisibleAttributeName}]){outline: none;}`;
const modality = insertRule => {
insertRule(rule);
const modality = styleElement => {
if (!canUseDOM) {
return;
}
let hadKeyboardEvent = false;
let keyboardThrottleTimeoutID = 0;
let hadKeyboardEvent = true;
let hadFocusVisibleRecently = false;
let hadFocusVisibleRecentlyTimeout = null;
const proto = window.Element.prototype;
const matches =
proto.matches ||
proto.mozMatchesSelector ||
proto.msMatchesSelector ||
proto.webkitMatchesSelector;
const inputTypesWhitelist = {
text: true,
search: true,
url: true,
tel: true,
email: true,
password: true,
number: true,
date: true,
month: true,
week: true,
time: true,
datetime: true,
'datetime-local': true
};
// These elements should always have a focus ring drawn, because they are
// associated with switching to a keyboard modality.
const keyboardModalityWhitelist = [
'input:not([type])',
'input[type=text]',
'input[type=search]',
'input[type=url]',
'input[type=tel]',
'input[type=email]',
'input[type=password]',
'input[type=number]',
'input[type=date]',
'input[type=month]',
'input[type=week]',
'input[type=time]',
'input[type=datetime]',
'input[type=datetime-local]',
'textarea',
'[role=textbox]'
].join(',');
/**
* Helper function for legacy browsers and iframes which sometimes focus
* elements like document, body, and non-interactive SVG.
*/
function isValidFocusTarget(el) {
if (
el &&
el !== document &&
el.nodeName !== 'HTML' &&
el.nodeName !== 'BODY' &&
'classList' in el &&
'contains' in el.classList
) {
return true;
}
return false;
}
/**
* Computes whether the given element should automatically trigger the
* `focus-ring`.
* `focus-visible` attribute being added, i.e. whether it should always match
* `:focus-visible` when focused.
*/
const focusTriggersKeyboardModality = el => {
if (matches) {
return matches.call(el, keyboardModalityWhitelist) && matches.call(el, ':not([readonly])');
} else {
return false;
}
};
function focusTriggersKeyboardModality(el) {
const type = el.type;
const tagName = el.tagName;
const isReadOnly = el.readOnly;
/**
* Add the focus ring style
*/
const addFocusRing = () => {
if (styleElement && ruleExists) {
styleElement.sheet.deleteRule(0);
ruleExists = false;
if (tagName === 'INPUT' && inputTypesWhitelist[type] && !isReadOnly) {
return true;
}
};
/**
* Remove the focus ring style
*/
const removeFocusRing = () => {
if (styleElement && !ruleExists) {
styleElement.sheet.insertRule(rule, 0);
ruleExists = true;
if (tagName === 'TEXTAREA' && !isReadOnly) {
return true;
}
};
/**
* On `keydown`, set `hadKeyboardEvent`, to be removed 100ms later if there
* are no further keyboard events. The 100ms throttle handles cases where
* focus is redirected programmatically after a keyboard event, such as
* opening a menu or dialog.
*/
const handleKeyDown = e => {
hadKeyboardEvent = true;
if (keyboardThrottleTimeoutID !== 0) {
clearTimeout(keyboardThrottleTimeoutID);
if (el.isContentEditable) {
return true;
}
keyboardThrottleTimeoutID = setTimeout(() => {
hadKeyboardEvent = false;
keyboardThrottleTimeoutID = 0;
}, 100);
};
/**
* Display the focus-ring when the keyboard was used to focus
*/
const handleFocus = e => {
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
addFocusRing();
}
};
/**
* Remove the focus-ring when the keyboard was used to focus
*/
const handleBlur = () => {
if (!hadKeyboardEvent) {
removeFocusRing();
}
};
if (document.body && document.body.addEventListener) {
removeFocusRing();
document.body.addEventListener('keydown', handleKeyDown, true);
document.body.addEventListener('focus', handleFocus, true);
document.body.addEventListener('blur', handleBlur, true);
return false;
}
/**
* Add the `focus-visible` attribute to the given element if it was not added by
* the author.
*/
function addFocusVisibleAttribute(el) {
if (el.hasAttribute(focusVisibleAttributeName)) {
return;
}
el.setAttribute(focusVisibleAttributeName, true);
}
/**
* Remove the `focus-visible` attribute from the given element if it was not
* originally added by the author.
*/
function removeFocusVisibleAttribute(el) {
el.removeAttribute(focusVisibleAttributeName);
}
/**
* Remove the `focus-visible` attribute from all elements in the document.
*/
function removeAllFocusVisibleAttributes() {
const list = document.querySelectorAll(`[${focusVisibleAttributeName}]`);
for (let i = 0; i < list.length; i += 1) {
removeFocusVisibleAttribute(list[i]);
}
}
/**
* Treat `keydown` as a signal that the user is in keyboard modality.
* Apply `focus-visible` to any current active element and keep track
* of our keyboard modality state with `hadKeyboardEvent`.
*/
function onKeyDown(e) {
if (e.key !== 'Tab' && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) {
return;
}
if (isValidFocusTarget(document.activeElement)) {
addFocusVisibleAttribute(document.activeElement);
}
hadKeyboardEvent = true;
}
/**
* If at any point a user clicks with a pointing device, ensure that we change
* the modality away from keyboard.
* This avoids the situation where a user presses a key on an already focused
* element, and then clicks on a different element, focusing it with a
* pointing device, while we still think we're in keyboard modality.
* It also avoids the situation where a user presses on an element within a
* previously keyboard-focused element (i.e., `e.target` is not the previously
* focused element, but one of its descendants) and we need to remove the
* focus ring because a `blur` event doesn't occur.
*/
function onPointerDown(e) {
if (hadKeyboardEvent === true) {
removeAllFocusVisibleAttributes();
}
hadKeyboardEvent = false;
}
/**
* On `focus`, add the `focus-visible` attribute to the target if:
* - the target received focus as a result of keyboard navigation, or
* - the event target is an element that will likely require interaction
* via the keyboard (e.g. a text box)
*/
function onFocus(e) {
// Prevent IE from focusing the document or HTML element.
if (!isValidFocusTarget(e.target)) {
return;
}
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
addFocusVisibleAttribute(e.target);
}
}
/**
* On `blur`, remove the `focus-visible` attribute from the target.
*/
function onBlur(e) {
if (!isValidFocusTarget(e.target)) {
return;
}
if (e.target.hasAttribute(focusVisibleAttributeName)) {
// To detect a tab/window switch, we look for a blur event followed
// rapidly by a visibility change.
// If we don't see a visibility change within 100ms, it's probably a
// regular focus change.
hadFocusVisibleRecently = true;
window.clearTimeout(hadFocusVisibleRecentlyTimeout);
hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {
hadFocusVisibleRecently = false;
window.clearTimeout(hadFocusVisibleRecentlyTimeout);
}, 100);
removeFocusVisibleAttribute(e.target);
}
}
/**
* If the user changes tabs, keep track of whether or not the previously
* focused element had the focus-visible attribute.
*/
function onVisibilityChange(e) {
if (document.visibilityState === 'hidden') {
// If the tab becomes active again, the browser will handle calling focus
// on the element (Safari actually calls it twice).
// If this tab change caused a blur on an element with focus-visible,
// re-apply the attribute when the user switches back to the tab.
if (hadFocusVisibleRecently) {
hadKeyboardEvent = true;
}
addInitialPointerMoveListeners();
}
}
/**
* Add a group of listeners to detect usage of any pointing devices.
* These listeners will be added when the polyfill first loads, and anytime
* the window is blurred, so that they are active when the window regains
* focus.
*/
function addInitialPointerMoveListeners() {
document.addEventListener('mousemove', onInitialPointerMove);
document.addEventListener('mousedown', onInitialPointerMove);
document.addEventListener('mouseup', onInitialPointerMove);
document.addEventListener('pointermove', onInitialPointerMove);
document.addEventListener('pointerdown', onInitialPointerMove);
document.addEventListener('pointerup', onInitialPointerMove);
document.addEventListener('touchmove', onInitialPointerMove);
document.addEventListener('touchstart', onInitialPointerMove);
document.addEventListener('touchend', onInitialPointerMove);
}
function removeInitialPointerMoveListeners() {
document.removeEventListener('mousemove', onInitialPointerMove);
document.removeEventListener('mousedown', onInitialPointerMove);
document.removeEventListener('mouseup', onInitialPointerMove);
document.removeEventListener('pointermove', onInitialPointerMove);
document.removeEventListener('pointerdown', onInitialPointerMove);
document.removeEventListener('pointerup', onInitialPointerMove);
document.removeEventListener('touchmove', onInitialPointerMove);
document.removeEventListener('touchstart', onInitialPointerMove);
document.removeEventListener('touchend', onInitialPointerMove);
}
/**
* When the polfyill first loads, assume the user is in keyboard modality.
* If any event is received from a pointing device (e.g. mouse, pointer,
* touch), turn off keyboard modality.
* This accounts for situations where focus enters the page from the URL bar.
*/
function onInitialPointerMove(e) {
// Work around a Safari quirk that fires a mousemove on <html> whenever the
// window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯
if (e.target.nodeName === 'HTML') {
return;
}
hadKeyboardEvent = false;
removeInitialPointerMoveListeners();
}
document.addEventListener('keydown', onKeyDown, true);
document.addEventListener('mousedown', onPointerDown, true);
document.addEventListener('pointerdown', onPointerDown, true);
document.addEventListener('touchstart', onPointerDown, true);
document.addEventListener('focus', onFocus, true);
document.addEventListener('blur', onBlur, true);
document.addEventListener('visibilitychange', onVisibilityChange, true);
addInitialPointerMoveListeners();
};
export default modality;

View File

@@ -42,12 +42,12 @@ const TextStylePropTypes = {
textShadowColor: ColorPropType,
textShadowOffset: shape({ width: number, height: number }),
textShadowRadius: number,
textTransform: oneOf(['capitalize', 'lowercase', 'none', 'uppercase']),
writingDirection: oneOf(['auto', 'ltr', 'rtl']),
/* @platform web */
textIndent: numberOrString,
textOverflow: string,
textRendering: oneOf(['auto', 'geometricPrecision', 'optimizeLegibility', 'optimizeSpeed']),
textTransform: oneOf(['capitalize', 'lowercase', 'none', 'uppercase']),
unicodeBidi: oneOf([
'normal',
'bidi-override',
@@ -57,6 +57,7 @@ const TextStylePropTypes = {
'plaintext'
]),
whiteSpace: string,
wordBreak: oneOf(['normal', 'break-all', 'break-word', 'keep-all']),
wordWrap: string,
MozOsxFontSmoothing: string,
WebkitFontSmoothing: string

View File

@@ -1,8 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/Text prop "children" 1`] = `
<span
class="r-ui-text-1ntzlq4 r-ui-textHasAncestor-z2plr"
data-testid="child"
dir="auto"
/>
`;
exports[`components/Text prop "onPress" 1`] = `
<div
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-cursor-1loqt21 rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
className="r-ui-text-1ntzlq4 r-ui-textIsRoot-gw3a6r r-cursor-1loqt21"
data-focusable={true}
dir="auto"
onClick={[Function]}
@@ -13,14 +21,14 @@ exports[`components/Text prop "onPress" 1`] = `
exports[`components/Text prop "selectable" 1`] = `
<div
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
className="r-ui-text-1ntzlq4 r-ui-textIsRoot-gw3a6r"
dir="auto"
/>
`;
exports[`components/Text prop "selectable" 2`] = `
<div
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-userSelect-lrvibr rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
className="r-ui-text-1ntzlq4 r-ui-textIsRoot-gw3a6r r-userSelect-lrvibr"
dir="auto"
/>
`;

View File

@@ -29,12 +29,12 @@ describe('components/Text', () => {
});
test('prop "children"', () => {
const children = <Text testID="1" />;
const component = shallow(<Text children={children} />);
expect(component.contains(children)).toEqual(true);
const children = <Text testID="child" />;
const component = render(<Text children={children} />);
expect(component.children()).toMatchSnapshot();
});
test('prop "numberOfLines"');
test('prop "numberOfLines"', () => {});
test('prop "onPress"', () => {
const onPress = e => {};

View File

@@ -13,6 +13,7 @@ import applyNativeMethods from '../../modules/applyNativeMethods';
import { bool } from 'prop-types';
import { Component } from 'react';
import createElement from '../createElement';
import css from '../StyleSheet/css';
import StyleSheet from '../StyleSheet';
import TextPropTypes from './TextPropTypes';
@@ -57,7 +58,7 @@ class Text extends Component<*> {
...otherProps
} = this.props;
const { isInAParentText } = this.context;
const { isInAParentText: hasTextAncestor } = this.context;
if (onPress) {
otherProps.accessible = true;
@@ -65,18 +66,20 @@ class Text extends Component<*> {
otherProps.onKeyDown = this._createEnterHandler(onPress);
}
// allow browsers to automatically infer the language writing direction
otherProps.dir = dir !== undefined ? dir : 'auto';
otherProps.className = css.combine(
classes.text,
hasTextAncestor ? classes.textHasAncestor : classes.textIsRoot,
numberOfLines === 1 && classes.textSingleLine
);
otherProps.style = [
styles.initial,
this.context.isInAParentText === true && styles.isInAParentText,
style,
selectable === false && styles.notSelectable,
numberOfLines === 1 && styles.singleLineStyle,
onPress && styles.pressable
];
// allow browsers to automatically infer the language writing direction
otherProps.dir = dir !== undefined ? dir : 'auto';
const component = isInAParentText ? 'span' : 'div';
const component = hasTextAncestor ? 'span' : 'div';
return createElement(component, otherProps);
}
@@ -97,41 +100,45 @@ class Text extends Component<*> {
}
}
const styles = StyleSheet.create({
initial: {
const classes = css.create({
text: {
backgroundColor: 'transparent',
borderWidth: 0,
boxSizing: 'border-box',
color: 'inherit',
display: 'inline',
fontFamily: 'System',
fontSize: 14,
fontStyle: 'inherit',
fontVariant: ['inherit'],
fontWeight: 'inherit',
lineHeight: 'inherit',
margin: 0,
padding: 0,
textDecorationLine: 'none',
whiteSpace: 'pre-wrap',
textAlign: 'inherit',
wordWrap: 'break-word'
},
isInAParentText: {
// inherit parent font styles
fontFamily: 'inherit',
fontSize: 'inherit',
textIsRoot: {
color: 'black',
font: 'normal 14px System',
textDecoration: 'none',
whiteSpace: 'pre-wrap'
},
textHasAncestor: {
color: 'inherit',
font: 'inherit',
textDecoration: 'inherit',
whiteSpace: 'inherit'
},
// "!important" is used to prevent essential styles from being overridden
// by merged styles
textSingleLine: {
maxWidth: '100%',
overflow: 'hidden !important',
textOverflow: 'ellipsis !important',
whiteSpace: 'nowrap !important'
}
});
const styles = StyleSheet.create({
notSelectable: {
userSelect: 'none'
},
pressable: {
cursor: 'pointer'
},
singleLineStyle: {
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
});

View File

@@ -12,7 +12,7 @@ const testIfDocumentIsFocused = (message, fn) => {
if (document.hasFocus && document.hasFocus()) {
test(message, fn);
} else {
test.skip(`${message} document is not focused`);
test.skip(`${message} document is not focused`, () => {});
}
};
@@ -180,86 +180,10 @@ describe('components/TextInput', () => {
});
describe('prop "onKeyPress"', () => {
test('backspace key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyDown', { which: 8 });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Backspace',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('tab key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyDown', { which: 9 });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Tab',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('enter key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { which: 13 });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Enter',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('space key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { which: 32 });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: ' ',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('arrow key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { which: 37 });
input.simulate('keyPress', { key: 'ArrowLeft' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
@@ -275,10 +199,105 @@ describe('components/TextInput', () => {
);
});
test('backspace key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyDown', { key: 'Backspace' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Backspace',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('enter key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { key: 'Enter' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Enter',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('escape key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { key: 'Escape' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Escape',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('space key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { key: ' ' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: ' ',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('tab key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyDown', { key: 'Tab' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
nativeEvent: {
altKey: undefined,
ctrlKey: undefined,
key: 'Tab',
metaKey: undefined,
shiftKey: undefined,
target: expect.anything()
}
})
);
});
test('text key', () => {
const onKeyPress = jest.fn();
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyPress', { which: 97 });
input.simulate('keyPress', { key: 'a' });
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
expect.objectContaining({
@@ -302,7 +321,7 @@ describe('components/TextInput', () => {
ctrlKey: true,
metaKey: true,
shiftKey: true,
which: 32
key: ' '
});
expect(onKeyPress).toHaveBeenCalledTimes(1);
expect(onKeyPress).toBeCalledWith(
@@ -324,7 +343,7 @@ describe('components/TextInput', () => {
const input = findNativeInput(mount(<TextInput onKeyPress={onKeyPress} />));
input.simulate('keyDown', {
metaKey: true,
which: 13
key: 'Enter'
});
expect(onKeyPress).toHaveBeenCalledTimes(1);
});
@@ -358,7 +377,7 @@ describe('components/TextInput', () => {
const input = findNativeInput(
mount(<TextInput defaultValue="12345" onSubmitEditing={onSubmitEditing} />)
);
input.simulate('keyPress', { which: 13 });
input.simulate('keyPress', { key: 'Enter' });
function onSubmitEditing(e) {
expect(e.nativeEvent.target).toBeDefined();
expect(e.nativeEvent.text).toBe('12345');
@@ -371,7 +390,7 @@ describe('components/TextInput', () => {
const input = findNativeTextarea(
mount(<TextInput defaultValue="12345" multiline onSubmitEditing={onSubmitEditing} />)
);
input.simulate('keyPress', { which: 13 });
input.simulate('keyPress', { key: 'Enter' });
expect(onSubmitEditing).not.toHaveBeenCalled();
});
@@ -391,11 +410,11 @@ describe('components/TextInput', () => {
);
// shift+enter should enter newline, not submit
input.simulate('keyPress', { which: 13, preventDefault, shiftKey: true });
input.simulate('keyPress', { key: 'Enter', preventDefault, shiftKey: true });
expect(onSubmitEditing).not.toHaveBeenCalledWith(expect.objectContaining({ shiftKey: true }));
expect(preventDefault).not.toHaveBeenCalled();
input.simulate('keyPress', { which: 13, preventDefault });
input.simulate('keyPress', { key: 'Enter', preventDefault });
expect(onSubmitEditing).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(1);
});

View File

@@ -14,8 +14,8 @@ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import { Component } from 'react';
import ColorPropType from '../ColorPropType';
import createElement from '../createElement';
import css from '../StyleSheet/css';
import findNodeHandle from '../findNodeHandle';
import StyleSheet from '../StyleSheet';
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import TextInputStylePropTypes from './TextInputStylePropTypes';
import TextInputState from '../../modules/TextInputState';
@@ -150,8 +150,6 @@ class TextInput extends Component<*> {
static State = TextInputState;
blur: Function;
clear() {
this._node.value = '';
}
@@ -256,6 +254,7 @@ class TextInput extends Component<*> {
Object.assign(otherProps, {
autoCorrect: autoCorrect ? 'on' : 'off',
className: classes.textinput,
dir: 'auto',
onBlur: normalizeEventHandler(this._handleBlur),
onChange: normalizeEventHandler(this._handleChange),
@@ -266,7 +265,7 @@ class TextInput extends Component<*> {
readOnly: !editable,
ref: this._setNode,
spellCheck: spellCheck != null ? spellCheck : autoCorrect,
style: [styles.initial, style]
style
});
if (multiline) {
@@ -317,15 +316,17 @@ class TextInput extends Component<*> {
// Prevent key events bubbling (see #612)
e.stopPropagation();
// Backspace, Tab, Cmd+Enter, and Arrow keys only fire 'keydown' DOM events
// Backspace, Escape, Tab, Cmd+Enter, and Arrow keys only fire 'keydown'
// DOM events
if (
e.which === 8 ||
e.which === 9 ||
(e.which === 13 && e.metaKey) ||
e.which === 37 ||
e.which === 38 ||
e.which === 39 ||
e.which === 40
e.key === 'ArrowLeft' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowRight' ||
e.key === 'ArrowDown' ||
e.key === 'Backspace' ||
e.key === 'Escape' ||
(e.key === 'Enter' && e.metaKey) ||
e.key === 'Tab'
) {
this._handleKeyPress(e);
}
@@ -337,43 +338,7 @@ class TextInput extends Component<*> {
const shouldBlurOnSubmit = blurOnSubmit == null ? blurOnSubmitDefault : blurOnSubmit;
if (onKeyPress) {
let keyValue;
switch (e.which) {
case 8:
keyValue = 'Backspace';
break;
case 9:
keyValue = 'Tab';
break;
case 13:
keyValue = 'Enter';
break;
case 32:
keyValue = ' ';
break;
case 37:
keyValue = 'ArrowLeft';
break;
case 38:
keyValue = 'ArrowUp';
break;
case 39:
keyValue = 'ArrowRight';
break;
case 40:
keyValue = 'ArrowDown';
break;
default: {
// Trim to only care about the keys that have a textual representation
if (e.shiftKey) {
keyValue = String.fromCharCode(e.which).trim();
} else {
keyValue = String.fromCharCode(e.which)
.toLowerCase()
.trim();
}
}
}
const keyValue = e.key;
if (keyValue) {
e.nativeEvent = {
@@ -388,7 +353,7 @@ class TextInput extends Component<*> {
}
}
if (!e.isDefaultPrevented() && e.which === 13 && !e.shiftKey) {
if (!e.isDefaultPrevented() && e.key === 'Enter' && !e.shiftKey) {
if ((blurOnSubmit || !multiline) && onSubmitEditing) {
// prevent "Enter" from inserting a newline
e.preventDefault();
@@ -396,6 +361,7 @@ class TextInput extends Component<*> {
onSubmitEditing(e);
}
if (shouldBlurOnSubmit) {
// $FlowFixMe
this.blur();
}
}
@@ -423,18 +389,15 @@ class TextInput extends Component<*> {
};
}
const styles = StyleSheet.create({
initial: {
const classes = css.create({
textinput: {
MozAppearance: 'textfield',
WebkitAppearance: 'none',
backgroundColor: 'transparent',
borderColor: 'black',
border: '0 solid black',
borderRadius: 0,
borderStyle: 'solid',
borderWidth: 0,
boxSizing: 'border-box',
fontFamily: 'System',
fontSize: 14,
fontFamily: '14px System',
padding: 0,
resize: 'none'
}

View File

@@ -2,8 +2,8 @@
import UIManager from '..';
const createStyledNode = (style = {}) => {
const root = document.createElement('div');
const createStyledNode = (name = 'div', style = {}) => {
const root = document.createElement(name);
Object.keys(style).forEach(prop => {
root.style[prop] = style[prop];
});
@@ -18,6 +18,29 @@ const componentStub = {
};
describe('apis/UIManager', () => {
describe('focus', () => {
test('sets tabIndex="-1" on elements not programmatically focusable by default', () => {
const node = createStyledNode();
UIManager.focus(node);
expect(node.getAttribute('tabIndex')).toEqual('-1');
});
test('doesn\'t set tabIndex="-1" on elements with an existing tabIndex', () => {
const node = createStyledNode();
node.tabIndex = 0;
UIManager.focus(node);
expect(node.getAttribute('tabIndex')).toEqual('0');
});
test('doesn\'t set tabIndex="-1" on elements focusable by default', () => {
['a', 'input', 'select', 'textarea'].forEach(name => {
const node = createStyledNode(name);
UIManager.focus(node);
expect(node.getAttribute('tabIndex')).toBeNull();
});
});
});
describe('updateView', () => {
test('supports className alias for class', () => {
const node = createStyledNode();
@@ -27,7 +50,7 @@ describe('apis/UIManager', () => {
});
test('adds correct DOM styles to existing style', () => {
const node = createStyledNode({ color: 'red' });
const node = createStyledNode('div', { color: 'red' });
const props = { style: { marginTop: 0, marginBottom: 0, opacity: 0 } };
UIManager.updateView(node, props, componentStub);
expect(node.getAttribute('style')).toEqual(
@@ -36,7 +59,7 @@ describe('apis/UIManager', () => {
});
test('replaces input and textarea text', () => {
const node = createStyledNode();
const node = createStyledNode('textarea');
node.value = 'initial';
const textProp = { text: 'expected-text' };
const valueProp = { value: 'expected-value' };

View File

@@ -7,28 +7,24 @@
* @noflow
*/
import getBoundingClientRect from '../../modules/getBoundingClientRect';
import setValueForStyles from '../../vendor/react-dom/setValueForStyles';
const getRect = node => {
const height = node.offsetHeight;
// Unlike the DOM's getBoundingClientRect, React Native layout measurements
// for "height" and "width" ignore scale transforms.
// https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements
const { x, y, top, left } = getBoundingClientRect(node);
const width = node.offsetWidth;
let left = node.offsetLeft;
let top = node.offsetTop;
node = node.offsetParent;
while (node && node.nodeType === 1 /* Node.ELEMENT_NODE */) {
left += node.offsetLeft - node.scrollLeft;
top += node.offsetTop - node.scrollTop;
node = node.offsetParent;
}
return { height, left, top, width };
const height = node.offsetHeight;
return { x, y, width, height, top, left };
};
const measureLayout = (node, relativeToNativeNode, callback) => {
const relativeNode = relativeToNativeNode || (node && node.parentNode);
if (node && relativeNode) {
setTimeout(() => {
const relativeRect = getRect(relativeNode);
const relativeRect = getBoundingClientRect(relativeNode);
const { height, left, top, width } = getRect(node);
const x = left - relativeRect.left;
const y = top - relativeRect.top;
@@ -37,6 +33,13 @@ const measureLayout = (node, relativeToNativeNode, callback) => {
}
};
const focusableElements = {
A: true,
INPUT: true,
SELECT: true,
TEXTAREA: true
};
const UIManager = {
blur(node) {
try {
@@ -46,6 +49,13 @@ const UIManager = {
focus(node) {
try {
const name = node.nodeName;
// A tabIndex of -1 allows element to be programmatically focused but
// prevents keyboard focus, so we don't want to set the value on elements
// that support keyboard focus by default.
if (node.getAttribute('tabIndex') == null && focusableElements[name] == null) {
node.setAttribute('tabIndex', '-1');
}
node.focus();
} catch (err) {}
},

View File

@@ -33,6 +33,7 @@ export type ViewProps = {
accessibilityLabel?: string,
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive',
accessibilityRole?: string,
accessibilityStates?: Array<string>,
accessibilityTraits?: string | Array<string>,
accessible?: boolean,
children?: any,
@@ -83,6 +84,16 @@ const ViewPropTypes = {
accessibilityLabel: string,
accessibilityLiveRegion: oneOf(['assertive', 'none', 'polite']),
accessibilityRole: string,
accessibilityStates: oneOf([
'disabled',
'selected',
/* web-only */ 'busy',
'checked',
'expanded',
'grabbed',
'invalid',
'pressed'
]),
accessibilityTraits: oneOfType([array, string]),
accessible: bool,
children: any,

View File

@@ -35,6 +35,7 @@ const ViewStylePropTypes = {
/**
* @platform web
*/
backdropFilter: string,
backgroundAttachment: string,
backgroundBlendMode: string,
backgroundClip: string,
@@ -51,6 +52,8 @@ const ViewStylePropTypes = {
overscrollBehavior: overscrollBehaviorType,
overscrollBehaviorX: overscrollBehaviorType,
overscrollBehaviorY: overscrollBehaviorType,
scrollSnapAlign: string,
scrollSnapType: string,
WebkitMaskImage: string,
WebkitOverflowScrolling: oneOf(['auto', 'touch'])
};

View File

@@ -1,8 +1,9 @@
const whitelist = {
const supportedProps = {
accessibilityComponentType: true,
accessibilityLabel: true,
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityStates: true,
accessibilityTraits: true,
accessible: true,
children: true,
@@ -67,7 +68,7 @@ const filterSupportedProps = props => {
const safeProps = {};
for (const prop in props) {
if (props.hasOwnProperty(prop)) {
if (whitelist[prop] || prop.indexOf('aria-') === 0 || prop.indexOf('data-') === 0) {
if (supportedProps[prop] || prop.indexOf('aria-') === 0 || prop.indexOf('data-') === 0) {
safeProps[prop] = props[prop];
}
}

View File

@@ -10,6 +10,7 @@ import applyLayout from '../../modules/applyLayout';
import applyNativeMethods from '../../modules/applyNativeMethods';
import { bool } from 'prop-types';
import createElement from '../createElement';
import css from '../StyleSheet/css';
import filterSupportedProps from './filterSupportedProps';
import invariant from 'fbjs/lib/invariant';
import StyleSheet from '../StyleSheet';
@@ -51,14 +52,18 @@ class View extends Component<ViewProps> {
const { isInAParentText } = this.context;
supportedProps.className = classes.view;
supportedProps.style = StyleSheet.compose(
styles.initial,
StyleSheet.compose(isInAParentText && styles.inline, this.props.style)
isInAParentText && styles.inline,
this.props.style
);
if (hitSlop) {
const hitSlopStyle = calculateHitSlopStyle(hitSlop);
const hitSlopChild = createElement('span', { style: [styles.hitSlop, hitSlopStyle] });
const hitSlopChild = createElement('span', {
className: classes.hitslop,
style: hitSlopStyle
});
supportedProps.children = React.Children.toArray([hitSlopChild, supportedProps.children]);
}
@@ -66,32 +71,45 @@ class View extends Component<ViewProps> {
}
}
const styles = StyleSheet.create({
// https://github.com/facebook/css-layout#default-values
initial: {
const classes = css.create({
view: {
alignItems: 'stretch',
borderWidth: 0,
borderStyle: 'solid',
border: '0 solid black',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
margin: 0,
minHeight: 0,
minWidth: 0,
padding: 0,
position: 'relative',
zIndex: 0,
// fix flexbox bugs
minHeight: 0,
minWidth: 0
},
inline: {
display: 'inline-flex'
// resets for if View is rendered as a link or list DOM element
backgroundColor: 'transparent',
color: 'inherit',
font: 'inherit',
listStyle: 'none',
textAlign: 'inherit',
textDecoration: 'none'
},
// this zIndex-ordering positions the hitSlop above the View but behind
// its children
hitSlop: {
...StyleSheet.absoluteFillObject,
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: -1
}
});
const styles = StyleSheet.create({
inline: {
display: 'inline-flex'
}
});
export default applyLayout(applyNativeMethods(View));

View File

@@ -25,4 +25,11 @@ describe('modules/AccessibilityUtil/propsToAriaRole', () => {
})
).toEqual('link');
});
test('when "accessibilityRole" is a native-only value', () => {
expect(propsToAriaRole({ accessibilityRole: 'none' })).toEqual('presentation');
expect(propsToAriaRole({ accessibilityRole: 'imagebutton' })).toEqual(undefined);
// not really native-only, but used to allow Web to render <label> around TextInput
expect(propsToAriaRole({ accessibilityRole: 'label' })).toEqual(undefined);
});
});

View File

@@ -7,6 +7,8 @@
* @flow
*/
const isDisabled = (props: Object) => props.disabled || props['aria-disabled'];
const isDisabled = (props: Object) =>
props.disabled ||
(Array.isArray(props.accessibilityStates) && props.accessibilityStates.indexOf('disabled') > -1);
export default isDisabled;

View File

@@ -15,7 +15,6 @@ const roleComponents = {
complementary: 'aside',
contentinfo: 'footer',
form: 'form',
label: 'label',
link: 'a',
list: 'ul',
listitem: 'li',
@@ -27,6 +26,11 @@ const roleComponents = {
const emptyObject = {};
const propsToAccessibilityComponent = (props: Object = emptyObject) => {
// special-case for "label" role which doesn't map to an ARIA role
if (props.accessibilityRole === 'label') {
return 'label';
}
const role = propsToAriaRole(props);
if (role) {
if (role === 'heading') {

View File

@@ -23,6 +23,21 @@ const accessibilityTraitsToRole = {
summary: 'region'
};
const accessibilityRoleToWebRole = {
adjustable: 'slider',
button: 'button',
header: 'heading',
image: 'img',
imagebutton: null,
keyboardkey: null,
label: null,
link: 'link',
none: 'presentation',
search: 'search',
summary: 'region',
text: null
};
/**
* Provides compatibility with React Native's "accessibilityTraits" (iOS) and
* "accessibilityComponentType" (Android), converting them to equivalent ARIA
@@ -34,7 +49,11 @@ const propsToAriaRole = ({
accessibilityTraits
}: Object) => {
if (accessibilityRole) {
return accessibilityRole;
const inferredRole = accessibilityRoleToWebRole[accessibilityRole];
if (inferredRole !== null) {
// ignore roles that don't map to web
return inferredRole || accessibilityRole;
}
}
if (accessibilityTraits) {
const trait = Array.isArray(accessibilityTraits) ? accessibilityTraits[0] : accessibilityTraits;

View File

@@ -375,9 +375,14 @@ const ScrollResponderMixin = {
({ x, y, animated } = x || emptyObject);
}
const node = this.scrollResponderGetScrollableNode();
UIManager.updateView(node, { style: { scrollBehavior: !animated ? 'auto' : 'smooth' } }, this);
node.scrollLeft = x || 0;
node.scrollTop = y || 0;
const left = x || 0;
const top = y || 0;
if (typeof node.scroll === 'function') {
node.scroll({ top, left, behavior: !animated ? 'auto' : 'smooth' });
} else {
node.scrollLeft = left;
node.scrollTop = top;
}
},
/**

View File

@@ -23,6 +23,8 @@ const TransformPropTypes = {
shape({ scale: number }),
shape({ scaleX: number }),
shape({ scaleY: number }),
shape({ scaleZ: number }),
shape({ scale3d: string }),
shape({ skewX: string }),
shape({ skewY: string }),
shape({ translateX: numberOrString }),

View File

@@ -2,10 +2,14 @@
exports[`modules/createDOMProps includes "rel" values for "a" elements (to securely open external links) 1`] = `" noopener noreferrer"`;
exports[`modules/createDOMProps includes cursor style for "button" role 1`] = `"rn-cursor-1loqt21"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 1`] = `"r-reset"`;
exports[`modules/createDOMProps includes reset styles for "a" elements 1`] = `"rn-backgroundColor-1niwhzg rn-color-homxoj rn-textDecoration-bauka4"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 2`] = `"r-reset"`;
exports[`modules/createDOMProps includes reset styles for "button" elements 1`] = `"rn-appearance-30o5oe rn-backgroundColor-1niwhzg rn-color-homxoj rn-fontFamily-poiln3 rn-fontSize-7cikom rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-textAlign-1ttztb7"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 3`] = `"r-reset"`;
exports[`modules/createDOMProps includes reset styles for "ul" elements 1`] = `"rn-listStyle-1ebb2ja"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 4`] = `"r-reset"`;
exports[`modules/createDOMProps includes cursor style for pressable roles 1`] = `"r-pointer"`;
exports[`modules/createDOMProps includes cursor style for pressable roles 2`] = `"r-pointer"`;

View File

@@ -35,18 +35,12 @@ describe('modules/createDOMProps', () => {
expect(createProps({ accessibilityRole, disabled: true })).toEqual(
expect.objectContaining({ 'aria-disabled': true, disabled: true, tabIndex: '-1' })
);
expect(createProps({ accessibilityRole, 'aria-disabled': true })).toEqual(
expect.objectContaining({ 'aria-disabled': true, disabled: true, tabIndex: '-1' })
);
});
test('when "disabled" is false', () => {
expect(createProps({ accessibilityRole, disabled: false })).toEqual(
expect.objectContaining({ 'data-focusable': true })
);
expect(createProps({ accessibilityRole, 'aria-disabled': false })).toEqual(
expect.objectContaining({ 'data-focusable': true })
);
});
test('when "importantForAccessibility" is "no"', () => {
@@ -88,18 +82,12 @@ describe('modules/createDOMProps', () => {
expect(createProps({ accessibilityRole, disabled: true })).toEqual(
expect.objectContaining({ 'aria-disabled': true, disabled: true })
);
expect(createProps({ accessibilityRole, 'aria-disabled': true })).toEqual(
expect.objectContaining({ 'aria-disabled': true, disabled: true })
);
});
test('when "disabled" is false', () => {
expect(createProps({ accessibilityRole, disabled: false })).toEqual(
expect.objectContaining({ 'data-focusable': true, tabIndex: '0' })
);
expect(createProps({ accessibilityRole, 'aria-disabled': false })).toEqual(
expect.objectContaining({ 'data-focusable': true, tabIndex: '0' })
);
});
test('when "importantForAccessibility" is "no"', () => {
@@ -164,12 +152,17 @@ describe('modules/createDOMProps', () => {
expect(props['aria-live']).toEqual('off');
});
describe('prop "accessibilityRole"', () => {
test('does not become "role" when value is "label"', () => {
const accessibilityRole = 'label';
const props = createProps({ accessibilityRole });
expect(props.role).toBeUndefined();
});
test('prop "accessibilityRole" becomes "role"', () => {
const accessibilityRole = 'button';
const props = createProps({ accessibilityRole });
expect(props.role).toEqual('button');
});
test('prop "accessibilityStates" becomes ARIA states', () => {
const accessibilityStates = ['disabled', 'selected'];
const props = createProps({ accessibilityStates });
expect(props['aria-disabled']).toEqual(true);
expect(props['aria-selected']).toEqual(true);
});
test('prop "className" is preserved', () => {
@@ -200,23 +193,15 @@ describe('modules/createDOMProps', () => {
expect(props.rel).toMatchSnapshot();
});
test('includes reset styles for "a" elements', () => {
const props = createDOMProps('a');
expect(props.className).toMatchSnapshot();
test('includes cursor style for pressable roles', () => {
expect(createDOMProps('span', { accessibilityRole: 'link' }).className).toMatchSnapshot();
expect(createDOMProps('span', { accessibilityRole: 'button' }).className).toMatchSnapshot();
});
test('includes reset styles for "button" elements', () => {
const props = createDOMProps('button');
expect(props.className).toMatchSnapshot();
});
test('includes cursor style for "button" role', () => {
const props = createDOMProps('span', { accessibilityRole: 'button' });
expect(props.className).toMatchSnapshot();
});
test('includes reset styles for "ul" elements', () => {
const props = createDOMProps('ul');
expect(props.className).toMatchSnapshot();
test('includes base reset style for browser-styled elements', () => {
expect(createDOMProps('a').className).toMatchSnapshot();
expect(createDOMProps('button').className).toMatchSnapshot();
expect(createDOMProps('li').className).toMatchSnapshot();
expect(createDOMProps('ul').className).toMatchSnapshot();
});
});

View File

@@ -13,40 +13,6 @@ import styleResolver from '../../exports/StyleSheet/styleResolver';
const emptyObject = {};
const resetStyles = StyleSheet.create({
ariaButton: {
cursor: 'pointer'
},
button: {
appearance: 'none',
backgroundColor: 'transparent',
color: 'inherit',
fontFamily: 'inherit',
fontSize: 'inherit',
fontStyle: 'inherit',
fontVariant: ['inherit'],
fontWeight: 'inherit',
lineHeight: 'inherit',
textAlign: 'inherit'
},
heading: {
fontFamily: 'inherit',
fontSize: 'inherit',
fontStyle: 'inherit',
fontVariant: ['inherit'],
fontWeight: 'inherit',
lineHeight: 'inherit'
},
link: {
backgroundColor: 'transparent',
color: 'inherit',
textDecorationLine: 'none'
},
list: {
listStyle: 'none'
}
});
const pointerEventsStyles = StyleSheet.create({
auto: {
pointerEvents: 'auto'
@@ -76,6 +42,7 @@ const createDOMProps = (component, props, styleResolver) => {
const {
accessibilityLabel,
accessibilityLiveRegion,
accessibilityStates,
importantForAccessibility,
nativeID,
placeholderTextColor,
@@ -104,7 +71,12 @@ const createDOMProps = (component, props, styleResolver) => {
if (accessibilityLiveRegion && accessibilityLiveRegion.constructor === String) {
domProps['aria-live'] = accessibilityLiveRegion === 'none' ? 'off' : accessibilityLiveRegion;
}
if (role && role.constructor === String && role !== 'label') {
if (Array.isArray(accessibilityStates)) {
for (let i = 0; i < accessibilityStates.length; i += 1) {
domProps[`aria-${accessibilityStates[i]}`] = true;
}
}
if (role && role.constructor === String) {
domProps.role = role;
}
@@ -123,6 +95,8 @@ const createDOMProps = (component, props, styleResolver) => {
importantForAccessibility !== 'no-hide-descendants';
if (
role === 'link' ||
component === 'a' ||
component === 'button' ||
component === 'input' ||
component === 'select' ||
component === 'textarea'
@@ -146,30 +120,56 @@ const createDOMProps = (component, props, styleResolver) => {
// STYLE
// Resolve React Native styles to optimized browser equivalent
const reactNativeStyle = [
component === 'a' && resetStyles.link,
component === 'button' && resetStyles.button,
role === 'heading' && resetStyles.heading,
component === 'ul' && resetStyles.list,
role === 'button' && !disabled && resetStyles.ariaButton,
const reactNativeStyle = StyleSheet.compose(
pointerEvents && pointerEventsStyles[pointerEvents],
providedStyle,
placeholderTextColor && { placeholderTextColor }
];
StyleSheet.compose(
providedStyle,
placeholderTextColor && { placeholderTextColor }
)
);
const { className, style } = styleResolver(reactNativeStyle);
if (className && className.constructor === String) {
domProps.className = props.className ? `${props.className} ${className}` : className;
}
if (style) {
domProps.style = style;
}
// CLASSNAME
// Apply static style resets
let c;
// style interactive elements for mouse and mobile browsers
if ((role === 'button' || role === 'link') && !disabled) {
c = 'r-pointer';
}
// style reset various elements (not all are used internally)
if (
component === 'a' ||
component === 'button' ||
component === 'li' ||
component === 'ul' ||
role === 'heading'
) {
c = 'r-reset' + (c != null ? ' ' + c : '');
}
// style from createElement use
if (props.className != null) {
c = props.className + (c != null ? ' ' + c : '');
}
// style from React Native StyleSheets
if (className != null && className !== '') {
c = (c != null ? c + ' ' : '') + className;
}
if (c != null) {
domProps.className = c;
}
// OTHER
// Native element ID
if (nativeID && nativeID.constructor === String) {
domProps.id = nativeID;
}
// Link security and automation test ids
// Link security
// https://mathiasbynens.github.io/rel-noopener/
// Note: using "noreferrer" doesn't impact referrer tracking for https
// transfers (i.e., from https to https).
if (component === 'a' && domProps.target === '_blank') {
domProps.rel = `${domProps.rel || ''} noopener noreferrer`;
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
/* global HTMLElement */
const getBoundingClientRect = (node: HTMLElement) => {
if (node) {
const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */
if (isElement && typeof node.getBoundingClientRect === 'function') {
return node.getBoundingClientRect();
}
}
};
export default getBoundingClientRect;

View File

@@ -1,14 +0,0 @@
const hasOwnProperty = Object.prototype.hasOwnProperty;
const mapKeyValue = (obj, fn) => {
const result = [];
for (const key in obj) {
if (hasOwnProperty.call(obj, key)) {
const r = fn(key, obj[key]);
r && result.push(r);
}
}
return result;
};
export default mapKeyValue;

View File

@@ -7,18 +7,11 @@
* @flow
*/
import getBoundingClientRect from '../getBoundingClientRect';
const emptyArray = [];
const emptyFunction = () => {};
const getRect = node => {
if (node) {
const isElement = node.nodeType === 1 /* Node.ELEMENT_NODE */;
if (isElement && typeof node.getBoundingClientRect === 'function') {
return node.getBoundingClientRect();
}
}
};
// Mobile Safari re-uses touch objects, so we copy the properties we want and normalize the identifier
const normalizeTouches = touches => {
if (!touches) {
@@ -35,13 +28,13 @@ const normalizeTouches = touches => {
clientY: touch.clientY,
force: touch.force,
get locationX() {
rect = rect || getRect(touch.target);
rect = rect || getBoundingClientRect(touch.target);
if (rect) {
return touch.pageX - rect.left;
}
},
get locationY() {
rect = rect || getRect(touch.target);
rect = rect || getBoundingClientRect(touch.target);
if (rect) {
return touch.pageY - rect.top;
}
@@ -121,13 +114,13 @@ function normalizeMouseEvent(nativeEvent) {
force: nativeEvent.force,
identifier: 0,
get locationX() {
rect = rect || getRect(nativeEvent.target);
rect = rect || getBoundingClientRect(nativeEvent.target);
if (rect) {
return nativeEvent.pageX - rect.left;
}
},
get locationY() {
rect = rect || getRect(nativeEvent.target);
rect = rect || getBoundingClientRect(nativeEvent.target);
if (rect) {
return nativeEvent.pageY - rect.top;
}

View File

@@ -7,7 +7,7 @@
* @flow
*/
import createPrefixer from 'inline-style-prefixer/static/createPrefixer';
import createPrefixer from 'inline-style-prefixer/lib/createPrefixer';
import staticData from './static';
const prefixAll = createPrefixer(staticData);

View File

@@ -1,14 +1,15 @@
import crossFade from 'inline-style-prefixer/static/plugins/crossFade';
import cursor from 'inline-style-prefixer/static/plugins/cursor';
import filter from 'inline-style-prefixer/static/plugins/filter';
import flex from 'inline-style-prefixer/static/plugins/flex';
import flexboxIE from 'inline-style-prefixer/static/plugins/flexboxIE';
import flexboxOld from 'inline-style-prefixer/static/plugins/flexboxOld';
import gradient from 'inline-style-prefixer/static/plugins/gradient';
import imageSet from 'inline-style-prefixer/static/plugins/imageSet';
import position from 'inline-style-prefixer/static/plugins/position';
import sizing from 'inline-style-prefixer/static/plugins/sizing';
import transition from 'inline-style-prefixer/static/plugins/transition';
import backgroundClip from 'inline-style-prefixer/lib/plugins/backgroundClip';
import crossFade from 'inline-style-prefixer/lib/plugins/crossFade';
import cursor from 'inline-style-prefixer/lib/plugins/cursor';
import filter from 'inline-style-prefixer/lib/plugins/filter';
import flex from 'inline-style-prefixer/lib/plugins/flex';
import flexboxIE from 'inline-style-prefixer/lib/plugins/flexboxIE';
import flexboxOld from 'inline-style-prefixer/lib/plugins/flexboxOld';
import gradient from 'inline-style-prefixer/lib/plugins/gradient';
import imageSet from 'inline-style-prefixer/lib/plugins/imageSet';
import position from 'inline-style-prefixer/lib/plugins/position';
import sizing from 'inline-style-prefixer/lib/plugins/sizing';
import transition from 'inline-style-prefixer/lib/plugins/transition';
const w = ['Webkit'];
const m = ['Moz'];
const ms = ['ms'];
@@ -18,6 +19,7 @@ const wmms = ['Webkit', 'Moz', 'ms'];
export default {
plugins: [
backgroundClip,
crossFade,
cursor,
filter,
@@ -120,6 +122,7 @@ export default {
flowInto: wms,
flowFrom: wms,
regionFragment: wms,
textOrientation: w,
textAlignLast: m,
tabSize: m,
wrapFlow: ms,
@@ -144,7 +147,7 @@ export default {
gridRowGap: ms,
gridArea: ms,
gridGap: ms,
textSizeAdjust: wms,
textSizeAdjust: ['ms', 'Webkit'],
borderImage: w,
borderImageOutset: w,
borderImageRepeat: w,

View File

@@ -1561,8 +1561,7 @@ class CellRenderer extends React.Component<
getItemLayout?: ?Function,
renderItem: renderItemType,
},
prevCellKey: ?string,
style: ?DangerouslyImpreciseStyleProp,
prevCellKey: ?string
},
$FlowFixMeState,
> {
@@ -1631,7 +1630,6 @@ class CellRenderer extends React.Component<
index,
inversionStyle,
parentProps,
style,
} = this.props;
const {renderItem, getItemLayout} = parentProps;
invariant(renderItem, 'no renderItem!');
@@ -1651,9 +1649,9 @@ class CellRenderer extends React.Component<
);
const cellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle, style]
: [styles.columnReverse, inversionStyle, style]
: horizontal ? [styles.row, inversionStyle, style] : [inversionStyle, style];
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal ? [styles.row, inversionStyle] : inversionStyle;
if (!CellRendererComponent) {
return (
<View style={cellStyle} onLayout={onLayout}>

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "website",
"version": "0.9.6",
"version": "0.9.13",
"scripts": {
"build": "build-storybook -o ./dist -c ./storybook/.storybook",
"start": "start-storybook -p 9001 -c ./storybook/.storybook",
@@ -12,10 +12,10 @@
"@storybook/react": "^3.4.3",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react-native-web": "0.9.6"
"react-native-web": "0.9.13"
},
"devDependencies": {
"babel-plugin-react-native-web": "0.9.6",
"babel-plugin-react-native-web": "0.9.13",
"url-loader": "^1.0.1",
"webpack": "^4.8.1"
}

View File

@@ -1,8 +0,0 @@
{
"presets": [
"react-native"
],
"plugins": [
"react-native-web"
]
}

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-web']
};
};

View File

@@ -101,6 +101,12 @@ const ScrollViewScreen = () => (
]}
/>
<DocItem
name="pagingEnabled"
typeInfo="?boolean = false"
description="When true, the scroll view snaps to individual items in the list when scrolling."
/>
<DocItem
name="scrollEnabled"
typeInfo="?boolean = true"

View File

@@ -266,7 +266,6 @@ const stylePropTypes = [
typeInfo: 'number | string'
},
{
label: 'web',
name: 'textTransform',
typeInfo: 'string'
},
@@ -279,6 +278,12 @@ const stylePropTypes = [
name: 'whiteSpace',
typeInfo: 'string'
},
{
label: 'web',
name: 'wordBreak',
typeInfo: 'enum("normal", "break-all", "break-word", "keep-all")'
},
{
label: 'web',
name: 'wordWrap',

View File

@@ -14,12 +14,12 @@ class AppText extends React.PureComponent {
};
render() {
const { style, ...rest } = this.props;
const { accessibilityRole, style, ...rest } = this.props;
const isInAParentText = this.context;
return (
<Text
{...rest}
accessibilityRole={rest.href ? 'link' : undefined}
accessibilityRole={rest.href ? 'link' : accessibilityRole}
style={[!isInAParentText && styles.baseText, style, rest.href && styles.link]}
/>
);

View File

@@ -8,7 +8,11 @@ import AppText from './AppText';
import React from 'react';
import { StyleSheet, View } from 'react-native';
const SectionTitle = ({ children }) => <AppText style={styles.sectionTitle}>{children}</AppText>;
const SectionTitle = ({ children }) => (
<AppText accessibilityRole="heading" aria-level="2" style={styles.sectionTitle}>
{children}
</AppText>
);
const Section = ({ children, title }) => (
<View>

View File

@@ -10,7 +10,11 @@ import insertBetween from './insertBetween';
import React from 'react';
import { StyleSheet, View } from 'react-native';
const Title = ({ children }) => <AppText style={styles.title}>{children}</AppText>;
const Title = ({ children }) => (
<AppText accessibilityRole="heading" style={styles.title}>
{children}
</AppText>
);
export const Description = ({ children }) => (
<AppText style={styles.description}>

View File

@@ -1,11 +1,11 @@
const createConfig = ({ modules }) => ({
presets: [
[
'babel-preset-env',
'@babel/preset-env',
{
loose: true,
modules,
exclude: ['transform-es2015-typeof-symbol'],
exclude: ['transform-typeof-symbol'],
targets: {
browsers: [
'chrome 38',
@@ -24,17 +24,19 @@ const createConfig = ({ modules }) => ({
}
}
],
'babel-preset-react',
'babel-preset-flow'
'@babel/preset-react',
'@babel/preset-flow'
],
plugins: [
['babel-plugin-transform-class-properties', { loose: true }],
['babel-plugin-transform-object-rest-spread', { useBuiltIns: true }],
['babel-plugin-transform-react-remove-prop-types', { mode: 'wrap' }]
'@babel/plugin-transform-flow-strip-types',
['babel-plugin-transform-react-remove-prop-types', { mode: 'wrap' }],
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
].concat(modules ? ['babel-plugin-add-module-exports'] : [])
});
module.exports =
process.env.BABEL_ENV === 'commonjs'
module.exports = function() {
return process.env.BABEL_ENV === 'commonjs' || process.env.NODE_ENV === 'test'
? createConfig({ modules: 'commonjs' })
: createConfig({ modules: false });
};

View File

@@ -1,6 +1,6 @@
'use strict';
const generator = require('inline-style-prefixer/generator');
const generator = require('inline-style-prefixer/lib/generator').default;
const path = require('path');
const browserList = {
@@ -19,8 +19,5 @@ const browserList = {
};
generator(browserList, {
staticPath: path.join(
__dirname,
'../../packages/react-native-web/src/modules/prefixStyles/static.js'
)
path: path.join(__dirname, '../../packages/react-native-web/src/modules/prefixStyles/static.js')
});

View File

@@ -11,7 +11,7 @@ module.exports = {
rootDir: process.cwd(),
roots: ['<rootDir>/packages'],
setupFiles: ['jest-canvas-mock'],
setupTestFrameworkScriptFile: require.resolve('./setupFramework.js'),
setupFilesAfterEnv: [require.resolve('./setupFramework.js')],
snapshotSerializers: ['enzyme-to-json/serializer'],
testEnvironment: 'jsdom',
timers: 'fake'

5988
yarn.lock

File diff suppressed because it is too large Load Diff