mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-31 18:21:38 +08:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd04d65b03 | ||
|
|
0ab984f507 | ||
|
|
3d1ad50a58 | ||
|
|
92554321df | ||
|
|
1c9270c4ea | ||
|
|
8a5f9cd7d9 | ||
|
|
aac6b796b2 | ||
|
|
c77ce19f1b | ||
|
|
25b74d30c4 | ||
|
|
4191d58694 | ||
|
|
11b861ae64 | ||
|
|
68bf08112a | ||
|
|
b277b3e509 | ||
|
|
c135dddbd1 | ||
|
|
ffc6368162 | ||
|
|
501c19fe9b | ||
|
|
e1da11fa1d | ||
|
|
b2a4d742a9 | ||
|
|
8b965fdfa0 | ||
|
|
8cfef85934 | ||
|
|
6db24e9358 | ||
|
|
13e36bee65 | ||
|
|
93e8e90a1a | ||
|
|
894fd0362d | ||
|
|
a1664927ce | ||
|
|
ae2abc578a | ||
|
|
5f7b3f5fef | ||
|
|
75f653818a | ||
|
|
2c52d41b75 | ||
|
|
83f749d983 | ||
|
|
bf5046415c | ||
|
|
885d4586a9 | ||
|
|
ea0a778ba3 | ||
|
|
0a7eda2505 | ||
|
|
35385e7b69 | ||
|
|
3fd29697c0 | ||
|
|
a26033be2d | ||
|
|
fdb4ee4aae | ||
|
|
08300f624f | ||
|
|
7f5a2807e2 | ||
|
|
292f045c52 |
10
.babelrc
10
.babelrc
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"optional": [
|
||||
"es7.classProperties",
|
||||
"runtime"
|
||||
],
|
||||
"stage": 1
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-1",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
|
||||
83
README.md
83
README.md
@@ -2,11 +2,11 @@
|
||||
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[![npm version][npm-image]][npm-url]
|
||||

|
||||
|
||||
[React Native][react-native-url] components and APIs for the Web.
|
||||
~17.7 KB minified and gzipped.
|
||||
|
||||
* [Slack: reactiflux channel #react-native-web][slack-url]
|
||||
* [Discord: #react-native-web on reactiflux][discord-url]
|
||||
* [Gitter: react-native-web][gitter-url]
|
||||
|
||||
## Table of contents
|
||||
@@ -16,6 +16,7 @@
|
||||
* [APIs](#apis)
|
||||
* [Components](#components)
|
||||
* [Styling](#styling)
|
||||
* [Accessibility](#accessibility)
|
||||
* [Contributing](#contributing)
|
||||
* [Thanks](#thanks)
|
||||
* [License](#license)
|
||||
@@ -96,7 +97,9 @@ const css = StyleSheet.renderToString();
|
||||
const Html = () => (
|
||||
<html>
|
||||
<head>
|
||||
<style id="react-stylesheet">{css}</style>
|
||||
<meta charSet="utf-8" />
|
||||
<meta content="initial-scale=1,width=device-width" name="viewport" />
|
||||
<style id="react-stylesheet" dangerouslySetInnerHTML={{ __html: css } />
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
@@ -111,13 +114,13 @@ Render styles on the client:
|
||||
// client.js
|
||||
import App from './components/App'
|
||||
import React, { StyleSheet } from 'react-native-web'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
React.render(
|
||||
<App />,
|
||||
document.getElementById('react-root')
|
||||
)
|
||||
const reactRoot = document.getElementById('react-root')
|
||||
const reactStyleSheet = document.getElementById('react-stylesheet')
|
||||
|
||||
document.getElementById('stylesheet').textContent = StyleSheet.renderToString()
|
||||
ReactDOM.render(<App />, reactRoot)
|
||||
reactStyleSheet.textContent = StyleSheet.renderToString()
|
||||
```
|
||||
|
||||
## APIs
|
||||
@@ -125,7 +128,8 @@ document.getElementById('stylesheet').textContent = StyleSheet.renderToString()
|
||||
### [`StyleSheet`](docs/apis/StyleSheet.md)
|
||||
|
||||
StyleSheet is a style abstraction that transforms inline styles to CSS on the
|
||||
client or the server. It provides a minimal CSS reset.
|
||||
client or the server. It provides a minimal CSS reset targeting elements and
|
||||
pseudo-elements beyond the reach of React inline styles.
|
||||
|
||||
## Components
|
||||
|
||||
@@ -138,13 +142,13 @@ and child content.
|
||||
|
||||
(TODO)
|
||||
|
||||
### [`ScrollView`](docs/components/ListView.md)
|
||||
### [`ScrollView`](docs/components/ScrollView.md)
|
||||
|
||||
(TODO)
|
||||
A scrollable view with event throttling.
|
||||
|
||||
### [`Text`](docs/components/Text.md)
|
||||
|
||||
Displays text as an inline block and supports basic press handling.
|
||||
Displays text inline and supports basic press handling.
|
||||
|
||||
### [`TextInput`](docs/components/TextInput.md)
|
||||
|
||||
@@ -160,14 +164,56 @@ The fundamental UI building block using flexbox for layout.
|
||||
|
||||
## Styling
|
||||
|
||||
React Native for Web relies on styles being defined in JavaScript.
|
||||
React Native for Web relies on styles being defined in JavaScript. Styling
|
||||
components can be achieved with inline styles or the use of
|
||||
[StyleSheet](docs/apis/StyleSheet.md).
|
||||
|
||||
The `View` component makes it easy to build common layouts with flexbox, such
|
||||
as stacked and nested boxes with margin and padding. See this [guide to
|
||||
flexbox][flexbox-guide-url].
|
||||
|
||||
Styling components can be achieved with inline styles or the use of
|
||||
[StyleSheet](docs/apis/StyleSheet.md).
|
||||
### Media Queries, pseudo-classes, and pseudo-elements
|
||||
|
||||
|
||||
Changing styles and/or the render tree in response to device adaptation can be
|
||||
controlled in JavaScript, e.g.,
|
||||
[react-media-queries](https://github.com/bloodyowl/react-media-queries),
|
||||
[media-query-fascade](https://github.com/tanem/media-query-facade), or
|
||||
[react-responsive](https://github.com/contra/react-responsive). This has the
|
||||
benefit of co-locating breakpoint-specific DOM and style changes.
|
||||
|
||||
Pseudo-classes like `:hover` and `:focus` can be implemented with the `onHover`
|
||||
and `onFocus` events.
|
||||
|
||||
Pseudo-elements are not supported.
|
||||
|
||||
## Accessibility
|
||||
|
||||
On the Web, assistive technologies derive useful information about the
|
||||
structure, purpose, and interactivity of apps from their [HTML
|
||||
elements][html-accessibility-url], attributes, and [ARIA in
|
||||
HTML][aria-in-html-url].
|
||||
|
||||
The most common and best supported accessibility features of the Web are
|
||||
exposed as the props: `accessible`, `accessibilityLabel`,
|
||||
`accessibilityLiveRegion`, and `accessibilityRole`.
|
||||
|
||||
React Native for Web does not provide a way to directly control the rendered
|
||||
HTML element. The `accessibilityRole` prop is used to infer an [analogous HTML
|
||||
element][html-aria-url] to use in addition, where possible. While this may
|
||||
contradict some ARIA recommendations, it also helps avoid certain HTML5
|
||||
conformance errors and accessibility anti-patterns (e.g., giving a `heading`
|
||||
role to a `button` element).
|
||||
|
||||
For example:
|
||||
|
||||
* `<View accessibilityRole='article' />` => `<article role='article' />`.
|
||||
* `<View accessibilityRole='banner' />` => `<header role='banner' />`.
|
||||
* `<View accessibilityRole='button' />` => `<button type='button' role='button' />`.
|
||||
* `<Text accessibilityRole='link' href='/' />` => `<a role='link' href='/' />`.
|
||||
* `<View accessibilityRole='main' />` => `<main role='main' />`.
|
||||
|
||||
See the component documentation for more details.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -177,7 +223,7 @@ welcome!
|
||||
## Thanks
|
||||
|
||||
Thanks to current and past members of the React and React Native teams (in
|
||||
particular Vjeux and Pete Hunt), and Tobias Koppers for Webpack and CSS loader.
|
||||
particular Vjeux and Pete Hunt).
|
||||
|
||||
Thanks to [react-tappable](https://github.com/JedWatson/react-tappable) for
|
||||
backing the current implementation of `Touchable`.
|
||||
@@ -187,12 +233,15 @@ backing the current implementation of `Touchable`.
|
||||
Copyright (c) 2015 Nicolas Gallagher. Released under the [MIT
|
||||
license](http://www.opensource.org/licenses/mit-license.php).
|
||||
|
||||
[aria-in-html-url]: https://w3c.github.io/aria-in-html/
|
||||
[contributing-url]: https://github.com/necolas/react-native-web/blob/master/CONTRIBUTING.md
|
||||
[discord-url]: http://join.reactiflux.com
|
||||
[flexbox-guide-url]: https://css-tricks.com/snippets/css/a-guide-to-flexbox/
|
||||
[gitter-url]: https://gitter.im/necolas/react-native-web
|
||||
[html-accessibility-url]: http://www.html5accessibility.com/
|
||||
[html-aria-url]: http://www.w3.org/TR/html-aria/
|
||||
[npm-image]: https://badge.fury.io/js/react-native-web.svg
|
||||
[npm-url]: https://npmjs.org/package/react-native-web
|
||||
[react-native-url]: https://facebook.github.io/react-native/
|
||||
[slack-url]: https://reactiflux.slack.com/messages/react-native-web/
|
||||
[travis-image]: https://travis-ci.org/necolas/react-native-web.svg?branch=master
|
||||
[travis-url]: https://travis-ci.org/necolas/react-native-web
|
||||
|
||||
@@ -4,7 +4,8 @@ var ROOT = path.join(__dirname, '..')
|
||||
|
||||
module.exports = {
|
||||
DIST_DIRECTORY: path.join(ROOT, 'dist'),
|
||||
EXAMPLES_DIRECTORY: path.join(ROOT, 'examples'),
|
||||
SRC_DIRECTORY: path.join(ROOT, 'src'),
|
||||
ROOT_DIRECTORY: ROOT,
|
||||
TEST_ENTRY: path.join(ROOT, 'src/tests.webpack.js')
|
||||
TEST_ENTRY: path.join(ROOT, 'tests.webpack.js')
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ module.exports = function (config) {
|
||||
'karma-chrome-launcher',
|
||||
'karma-firefox-launcher',
|
||||
'karma-mocha',
|
||||
'karma-mocha-reporter',
|
||||
'karma-sourcemap-loader',
|
||||
'karma-spec-reporter',
|
||||
'karma-webpack'
|
||||
],
|
||||
preprocessors: {
|
||||
[constants.TEST_ENTRY]: [ 'webpack', 'sourcemap' ]
|
||||
},
|
||||
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'mocha' ],
|
||||
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
|
||||
singleRun: true,
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var webpack = require('webpack')
|
||||
|
||||
var DedupePlugin = webpack.optimize.DedupePlugin
|
||||
var DefinePlugin = webpack.DefinePlugin
|
||||
var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin
|
||||
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin
|
||||
|
||||
@@ -10,6 +11,11 @@ var plugins = [
|
||||
]
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
plugins.push(
|
||||
new DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
})
|
||||
)
|
||||
plugins.push(
|
||||
new UglifyJsPlugin({
|
||||
compress: {
|
||||
|
||||
@@ -5,13 +5,13 @@ var path = require('path')
|
||||
|
||||
module.exports = assign({}, base, {
|
||||
devServer: {
|
||||
contentBase: constants.SRC_DIRECTORY
|
||||
contentBase: constants.EXAMPLES_DIRECTORY
|
||||
},
|
||||
entry: {
|
||||
example: path.join(constants.SRC_DIRECTORY, 'example')
|
||||
example: path.join(constants.EXAMPLES_DIRECTORY, 'index')
|
||||
},
|
||||
output: {
|
||||
filename: 'example.js',
|
||||
filename: 'examples.js',
|
||||
path: constants.DIST_DIRECTORY
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
# StyleSheet
|
||||
|
||||
React Native for Web will automatically vendor-prefix styles applied to the
|
||||
libraries components. The `StyleSheet` abstraction converts predefined styles
|
||||
library's components. The `StyleSheet` abstraction converts predefined styles
|
||||
to CSS without a compile-time step. Some styles cannot be resolved outside of
|
||||
the render loop and are applied as inline styles.
|
||||
|
||||
The `style`-to-`className` conversion strategy is optimized to minimize the
|
||||
amount of CSS required. Unique declarations are defined using "atomic" CSS – a
|
||||
unique class name for a unique declaration.
|
||||
|
||||
React Native for Web includes a CSS reset to remove unwanted user agent styles
|
||||
from elements and pseudo-elements beyond the reach of React (e.g., `html` and
|
||||
`body`).
|
||||
|
||||
Create a new StyleSheet:
|
||||
|
||||
```
|
||||
```js
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 4,
|
||||
@@ -55,17 +47,53 @@ StyleSheet.renderToString()
|
||||
|
||||
**create**(obj: {[key: string]: any})
|
||||
|
||||
**destroy**()
|
||||
|
||||
Clears all style information.
|
||||
|
||||
**renderToString**()
|
||||
|
||||
## Strategy
|
||||
Renders a CSS Style Sheet.
|
||||
|
||||
Mapping entire `style` objects to CSS rules can lead to increasingly large CSS
|
||||
files. Each new component adds new rules to the stylesheet.
|
||||
## About
|
||||
|
||||

|
||||
### Strategy
|
||||
|
||||
React Native for Web uses an alternative strategy: mapping declarations to
|
||||
declarations.
|
||||
React Native for Web uses a `style`-to-`className` conversion strategy that is
|
||||
designed to avoid issues arising from the [7 deadly sins of
|
||||
CSS](https://speakerdeck.com/vjeux/react-css-in-js):
|
||||
|
||||
1. Global namespace
|
||||
2. Dependency hell
|
||||
3. Dead code elimination
|
||||
4. Code minification
|
||||
5. Sharing constants
|
||||
6. Non-deterministic resolution
|
||||
7. Breaking isolation
|
||||
|
||||
The strategy also minimizes the amount of generated CSS, making it more viable
|
||||
to inline the style sheet when pre-rendering pages on the server. There is one
|
||||
unique selector per unique style _declaration_.
|
||||
|
||||
```js
|
||||
// definition
|
||||
{
|
||||
heading: {
|
||||
color: 'gray',
|
||||
fontSize: '2rem'
|
||||
},
|
||||
text: {
|
||||
color: 'gray',
|
||||
fontSize: '1.25rem'
|
||||
}
|
||||
}
|
||||
|
||||
// css
|
||||
//
|
||||
// .a { color: gray; }
|
||||
// .b { font-size: 2rem; }
|
||||
// .c { font-size: 1.25rem; }
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
@@ -102,16 +130,36 @@ In production the class names are obfuscated.
|
||||
(CSS libraries like [Atomic CSS](http://acss.io/),
|
||||
[Basscss](http://www.basscss.com/), [SUIT CSS](https://suitcss.github.io/), and
|
||||
[tachyons](http://tachyons.io/) are attempts to limit style scope and limit
|
||||
stylesheet growth in a similar way. But they're CSS utility libraries, each with a
|
||||
style sheet growth in a similar way. But they're CSS utility libraries, each with a
|
||||
particular set of classes and features to learn. All of them require developers
|
||||
to manually connect CSS classes for given styles.)
|
||||
|
||||
## Media Queries, pseudo-classes, and pseudo-elements
|
||||
### Reset
|
||||
|
||||
Media Queries in JavaScript can be used to modify the render tree and styles.
|
||||
This has the benefit of co-locating breakpoint-specific DOM and style changes.
|
||||
React Native for Web includes a very small CSS reset taken from
|
||||
[normalize.css](https://necolas.github.io/normalize.css/). It removes unwanted
|
||||
User Agent styles from (pseudo-)elements beyond the reach of React (e.g.,
|
||||
`html`, `body`) or inline styles (e.g., `::-moz-focus-inner`).
|
||||
|
||||
Pseudo-classes like `:hover` and `:focus` can be replaced with JavaScript
|
||||
events.
|
||||
```css
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
Pseudo-elements are not supported.
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,14 +57,14 @@ could be an http address or a base64 encoded image.
|
||||
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
+ ...[View#style](View.md)
|
||||
|
||||
Defaults:
|
||||
|
||||
```js
|
||||
{
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'lightGray'
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,11 +76,9 @@ Used to locate a view in end-to-end tests.
|
||||
|
||||
```js
|
||||
import placeholderAvatar from './placeholderAvatar.png'
|
||||
import React, { Image } from 'react-native-web'
|
||||
import React, { Component, Image, PropTypes, StyleSheet } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
|
||||
class Avatar extends Component {
|
||||
export default class ImageExample extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { loading: true }
|
||||
@@ -112,32 +110,36 @@ class Avatar extends Component {
|
||||
onLoad={this._onLoad.bind(this)}
|
||||
resizeMode='cover'
|
||||
source={{ uri: user.avatarUrl }}
|
||||
style={{ ...styles.base, ...styles[size], ...loadingStyle }}
|
||||
style={{
|
||||
...styles.base,
|
||||
...styles[size],
|
||||
...loadingStyle
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderColor: 'white',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '5px'
|
||||
borderRadius: 5,
|
||||
borderWidth: 5
|
||||
},
|
||||
loading: {
|
||||
opacity: 0.5
|
||||
},
|
||||
small: {
|
||||
height: '32px',
|
||||
width: '32px'
|
||||
height: 32,
|
||||
width: 32
|
||||
},
|
||||
normal: {
|
||||
height: '48px',
|
||||
width: '48px'
|
||||
height: 48,
|
||||
width: 48
|
||||
},
|
||||
large: {
|
||||
height: '64px',
|
||||
width: '64px'
|
||||
height: 64,
|
||||
width: 64
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -10,28 +10,17 @@ Content to display over the image.
|
||||
|
||||
**style**: style
|
||||
|
||||
+ `property` type
|
||||
|
||||
Defaults:
|
||||
|
||||
```js
|
||||
{
|
||||
}
|
||||
```
|
||||
+ ...[View#style](View.md)
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { ListView } from 'react-native-web'
|
||||
import React, { Component, ListView, PropTypes } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
export default class ListViewExample extends Component {
|
||||
static propTypes = {}
|
||||
|
||||
class Example extends Component {
|
||||
static propTypes = {
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
}
|
||||
static defaultProps = {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# ScrollView
|
||||
|
||||
TODO
|
||||
Scrollable `View` for use with bounded height, either by setting the height of
|
||||
the view directly (discouraged) or by bounding the height of ancestor views.
|
||||
|
||||
## Props
|
||||
|
||||
@@ -11,49 +12,72 @@ Child content.
|
||||
**contentContainerStyle**: style
|
||||
|
||||
These styles will be applied to the scroll view content container which wraps
|
||||
all of the child views. Example:
|
||||
all of the child views.
|
||||
|
||||
**horizontal**: bool = false
|
||||
|
||||
When true, the scroll view's children are arranged horizontally in a row instead of vertically in a column. Default: `false`.
|
||||
When true, the scroll view's children are arranged horizontally in a row
|
||||
instead of vertically in a column.
|
||||
|
||||
**onScroll**: function
|
||||
|
||||
Fires at most once per frame during scrolling. The frequency of the events can be contolled using the `scrollEventThrottle` prop.
|
||||
Fires at most once per frame during scrolling. The frequency of the events can
|
||||
be contolled using the `scrollEventThrottle` prop.
|
||||
|
||||
**scrollEnabled**: bool
|
||||
**scrollEnabled**: bool = true
|
||||
|
||||
When false, the content does not scroll. Default: `true`.
|
||||
When false, the content does not scroll.
|
||||
|
||||
**scrollEventThrottle**: number
|
||||
**scrollEventThrottle**: number = 0
|
||||
|
||||
This controls how often the scroll event will be fired while scrolling (in
|
||||
events per seconds). A higher number yields better accuracy for code that is
|
||||
tracking the scroll position, but can lead to scroll performance problems.
|
||||
Default: `0` (the scroll event will be sent only once each time the view is
|
||||
scrolled.)
|
||||
tracking the scroll position, but can lead to scroll performance problems. The
|
||||
default value is `0`, which means the scroll event will be sent only once each
|
||||
time the view is scrolled.
|
||||
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
+ ...[View#style](View.md)
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { ScrollView } from 'react-native-web'
|
||||
import React, { Component, ScrollView, StyleSheet } from 'react-native-web'
|
||||
import Item from './Item'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
|
||||
class Example extends Component {
|
||||
static propTypes = {
|
||||
export default class ScrollViewExample extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
items: Array.from({ length: 20 }).map((_, i) => ({ id: i }))
|
||||
}
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onScroll(e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScrollView
|
||||
children={this.state.items.map((item) => <Item {...item} />)}
|
||||
contentContainerStyle={styles.container}
|
||||
horizontal
|
||||
onScroll={(e) => this.onScroll(e)}
|
||||
scrollEventThrottle={60}
|
||||
style={styles.root}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
borderWidth: 1
|
||||
},
|
||||
container: {
|
||||
padding: 10
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# Text
|
||||
|
||||
`Text` is component for displaying text. It supports style, basic touch
|
||||
handling, and inherits typographic styles from ancestor elements. In a
|
||||
divergence from React Native, components other than `Text` can be children of a
|
||||
`Text` component.
|
||||
handling, and inherits typographic styles from ancestor elements.
|
||||
|
||||
The `Text` is unique relative to layout: child elements use text layout
|
||||
(`inline-block`) rather than flexbox layout. This means that elements inside of
|
||||
a `Text` are not rectangles, as they wrap when reaching the edge of their
|
||||
(`inline`) rather than flexbox layout. This means that elements inside of a
|
||||
`Text` are not rectangles, as they wrap when reaching the edge of their
|
||||
container.
|
||||
|
||||
Unsupported React Native props:
|
||||
@@ -23,6 +21,17 @@ NOTE: `Text` will transfer all other props to the rendered HTML element.
|
||||
Defines the text available to assistive technologies upon interaction with the
|
||||
element. (This is implemented using `aria-label`.)
|
||||
|
||||
(web) **accessibilityRole**: oneOf(roles)
|
||||
|
||||
Allows assistive technologies to present and support interaction with the view
|
||||
in a manner that is consistent with user expectations for similar views of that
|
||||
type. For example, marking a touchable view with an `accessibilityRole` of
|
||||
`button`. (This is implemented using [ARIA roles](http://www.w3.org/TR/wai-aria/roles#role_definitions)).
|
||||
|
||||
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.
|
||||
|
||||
(web) **accessible**: bool = true
|
||||
|
||||
When `false`, the text is hidden from assistive technologies. (This is
|
||||
@@ -32,10 +41,6 @@ implemented using `aria-hidden`.)
|
||||
|
||||
Child content.
|
||||
|
||||
(web) **component**: function | string = 'span'
|
||||
|
||||
Backing component.
|
||||
|
||||
**numberOfLines**: number
|
||||
|
||||
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
|
||||
@@ -46,22 +51,20 @@ This function is called on press.
|
||||
|
||||
**style**: style
|
||||
|
||||
+ `backgroundColor`
|
||||
+ ...[View#style](View.md)
|
||||
+ `color`
|
||||
+ `direction`
|
||||
+ `fontFamily`
|
||||
+ `fontSize`
|
||||
+ `fontStyle`
|
||||
+ `fontWeight`
|
||||
+ `letterSpacing`
|
||||
+ `lineHeight`
|
||||
+ `margin`
|
||||
+ `padding`
|
||||
+ `textAlign`
|
||||
+ `textDecoration`
|
||||
+ `textTransform`
|
||||
+ `whiteSpace`
|
||||
+ `wordWrap`
|
||||
+ `writingDirection`
|
||||
|
||||
**testID**: string
|
||||
|
||||
@@ -70,18 +73,18 @@ Used to locate this view in end-to-end tests.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Text } from 'react-native-web'
|
||||
import React, { Component, PropTypes, StyleSheet, Text } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
class PrettyText extends Component {
|
||||
export default class PrettyText extends Component {
|
||||
static propTypes = {
|
||||
...Text.propTypes,
|
||||
color: PropTypes.oneOf(['white', 'gray', 'red']),
|
||||
size: PropTypes.oneOf(['small', 'normal', 'large']),
|
||||
weight: PropTypes.oneOf(['light', 'normal', 'bold'])
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
...Text.defaultProps,
|
||||
color: 'gray',
|
||||
size: 'normal',
|
||||
weight: 'normal'
|
||||
@@ -95,16 +98,16 @@ class PrettyText extends Component {
|
||||
...other
|
||||
style={{
|
||||
...style,
|
||||
...localStyle.color[color],
|
||||
...localStyle.size[size],
|
||||
...localStyle.weight[weight]
|
||||
...styles.color[color],
|
||||
...styles.size[size],
|
||||
...styles.weight[weight]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const localStyle = {
|
||||
const styles = StyleSheet.create({
|
||||
color: {
|
||||
white: { color: 'white' },
|
||||
gray: { color: 'gray' },
|
||||
@@ -120,5 +123,5 @@ const localStyle = {
|
||||
normal: { fontWeight: '400' },
|
||||
bold: { fontWeight: '700' }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -48,9 +48,10 @@ updating the `value` prop to keep the controlled state in sync.
|
||||
|
||||
If `false`, text is not editable (i.e., read-only).
|
||||
|
||||
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'url') = 'default'
|
||||
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'search', 'url', 'web-search') = 'default'
|
||||
|
||||
Determines which keyboard to open.
|
||||
Determines which keyboard to open. (NOTE: Safari iOS requires an ancestral
|
||||
`<form action>` element to display the `search` keyboard).
|
||||
|
||||
(Not available when `multiline` is `true`.)
|
||||
|
||||
@@ -111,11 +112,12 @@ object is passed as an argument to the callback handler.
|
||||
|
||||
**placeholder**: string
|
||||
|
||||
The string that will be rendered before text input has been entered.
|
||||
The string that will be rendered in an empty `TextInput` before text has been
|
||||
entered.
|
||||
|
||||
**placeholderTextColor**: string
|
||||
|
||||
TODO. The text color of the placeholder string.
|
||||
The text color of the placeholder string.
|
||||
|
||||
**secureTextEntry**: bool = false
|
||||
|
||||
@@ -130,19 +132,8 @@ If `true`, all text will automatically be selected on focus.
|
||||
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
|
||||
+ `color`
|
||||
+ `direction`
|
||||
+ `fontFamily`
|
||||
+ `fontSize`
|
||||
+ `fontStyle`
|
||||
+ `fontWeight`
|
||||
+ `letterSpacing`
|
||||
+ `lineHeight`
|
||||
+ `textAlign`
|
||||
+ `textDecoration`
|
||||
+ `textTransform`
|
||||
+ ...[Text#style](Text.md)
|
||||
+ `outline`
|
||||
|
||||
**testID**: string
|
||||
|
||||
@@ -159,16 +150,18 @@ user edits to the value set `editable={false}`.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { TextInput } from 'react-native-web'
|
||||
import React, { Component, StyleSheet, TextInput } from 'react-native-web'
|
||||
|
||||
const { Component } = React
|
||||
|
||||
class AppTextInput extends Component {
|
||||
export default class TextInputExample extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { isFocused: false }
|
||||
}
|
||||
|
||||
_onBlur(e) {
|
||||
this.setState({ isFocused: false })
|
||||
}
|
||||
|
||||
_onFocus(e) {
|
||||
this.setState({ isFocused: true })
|
||||
}
|
||||
@@ -180,24 +173,25 @@ class AppTextInput extends Component {
|
||||
maxNumberOfLines={5}
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
onBlur={this._onBlur.bind(this)}
|
||||
onFocus={this._onFocus.bind(this)}
|
||||
placeholder={`What's happening?`}
|
||||
style={
|
||||
style={{
|
||||
...styles.default
|
||||
...(this.state.isFocused && styles.focused)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
const styles = StyleSheet.create({
|
||||
default: {
|
||||
borderColor: 'gray',
|
||||
borderWidth: '0 0 2px 0'
|
||||
borderBottomWidth: 2
|
||||
},
|
||||
focused: {
|
||||
borderColor: 'blue'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -79,21 +79,17 @@ Delay in ms, from the release of the touch, before `onPressOut` is called.
|
||||
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
+ ...[View#style](View.md)
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Touchable } from 'react-native-web'
|
||||
import React, { Component, PropTypes, Touchable } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
export default class Example extends Component {
|
||||
static propTypes = {}
|
||||
|
||||
class Example extends Component {
|
||||
static propTypes = {
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
}
|
||||
static defaultProps = {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
@@ -54,10 +54,6 @@ assistive technologies of a `role` value change.
|
||||
When `false`, the view is hidden from assistive technologies. (This is
|
||||
implemented using `aria-hidden`.)
|
||||
|
||||
(web) **component**: function | string = 'div'
|
||||
|
||||
The React Component for this view.
|
||||
|
||||
**onLayout**: function
|
||||
|
||||
(TODO)
|
||||
@@ -113,6 +109,12 @@ from `style`.
|
||||
+ `justifyContent`
|
||||
+ `left`
|
||||
+ `margin`
|
||||
+ `marginBottom`
|
||||
+ `marginHorizontal`
|
||||
+ `marginLeft`
|
||||
+ `marginRight`
|
||||
+ `marginTop`
|
||||
+ `marginVertical`
|
||||
+ `maxHeight`
|
||||
+ `maxWidth`
|
||||
+ `minHeight`
|
||||
@@ -123,6 +125,12 @@ from `style`.
|
||||
+ `overflowX`
|
||||
+ `overflowY`
|
||||
+ `padding`
|
||||
+ `paddingBottom`
|
||||
+ `paddingHorizontal`
|
||||
+ `paddingLeft`
|
||||
+ `paddingRight`
|
||||
+ `paddingTop`
|
||||
+ `paddingVertical`
|
||||
+ `position`
|
||||
+ `right`
|
||||
+ `top`
|
||||
@@ -159,11 +167,9 @@ Used to locate this view in end-to-end tests.
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { View } from 'react-native-web'
|
||||
import React, { Component, PropTypes, StyleSheet, View } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
class Example extends Component {
|
||||
export default class ViewExample extends Component {
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
@@ -177,14 +183,12 @@ class Example extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
cell: {
|
||||
flexGrow: 1
|
||||
}
|
||||
}
|
||||
|
||||
export default Example
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,50 +1,42 @@
|
||||
import React, { Image, StyleSheet, Text, TextInput, Touchable, View } from '.'
|
||||
import ReactDOM from 'react-dom'
|
||||
import GridView from './GridView'
|
||||
import Heading from './Heading'
|
||||
import MediaQueryWidget from './MediaQueryWidget'
|
||||
import React, { Image, StyleSheet, ScrollView, Text, TextInput, Touchable, View } from '../../src'
|
||||
|
||||
const Heading = ({ children, level = '1', size = 'normal' }) => (
|
||||
<Text
|
||||
children={children}
|
||||
component={`h${level}`}
|
||||
style={headingStyles.size[size]}
|
||||
/>
|
||||
)
|
||||
|
||||
const headingStyles = StyleSheet.create({
|
||||
size: {
|
||||
xlarge: {
|
||||
fontSize: '2rem',
|
||||
marginBottom: '1em'
|
||||
},
|
||||
large: {
|
||||
fontSize: '1.5rem',
|
||||
marginBottom: '1em',
|
||||
marginTop: '1em'
|
||||
},
|
||||
normal: {
|
||||
fontSize: '1.25rem',
|
||||
marginBottom: '0.5em',
|
||||
marginTop: '0.5em'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class Example extends React.Component {
|
||||
export default class App extends React.Component {
|
||||
static propTypes = {
|
||||
mediaQuery: React.PropTypes.object,
|
||||
style: View.propTypes.style
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.state = {
|
||||
scrollEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mediaQuery } = this.props
|
||||
const rootStyles = {
|
||||
...(styles.root.common),
|
||||
...(mediaQuery.small.matches && styles.root.mqSmall),
|
||||
...(mediaQuery.large.matches && styles.root.mqLarge)
|
||||
}
|
||||
|
||||
return (
|
||||
<View accessibilityRole='main' style={styles.root}>
|
||||
<Heading level='1' size='xlarge'>React Native Web</Heading>
|
||||
<View accessibilityRole='main' style={rootStyles}>
|
||||
<Heading size='xlarge'>React Native for Web</Heading>
|
||||
<Text>React Native Web takes the core components from <Text
|
||||
component='a' href='https://facebook.github.io/react-native/'>React
|
||||
accessibilityRole='link' href='https://facebook.github.io/react-native/'>React
|
||||
Native</Text> and brings them to the web. These components provide
|
||||
simple building blocks – touch handling, flexbox layout,
|
||||
scroll views – from which more complex components and apps can be
|
||||
constructed.</Text>
|
||||
|
||||
<Heading level='2' size='large'>Image</Heading>
|
||||
<MediaQueryWidget mediaQuery={mediaQuery} />
|
||||
|
||||
<Heading size='large'>Image</Heading>
|
||||
<Image
|
||||
accessibilityLabel='accessible image'
|
||||
children={<Text>Inner content</Text>}
|
||||
@@ -67,7 +59,7 @@ class Example extends React.Component {
|
||||
testID='Example.image'
|
||||
/>
|
||||
|
||||
<Heading level='2' size='large'>Text</Heading>
|
||||
<Heading size='large'>Text</Heading>
|
||||
<Text
|
||||
onPress={(e) => { console.log('Text.onPress', e) }}
|
||||
testID={'Example.text'}
|
||||
@@ -92,7 +84,7 @@ class Example extends React.Component {
|
||||
hendrerit consequat.
|
||||
</Text>
|
||||
|
||||
<Heading level='2' size='large'>TextInput</Heading>
|
||||
<Heading size='large'>TextInput</Heading>
|
||||
<TextInput
|
||||
keyboardType='default'
|
||||
onBlur={(e) => { console.log('TextInput.onBlur', e) }}
|
||||
@@ -103,10 +95,10 @@ class Example extends React.Component {
|
||||
/>
|
||||
<TextInput secureTextEntry />
|
||||
<TextInput defaultValue='read only' editable={false} />
|
||||
<TextInput keyboardType='email-address' />
|
||||
<TextInput keyboardType='email-address' placeholder='you@domain.com' placeholderTextColor='red' />
|
||||
<TextInput keyboardType='numeric' />
|
||||
<TextInput keyboardType='phone-pad' />
|
||||
<TextInput keyboardType='url' selectTextOnFocus />
|
||||
<TextInput defaultValue='https://delete-me' keyboardType='url' placeholder='https://www.some-website.com' selectTextOnFocus />
|
||||
<TextInput
|
||||
defaultValue='default value'
|
||||
maxNumberOfLines={10}
|
||||
@@ -114,7 +106,7 @@ class Example extends React.Component {
|
||||
numberOfLines={5}
|
||||
/>
|
||||
|
||||
<Heading level='2' size='large'>Touchable</Heading>
|
||||
<Heading size='large'>Touchable</Heading>
|
||||
<Touchable
|
||||
accessibilityLabel={'Touchable element'}
|
||||
activeHighlight='lightblue'
|
||||
@@ -129,8 +121,8 @@ class Example extends React.Component {
|
||||
</View>
|
||||
</Touchable>
|
||||
|
||||
<Heading level='2' size='large'>View</Heading>
|
||||
<Heading level='3'>Default layout</Heading>
|
||||
<Heading size='large'>View</Heading>
|
||||
<Heading>Default layout</Heading>
|
||||
<View>
|
||||
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
|
||||
return (
|
||||
@@ -141,7 +133,7 @@ class Example extends React.Component {
|
||||
})}
|
||||
</View>
|
||||
|
||||
<Heading level='3'>Row layout</Heading>
|
||||
<Heading>Row layout</Heading>
|
||||
<View style={styles.row}>
|
||||
{[ 1, 2, 3, 4, 5, 6 ].map((item, i) => {
|
||||
return (
|
||||
@@ -152,13 +144,13 @@ class Example extends React.Component {
|
||||
})}
|
||||
</View>
|
||||
|
||||
<Heading level='3'>pointerEvents</Heading>
|
||||
<View style={styles.row}>
|
||||
<Heading>pointerEvents</Heading>
|
||||
<GridView alley='10px'>
|
||||
{['box-none', 'box-only', 'none'].map((value, i) => {
|
||||
return (
|
||||
<View
|
||||
accessibilityRole='link'
|
||||
children={value}
|
||||
component='a'
|
||||
href='https://google.com'
|
||||
key={i}
|
||||
pointerEvents={value}
|
||||
@@ -166,6 +158,52 @@ class Example extends React.Component {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</GridView>
|
||||
|
||||
<Heading size='large'>ScrollView</Heading>
|
||||
<label>
|
||||
<input
|
||||
checked={this.state.scrollEnabled}
|
||||
onChange={() => this.setState({
|
||||
scrollEnabled: !this.state.scrollEnabled
|
||||
})}
|
||||
type='checkbox'
|
||||
/> Enable scroll
|
||||
</label>
|
||||
|
||||
<Heading>Default layout</Heading>
|
||||
<View style={styles.scrollViewContainer}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollViewContentContainerStyle}
|
||||
onScroll={e => console.log('ScrollView.onScroll', e)}
|
||||
scrollEnabled={this.state.scrollEnabled}
|
||||
scrollEventThrottle={1} // 1 event per second
|
||||
style={styles.scrollViewStyle}
|
||||
>
|
||||
{Array.from({ length: 50 }).map((item, i) => (
|
||||
<View key={i} style={styles.box}>
|
||||
<Text>{i}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<Heading>Horizontal layout</Heading>
|
||||
<View style={styles.scrollViewContainer}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollViewContentContainerStyle}
|
||||
horizontal
|
||||
onScroll={e => console.log('ScrollView.onScroll', e)}
|
||||
scrollEnabled={this.state.scrollEnabled}
|
||||
scrollEventThrottle={1} // 1 event per second
|
||||
style={styles.scrollViewStyle}
|
||||
>
|
||||
{Array.from({ length: 50 }).map((item, i) => (
|
||||
<View key={i} style={{...styles.box, ...styles.horizontalBox}}>
|
||||
<Text>{i}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -174,8 +212,16 @@ class Example extends React.Component {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto'
|
||||
common: {
|
||||
marginVertical: 0,
|
||||
marginHorizontal: 'auto'
|
||||
},
|
||||
mqSmall: {
|
||||
maxWidth: '400px'
|
||||
},
|
||||
mqLarge: {
|
||||
maxWidth: '600px'
|
||||
}
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -185,7 +231,10 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
borderWidth: '1px'
|
||||
borderWidth: 1
|
||||
},
|
||||
horizontalBox: {
|
||||
width: '50px'
|
||||
},
|
||||
boxFull: {
|
||||
width: '100%'
|
||||
@@ -202,9 +251,14 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
height: '200px',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
scrollViewContainer: {
|
||||
height: '200px'
|
||||
},
|
||||
scrollViewStyle: {
|
||||
borderWidth: '1px'
|
||||
},
|
||||
scrollViewContentContainerStyle: {
|
||||
padding: '10px'
|
||||
}
|
||||
})
|
||||
|
||||
ReactDOM.render(<Example />, document.getElementById('react-root'))
|
||||
|
||||
document.getElementById('react-stylesheet').textContent = StyleSheet.renderToString()
|
||||
65
examples/components/GridView.js
Normal file
65
examples/components/GridView.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { Component, PropTypes, StyleSheet, View } from '../../src'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
contentContainer: {
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1
|
||||
},
|
||||
// distribute all space (rather than extra space)
|
||||
column: {
|
||||
flexBasis: '0%'
|
||||
}
|
||||
})
|
||||
|
||||
export default class GridView extends Component {
|
||||
static propTypes = {
|
||||
alley: PropTypes.string,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.arrayOf(PropTypes.element)
|
||||
]),
|
||||
gutter: PropTypes.string,
|
||||
style: PropTypes.object
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
alley: '0',
|
||||
gutter: '0'
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alley, children, gutter, style, ...other } = this.props
|
||||
|
||||
const rootStyle = {
|
||||
...style,
|
||||
...styles.root
|
||||
}
|
||||
|
||||
const contentContainerStyle = {
|
||||
...styles.contentContainer,
|
||||
marginHorizontal: `calc(-0.5 * ${alley})`,
|
||||
paddingHorizontal: `${gutter}`
|
||||
}
|
||||
|
||||
const newChildren = React.Children.map(children, (child) => {
|
||||
return child && React.cloneElement(child, {
|
||||
style: {
|
||||
...child.props.style,
|
||||
...styles.column,
|
||||
marginHorizontal: `calc(0.5 * ${alley})`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='GridView' {...other} style={rootStyle}>
|
||||
<View style={contentContainerStyle}>
|
||||
{newChildren}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
33
examples/components/Heading.js
Normal file
33
examples/components/Heading.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { StyleSheet, Text } from '../../src'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
fontFamily: '"Helvetica Neue", arial, sans-serif'
|
||||
},
|
||||
size: {
|
||||
xlarge: {
|
||||
fontSize: '2rem',
|
||||
marginBottom: '1em'
|
||||
},
|
||||
large: {
|
||||
fontSize: '1.5rem',
|
||||
marginBottom: '1em',
|
||||
marginTop: '1em'
|
||||
},
|
||||
normal: {
|
||||
fontSize: '1.25rem',
|
||||
marginBottom: '0.5em',
|
||||
marginTop: '0.5em'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const Heading = ({ children, size = 'normal' }) => (
|
||||
<Text
|
||||
accessibilityRole='heading'
|
||||
children={children}
|
||||
style={{ ...styles.root, ...styles.size[size] }}
|
||||
/>
|
||||
)
|
||||
|
||||
export default Heading
|
||||
36
examples/components/MediaQueryWidget.js
Normal file
36
examples/components/MediaQueryWidget.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { StyleSheet, Text, View } from '../../src'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
marginVertical: 10,
|
||||
padding: 10,
|
||||
textAlign: 'center'
|
||||
},
|
||||
heading: {
|
||||
fontWeight: 'bold',
|
||||
padding: 5
|
||||
}
|
||||
})
|
||||
|
||||
const MediaQueryWidget = ({ mediaQuery = {} }) => {
|
||||
const active = Object.keys(mediaQuery).reduce((active, alias) => {
|
||||
if (mediaQuery[alias].matches) {
|
||||
active = {
|
||||
alias,
|
||||
mql: mediaQuery[alias]
|
||||
}
|
||||
}
|
||||
return active
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<Text style={styles.heading}>Active Media Query</Text>
|
||||
<Text>{`"${active.alias}"`} {active.mql && active.mql.media}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaQueryWidget
|
||||
@@ -3,7 +3,6 @@
|
||||
<title>React Native for Web</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="The core React Native components adapted and expanded upon for the web">
|
||||
<style>html { font-family: sans-serif; }</style>
|
||||
<style id="react-stylesheet"></style>
|
||||
<div id="react-root"></div>
|
||||
<script src="/example.js"></script>
|
||||
<script src="/examples.js"></script>
|
||||
22
examples/index.js
Normal file
22
examples/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { MediaProvider, matchMedia } from 'react-media-queries'
|
||||
import App from './components/App'
|
||||
import createGetter from 'react-media-queries/lib/createMediaQueryGetter'
|
||||
import createListener from 'react-media-queries/lib/createMediaQueryListener'
|
||||
import React, { StyleSheet } from '../src'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
const mediaQueries = {
|
||||
small: '(min-width: 300px)',
|
||||
medium: '(min-width: 400px)',
|
||||
large: '(min-width: 500px)'
|
||||
}
|
||||
const ResponsiveApp = matchMedia()(App)
|
||||
|
||||
ReactDOM.render(
|
||||
<MediaProvider getMedia={createGetter(mediaQueries)} listener={createListener(mediaQueries)}>
|
||||
<ResponsiveApp />
|
||||
</MediaProvider>,
|
||||
document.getElementById('react-root')
|
||||
)
|
||||
|
||||
document.getElementById('react-stylesheet').textContent = StyleSheet.renderToString()
|
||||
61
package.json
61
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-web",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.12",
|
||||
"description": "React Native for Web",
|
||||
"main": "dist/react-native-web.js",
|
||||
"files": [
|
||||
@@ -9,43 +9,48 @@
|
||||
"scripts": {
|
||||
"build": "rm -rf ./dist && webpack --config config/webpack.config.publish.js --sort-assets-by --progress",
|
||||
"examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet",
|
||||
"lint": "eslint config src",
|
||||
"prepublish": "NODE_ENV=publish npm run build",
|
||||
"lint": "eslint config examples src",
|
||||
"prepublish": "npm run build",
|
||||
"test": "npm run lint && npm run test:unit",
|
||||
"test:unit": "karma start config/karma.config.js",
|
||||
"test:watch": "npm run test:unit -- --no-single-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"inline-style-prefixer": "^0.3.3",
|
||||
"inline-style-prefixer": "^0.5.3",
|
||||
"lodash.debounce": "^3.1.1",
|
||||
"react-tappable": "^0.7.1",
|
||||
"react-textarea-autosize": "^3.0.0"
|
||||
"react-textarea-autosize": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^5.8.23",
|
||||
"babel-eslint": "^4.1.1",
|
||||
"babel-loader": "^5.3.2",
|
||||
"babel-runtime": "^5.8.20",
|
||||
"eslint": "^1.3.1",
|
||||
"eslint-config-standard": "^4.3.1",
|
||||
"eslint-config-standard-react": "^1.0.4",
|
||||
"eslint-plugin-react": "^3.3.1",
|
||||
"eslint-plugin-standard": "^1.3.0",
|
||||
"karma": "^0.13.9",
|
||||
"karma-browserstack-launcher": "^0.1.5",
|
||||
"karma-chrome-launcher": "^0.2.0",
|
||||
"karma-firefox-launcher": "^0.1.6",
|
||||
"karma-mocha": "^0.2.0",
|
||||
"karma-mocha-reporter": "^1.1.1",
|
||||
"karma-sourcemap-loader": "^0.3.5",
|
||||
"babel-core": "^6.2.4",
|
||||
"babel-eslint": "^4.1.6",
|
||||
"babel-loader": "^6.2.0",
|
||||
"babel-preset-es2015": "^6.2.4",
|
||||
"babel-preset-react": "^6.2.4",
|
||||
"babel-preset-stage-1": "^6.2.4",
|
||||
"babel-runtime": "^6.2.4",
|
||||
"eslint": "^1.10.3",
|
||||
"eslint-config-standard": "^4.4.0",
|
||||
"eslint-config-standard-react": "^1.2.1",
|
||||
"eslint-plugin-react": "^3.11.2",
|
||||
"eslint-plugin-standard": "^1.3.1",
|
||||
"karma": "^0.13.15",
|
||||
"karma-browserstack-launcher": "^0.1.7",
|
||||
"karma-chrome-launcher": "^0.2.2",
|
||||
"karma-firefox-launcher": "^0.1.7",
|
||||
"karma-mocha": "^0.2.1",
|
||||
"karma-sourcemap-loader": "^0.3.6",
|
||||
"karma-spec-reporter": "0.0.23",
|
||||
"karma-webpack": "^1.7.0",
|
||||
"mocha": "^2.3.0",
|
||||
"node-libs-browser": "^0.5.2",
|
||||
"mocha": "^2.3.4",
|
||||
"node-libs-browser": "^0.5.3",
|
||||
"object-assign": "^4.0.1",
|
||||
"react": "^0.14.0",
|
||||
"react-addons-test-utils": "^0.14.0",
|
||||
"react-dom": "^0.14.0",
|
||||
"webpack": "^1.12.1",
|
||||
"webpack-dev-server": "^1.10.1"
|
||||
"react": "^0.14.3",
|
||||
"react-addons-test-utils": "^0.14.3",
|
||||
"react-dom": "^0.14.3",
|
||||
"react-media-queries": "^2.0.1",
|
||||
"webpack": "^1.12.9",
|
||||
"webpack-dev-server": "^1.14.0"
|
||||
},
|
||||
"author": "Nicolas Gallagher",
|
||||
"license": "MIT",
|
||||
|
||||
62
src/components/CoreComponent/__tests__/index-test.js
Normal file
62
src/components/CoreComponent/__tests__/index-test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
import CoreComponent from '../'
|
||||
|
||||
suite('components/CoreComponent', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const dom = utils.renderToDOM(<CoreComponent accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(dom.getAttribute('aria-label'), accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const dom = utils.renderToDOM(<CoreComponent accessibilityLiveRegion={accessibilityLiveRegion} />)
|
||||
assert.equal(dom.getAttribute('aria-live'), accessibilityLiveRegion)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'banner'
|
||||
let dom = utils.renderToDOM(<CoreComponent accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(dom.getAttribute('role'), accessibilityRole)
|
||||
assert.equal((dom.tagName).toLowerCase(), 'header')
|
||||
|
||||
const button = 'button'
|
||||
dom = utils.renderToDOM(<CoreComponent accessibilityRole={button} />)
|
||||
assert.equal(dom.getAttribute('type'), button)
|
||||
assert.equal((dom.tagName).toLowerCase(), button)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
// accessible (implicit)
|
||||
let dom = utils.renderToDOM(<CoreComponent />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
||||
// accessible (explicit)
|
||||
dom = utils.renderToDOM(<CoreComponent accessible />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
||||
// not accessible
|
||||
dom = utils.renderToDOM(<CoreComponent accessible={false} />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), 'true')
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
const component = 'main'
|
||||
const dom = utils.renderToDOM(<CoreComponent component={component} />)
|
||||
const tagName = (dom.tagName).toLowerCase()
|
||||
assert.equal(tagName, component)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
// no testID
|
||||
let dom = utils.renderToDOM(<CoreComponent />)
|
||||
assert.equal(dom.getAttribute('data-testid'), null)
|
||||
// with testID
|
||||
const testID = 'Example.testID'
|
||||
dom = utils.renderToDOM(<CoreComponent testID={testID} />)
|
||||
assert.equal(dom.getAttribute('data-testid'), testID)
|
||||
})
|
||||
})
|
||||
@@ -2,18 +2,40 @@ import React, { PropTypes } from 'react'
|
||||
import StylePropTypes from '../../modules/StylePropTypes'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
|
||||
const roleComponents = {
|
||||
article: 'article',
|
||||
banner: 'header',
|
||||
button: 'button',
|
||||
complementary: 'aside',
|
||||
contentinfo: 'footer',
|
||||
form: 'form',
|
||||
heading: 'h1',
|
||||
link: 'a',
|
||||
list: 'ul',
|
||||
listitem: 'li',
|
||||
main: 'main',
|
||||
navigation: 'nav',
|
||||
region: 'section'
|
||||
}
|
||||
|
||||
class CoreComponent extends React.Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityLiveRegion: PropTypes.oneOf(['assertive', 'off', 'polite']),
|
||||
accessibilityRole: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
component: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string
|
||||
]),
|
||||
style: PropTypes.object,
|
||||
testID: PropTypes.string
|
||||
testID: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
component: 'div'
|
||||
}
|
||||
|
||||
@@ -21,16 +43,28 @@ class CoreComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
component: Component,
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityRole,
|
||||
accessible,
|
||||
component,
|
||||
testID,
|
||||
type,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
const Component = roleComponents[accessibilityRole] || component
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...other}
|
||||
{...StyleSheet.resolve(other)}
|
||||
aria-hidden={accessible ? null : true}
|
||||
aria-label={accessibilityLabel}
|
||||
aria-live={accessibilityLiveRegion}
|
||||
data-testid={testID}
|
||||
role={accessibilityRole}
|
||||
type={accessibilityRole === 'button' ? 'button' : type}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { assertProps, render, renderToDOM } from '../../../modules/specHelpers'
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
@@ -8,30 +8,34 @@ import Image from '../'
|
||||
|
||||
suite('components/Image', () => {
|
||||
test('default accessibility', () => {
|
||||
const dom = renderToDOM(<Image />)
|
||||
const dom = utils.renderToDOM(<Image />)
|
||||
assert.equal(dom.getAttribute('role'), 'img')
|
||||
})
|
||||
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
assertProps.accessibilityLabel(Image)
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Image accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
assertProps.accessible(Image)
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Image accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"')
|
||||
|
||||
test('prop "defaultSource"', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' }
|
||||
const dom = renderToDOM(<Image defaultSource={defaultSource} />)
|
||||
const dom = utils.renderToDOM(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = dom.style.backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
|
||||
})
|
||||
|
||||
test('prop "onError"', function (done) {
|
||||
this.timeout(5000)
|
||||
render(<Image
|
||||
utils.render(<Image
|
||||
onError={onError}
|
||||
source={{ uri: 'https://google.com/favicon.icox' }}
|
||||
/>)
|
||||
@@ -43,7 +47,7 @@ suite('components/Image', () => {
|
||||
|
||||
test('prop "onLoad"', function (done) {
|
||||
this.timeout(5000)
|
||||
render(<Image
|
||||
utils.render(<Image
|
||||
onLoad={onLoad}
|
||||
source={{ uri: 'https://google.com/favicon.ico' }}
|
||||
/>)
|
||||
@@ -62,10 +66,12 @@ suite('components/Image', () => {
|
||||
test('prop "source"')
|
||||
|
||||
test('prop "style"', () => {
|
||||
assertProps.style(Image)
|
||||
utils.assertProps.style(Image)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
assertProps.testID(Image)
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<Image testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,10 +17,10 @@ const imageStyleKeys = Object.keys(ImageStylePropTypes)
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'lightgray',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 100%'
|
||||
backgroundSize: 'cover'
|
||||
},
|
||||
img: {
|
||||
borderWidth: 0,
|
||||
@@ -55,18 +55,17 @@ const styles = StyleSheet.create({
|
||||
class Image extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
|
||||
const { uri } = props.source
|
||||
// state
|
||||
this.state = { status: props.source.uri ? STATUS_PENDING : STATUS_IDLE }
|
||||
|
||||
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
|
||||
// autobinding
|
||||
this._onError = this._onError.bind(this)
|
||||
this._onLoad = this._onLoad.bind(this)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
defaultSource: PropTypes.object,
|
||||
onError: PropTypes.func,
|
||||
@@ -84,7 +83,7 @@ class Image extends React.Component {
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
defaultSource: {},
|
||||
resizeMode: 'stretch',
|
||||
resizeMode: 'cover',
|
||||
source: {},
|
||||
style: styles.initial
|
||||
}
|
||||
@@ -102,8 +101,8 @@ class Image extends React.Component {
|
||||
|
||||
_destroyImageLoader() {
|
||||
if (this.image) {
|
||||
this.image.onload = null
|
||||
this.image.onerror = null
|
||||
this.image.onload = null
|
||||
this.image = null
|
||||
}
|
||||
}
|
||||
@@ -124,8 +123,8 @@ class Image extends React.Component {
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.setState({ status: STATUS_LOADED })
|
||||
this._onLoadEnd()
|
||||
if (onLoad) onLoad(event)
|
||||
this._onLoadEnd()
|
||||
}
|
||||
|
||||
_onLoadEnd() {
|
||||
@@ -194,7 +193,6 @@ class Image extends React.Component {
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole='img'
|
||||
accessible={accessible}
|
||||
component='div'
|
||||
style={{
|
||||
...styles.initial,
|
||||
...resolvedStyle,
|
||||
|
||||
4
src/components/ScrollView/ScrollViewStylePropTypes.js
Normal file
4
src/components/ScrollView/ScrollViewStylePropTypes.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import View from '../View'
|
||||
export default {
|
||||
...(View.stylePropTypes)
|
||||
}
|
||||
@@ -1 +1,11 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
|
||||
import ScrollView from '../'
|
||||
|
||||
suite('components/ScrollView', () => {
|
||||
test('prop "style"', () => {
|
||||
utils.assertProps.style(ScrollView)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,138 @@
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import debounce from 'lodash.debounce'
|
||||
import React, { PropTypes } from 'react'
|
||||
import ScrollViewStylePropTypes from './ScrollViewStylePropTypes'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
import View from '../View'
|
||||
|
||||
const scrollViewStyleKeys = Object.keys(ScrollViewStylePropTypes)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
overflow: 'scroll'
|
||||
},
|
||||
initialContentContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
})
|
||||
|
||||
class ScrollView extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any
|
||||
children: PropTypes.any,
|
||||
contentContainerStyle: PropTypes.shape(ScrollViewStylePropTypes),
|
||||
horizontal: PropTypes.bool,
|
||||
onScroll: PropTypes.func,
|
||||
scrollEnabled: PropTypes.bool,
|
||||
scrollEventThrottle: PropTypes.number,
|
||||
style: PropTypes.shape(ScrollViewStylePropTypes)
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
className: ''
|
||||
contentContainerStyle: styles.initialContentContainer,
|
||||
horizontal: false,
|
||||
scrollEnabled: true,
|
||||
scrollEventThrottle: 0,
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100)
|
||||
this.state = {
|
||||
isScrolling: false
|
||||
}
|
||||
}
|
||||
|
||||
_onScroll(e) {
|
||||
const { scrollEventThrottle } = this.props
|
||||
const { isScrolling, scrollLastTick } = this.state
|
||||
|
||||
// A scroll happened, so the scroll bumps the debounce.
|
||||
this._debouncedOnScrollEnd(e)
|
||||
|
||||
if (isScrolling) {
|
||||
// Scroll last tick may have changed, check if we need to notify
|
||||
if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) {
|
||||
this._onScrollTick(e)
|
||||
}
|
||||
} else {
|
||||
// Weren't scrolling, so we must have just started
|
||||
this._onScrollStart(e)
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollStart() {
|
||||
this.setState({
|
||||
isScrolling: true,
|
||||
scrollLastTick: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
_onScrollTick(e) {
|
||||
const { onScroll } = this.props
|
||||
this.setState({
|
||||
scrollLastTick: Date.now()
|
||||
})
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
|
||||
_onScrollEnd(e) {
|
||||
const { onScroll } = this.props
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
})
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
|
||||
_shouldEmitScrollEvent(lastTick, eventThrottle) {
|
||||
const timeSinceLastTick = Date.now() - lastTick
|
||||
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
|
||||
}
|
||||
|
||||
_maybePreventScroll(e) {
|
||||
const { scrollEnabled } = this.props
|
||||
if (!scrollEnabled) e.preventDefault()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
contentContainerStyle,
|
||||
horizontal,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
const resolvedStyle = pickProps(style, scrollViewStyleKeys)
|
||||
const resolvedContentContainerStyle = pickProps(contentContainerStyle, scrollViewStyleKeys)
|
||||
|
||||
return (
|
||||
<View {...this.props} />
|
||||
<View
|
||||
_className='ScrollView'
|
||||
onScroll={(e) => this._onScroll(e)}
|
||||
onTouchMove={(e) => this._maybePreventScroll(e)}
|
||||
onWheel={(e) => this._maybePreventScroll(e)}
|
||||
style={{
|
||||
...styles.initial,
|
||||
...resolvedStyle
|
||||
}}
|
||||
>
|
||||
{children ? (
|
||||
<View
|
||||
children={children}
|
||||
style={{
|
||||
...styles.initialContentContainer,
|
||||
...resolvedContentContainerStyle,
|
||||
...(horizontal && styles.row)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import View from '../View'
|
||||
|
||||
export default {
|
||||
...View.stylePropTypes,
|
||||
...pickProps(CoreComponent.stylePropTypes, [
|
||||
'backgroundColor',
|
||||
'color',
|
||||
'direction',
|
||||
'font',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontStyle',
|
||||
'fontWeight',
|
||||
'letterSpacing',
|
||||
'lineHeight',
|
||||
'margin',
|
||||
'marginBottom',
|
||||
'marginLeft',
|
||||
'marginRight',
|
||||
'marginTop',
|
||||
'padding',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
'paddingRight',
|
||||
'paddingTop',
|
||||
'textAlign',
|
||||
'textDecoration',
|
||||
'textTransform',
|
||||
'whiteSpace',
|
||||
'wordWrap'
|
||||
'wordWrap',
|
||||
'writingDirection'
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { assertProps, renderToDOM, shallowRender } from '../../../modules/specHelpers'
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
@@ -9,27 +9,33 @@ import Text from '../'
|
||||
|
||||
suite('components/Text', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
assertProps.accessibilityLabel(Text)
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Text accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<Text accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
assertProps.accessible(Text)
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Text accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = shallowRender(<Text>{children}</Text>)
|
||||
const result = utils.shallowRender(<Text>{children}</Text>)
|
||||
assert.equal(result.props.children, children)
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
assertProps.component(Text, 'span')
|
||||
})
|
||||
|
||||
test('prop "numberOfLines"')
|
||||
|
||||
test('prop "onPress"', (done) => {
|
||||
const dom = renderToDOM(<Text onPress={onPress} />)
|
||||
const dom = utils.renderToDOM(<Text onPress={onPress} />)
|
||||
ReactTestUtils.Simulate.click(dom)
|
||||
function onPress(e) {
|
||||
assert.ok(e.nativeEvent)
|
||||
@@ -38,10 +44,12 @@ suite('components/Text', () => {
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
assertProps.style(Text)
|
||||
utils.assertProps.style(Text)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
assertProps.testID(Text)
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<Text testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,10 +9,11 @@ const textStyleKeys = Object.keys(TextStylePropTypes)
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
color: 'inherit',
|
||||
display: 'inline-block',
|
||||
display: 'inline',
|
||||
font: 'inherit',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
textDecoration: 'none',
|
||||
wordWrap: 'break-word'
|
||||
},
|
||||
singleLineStyle: {
|
||||
@@ -26,10 +27,10 @@ const styles = StyleSheet.create({
|
||||
class Text extends React.Component {
|
||||
static propTypes = {
|
||||
_className: PropTypes.string, // escape-hatch for code migrations
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
component: CoreComponent.propTypes.component,
|
||||
numberOfLines: PropTypes.number,
|
||||
onPress: PropTypes.func,
|
||||
style: PropTypes.shape(TextStylePropTypes),
|
||||
@@ -41,7 +42,6 @@ class Text extends React.Component {
|
||||
static defaultProps = {
|
||||
_className: '',
|
||||
accessible: true,
|
||||
component: 'span',
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
@@ -52,14 +52,9 @@ class Text extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
_className,
|
||||
accessibilityLabel,
|
||||
accessible,
|
||||
children,
|
||||
component,
|
||||
numberOfLines,
|
||||
onPress,
|
||||
style,
|
||||
testID,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
@@ -69,18 +64,14 @@ class Text extends React.Component {
|
||||
return (
|
||||
<CoreComponent
|
||||
{...other}
|
||||
aria-hidden={accessible ? null : true}
|
||||
aria-label={accessibilityLabel}
|
||||
children={children}
|
||||
className={className}
|
||||
component={component}
|
||||
component='span'
|
||||
onClick={this._onPress.bind(this)}
|
||||
style={{
|
||||
...styles.initial,
|
||||
...resolvedStyle,
|
||||
...(numberOfLines === 1 && styles.singleLineStyle)
|
||||
}}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import View from '../View'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React from 'react'
|
||||
import Text from '../Text'
|
||||
|
||||
export default {
|
||||
...(View.stylePropTypes),
|
||||
...pickProps(CoreComponent.stylePropTypes, [
|
||||
'color',
|
||||
'direction',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontStyle',
|
||||
'fontWeight',
|
||||
'letterSpacing',
|
||||
'lineHeight',
|
||||
'textAlign',
|
||||
'textDecoration',
|
||||
'textTransform'
|
||||
])
|
||||
...Text.stylePropTypes,
|
||||
outline: React.PropTypes.string
|
||||
}
|
||||
|
||||
@@ -7,129 +7,133 @@ import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
import TextInput from '../'
|
||||
|
||||
const findInput = (dom) => dom.querySelector('input, textarea')
|
||||
const findShallowInput = (vdom) => vdom.props.children.props.children[0]
|
||||
const findShallowPlaceholder = (vdom) => vdom.props.children.props.children[1]
|
||||
|
||||
suite('components/TextInput', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
utils.assertProps.accessibilityLabel(TextInput)
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<TextInput accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "autoComplete"', () => {
|
||||
// off
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.getAttribute('autocomplete'), undefined)
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('autocomplete'), undefined)
|
||||
// on
|
||||
dom = utils.renderToDOM(<TextInput autoComplete />)
|
||||
assert.equal(dom.getAttribute('autocomplete'), 'on')
|
||||
input = findInput(utils.renderToDOM(<TextInput autoComplete />))
|
||||
assert.equal(input.getAttribute('autocomplete'), 'on')
|
||||
})
|
||||
|
||||
test('prop "autoFocus"', () => {
|
||||
// false
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.deepEqual(document.activeElement, document.body)
|
||||
// true
|
||||
dom = utils.renderToDOM(<TextInput autoFocus />)
|
||||
assert.deepEqual(document.activeElement, dom)
|
||||
input = findInput(utils.renderToDOM(<TextInput autoFocus />))
|
||||
assert.deepEqual(document.activeElement, input)
|
||||
})
|
||||
|
||||
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
|
||||
const defaultValue = 'defaultValue'
|
||||
// false
|
||||
let dom = utils.renderAndInject(<TextInput defaultValue={defaultValue} />)
|
||||
dom.focus()
|
||||
assert.equal(dom.value, defaultValue)
|
||||
let input = findInput(utils.renderAndInject(<TextInput defaultValue={defaultValue} />))
|
||||
input.focus()
|
||||
assert.equal(input.value, defaultValue)
|
||||
// true
|
||||
dom = utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />)
|
||||
dom.focus()
|
||||
assert.equal(dom.value, '')
|
||||
input = findInput(utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />))
|
||||
input.focus()
|
||||
assert.equal(input.value, '')
|
||||
})
|
||||
|
||||
test('prop "defaultValue"', () => {
|
||||
const defaultValue = 'defaultValue'
|
||||
const result = utils.shallowRender(<TextInput defaultValue={defaultValue} />)
|
||||
assert.equal(result.props.defaultValue, defaultValue)
|
||||
const input = findShallowInput(utils.shallowRender(<TextInput defaultValue={defaultValue} />))
|
||||
assert.equal(input.props.defaultValue, defaultValue)
|
||||
})
|
||||
|
||||
test('prop "editable"', () => {
|
||||
// true
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.getAttribute('readonly'), undefined)
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('readonly'), undefined)
|
||||
// false
|
||||
dom = utils.renderToDOM(<TextInput editable={false} />)
|
||||
assert.equal(dom.getAttribute('readonly'), '')
|
||||
input = findInput(utils.renderToDOM(<TextInput editable={false} />))
|
||||
assert.equal(input.getAttribute('readonly'), '')
|
||||
})
|
||||
|
||||
test('prop "keyboardType"', () => {
|
||||
// default
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.getAttribute('type'), undefined)
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='default' />)
|
||||
assert.equal(dom.getAttribute('type'), undefined)
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('type'), undefined)
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='default' />))
|
||||
assert.equal(input.getAttribute('type'), undefined)
|
||||
// email-address
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='email-address' />)
|
||||
assert.equal(dom.getAttribute('type'), 'email')
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='email-address' />))
|
||||
assert.equal(input.getAttribute('type'), 'email')
|
||||
// numeric
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='numeric' />)
|
||||
assert.equal(dom.getAttribute('type'), 'number')
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='numeric' />))
|
||||
assert.equal(input.getAttribute('type'), 'number')
|
||||
// phone-pad
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='phone-pad' />)
|
||||
assert.equal(dom.getAttribute('type'), 'tel')
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='phone-pad' />))
|
||||
assert.equal(input.getAttribute('type'), 'tel')
|
||||
// url
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='url' />)
|
||||
assert.equal(dom.getAttribute('type'), 'url')
|
||||
input = findInput(utils.renderToDOM(<TextInput keyboardType='url' />))
|
||||
assert.equal(input.getAttribute('type'), 'url')
|
||||
})
|
||||
|
||||
test('prop "maxLength"', () => {
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.getAttribute('maxlength'), undefined)
|
||||
dom = utils.renderToDOM(<TextInput maxLength={10} />)
|
||||
assert.equal(dom.getAttribute('maxlength'), '10')
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.getAttribute('maxlength'), undefined)
|
||||
input = findInput(utils.renderToDOM(<TextInput maxLength={10} />))
|
||||
assert.equal(input.getAttribute('maxlength'), '10')
|
||||
})
|
||||
|
||||
test('prop "maxNumberOfLines"', () => {
|
||||
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
|
||||
const value = (() => {
|
||||
const generateValue = () => {
|
||||
let str = ''
|
||||
while (str.length < 100) str += 'x'
|
||||
return str
|
||||
}())
|
||||
let dom = utils.renderAndInject(
|
||||
}
|
||||
|
||||
let input = findInput(utils.renderAndInject(
|
||||
<TextInput
|
||||
maxNumberOfLines={3}
|
||||
multiline
|
||||
style={style}
|
||||
value={value}
|
||||
value={generateValue()}
|
||||
/>
|
||||
)
|
||||
const height = dom.getBoundingClientRect().height
|
||||
))
|
||||
const height = input.getBoundingClientRect().height
|
||||
// need a range because of cross-browser differences
|
||||
assert.ok(height >= 60, height)
|
||||
assert.ok(height <= 66, height)
|
||||
assert.ok(height >= 42, height)
|
||||
assert.ok(height <= 48, height)
|
||||
})
|
||||
|
||||
test('prop "multiline"', () => {
|
||||
// false
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.tagName, 'INPUT')
|
||||
let input = findInput(utils.renderToDOM(<TextInput />))
|
||||
assert.equal(input.tagName, 'INPUT')
|
||||
// true
|
||||
dom = utils.renderToDOM(<TextInput multiline />)
|
||||
assert.equal(dom.tagName, 'TEXTAREA')
|
||||
input = findInput(utils.renderToDOM(<TextInput multiline />))
|
||||
assert.equal(input.tagName, 'TEXTAREA')
|
||||
})
|
||||
|
||||
test('prop "numberOfLines"', () => {
|
||||
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
|
||||
// missing multiline
|
||||
let dom = utils.renderToDOM(<TextInput numberOfLines={2} />)
|
||||
assert.equal(dom.tagName, 'INPUT')
|
||||
let input = findInput(utils.renderToDOM(<TextInput numberOfLines={2} />))
|
||||
assert.equal(input.tagName, 'INPUT')
|
||||
// with multiline
|
||||
dom = utils.renderAndInject(<TextInput multiline numberOfLines={2} style={style} />)
|
||||
assert.equal(dom.tagName, 'TEXTAREA')
|
||||
const height = dom.getBoundingClientRect().height
|
||||
input = findInput(utils.renderAndInject(<TextInput multiline numberOfLines={2} />))
|
||||
assert.equal(input.tagName, 'TEXTAREA')
|
||||
const height = input.getBoundingClientRect().height
|
||||
// need a range because of cross-browser differences
|
||||
assert.ok(height >= 40)
|
||||
assert.ok(height <= 46)
|
||||
assert.ok(height >= 30, height)
|
||||
assert.ok(height <= 36, height)
|
||||
})
|
||||
|
||||
test('prop "onBlur"', (done) => {
|
||||
const input = utils.renderToDOM(<TextInput onBlur={onBlur} />)
|
||||
const input = findInput(utils.renderToDOM(<TextInput onBlur={onBlur} />))
|
||||
ReactTestUtils.Simulate.blur(input)
|
||||
function onBlur(e) {
|
||||
assert.ok(e)
|
||||
@@ -138,7 +142,7 @@ suite('components/TextInput', () => {
|
||||
})
|
||||
|
||||
test('prop "onChange"', (done) => {
|
||||
const input = utils.renderToDOM(<TextInput onChange={onChange} />)
|
||||
const input = findInput(utils.renderToDOM(<TextInput onChange={onChange} />))
|
||||
ReactTestUtils.Simulate.change(input)
|
||||
function onChange(e) {
|
||||
assert.ok(e)
|
||||
@@ -148,7 +152,7 @@ suite('components/TextInput', () => {
|
||||
|
||||
test('prop "onChangeText"', (done) => {
|
||||
const newText = 'newText'
|
||||
const input = utils.renderToDOM(<TextInput onChangeText={onChangeText} />)
|
||||
const input = findInput(utils.renderToDOM(<TextInput onChangeText={onChangeText} />))
|
||||
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
|
||||
function onChangeText(text) {
|
||||
assert.equal(text, newText)
|
||||
@@ -157,7 +161,7 @@ suite('components/TextInput', () => {
|
||||
})
|
||||
|
||||
test('prop "onFocus"', (done) => {
|
||||
const input = utils.renderToDOM(<TextInput onFocus={onFocus} />)
|
||||
const input = findInput(utils.renderToDOM(<TextInput onFocus={onFocus} />))
|
||||
ReactTestUtils.Simulate.focus(input)
|
||||
function onFocus(e) {
|
||||
assert.ok(e)
|
||||
@@ -168,7 +172,7 @@ suite('components/TextInput', () => {
|
||||
test('prop "onLayout"')
|
||||
|
||||
test('prop "onSelectionChange"', (done) => {
|
||||
const input = utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />)
|
||||
const input = findInput(utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />))
|
||||
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
|
||||
function onSelectionChange(e) {
|
||||
assert.equal(e.selectionEnd, 3)
|
||||
@@ -177,30 +181,42 @@ suite('components/TextInput', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "placeholder"')
|
||||
test('prop "placeholder"', () => {
|
||||
const placeholder = 'placeholder'
|
||||
const result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
|
||||
assert.equal(result.props.children, placeholder)
|
||||
})
|
||||
|
||||
test('prop "placeholderTextColor"')
|
||||
test('prop "placeholderTextColor"', () => {
|
||||
const placeholder = 'placeholder'
|
||||
|
||||
let result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} />))
|
||||
assert.equal(result.props.style.color, 'darkgray')
|
||||
|
||||
result = findShallowPlaceholder(utils.shallowRender(<TextInput placeholder={placeholder} placeholderTextColor='red' />))
|
||||
assert.equal(result.props.style.color, 'red')
|
||||
})
|
||||
|
||||
test('prop "secureTextEntry"', () => {
|
||||
let dom = utils.renderToDOM(<TextInput secureTextEntry />)
|
||||
assert.equal(dom.getAttribute('type'), 'password')
|
||||
let input = findInput(utils.renderToDOM(<TextInput secureTextEntry />))
|
||||
assert.equal(input.getAttribute('type'), 'password')
|
||||
// ignored for multiline
|
||||
dom = utils.renderToDOM(<TextInput multiline secureTextEntry />)
|
||||
assert.equal(dom.getAttribute('type'), undefined)
|
||||
input = findInput(utils.renderToDOM(<TextInput multiline secureTextEntry />))
|
||||
assert.equal(input.getAttribute('type'), undefined)
|
||||
})
|
||||
|
||||
utils.testIfDocumentFocused('prop "selectTextOnFocus"', () => {
|
||||
const text = 'Text'
|
||||
// false
|
||||
let dom = utils.renderAndInject(<TextInput defaultValue={text} />)
|
||||
dom.focus()
|
||||
assert.equal(dom.selectionEnd, 0)
|
||||
assert.equal(dom.selectionStart, 0)
|
||||
let input = findInput(utils.renderAndInject(<TextInput defaultValue={text} />))
|
||||
input.focus()
|
||||
assert.equal(input.selectionEnd, 0)
|
||||
assert.equal(input.selectionStart, 0)
|
||||
// true
|
||||
dom = utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />)
|
||||
dom.focus()
|
||||
assert.equal(dom.selectionEnd, 4)
|
||||
assert.equal(dom.selectionStart, 0)
|
||||
input = findInput(utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />))
|
||||
input.focus()
|
||||
assert.equal(input.selectionEnd, 4)
|
||||
assert.equal(input.selectionStart, 0)
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
@@ -208,12 +224,14 @@ suite('components/TextInput', () => {
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
utils.assertProps.testID(TextInput)
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<TextInput testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
|
||||
test('prop "value"', () => {
|
||||
const value = 'value'
|
||||
const result = utils.shallowRender(<TextInput value={value} />)
|
||||
assert.equal(result.props.value, value)
|
||||
const input = findShallowInput(utils.shallowRender(<TextInput value={value} />))
|
||||
assert.equal(input.props.value, value)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,27 +3,50 @@ import CoreComponent from '../CoreComponent'
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
import Text from '../Text'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import TextInputStylePropTypes from './TextInputStylePropTypes'
|
||||
import View from '../View'
|
||||
|
||||
const textInputStyleKeys = Object.keys(TextInputStylePropTypes)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
...View.defaultProps.style,
|
||||
borderColor: 'black',
|
||||
borderWidth: 1
|
||||
},
|
||||
input: {
|
||||
appearance: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'black',
|
||||
borderWidth: '1px',
|
||||
borderWidth: 0,
|
||||
boxSizing: 'border-box',
|
||||
color: 'inherit',
|
||||
flexGrow: 1,
|
||||
font: 'inherit',
|
||||
padding: 0
|
||||
padding: 0,
|
||||
zIndex: 1
|
||||
},
|
||||
placeholder: {
|
||||
bottom: 0,
|
||||
color: 'darkgray',
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
whiteSpace: 'pre'
|
||||
}
|
||||
})
|
||||
|
||||
class TextInput extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { showPlaceholder: !props.value && !props.defaultValue }
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
...View.propTypes,
|
||||
autoComplete: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
clearTextOnFocus: PropTypes.bool,
|
||||
@@ -61,20 +84,26 @@ class TextInput extends React.Component {
|
||||
|
||||
_onBlur(e) {
|
||||
const { onBlur } = this.props
|
||||
const value = e.target.value
|
||||
this.setState({ showPlaceholder: value === '' })
|
||||
if (onBlur) onBlur(e)
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
const { onChange, onChangeText } = this.props
|
||||
if (onChangeText) onChangeText(e.target.value)
|
||||
const value = e.target.value
|
||||
this.setState({ showPlaceholder: value === '' })
|
||||
if (onChangeText) onChangeText(value)
|
||||
if (onChange) onChange(e)
|
||||
}
|
||||
|
||||
_onFocus(e) {
|
||||
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
|
||||
const node = ReactDOM.findDOMNode(this)
|
||||
const node = ReactDOM.findDOMNode(this.refs.input)
|
||||
const value = e.target.value
|
||||
if (clearTextOnFocus) node.value = ''
|
||||
if (selectTextOnFocus) node.select()
|
||||
this.setState({ showPlaceholder: value === '' })
|
||||
if (onFocus) onFocus(e)
|
||||
}
|
||||
|
||||
@@ -92,7 +121,9 @@ class TextInput extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
/* eslint-disable react/prop-types */
|
||||
accessibilityLabel,
|
||||
/* eslint-enable react/prop-types */
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
@@ -102,11 +133,9 @@ class TextInput extends React.Component {
|
||||
maxNumberOfLines,
|
||||
multiline,
|
||||
numberOfLines,
|
||||
onBlur,
|
||||
onChange,
|
||||
onChangeText,
|
||||
onSelectionChange,
|
||||
placeholder,
|
||||
placeholderTextColor,
|
||||
secureTextEntry,
|
||||
style,
|
||||
testID,
|
||||
@@ -126,6 +155,10 @@ class TextInput extends React.Component {
|
||||
case 'phone-pad':
|
||||
type = 'tel'
|
||||
break
|
||||
case 'search':
|
||||
case 'web-search':
|
||||
type = 'search'
|
||||
break
|
||||
case 'url':
|
||||
type = 'url'
|
||||
break
|
||||
@@ -136,23 +169,16 @@ class TextInput extends React.Component {
|
||||
}
|
||||
|
||||
const propsCommon = {
|
||||
'aria-label': accessibilityLabel,
|
||||
autoComplete: autoComplete && 'on',
|
||||
autoFocus,
|
||||
className: 'TextInput',
|
||||
defaultValue,
|
||||
maxLength,
|
||||
onBlur: onBlur && this._onBlur.bind(this),
|
||||
onChange: (onChange || onChangeText) && this._onChange.bind(this),
|
||||
onBlur: this._onBlur.bind(this),
|
||||
onChange: this._onChange.bind(this),
|
||||
onFocus: this._onFocus.bind(this),
|
||||
onSelect: onSelectionChange && this._onSelectionChange.bind(this),
|
||||
placeholder,
|
||||
readOnly: !editable,
|
||||
style: {
|
||||
...styles.initial,
|
||||
...resolvedStyle
|
||||
},
|
||||
testID,
|
||||
style: { ...styles.input, outline: style.outline },
|
||||
value
|
||||
}
|
||||
|
||||
@@ -172,7 +198,26 @@ class TextInput extends React.Component {
|
||||
const props = multiline ? propsMultiline : propsSingleline
|
||||
|
||||
return (
|
||||
<CoreComponent {...props} />
|
||||
<CoreComponent
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
className='TextInput'
|
||||
style={{
|
||||
...styles.initial,
|
||||
...resolvedStyle
|
||||
}}
|
||||
testID={testID}
|
||||
>
|
||||
<View style={{ flexGrow: 1 }}>
|
||||
<CoreComponent {...props} ref='input' />
|
||||
{placeholder && this.state.showPlaceholder && <Text
|
||||
pointerEvents='none'
|
||||
style={{
|
||||
...styles.placeholder,
|
||||
...(placeholderTextColor && { color: placeholderTextColor })
|
||||
}}
|
||||
>{placeholder}</Text>}
|
||||
</View>
|
||||
</CoreComponent>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { assertProps, shallowRender } from '../../../modules/specHelpers'
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
@@ -11,19 +11,25 @@ const requiredProps = { children }
|
||||
|
||||
suite('components/Touchable', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
assertProps.accessibilityLabel(Touchable, requiredProps)
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
assertProps.accessibilityRole(Touchable, requiredProps)
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
assertProps.accessible(Touchable, requiredProps)
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const result = shallowRender(<Touchable {...requiredProps} />)
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} />)
|
||||
assert.deepEqual(result.props.children, children)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,9 +25,9 @@ class Touchable extends React.Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityRole: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
accessibilityLabel: View.propTypes.accessibilityLabel,
|
||||
accessibilityRole: View.propTypes.accessibilityRole,
|
||||
accessible: View.propTypes.accessible,
|
||||
activeOpacity: PropTypes.number,
|
||||
activeUnderlayColor: PropTypes.string,
|
||||
children: PropTypes.element,
|
||||
@@ -45,7 +45,6 @@ class Touchable extends React.Component {
|
||||
accessibilityRole: 'button',
|
||||
activeOpacity: 1,
|
||||
activeUnderlayColor: 'transparent',
|
||||
component: 'div',
|
||||
delayLongPress: 1000,
|
||||
delayPressIn: 0,
|
||||
delayPressOut: 0,
|
||||
|
||||
@@ -54,6 +54,8 @@ export default {
|
||||
'left',
|
||||
// margin
|
||||
'margin',
|
||||
'marginHorizontal',
|
||||
'marginVertical',
|
||||
'marginBottom',
|
||||
'marginLeft',
|
||||
'marginRight',
|
||||
@@ -70,6 +72,8 @@ export default {
|
||||
'overflowY',
|
||||
// padding
|
||||
'padding',
|
||||
'paddingHorizontal',
|
||||
'paddingVertical',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
'paddingRight',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { assertProps, shallowRender } from '../../../modules/specHelpers'
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
@@ -8,41 +8,47 @@ import View from '../'
|
||||
|
||||
suite('components/View', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
assertProps.accessibilityLabel(View)
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<View accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
assertProps.accessibilityLiveRegion(View)
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const result = utils.shallowRender(<View accessibilityLiveRegion={accessibilityLiveRegion} />)
|
||||
assert.equal(result.props.accessibilityLiveRegion, accessibilityLiveRegion)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
assertProps.accessibilityRole(View)
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<View accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
assertProps.accessible(View)
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<View accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = shallowRender(<View>{children}</View>)
|
||||
const result = utils.shallowRender(<View>{children}</View>)
|
||||
assert.equal(result.props.children, children)
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
assertProps.component(View)
|
||||
})
|
||||
|
||||
test('prop "pointerEvents"', () => {
|
||||
const result = shallowRender(<View pointerEvents='box-only' />)
|
||||
const result = utils.shallowRender(<View pointerEvents='box-only' />)
|
||||
assert.equal(result.props.style.pointerEvents, 'box-only')
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
assertProps.style(View)
|
||||
utils.assertProps.style(View)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
assertProps.testID(View)
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<View testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ const styles = StyleSheet.create({
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
textDecoration: 'none',
|
||||
// button reset
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
@@ -32,12 +33,11 @@ const styles = StyleSheet.create({
|
||||
class View extends React.Component {
|
||||
static propTypes = {
|
||||
_className: PropTypes.string, // escape-hatch for code migrations
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityLiveRegion: PropTypes.oneOf(['assertive', 'off', 'polite']),
|
||||
accessibilityRole: PropTypes.string,
|
||||
accessible: PropTypes.bool,
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion,
|
||||
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
component: CoreComponent.propTypes.component,
|
||||
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
|
||||
style: PropTypes.shape(ViewStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
@@ -48,20 +48,14 @@ class View extends React.Component {
|
||||
static defaultProps = {
|
||||
_className: '',
|
||||
accessible: true,
|
||||
component: 'div',
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
_className,
|
||||
accessibilityLabel,
|
||||
accessibilityLiveRegion,
|
||||
accessibilityRole,
|
||||
accessible,
|
||||
pointerEvents,
|
||||
style,
|
||||
testID,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
@@ -72,17 +66,12 @@ class View extends React.Component {
|
||||
return (
|
||||
<CoreComponent
|
||||
{...other}
|
||||
aria-hidden={accessible ? null : true}
|
||||
aria-label={accessibilityLabel}
|
||||
aria-live={accessibilityLiveRegion}
|
||||
className={className}
|
||||
role={accessibilityRole}
|
||||
style={{
|
||||
...styles.initial,
|
||||
...resolvedStyle,
|
||||
...pointerEventsStyle
|
||||
}}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
16
src/index.js
16
src/index.js
@@ -11,11 +11,7 @@ import TextInput from './components/TextInput'
|
||||
import Touchable from './components/Touchable'
|
||||
import View from './components/View'
|
||||
|
||||
export default React
|
||||
|
||||
export {
|
||||
StyleSheet,
|
||||
|
||||
const ReactNative = {
|
||||
// components
|
||||
Image,
|
||||
ListView,
|
||||
@@ -23,5 +19,13 @@ export {
|
||||
Text,
|
||||
TextInput,
|
||||
Touchable,
|
||||
View
|
||||
View,
|
||||
|
||||
// apis
|
||||
StyleSheet,
|
||||
|
||||
// React
|
||||
...React
|
||||
}
|
||||
|
||||
module.exports = ReactNative
|
||||
|
||||
@@ -3,10 +3,15 @@ import { PropTypes } from 'react'
|
||||
const { number, string } = PropTypes
|
||||
const numberOrString = PropTypes.oneOfType([ number, string ])
|
||||
|
||||
/**
|
||||
* Any properties marked @private are used internally in resets or property
|
||||
* mappings.
|
||||
*/
|
||||
export default {
|
||||
alignContent: string,
|
||||
alignItems: string,
|
||||
alignSelf: string,
|
||||
appearance: string,
|
||||
backfaceVisibility: string,
|
||||
backgroundAttachment: string,
|
||||
backgroundClip: string,
|
||||
@@ -16,7 +21,6 @@ export default {
|
||||
backgroundPosition: string,
|
||||
backgroundRepeat: string,
|
||||
backgroundSize: string,
|
||||
border: string,
|
||||
borderColor: string,
|
||||
borderBottomColor: string,
|
||||
borderLeftColor: string,
|
||||
@@ -38,20 +42,21 @@ export default {
|
||||
borderRightWidth: numberOrString,
|
||||
borderTopWidth: numberOrString,
|
||||
bottom: numberOrString,
|
||||
boxShadow: string,
|
||||
boxSizing: string,
|
||||
clear: string,
|
||||
color: string,
|
||||
cursor: string,
|
||||
direction: string,
|
||||
display: string,
|
||||
flex: string,
|
||||
direction: string, /* @private */
|
||||
flex: string, /* @private */
|
||||
flexBasis: string,
|
||||
flexDirection: string,
|
||||
flexGrow: numberOrString,
|
||||
flexShrink: numberOrString,
|
||||
flexWrap: string,
|
||||
float: string,
|
||||
font: string,
|
||||
font: string, /* @private */
|
||||
fontFamily: string,
|
||||
fontSize: numberOrString,
|
||||
fontStyle: string,
|
||||
@@ -61,35 +66,44 @@ export default {
|
||||
left: numberOrString,
|
||||
letterSpacing: string,
|
||||
lineHeight: numberOrString,
|
||||
listStyle: string,
|
||||
margin: numberOrString,
|
||||
marginBottom: numberOrString,
|
||||
marginHorizontal: numberOrString,
|
||||
marginLeft: numberOrString,
|
||||
marginRight: numberOrString,
|
||||
marginTop: numberOrString,
|
||||
marginVertical: numberOrString,
|
||||
maxHeight: numberOrString,
|
||||
maxWidth: numberOrString,
|
||||
minHeight: numberOrString,
|
||||
minWidth: numberOrString,
|
||||
opacity: numberOrString,
|
||||
order: numberOrString,
|
||||
outline: string,
|
||||
overflow: string,
|
||||
overflowX: string,
|
||||
overflowY: string,
|
||||
padding: numberOrString,
|
||||
paddingBottom: numberOrString,
|
||||
paddingHorizontal: numberOrString,
|
||||
paddingLeft: numberOrString,
|
||||
paddingRight: numberOrString,
|
||||
paddingTop: numberOrString,
|
||||
paddingVertical: numberOrString,
|
||||
position: string,
|
||||
right: numberOrString,
|
||||
textAlign: string,
|
||||
textDecoration: string,
|
||||
textOverflow: string,
|
||||
textTransform: string,
|
||||
top: numberOrString,
|
||||
userSelect: string,
|
||||
verticalAlign: string,
|
||||
visibility: string,
|
||||
whiteSpace: string,
|
||||
width: numberOrString,
|
||||
wordWrap: string,
|
||||
writingDirection: string,
|
||||
zIndex: numberOrString
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class Store {
|
||||
const getCssSelector = (property, value) => {
|
||||
let className = this.get(property, value)
|
||||
if (!obfuscate && className) {
|
||||
className = className.replace(/[:?.%\\$#]/g, '\\$&')
|
||||
className = className.replace(/[,":?.%\\$#]/g, '\\$&')
|
||||
}
|
||||
return className
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export default class Store {
|
||||
if (!exists) {
|
||||
this._counter += 1
|
||||
if (this._options.obfuscateClassNames) {
|
||||
this._classNames[key] = `_rn_${this._counter}`
|
||||
this._classNames[key] = `_s_${this._counter}`
|
||||
} else {
|
||||
const val = `${value}`.replace(/\s/g, '-')
|
||||
this._classNames[key] = `${property}:${val}`
|
||||
|
||||
@@ -59,10 +59,10 @@ suite('modules/StyleSheet/Store', () => {
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'alignItems:center': '_rn_1',
|
||||
'flexGrow:0': '_rn_2',
|
||||
'flexGrow:1': '_rn_3',
|
||||
'flexGrow:2': '_rn_4'
|
||||
'alignItems:center': '_s_1',
|
||||
'flexGrow:0': '_s_2',
|
||||
'flexGrow:1': '_s_3',
|
||||
'flexGrow:2': '_s_4'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,8 +82,9 @@ suite('modules/StyleSheet/Store', () => {
|
||||
|
||||
test('replaces space characters', () => {
|
||||
const store = new Store()
|
||||
|
||||
store.set('margin', '0 auto')
|
||||
assert.deepEqual(store.get('margin', '0 auto'), 'margin:0-auto')
|
||||
assert.equal(store.get('margin', '0 auto'), 'margin\:0-auto')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,17 +92,17 @@ suite('modules/StyleSheet/Store', () => {
|
||||
test('human-readable style sheet', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('color', '#fff')
|
||||
store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif')
|
||||
store.set('marginBottom', 0)
|
||||
store.set('margin', 1)
|
||||
store.set('margin', 2)
|
||||
store.set('margin', 3)
|
||||
store.set('width', '100%')
|
||||
|
||||
const expected = '/* 5 unique declarations */\n' +
|
||||
'.alignItems\\:center{align-items:center;}\n' +
|
||||
'.margin\\:1px{margin:1px;}\n' +
|
||||
'.margin\\:2px{margin:2px;}\n' +
|
||||
'.margin\\:3px{margin:3px;}\n' +
|
||||
'.marginBottom\\:0px{margin-bottom:0px;}'
|
||||
'.color\\:\\#fff{color:#fff;}\n' +
|
||||
'.fontFamily\\:\\"Helvetica-Neue\\"\\,-Arial\\,-sans-serif{font-family:"Helvetica Neue", Arial, sans-serif;}\n' +
|
||||
'.marginBottom\\:0px{margin-bottom:0px;}\n' +
|
||||
'.width\\:100\\%{width:100%;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
@@ -115,11 +116,11 @@ suite('modules/StyleSheet/Store', () => {
|
||||
store.set('margin', 3)
|
||||
|
||||
const expected = '/* 5 unique declarations */\n' +
|
||||
'._rn_1{align-items:center;}\n' +
|
||||
'._rn_3{margin:1px;}\n' +
|
||||
'._rn_4{margin:2px;}\n' +
|
||||
'._rn_5{margin:3px;}\n' +
|
||||
'._rn_2{margin-bottom:0px;}'
|
||||
'._s_1{align-items:center;}\n' +
|
||||
'._s_3{margin:1px;}\n' +
|
||||
'._s_4{margin:2px;}\n' +
|
||||
'._s_5{margin:3px;}\n' +
|
||||
'._s_2{margin-bottom:0px;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
29
src/modules/StyleSheet/__tests__/expandStyle-test.js
Normal file
29
src/modules/StyleSheet/__tests__/expandStyle-test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import expandStyle from '../expandStyle'
|
||||
|
||||
suite('modules/StyleSheet/expandStyle', () => {
|
||||
test('style property', () => {
|
||||
const initial = {
|
||||
borderTopWidth: 1,
|
||||
borderWidth: 2,
|
||||
marginTop: 50,
|
||||
marginVertical: 25,
|
||||
margin: 10
|
||||
}
|
||||
|
||||
const expectedStyle = {
|
||||
borderTopWidth: 1,
|
||||
borderLeftWidth: 2,
|
||||
borderRightWidth: 2,
|
||||
borderBottomWidth: 2,
|
||||
marginTop: 50,
|
||||
marginBottom: 25,
|
||||
marginLeft: 10,
|
||||
marginRight: 10
|
||||
}
|
||||
|
||||
assert.deepEqual(expandStyle(initial), expectedStyle)
|
||||
})
|
||||
})
|
||||
@@ -16,8 +16,9 @@ const fixture = {
|
||||
backgroundSize: 'contain'
|
||||
}
|
||||
},
|
||||
ignored: {
|
||||
pading: 0
|
||||
position: {
|
||||
left: { left: 0 },
|
||||
right: { right: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +28,9 @@ suite('modules/StyleSheet/getStyleObjects', () => {
|
||||
assert.deepEqual(actual, [
|
||||
{ margin: 0, padding: 0 },
|
||||
{ backgroundSize: 'auto' },
|
||||
{ backgroundSize: 'contain' }
|
||||
{ backgroundSize: 'contain' },
|
||||
{ left: 0 },
|
||||
{ right: 0 }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,8 +6,11 @@ import hyphenate from '../hyphenate'
|
||||
suite('modules/StyleSheet/hyphenate', () => {
|
||||
test('style property', () => {
|
||||
assert.equal(hyphenate('alignItems'), 'align-items')
|
||||
assert.equal(hyphenate('color'), 'color')
|
||||
})
|
||||
test('vendor prefixed style property', () => {
|
||||
assert.equal(hyphenate('WebkitAppearance'), '-webkit-appearance')
|
||||
assert.equal(hyphenate('MozTransition'), '-moz-transition')
|
||||
assert.equal(hyphenate('msTransition'), '-ms-transition')
|
||||
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resetCSS, predefinedCSS } from '../predefs'
|
||||
import assert from 'assert'
|
||||
import StyleSheet from '..'
|
||||
|
||||
const styles = { root: { border: 0 } }
|
||||
const styles = { root: { borderWidth: 1 } }
|
||||
|
||||
suite('modules/StyleSheet', () => {
|
||||
setup(() => {
|
||||
@@ -20,14 +20,17 @@ suite('modules/StyleSheet', () => {
|
||||
assert.equal(
|
||||
StyleSheet.renderToString(),
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 1 unique declarations */\n` +
|
||||
`.border\\:0px{border:0px;}`
|
||||
`/* 4 unique declarations */\n` +
|
||||
`.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` +
|
||||
`.borderLeftWidth\\:1px{border-left-width:1px;}\n` +
|
||||
`.borderRightWidth\\:1px{border-right-width:1px;}\n` +
|
||||
`.borderTopWidth\\:1px{border-top-width:1px;}`
|
||||
)
|
||||
})
|
||||
|
||||
test('resolve', () => {
|
||||
const props = { className: 'className', style: styles.root }
|
||||
const expected = { className: 'className border:0px', style: {} }
|
||||
const expected = { className: 'className borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
|
||||
StyleSheet.create(styles)
|
||||
assert.deepEqual(StyleSheet.resolve(props), expected)
|
||||
})
|
||||
|
||||
@@ -3,14 +3,27 @@
|
||||
import assert from 'assert'
|
||||
import isStyleObject from '../isStyleObject'
|
||||
|
||||
const style = { margin: 0 }
|
||||
const notStyle = { root: style }
|
||||
const styles = {
|
||||
root: {
|
||||
margin: 0
|
||||
},
|
||||
align: {
|
||||
left: {
|
||||
textAlign: 'left'
|
||||
},
|
||||
right: {
|
||||
textAlign: 'right'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('modules/StyleSheet/isStyleObject', () => {
|
||||
test('returns "true" for style objects', () => {
|
||||
assert.ok(isStyleObject(style) === true)
|
||||
})
|
||||
test('returns "false" for non-style objects', () => {
|
||||
assert.ok(isStyleObject(notStyle) === false)
|
||||
assert.ok(isStyleObject(styles) === false)
|
||||
assert.ok(isStyleObject(styles.align) === false)
|
||||
})
|
||||
test('returns "true" for style objects', () => {
|
||||
assert.ok(isStyleObject(styles.root) === true)
|
||||
assert.ok(isStyleObject(styles.align.left) === true)
|
||||
})
|
||||
})
|
||||
|
||||
52
src/modules/StyleSheet/expandStyle.js
Normal file
52
src/modules/StyleSheet/expandStyle.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const styleShortHands = {
|
||||
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
|
||||
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
|
||||
borderStyle: [ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle' ],
|
||||
borderWidth: [ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth' ],
|
||||
margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ],
|
||||
marginHorizontal: [ 'marginRight', 'marginLeft' ],
|
||||
marginVertical: [ 'marginTop', 'marginBottom' ],
|
||||
padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ],
|
||||
paddingHorizontal: [ 'paddingRight', 'paddingLeft' ],
|
||||
paddingVertical: [ 'paddingTop', 'paddingBottom' ],
|
||||
writingDirection: [ 'direction' ]
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha-sort properties, apart from shorthands which appear before the
|
||||
* properties they expand into. This ensures that more specific styles override
|
||||
* the shorthands, whatever the order in which they were originally declared.
|
||||
*/
|
||||
const sortProps = (propsArray) => propsArray.sort((a, b) => {
|
||||
const expandedA = styleShortHands[a]
|
||||
const expandedB = styleShortHands[b]
|
||||
if (expandedA && expandedA.indexOf(b) > -1) {
|
||||
return -1
|
||||
} else if (expandedB && expandedB.indexOf(a) > -1) {
|
||||
return 1
|
||||
}
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
})
|
||||
|
||||
/**
|
||||
* Expand the shorthand properties to isolate every declaration from the others.
|
||||
*/
|
||||
const expandStyle = (style) => {
|
||||
const propsArray = Object.keys(style)
|
||||
const sortedProps = sortProps(propsArray)
|
||||
|
||||
return sortedProps.reduce((resolvedStyle, key) => {
|
||||
const expandedProps = styleShortHands[key]
|
||||
const value = style[key]
|
||||
if (expandedProps) {
|
||||
expandedProps.forEach((prop, i) => {
|
||||
resolvedStyle[expandedProps[i]] = value
|
||||
})
|
||||
} else {
|
||||
resolvedStyle[key] = value
|
||||
}
|
||||
return resolvedStyle
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default expandStyle
|
||||
@@ -1 +1 @@
|
||||
export default (string) => string.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
export default (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
|
||||
import expandStyle from './expandStyle'
|
||||
import getStyleObjects from './getStyleObjects'
|
||||
import prefixer from './prefixer'
|
||||
import Store from './Store'
|
||||
import StylePropTypes from '../StylePropTypes'
|
||||
|
||||
/**
|
||||
* Initialize the store with pointer-event styles mapping to our custom pointer
|
||||
@@ -17,11 +19,18 @@ let store = createStore()
|
||||
*/
|
||||
const create = (styles: Object): Object => {
|
||||
const rules = getStyleObjects(styles)
|
||||
|
||||
rules.forEach((rule) => {
|
||||
Object.keys(rule).forEach(property => {
|
||||
const value = rule[property]
|
||||
// add each declaration to the store
|
||||
store.set(property, value)
|
||||
const style = expandStyle(rule)
|
||||
|
||||
Object.keys(style).forEach((property) => {
|
||||
if (!StylePropTypes[property]) {
|
||||
console.error(`ReactNativeWeb: the style property "${property}" is not supported`)
|
||||
} else {
|
||||
const value = style[property]
|
||||
// add each declaration to the store
|
||||
store.set(property, value)
|
||||
}
|
||||
})
|
||||
})
|
||||
return styles
|
||||
@@ -49,15 +58,19 @@ const renderToString = () => {
|
||||
const resolve = ({ className = '', style = {} }) => {
|
||||
let _className
|
||||
let _style = {}
|
||||
const expandedStyle = expandStyle(style)
|
||||
|
||||
const classList = [ className ]
|
||||
for (const prop in style) {
|
||||
let styleClass = store.get(prop, style[prop])
|
||||
for (const prop in expandedStyle) {
|
||||
if (!StylePropTypes[prop]) {
|
||||
continue
|
||||
}
|
||||
let styleClass = store.get(prop, expandedStyle[prop])
|
||||
|
||||
if (styleClass) {
|
||||
classList.push(styleClass)
|
||||
} else {
|
||||
_style[prop] = style[prop]
|
||||
_style[prop] = expandedStyle[prop]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { pickProps } from '../filterObjectProps'
|
||||
import StylePropTypes from '../StylePropTypes'
|
||||
import isObject from './isObject'
|
||||
|
||||
const isStyleObject = (obj) => {
|
||||
const declarations = pickProps(obj, Object.keys(StylePropTypes))
|
||||
return Object.keys(declarations).length > 0
|
||||
const values = Object.keys(obj).map((key) => obj[key])
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
if (isObject(values[i])) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default isStyleObject
|
||||
|
||||
@@ -6,23 +6,20 @@ export const resetCSS =
|
||||
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
|
||||
body {margin:0}
|
||||
button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}
|
||||
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}`
|
||||
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}
|
||||
ol,ul,li {list-style:none}`
|
||||
|
||||
/**
|
||||
* Custom pointer event styles
|
||||
*/
|
||||
export const predefinedCSS =
|
||||
`/* pointer-events */
|
||||
._rn_pe-a {pointer-events:auto}
|
||||
._rn_pe-bn {pointer-events:none}
|
||||
._rn_pe-bn * {pointer-events:auto}
|
||||
._rn_pe-bo {pointer-events:auto}
|
||||
._rn_pe-bo * {pointer-events:none}
|
||||
._rn_pe-n {pointer-events:none}`
|
||||
._s_pe-a, ._s_pe-bo, ._s_pe-bn * {pointer-events:auto}
|
||||
._s_pe-n, ._s_pe-bo *, ._s_pe-bn {pointer-events:none}`
|
||||
|
||||
export const predefinedClassNames = {
|
||||
'pointerEvents:auto': '_rn_pe-a',
|
||||
'pointerEvents:box-none': '_rn_pe-bn',
|
||||
'pointerEvents:box-only': '_rn_pe-bo',
|
||||
'pointerEvents:none': '_rn_pe-n'
|
||||
'pointerEvents:auto': '_s_pe-a',
|
||||
'pointerEvents:box-none': '_s_pe-bn',
|
||||
'pointerEvents:box-only': '_s_pe-bo',
|
||||
'pointerEvents:none': '_s_pe-n'
|
||||
}
|
||||
|
||||
@@ -6,44 +6,6 @@ import ReactDOM from 'react-dom'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
export const assertProps = {
|
||||
accessibilityLabel: function (Component, props) {
|
||||
// with label
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const dom = renderToDOM(<Component {...props} accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(dom.getAttribute('aria-label'), accessibilityLabel)
|
||||
},
|
||||
|
||||
accessibilityLiveRegion: function (Component, props) {
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const dom = renderToDOM(<Component {...props} accessibilityLiveRegion={accessibilityLiveRegion} />)
|
||||
assert.equal(dom.getAttribute('aria-live'), accessibilityLiveRegion)
|
||||
},
|
||||
|
||||
accessibilityRole: function (Component, props) {
|
||||
const accessibilityRole = 'main'
|
||||
const dom = renderToDOM(<Component {...props} accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(dom.getAttribute('role'), accessibilityRole)
|
||||
},
|
||||
|
||||
accessible: function (Component, props) {
|
||||
// accessible (implicit)
|
||||
let dom = renderToDOM(<Component {...props} />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
||||
// accessible (explicit)
|
||||
dom = renderToDOM(<Component {...props} accessible />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), null)
|
||||
// not accessible
|
||||
dom = renderToDOM(<Component {...props} accessible={false} />)
|
||||
assert.equal(dom.getAttribute('aria-hidden'), 'true')
|
||||
},
|
||||
|
||||
component: function (Component, props) {
|
||||
const component = 'main'
|
||||
const dom = renderToDOM(<Component {...props} component={component} />)
|
||||
const tagName = (dom.tagName).toLowerCase()
|
||||
assert.equal(tagName, component)
|
||||
},
|
||||
|
||||
style: function (Component, props) {
|
||||
let shallow
|
||||
// default styles
|
||||
@@ -67,16 +29,6 @@ export const assertProps = {
|
||||
shallow.props.style,
|
||||
{ ...Component.defaultProps.style, ...styleToMerge }
|
||||
)
|
||||
},
|
||||
|
||||
testID: function (Component, props) {
|
||||
// no testID
|
||||
let dom = renderToDOM(<Component {...props} />)
|
||||
assert.equal(dom.getAttribute('data-testid'), null)
|
||||
// with testID
|
||||
const testID = 'Example.testID'
|
||||
dom = renderToDOM(<Component {...props} testID={testID} />)
|
||||
assert.equal(dom.getAttribute('data-testid'), testID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
*
|
||||
* See: https://github.com/webpack/docs/wiki/context
|
||||
*/
|
||||
var context = require.context('.', true, /-test\.js$/)
|
||||
var context = require.context('./src', true, /-test\.js$/)
|
||||
context.keys().forEach(context)
|
||||
Reference in New Issue
Block a user