mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-30 23:23:35 +08:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a19b57df4d | ||
|
|
1c444569ae | ||
|
|
0b8c4b8746 | ||
|
|
6772233837 | ||
|
|
cd89f88d96 | ||
|
|
b59bdb17b2 | ||
|
|
2bfa579fe4 | ||
|
|
f4c6e33b2f | ||
|
|
39b273f9d8 | ||
|
|
ec9985a3b3 | ||
|
|
0aa29d8816 | ||
|
|
45777e0405 | ||
|
|
84e06564d4 | ||
|
|
1d1b633317 | ||
|
|
565ec2fee8 | ||
|
|
33082f988e | ||
|
|
a2fb65a79c | ||
|
|
e727193809 | ||
|
|
d6db206ec4 | ||
|
|
b3beea9bb3 | ||
|
|
ef4de789ae | ||
|
|
86037ab740 | ||
|
|
6c20646f10 | ||
|
|
b5aa68dc7a | ||
|
|
5668c40ee6 | ||
|
|
be86250ac6 | ||
|
|
1e04dfc306 | ||
|
|
283ab2fa2e | ||
|
|
09dd9a224a | ||
|
|
c7524b7b6f | ||
|
|
5453c8843a | ||
|
|
e0f836ccb5 | ||
|
|
eada8e7fc7 | ||
|
|
114fb5f8c7 | ||
|
|
4aa87c79fa | ||
|
|
9107fd3de9 | ||
|
|
c72173ff88 | ||
|
|
edf0fda75a | ||
|
|
3b848fe378 | ||
|
|
7eff1a644e | ||
|
|
2750d70a93 | ||
|
|
65559f50e6 | ||
|
|
77fd21ea44 | ||
|
|
38e4de76cd | ||
|
|
0f42cd83e1 | ||
|
|
e6cbea82c4 | ||
|
|
6d4c9e881f |
10
.babelrc
10
.babelrc
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"optional": [
|
||||
"es7.classProperties",
|
||||
"runtime"
|
||||
],
|
||||
"stage": 1
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-1",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
dist
|
||||
docs
|
||||
@@ -3,9 +3,6 @@
|
||||
"parser": "babel-eslint",
|
||||
// based on https://github.com/feross/standard
|
||||
"extends": [ "standard", "standard-react" ],
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
// overrides of the standard style
|
||||
"space-before-function-paren": [ 2, { "anonymous": "always", "named": "never" } ],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.12"
|
||||
- "4.1"
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
|
||||
@@ -67,10 +67,10 @@ not want to merge into the project.
|
||||
|
||||
Development commands:
|
||||
|
||||
* `npm start` – start the dev server and develop against live examples
|
||||
* `npm run build` – build the library
|
||||
* `npm run examples` – start the dev server and develop against live examples
|
||||
* `npm run lint` – run the linter
|
||||
* `npm run specs` – run the unit tests
|
||||
* `npm run build` – generate a build
|
||||
* `npm run test` – run the linter and unit tests
|
||||
|
||||
Please follow this process for submitting a patch:
|
||||
|
||||
|
||||
260
README.md
260
README.md
@@ -2,16 +2,21 @@
|
||||
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[![npm version][npm-image]][npm-url]
|
||||

|
||||
|
||||
The core [React Native][react-native-url] components adapted and expanded upon
|
||||
for the web, backed by a precomputed CSS library. ~19KB minified and gzipped.
|
||||
[React Native][react-native-url] components and APIs for the Web.
|
||||
|
||||
* [Discord: #react-native-web on reactiflux][discord-url]
|
||||
* [Gitter: react-native-web][gitter-url]
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Install](#install)
|
||||
* [Use](#use)
|
||||
* [Example](#example)
|
||||
* [APIs](#apis)
|
||||
* [Components](#components)
|
||||
* [Styling](#styling)
|
||||
* [Accessibility](#accessibility)
|
||||
* [Contributing](#contributing)
|
||||
* [Thanks](#thanks)
|
||||
* [License](#license)
|
||||
@@ -19,90 +24,26 @@ for the web, backed by a precomputed CSS library. ~19KB minified and gzipped.
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install --save react react-native-web
|
||||
npm install --save react react-dom react-native-web
|
||||
```
|
||||
|
||||
## Use
|
||||
## Example
|
||||
|
||||
React Native for Web exports its components and a reference to the `React`
|
||||
installation. Styles are authored in JavaScript as plain objects.
|
||||
installation. Styles are defined with, and used as JavaScript objects.
|
||||
|
||||
Component:
|
||||
|
||||
```js
|
||||
import React, { View } from 'react-native-web'
|
||||
import React, { Image, StyleSheet, Text, View } from 'react-native-web'
|
||||
|
||||
class MyComponent extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.root} />
|
||||
)
|
||||
}
|
||||
}
|
||||
const Title = ({ children }) => <Text style={styles.title}>{children}</Text>
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
borderColor: 'currentcolor'
|
||||
borderWidth: '5px',
|
||||
flexDirection: 'row'
|
||||
height: '5em'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### [`Image`](docs/components/Image.md)
|
||||
|
||||
An accessibile image component with support for image resizing, default image,
|
||||
and child content.
|
||||
|
||||
### [`ListView`](docs/components/ListView.md)
|
||||
|
||||
(TODO)
|
||||
|
||||
### [`ScrollView`](docs/components/ListView.md)
|
||||
|
||||
(TODO)
|
||||
|
||||
### [`Swipeable`](docs/components/Swipeable.md)
|
||||
|
||||
Touch bindings for swipe gestures.
|
||||
|
||||
### [`Text`](docs/components/Text.md)
|
||||
|
||||
Displays text as an inline block and supports basic press handling.
|
||||
|
||||
### [`TextInput`](docs/components/TextInput.md)
|
||||
|
||||
Accessible single- and multi-line text input via a keyboard. Supports features
|
||||
|
||||
### [`Touchable`](docs/components/Touchable.md)
|
||||
|
||||
Touch bindings for press and long press.
|
||||
|
||||
### [`View`](docs/components/View.md)
|
||||
|
||||
The fundamental UI building block: layout with flexbox, layout and positioning
|
||||
styles, and accessibility controls.
|
||||
|
||||
## Styling
|
||||
|
||||
React Native for Web provides a mechanism to declare all your styles and layout
|
||||
inline with the components that use them. The `View` component makes it easy to
|
||||
build common layouts with flexbox, such as stacked and nested boxes with margin
|
||||
and padding.
|
||||
|
||||
Styling is identical to using inline styles in React, but most inline styles
|
||||
are converted to single-purpose classes. The current implementation includes
|
||||
300+ precomputed CSS declarations (~4.5KB gzipped) that cover a large
|
||||
proportion of common styles. A more sophisticated build-time implementation may
|
||||
produce a slightly larger CSS file for large apps, and fall back to fewer
|
||||
inline styles. Read more about the [styling
|
||||
strategy](docs/style.md).
|
||||
|
||||
See this [guide to flexbox][flexbox-guide-url].
|
||||
|
||||
```js
|
||||
import React, { Image, Text, View } from 'react-native-web'
|
||||
const Summary = ({ children }) => (
|
||||
<View style={styles.text}>
|
||||
<Text style={styles.subtitle}>{children}</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
@@ -112,20 +53,14 @@ class App extends React.Component {
|
||||
source={{ uri: 'http://facebook.github.io/react/img/logo_og.png' }}
|
||||
style={styles.image}
|
||||
/>
|
||||
<View style={styles.text}>
|
||||
<Text style={styles.title}>
|
||||
React Native Web
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Build high quality web apps using React
|
||||
</Text>
|
||||
</View>
|
||||
<Title>React Native Web</Title>
|
||||
<Summary>Build high quality web apps using React</Summary>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const styles = {
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
margin: 40
|
||||
@@ -146,21 +81,139 @@ const styles = {
|
||||
subtitle: {
|
||||
fontSize: '1rem'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Combine and override style objects:
|
||||
Pre-render styles on the server:
|
||||
|
||||
```js
|
||||
import baseStyle from './baseStyle'
|
||||
// server.js
|
||||
import App from './components/App'
|
||||
import React, { StyleSheet } from 'react-native-web'
|
||||
|
||||
const buttonStyle = {
|
||||
...baseStyle,
|
||||
backgroundColor: '#333',
|
||||
color: '#fff'
|
||||
}
|
||||
const html = React.renderToString(<App />);
|
||||
const css = StyleSheet.renderToString();
|
||||
|
||||
const Html = () => (
|
||||
<html>
|
||||
<head>
|
||||
<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 }} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
```
|
||||
|
||||
Render styles on the client:
|
||||
|
||||
```js
|
||||
// client.js
|
||||
import App from './components/App'
|
||||
import React, { StyleSheet } from 'react-native-web'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
const reactRoot = document.getElementById('react-root')
|
||||
const reactStyleSheet = document.getElementById('react-stylesheet')
|
||||
|
||||
ReactDOM.render(<App />, reactRoot)
|
||||
reactStyleSheet.textContent = StyleSheet.renderToString()
|
||||
```
|
||||
|
||||
## APIs
|
||||
|
||||
### [`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.
|
||||
|
||||
## Components
|
||||
|
||||
### [`Image`](docs/components/Image.md)
|
||||
|
||||
An accessibile image component with support for image resizing, default image,
|
||||
and child content.
|
||||
|
||||
### [`ListView`](docs/components/ListView.md)
|
||||
|
||||
(TODO)
|
||||
|
||||
### [`ScrollView`](docs/components/ScrollView.md)
|
||||
|
||||
A scrollable view with event throttling.
|
||||
|
||||
### [`Text`](docs/components/Text.md)
|
||||
|
||||
Displays text as an inline block and supports basic press handling.
|
||||
|
||||
### [`TextInput`](docs/components/TextInput.md)
|
||||
|
||||
Accessible single- and multi-line text input via a keyboard.
|
||||
|
||||
### [`Touchable`](docs/components/Touchable.md)
|
||||
|
||||
Touch bindings for press and long press.
|
||||
|
||||
### [`View`](docs/components/View.md)
|
||||
|
||||
The fundamental UI building block using flexbox for layout.
|
||||
|
||||
## Styling
|
||||
|
||||
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].
|
||||
|
||||
### 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
|
||||
|
||||
Please read the [contribution guidelines][contributing-url]. Contributions are
|
||||
@@ -169,21 +222,24 @@ 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-swipeable](https://github.com/dogfessional/react-swipeable/)
|
||||
for the current implementation of `Swipeable`, and to
|
||||
[react-tappable](https://github.com/JedWatson/react-tappable) for backing the
|
||||
current implementation of `Touchable`.
|
||||
Thanks to [react-tappable](https://github.com/JedWatson/react-tappable) for
|
||||
backing the current implementation of `Touchable`.
|
||||
|
||||
## License
|
||||
|
||||
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/
|
||||
[npm-image]: https://img.shields.io/npm/v/react-native-web.svg
|
||||
[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/
|
||||
[travis-image]: https://travis-ci.org/necolas/react-native-web.svg?branch=master
|
||||
|
||||
11
config/constants.js
Normal file
11
config/constants.js
Normal file
@@ -0,0 +1,11 @@
|
||||
var path = require('path')
|
||||
|
||||
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, 'tests.webpack.js')
|
||||
}
|
||||
@@ -1,46 +1,55 @@
|
||||
var assign = require('object-assign')
|
||||
var path = require('path')
|
||||
var webpackConfig = require('./webpack-base.config.js')
|
||||
var constants = require('./constants')
|
||||
var webpack = require('webpack')
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: path.resolve(__dirname, '..'),
|
||||
browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ],
|
||||
basePath: constants.ROOT_DIRECTORY,
|
||||
browsers: process.env.TRAVIS ? [ 'Firefox' ] : [ 'Chrome' ],
|
||||
browserNoActivityTimeout: 60000,
|
||||
client: {
|
||||
captureConsole: true,
|
||||
mocha: {
|
||||
ui: 'tdd'
|
||||
},
|
||||
mocha: { ui: 'tdd' },
|
||||
useIframe: true
|
||||
},
|
||||
files: [
|
||||
'src/specs.bundle.js'
|
||||
],
|
||||
frameworks: [
|
||||
'mocha'
|
||||
constants.TEST_ENTRY
|
||||
],
|
||||
frameworks: [ 'mocha' ],
|
||||
plugins: [
|
||||
'karma-chrome-launcher',
|
||||
'karma-firefox-launcher',
|
||||
'karma-mocha',
|
||||
'karma-sourcemap-loader',
|
||||
'karma-spec-reporter',
|
||||
'karma-webpack'
|
||||
],
|
||||
preprocessors: {
|
||||
'src/specs.bundle.js': [ 'webpack', 'sourcemap' ]
|
||||
[constants.TEST_ENTRY]: [ 'webpack', 'sourcemap' ]
|
||||
},
|
||||
reporters: [ 'dots' ],
|
||||
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
|
||||
singleRun: true,
|
||||
webpack: assign({}, webpackConfig, { devtool: 'inline' }),
|
||||
webpackMiddleware: {
|
||||
stats: {
|
||||
assetsSort: 'name',
|
||||
colors: true,
|
||||
children: false,
|
||||
chunks: false,
|
||||
modules: false
|
||||
}
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel',
|
||||
query: { cacheDirectory: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify('test')
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
webpackServer: {
|
||||
noInfo: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
var autoprefixer = require('autoprefixer-core')
|
||||
|
||||
module.exports = {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: [
|
||||
'style-loader',
|
||||
'css-loader?module&localIdentName=[hash:base64:5]',
|
||||
'!postcss-loader'
|
||||
].join('!')
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
query: { cacheDirectory: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
postcss: [ autoprefixer ]
|
||||
}
|
||||
39
config/webpack.config.base.js
Normal file
39
config/webpack.config.base.js
Normal file
@@ -0,0 +1,39 @@
|
||||
var webpack = require('webpack')
|
||||
|
||||
var DedupePlugin = webpack.optimize.DedupePlugin
|
||||
var EnvironmentPlugin = webpack.EnvironmentPlugin
|
||||
var OccurenceOrderPlugin = webpack.optimize.OccurenceOrderPlugin
|
||||
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin
|
||||
|
||||
var plugins = [
|
||||
new DedupePlugin(),
|
||||
new EnvironmentPlugin('NODE_ENV'),
|
||||
new OccurenceOrderPlugin()
|
||||
]
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
plugins.push(
|
||||
new UglifyJsPlugin({
|
||||
compress: {
|
||||
dead_code: true,
|
||||
drop_console: true,
|
||||
screw_ie8: true,
|
||||
warnings: true
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
query: { cacheDirectory: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: plugins
|
||||
}
|
||||
17
config/webpack.config.example.js
Normal file
17
config/webpack.config.example.js
Normal file
@@ -0,0 +1,17 @@
|
||||
var assign = require('object-assign')
|
||||
var base = require('./webpack.config.base')
|
||||
var constants = require('./constants')
|
||||
var path = require('path')
|
||||
|
||||
module.exports = assign({}, base, {
|
||||
devServer: {
|
||||
contentBase: constants.EXAMPLES_DIRECTORY
|
||||
},
|
||||
entry: {
|
||||
example: path.join(constants.EXAMPLES_DIRECTORY, 'index')
|
||||
},
|
||||
output: {
|
||||
filename: 'examples.js',
|
||||
path: constants.DIST_DIRECTORY
|
||||
}
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
var assign = require('object-assign')
|
||||
var base = require('./webpack-base.config.js')
|
||||
var webpack = require('webpack')
|
||||
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin
|
||||
|
||||
var plugins = []
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
plugins.push(
|
||||
new UglifyJsPlugin({
|
||||
compress: {
|
||||
dead_code: true,
|
||||
drop_console: true,
|
||||
screw_ie8: true,
|
||||
warnings: true
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = assign({}, base, {
|
||||
entry: {
|
||||
main: './src/index'
|
||||
},
|
||||
externals: [{
|
||||
react: true
|
||||
}],
|
||||
output: {
|
||||
filename: 'react-native-web.js',
|
||||
library: 'ReactNativeWeb',
|
||||
libraryTarget: 'commonjs2',
|
||||
path: './dist'
|
||||
},
|
||||
plugins: plugins
|
||||
})
|
||||
19
config/webpack.config.publish.js
Normal file
19
config/webpack.config.publish.js
Normal file
@@ -0,0 +1,19 @@
|
||||
var assign = require('object-assign')
|
||||
var base = require('./webpack.config.base')
|
||||
var constants = require('./constants')
|
||||
|
||||
module.exports = assign({}, base, {
|
||||
entry: {
|
||||
main: constants.SRC_DIRECTORY
|
||||
},
|
||||
externals: [{
|
||||
'react': true,
|
||||
'react-dom': true
|
||||
}],
|
||||
output: {
|
||||
filename: 'react-native-web.js',
|
||||
library: 'ReactNativeWeb',
|
||||
libraryTarget: 'commonjs2',
|
||||
path: constants.DIST_DIRECTORY
|
||||
}
|
||||
})
|
||||
165
docs/apis/StyleSheet.md
Normal file
165
docs/apis/StyleSheet.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# StyleSheet
|
||||
|
||||
React Native for Web will automatically vendor-prefix styles applied to the
|
||||
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.
|
||||
|
||||
Create a new StyleSheet:
|
||||
|
||||
```js
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 4,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#d6d7da',
|
||||
},
|
||||
title: {
|
||||
fontSize: 19,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
activeTitle: {
|
||||
color: 'red',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Use styles:
|
||||
|
||||
```js
|
||||
<View style={styles.container}>
|
||||
<Text
|
||||
style={{
|
||||
...styles.title,
|
||||
...(this.props.isActive && styles.activeTitle)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
```
|
||||
|
||||
Render styles on the server or in the browser:
|
||||
|
||||
```js
|
||||
StyleSheet.renderToString()
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
**create**(obj: {[key: string]: any})
|
||||
|
||||
**destroy**()
|
||||
|
||||
Clears all style information.
|
||||
|
||||
**renderToString**()
|
||||
|
||||
Renders a CSS Style Sheet.
|
||||
|
||||
## About
|
||||
|
||||
### Strategy
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
<View style={styles.root}>...</View>
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Yields (in development):
|
||||
|
||||
```html
|
||||
<div className="background:transparent display:flex flexGrow:1 justifyContent:center">...</div>
|
||||
```
|
||||
|
||||
And is backed by the following CSS:
|
||||
|
||||
```css
|
||||
.background\:transparent {background:transparent;}
|
||||
.display\:flex {display:flex;}
|
||||
.flexGrow\:1 {flex-grow:1;}
|
||||
.justifyContext\:center {justify-content:center;}
|
||||
```
|
||||
|
||||
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
|
||||
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.)
|
||||
|
||||
### Reset
|
||||
|
||||
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`).
|
||||
|
||||
```css
|
||||
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;
|
||||
}
|
||||
```
|
||||
@@ -3,47 +3,59 @@
|
||||
An accessibile image component with support for image resizing, default image,
|
||||
and child content.
|
||||
|
||||
Unsupported React Native props:
|
||||
`capInsets` (ios),
|
||||
`onProgress` (ios)
|
||||
|
||||
## Props
|
||||
|
||||
**accessibilityLabel** string
|
||||
**accessibilityLabel**: string
|
||||
|
||||
The text that's read by the screen reader when the user interacts with the image.
|
||||
The text that's read by a screenreader when someone interacts with the image.
|
||||
|
||||
**children** any
|
||||
**accessible**: bool
|
||||
|
||||
When `false`, the view is hidden from screenreaders. Default: `true`.
|
||||
|
||||
**children**: any
|
||||
|
||||
Content to display over the image.
|
||||
|
||||
**defaultSource** { uri: string }
|
||||
**defaultSource**: { uri: string }
|
||||
|
||||
An image to display as a placeholder while downloading the final image off the network.
|
||||
|
||||
**onError** function
|
||||
**onError**: function
|
||||
|
||||
Invoked on load error with `{nativeEvent: {error}}`.
|
||||
|
||||
**onLoad** function
|
||||
**onLayout**: function
|
||||
|
||||
TODO
|
||||
|
||||
**onLoad**: function
|
||||
|
||||
Invoked when load completes successfully.
|
||||
|
||||
**onLoadEnd** function
|
||||
**onLoadEnd**: function
|
||||
|
||||
Invoked when load either succeeds or fails,
|
||||
|
||||
**onLoadStart** function
|
||||
**onLoadStart**: function
|
||||
|
||||
Invoked on load start.
|
||||
|
||||
**resizeMode** oneOf('clip', 'contain', 'cover', 'stretch')
|
||||
**resizeMode**: oneOf('contain', 'cover', 'none', 'stretch') = 'stretch'
|
||||
|
||||
Determines how to resize the image when the frame doesn't match the raw image
|
||||
dimensions. Default: `cover`.
|
||||
dimensions.
|
||||
|
||||
**source** { uri: string }
|
||||
**source**: { uri: string }
|
||||
|
||||
`uri` is a string representing the resource identifier for the image, which
|
||||
could be an http address or a base64 encoded image.
|
||||
|
||||
**style** style
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
|
||||
@@ -52,11 +64,11 @@ Defaults:
|
||||
```js
|
||||
{
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'lightGray'
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
```
|
||||
|
||||
**testID** string
|
||||
**testID**: string
|
||||
|
||||
Used to locate a view in end-to-end tests.
|
||||
|
||||
@@ -64,14 +76,14 @@ 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, { Image, StyleSheet } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
|
||||
class Avatar extends Component {
|
||||
export default class Avatar extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { isLoaded: false }
|
||||
this.state = { loading: true }
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
@@ -86,12 +98,12 @@ class Avatar extends Component {
|
||||
|
||||
_onLoad(e) {
|
||||
console.log('Avatar.onLoad', e)
|
||||
this.setState({ isLoaded: true })
|
||||
this.setState({ loading: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { size, testID, user } = this.props
|
||||
const { isLoaded } = this.state
|
||||
const loadingStyle = this.state.loading ? { styles.loading } : { }
|
||||
|
||||
return (
|
||||
<Image
|
||||
@@ -100,18 +112,20 @@ class Avatar extends Component {
|
||||
onLoad={this._onLoad.bind(this)}
|
||||
resizeMode='cover'
|
||||
source={{ uri: user.avatarUrl }}
|
||||
style={ ...style.base, ...style[size], ...style[isLoaded] }
|
||||
style={{ ...styles.base, ...styles[size], ...loadingStyle }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const style = {
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderColor: 'white',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '5px',
|
||||
opacity: 0.5,
|
||||
borderWidth: '5px'
|
||||
},
|
||||
loading: {
|
||||
opacity: 0.5
|
||||
},
|
||||
small: {
|
||||
height: '32px',
|
||||
@@ -125,8 +139,5 @@ const style = {
|
||||
height: '64px',
|
||||
width: '64px'
|
||||
}
|
||||
isLoaded: {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# ListView
|
||||
|
||||
TODO
|
||||
|
||||
## Props
|
||||
|
||||
**children** any
|
||||
**children**: any
|
||||
|
||||
Content to display over the image.
|
||||
|
||||
**style** style
|
||||
**style**: style
|
||||
|
||||
+ `property` type
|
||||
|
||||
@@ -17,7 +19,6 @@ Defaults:
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
|
||||
@@ -1,40 +1,84 @@
|
||||
# ScrollView
|
||||
|
||||
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
|
||||
|
||||
**children** any
|
||||
**children**: any
|
||||
|
||||
Content to display over the image.
|
||||
Child content.
|
||||
|
||||
**style** style
|
||||
**contentContainerStyle**: style
|
||||
|
||||
+ `property` type
|
||||
These styles will be applied to the scroll view content container which wraps
|
||||
all of the child views.
|
||||
|
||||
Defaults:
|
||||
**horizontal**: bool = false
|
||||
|
||||
```js
|
||||
{
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
**scrollEnabled**: bool = true
|
||||
|
||||
When false, the content does not scroll.
|
||||
|
||||
**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. 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
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { ScrollView } from 'react-native-web'
|
||||
import React, { ScrollView, StyleSheet } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
import Item from './Item'
|
||||
|
||||
class Example extends Component {
|
||||
static propTypes = {
|
||||
export default class App extends React.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: '1px'
|
||||
},
|
||||
container: {
|
||||
padding: '10px'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# Swipeable
|
||||
|
||||
## Props
|
||||
|
||||
**delta** number
|
||||
|
||||
Number of pixels that must be swiped before events are dispatched. Default: `10`.
|
||||
|
||||
**flickThreshold** number
|
||||
|
||||
The velocity threshold at which a swipe is considered a flick. Default: `0.6`.
|
||||
|
||||
**onSwiped** function
|
||||
|
||||
(SyntheticTouchEvent, deltaX, deltaY, isFlick) => swipeHandler
|
||||
|
||||
Called once a swipe has ended.
|
||||
|
||||
**onSwipedDown** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called once a swipe-down has ended.
|
||||
|
||||
**onSwipedLeft** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called once a swipe-left has ended.
|
||||
|
||||
**onSwipedUp** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called once a swipe-up has ended.
|
||||
|
||||
**onSwipedRight** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called once a swipe-right has ended.
|
||||
|
||||
**onSwipingDown** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called while a swipe-down is in progress.
|
||||
|
||||
**onSwipingLeft** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called while a swipe-left is in progress.
|
||||
|
||||
**onSwipingRight** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called while a swipe-right is in progress.
|
||||
|
||||
**onSwipingUp** function
|
||||
|
||||
(SyntheticTouchEvent, delta, isFlick) => swipeHandler
|
||||
|
||||
Called while a swipe-up is in progress.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Swipeable } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React;
|
||||
|
||||
class Example extends Component {
|
||||
static propTypes = {
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
}
|
||||
|
||||
_onSwiped(event, x, y, isFlick) {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Swipeable
|
||||
onSwiped={this._onSwiped.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -10,25 +10,48 @@ The `Text` is unique relative to layout: child elements use text layout
|
||||
a `Text` are not rectangles, as they wrap when reaching the edge of their
|
||||
container.
|
||||
|
||||
Unsupported React Native props:
|
||||
`allowFontScaling` (ios),
|
||||
`suppressHighlighting` (ios)
|
||||
|
||||
## Props
|
||||
|
||||
**children** any
|
||||
NOTE: `Text` will transfer all other props to the rendered HTML element.
|
||||
|
||||
Child content
|
||||
(web) **accessibilityLabel**: string
|
||||
|
||||
**component** function, string
|
||||
Defines the text available to assistive technologies upon interaction with the
|
||||
element. (This is implemented using `aria-label`.)
|
||||
|
||||
Default is `span`.
|
||||
(web) **accessibilityRole**: oneOf(roles)
|
||||
|
||||
**numberOfLines** number
|
||||
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)).
|
||||
|
||||
Truncates the text with an ellipsis after this many lines.
|
||||
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.
|
||||
|
||||
**onPress** function
|
||||
(web) **accessible**: bool = true
|
||||
|
||||
When `false`, the text is hidden from assistive technologies. (This is
|
||||
implemented using `aria-hidden`.)
|
||||
|
||||
**children**: any
|
||||
|
||||
Child content.
|
||||
|
||||
**numberOfLines**: number
|
||||
|
||||
Truncates the text with an ellipsis after this many lines. Currently only supports `1`.
|
||||
|
||||
**onPress**: function
|
||||
|
||||
This function is called on press.
|
||||
|
||||
**style** style
|
||||
**style**: style
|
||||
|
||||
+ `backgroundColor`
|
||||
+ `color`
|
||||
@@ -47,14 +70,14 @@ This function is called on press.
|
||||
+ `whiteSpace`
|
||||
+ `wordWrap`
|
||||
|
||||
**testID** string
|
||||
**testID**: string
|
||||
|
||||
Used to locate this view in end-to-end tests.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Text } from 'react-native-web'
|
||||
import React, { StyleSheet, Text } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
@@ -88,7 +111,7 @@ class PrettyText extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const localStyle = {
|
||||
const localStyle = StyleSheet.create({
|
||||
color: {
|
||||
white: { color: 'white' },
|
||||
gray: { color: 'gray' },
|
||||
@@ -104,5 +127,5 @@ const localStyle = {
|
||||
normal: { fontWeight: '400' },
|
||||
bold: { fontWeight: '700' }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -5,67 +5,130 @@ such as auto-complete, auto-focus, placeholder text, and event callbacks.
|
||||
|
||||
Note: some props are exclusive to or excluded from `multiline`.
|
||||
|
||||
Unsupported React Native props:
|
||||
`autoCapitalize`,
|
||||
`autoCorrect`,
|
||||
`onEndEditing`,
|
||||
`onSubmitEditing`,
|
||||
`clearButtonMode` (ios),
|
||||
`enablesReturnKeyAutomatically` (ios),
|
||||
`returnKeyType` (ios),
|
||||
`selectionState` (ios),
|
||||
`textAlign` (android),
|
||||
`textAlignVertical` (android),
|
||||
`underlineColorAndroid` (android)
|
||||
|
||||
## Props
|
||||
|
||||
**autoComplete** bool
|
||||
(web) **accessibilityLabel**: string
|
||||
|
||||
Defines the text label available to assistive technologies upon interaction
|
||||
with the element. (This is implemented using `aria-label`.)
|
||||
|
||||
(web) **autoComplete**: bool = false
|
||||
|
||||
Indicates whether the value of the control can be automatically completed by the browser.
|
||||
|
||||
**autoFocus** bool
|
||||
**autoFocus**: bool = false
|
||||
|
||||
If true, focuses the input on `componentDidMount`. Only the first form element
|
||||
in a document with `autofocus` is focused. Default: `false`.
|
||||
in a document with `autofocus` is focused.
|
||||
|
||||
**defaultValue** string
|
||||
**clearTextOnFocus**: bool = false
|
||||
|
||||
If `true`, clears the text field automatically when focused.
|
||||
|
||||
**defaultValue**: string
|
||||
|
||||
Provides an initial value that will change when the user starts typing. Useful
|
||||
for simple use-cases where you don't want to deal with listening to events and
|
||||
updating the `value` prop to keep the controlled state in sync.
|
||||
|
||||
**editable** bool
|
||||
**editable**: bool = true
|
||||
|
||||
If false, text is not editable. Default: `true`.
|
||||
If `false`, text is not editable (i.e., read-only).
|
||||
|
||||
**keyboardType** oneOf('default', 'email', 'numeric', 'search', 'tel', 'url')
|
||||
**keyboardType**: oneOf('default', 'email-address', 'numeric', 'phone-pad', 'url') = 'default'
|
||||
|
||||
Determines which keyboard to open, e.g. `email`. Default: `default`. (Not
|
||||
available when `multiline` is `true`.)
|
||||
Determines which keyboard to open.
|
||||
|
||||
**multiline** bool
|
||||
(Not available when `multiline` is `true`.)
|
||||
|
||||
If true, the text input can be multiple lines. Default: `false`.
|
||||
**maxLength**: number
|
||||
|
||||
**onBlur** function
|
||||
Limits the maximum number of characters that can be entered.
|
||||
|
||||
(web) **maxNumberOfLines**: number = numberOfLines
|
||||
|
||||
Limits the maximum number of lines for a multiline `TextInput`.
|
||||
|
||||
(Requires `multiline` to be `true`.)
|
||||
|
||||
**multiline**: bool = false
|
||||
|
||||
If true, the text input can be multiple lines.
|
||||
|
||||
**numberOfLines**: number = 2
|
||||
|
||||
Sets the initial number of lines for a multiline `TextInput`.
|
||||
|
||||
(Requires `multiline` to be `true`.)
|
||||
|
||||
**onBlur**: function
|
||||
|
||||
Callback that is called when the text input is blurred.
|
||||
|
||||
**onChange** function
|
||||
**onChange**: function
|
||||
|
||||
Callback that is called when the text input's text changes.
|
||||
|
||||
**onChangeText** function
|
||||
**onChangeText**: function
|
||||
|
||||
Callback that is called when the text input's text changes. Changed text is
|
||||
passed as an argument to the callback handler.
|
||||
Callback that is called when the text input's text changes. The text is passed
|
||||
as an argument to the callback handler.
|
||||
|
||||
**onFocus** function
|
||||
**onFocus**: function
|
||||
|
||||
Callback that is called when the text input is focused.
|
||||
|
||||
**placeholder** string
|
||||
**onLayout**: function
|
||||
|
||||
TODO
|
||||
|
||||
(web) **onSelectionChange**: function
|
||||
|
||||
Callback that is called when the text input's selection changes. The following
|
||||
object is passed as an argument to the callback handler.
|
||||
|
||||
```js
|
||||
{
|
||||
selectionDirection,
|
||||
selectionEnd,
|
||||
selectionStart,
|
||||
nativeEvent
|
||||
}
|
||||
```
|
||||
|
||||
**placeholder**: string
|
||||
|
||||
The string that will be rendered before text input has been entered.
|
||||
|
||||
**placeholderTextColor** string
|
||||
**placeholderTextColor**: string
|
||||
|
||||
The text color of the placeholder string.
|
||||
TODO. The text color of the placeholder string.
|
||||
|
||||
**secureTextEntry** bool
|
||||
**secureTextEntry**: bool = false
|
||||
|
||||
If true, the text input obscures the text entered so that sensitive text like
|
||||
passwords stay secure. Default: `false`. (Not available when `multiline` is `true`.)
|
||||
passwords stay secure.
|
||||
|
||||
**style** style
|
||||
(Not available when `multiline` is `true`.)
|
||||
|
||||
**selectTextOnFocus**: bool = false
|
||||
|
||||
If `true`, all text will automatically be selected on focus.
|
||||
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
|
||||
@@ -81,31 +144,58 @@ passwords stay secure. Default: `false`. (Not available when `multiline` is `tru
|
||||
+ `textDecoration`
|
||||
+ `textTransform`
|
||||
|
||||
**testID** string
|
||||
**testID**: string
|
||||
|
||||
Used to locate this view in end-to-end tests.
|
||||
|
||||
**value**: string
|
||||
|
||||
The value to show for the text input. `TextInput` is a controlled component,
|
||||
which means the native `value` will be forced to match this prop if provided.
|
||||
Read about how [React form
|
||||
components](https://facebook.github.io/react/docs/forms.html) work. To prevent
|
||||
user edits to the value set `editable={false}`.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { TextInput } from 'react-native-web'
|
||||
import React, { StyleSheet, TextInput } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
class AppTextInput extends Component {
|
||||
static propTypes = {
|
||||
export default class AppTextInput extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { isFocused: false }
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
_onFocus(e) {
|
||||
this.setState({ isFocused: true })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TextInput />
|
||||
<TextInput
|
||||
accessibilityLabel='Write a status update'
|
||||
maxNumberOfLines={5}
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
onFocus={this._onFocus.bind(this)}
|
||||
placeholder={`What's happening?`}
|
||||
style={{
|
||||
...styles.default
|
||||
...(this.state.isFocused && styles.focused)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
default: {
|
||||
borderColor: 'gray',
|
||||
borderWidth: '0 0 2px 0'
|
||||
},
|
||||
focused: {
|
||||
borderColor: 'blue'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,40 +1,85 @@
|
||||
# Touchable
|
||||
|
||||
A wrapper for making views respond to mouse, keyboard, and touch presses. On
|
||||
press in, the touchable area can display a highlight color, and the opacity of
|
||||
the wrapped view can be decreased.
|
||||
|
||||
This component combines the various `Touchable*` components from React Native.
|
||||
|
||||
Unsupported React Native props:
|
||||
`accessibilityComponentType` (android) – use `accessibilityRole`,
|
||||
`accessibilityTraits` (ios) – use `accessibilityRole`,
|
||||
`onHideUnderlay` – use `onPressOut`,
|
||||
`onShowUnderlay` – use `onPressIn`,
|
||||
`underlayColor` – use `activeUnderlayColor`
|
||||
|
||||
## Props
|
||||
|
||||
**activeHighlight** string
|
||||
**accessibilityLabel**: string
|
||||
|
||||
Sets the color of the background highlight when `onPressIn` is called. The
|
||||
highlight is removed when `onPressOut` is called. Default: `transparent`.
|
||||
Overrides the text that's read by the screen reader when the user interacts
|
||||
with the element.
|
||||
|
||||
**activeOpacity** number
|
||||
(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.
|
||||
|
||||
**accessible**: bool = true
|
||||
|
||||
When `false`, the view is hidden from screenreaders.
|
||||
|
||||
**activeOpacity**: number = 1
|
||||
|
||||
Sets the opacity of the child view when `onPressIn` is called. The opacity is
|
||||
reset when `onPressOut` is called. Default: `1`.
|
||||
reset when `onPressOut` is called.
|
||||
|
||||
**component** function or string
|
||||
(web) **activeUnderlayColor**: string = 'transparent'
|
||||
|
||||
The backing component. Default: `div`.
|
||||
Sets the color of the background highlight when `onPressIn` is called. The
|
||||
highlight is removed when `onPressOut` is called.
|
||||
|
||||
**delayLongPress** number
|
||||
**children**: element
|
||||
|
||||
Delay in ms, from `onPressIn`, before `onLongPress` is called. Default: `1000`.
|
||||
A single child element.
|
||||
|
||||
**delayPressIn** number (TODO)
|
||||
**delayLongPress**: number = 1000
|
||||
|
||||
Delay in ms, from the start of the touch, before `onPressIn` is called. Default: `0`.
|
||||
Delay in ms, from `onPressIn`, before `onLongPress` is called.
|
||||
|
||||
**delayPressOut** number (TODO)
|
||||
**delayPressIn**: number = 0
|
||||
|
||||
Delay in ms, from the release of the touch, before `onPressOut` is called. Default: `0`.
|
||||
(TODO)
|
||||
|
||||
**onLongPress** function
|
||||
Delay in ms, from the start of the touch, before `onPressIn` is called.
|
||||
|
||||
**onPress** function
|
||||
**delayPressOut**: number = 0
|
||||
|
||||
**onPressIn** function
|
||||
(TODO)
|
||||
|
||||
**onPressOut** function
|
||||
Delay in ms, from the release of the touch, before `onPressOut` is called.
|
||||
|
||||
**onLayout**: function
|
||||
|
||||
(TODO)
|
||||
|
||||
**onLongPress**: function
|
||||
|
||||
**onPress**: function
|
||||
|
||||
**onPressIn**: function
|
||||
|
||||
**onPressOut**: function
|
||||
|
||||
**style**: style
|
||||
|
||||
[View](View.md) style
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
# View
|
||||
|
||||
`View` is the fundamental UI building block. It is a component that supports
|
||||
style, layout with flexbox, and accessibility controls. It can be nested
|
||||
style, layout with flexbox, and accessibility controls. It can be nested
|
||||
inside another `View` and has 0-to-many children of any type.
|
||||
|
||||
Unsupported React Native props:
|
||||
`accessibilityComponentType` (android) – use `accessibilityRole`,
|
||||
`accessibilityTraits` (ios) – use `accessibilityRole`,
|
||||
`collapsable` (android),
|
||||
`importantForAccessibility` (android),
|
||||
`needsOffscreenAlphaCompositing` (android),
|
||||
`onAccessibilityTap`,
|
||||
`onMagicTap`,
|
||||
`onMoveShouldSetResponder`,
|
||||
`onResponder*`,
|
||||
`onStartShouldSetResponder`,
|
||||
`onStartShouldSetResponderCapture`
|
||||
`removeClippedSubviews` (ios),
|
||||
`renderToHardwareTextureAndroid` (android),
|
||||
`shouldRasterizeIOS` (ios)
|
||||
|
||||
## Props
|
||||
|
||||
**accessibilityLabel** string
|
||||
NOTE: `View` will transfer all other props to the rendered HTML element.
|
||||
|
||||
Overrides the text that's read by the screen reader when the user interacts
|
||||
with the element. This is implemented using `aria-label`.
|
||||
**accessibilityLabel**: string
|
||||
|
||||
**component** function, string
|
||||
Defines the text available to assistive technologies upon interaction with the
|
||||
element. (This is implemented using `aria-label`.)
|
||||
|
||||
Default is `div`.
|
||||
**accessibilityLiveRegion**: oneOf('assertive', 'off', 'polite') = 'off'
|
||||
|
||||
**pointerEvents** oneOf('auto', 'box-only', 'box-none', 'none')
|
||||
Indicates to assistive technologies whether to notify the user when the view
|
||||
changes. The values of this attribute are expressed in degrees of importance.
|
||||
When regions are specified as `polite` (recommended), updates take low
|
||||
priority. When regions are specified as `assertive`, assistive technologies
|
||||
will interrupt and immediately notify the user. (This is implemented using
|
||||
[`aria-live`](http://www.w3.org/TR/wai-aria/states_and_properties#aria-live).)
|
||||
|
||||
We deviate from the CSS spec by supporting additional `pointerEvents` modes,
|
||||
therefore `pointerEvents` is excluded from `style`.
|
||||
(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.
|
||||
|
||||
**accessible**: bool = true
|
||||
|
||||
When `false`, the view is hidden from assistive technologies. (This is
|
||||
implemented using `aria-hidden`.)
|
||||
|
||||
**onLayout**: function
|
||||
|
||||
(TODO)
|
||||
|
||||
**pointerEvents**: oneOf('auto', 'box-only', 'box-none', 'none') = 'auto'
|
||||
|
||||
Configure the `pointerEvents` of the view. The enhanced `pointerEvents` modes
|
||||
provided are not part of the CSS spec, therefore, `pointerEvents` is excluded
|
||||
from `style`.
|
||||
|
||||
`box-none` is the equivalent of:
|
||||
|
||||
@@ -34,7 +78,7 @@ therefore `pointerEvents` is excluded from `style`.
|
||||
.box-only * { pointer-events: none }
|
||||
```
|
||||
|
||||
**style** style
|
||||
**style**: style
|
||||
|
||||
+ `alignContent`
|
||||
+ `alignItems`
|
||||
@@ -55,6 +99,7 @@ therefore `pointerEvents` is excluded from `style`.
|
||||
+ `bottom`
|
||||
+ `boxShadow`
|
||||
+ `boxSizing`
|
||||
+ `cursor`
|
||||
+ `flexBasis`
|
||||
+ `flexDirection`
|
||||
+ `flexGrow`
|
||||
@@ -103,14 +148,14 @@ Default:
|
||||
|
||||
(See [facebook/css-layout](https://github.com/facebook/css-layout)).
|
||||
|
||||
**testID** string
|
||||
**testID**: string
|
||||
|
||||
Used to locate this view in end-to-end tests.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { View } from 'react-native-web'
|
||||
import React, { StyleSheet, View } from 'react-native-web'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
@@ -128,14 +173,14 @@ class Example extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
cell: {
|
||||
flexGrow: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default Example
|
||||
```
|
||||
|
||||
123
docs/style.md
123
docs/style.md
@@ -1,123 +0,0 @@
|
||||
# Styling strategy
|
||||
|
||||
Using the `style` attribute would normally produce inline styles. There are
|
||||
several existing approaches to using the `style` attribute, some of which
|
||||
convert inline styles to static CSS:
|
||||
[jsxstyle](https://github.com/petehunt/jsxstyle),
|
||||
[react-free-style](https://github.com/blakeembrey/react-free-style/),
|
||||
[react-inline](https://github.com/martinandert/react-inline),
|
||||
[react-native](https://facebook.github.io/react-native/),
|
||||
[react-style](https://github.com/js-next/react-style),
|
||||
[stilr](https://github.com/kodyl/stilr).
|
||||
|
||||
## Style syntax: native vs proprietary data structure
|
||||
|
||||
React Native for Web diverges from React Native by using plain JS objects for
|
||||
styles:
|
||||
|
||||
```js
|
||||
<Text style={styles.root}>...</Text>
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Most approaches to managing style in React introduce a proprietary data
|
||||
structure, often via an implementation of `Stylesheet.create`.
|
||||
|
||||
```js
|
||||
<Text style={styles.root}>...</Text>
|
||||
|
||||
const styles = Stylesheet.create({
|
||||
root: {
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## JS-to-CSS: conversion strategies
|
||||
|
||||
Mapping entire `style` objects to CSS rules can lead to increasingly large CSS
|
||||
files. Each new component adds new rules to the stylesheet.
|
||||
|
||||

|
||||
|
||||
One strategy for converting styles from JS to CSS is to map style objects to
|
||||
CSS rules. Another strategy is to map declarations to declarations.
|
||||
|
||||
React Native for Web currently includes a proof-of-concept implementation of
|
||||
the latter strategy. This results in smaller CSS files because all applications
|
||||
has fewer unique declarations than total declarations. Creating a new component
|
||||
with no new unique declarations results in no change to the CSS file.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
<Text style={styles.root}>...</Text>
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Yields:
|
||||
|
||||
```html
|
||||
<span className="_abcde _fghij _klmno _pqrst">...</span>
|
||||
```
|
||||
|
||||
And is backed by:
|
||||
|
||||
```css
|
||||
._abcde { background: transparent }
|
||||
._fghij { display: flex }
|
||||
._klmno { flex-grow: 1 }
|
||||
._pqrst { justify-content: center }
|
||||
```
|
||||
|
||||
The current implementation uses a precomputed CSS library of single-declaration
|
||||
rules, with obfuscated selectors. This handles a signficant portion of possible
|
||||
declarations. A build-time implementation would produce more accurate CSS
|
||||
files and fall through to inline styles significantly less often.
|
||||
|
||||
(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
|
||||
particular set of classes and features to learn. All of them require developers
|
||||
to manually connect CSS classes for given styles.)
|
||||
|
||||
## Dynamic styles: use inline styles
|
||||
|
||||
Some styles cannot be resolved ahead of time and continue to rely on inline
|
||||
styles:
|
||||
|
||||
```js
|
||||
<View style={{ backgroundColor: (Math.random() > 0.5 ? 'red' : 'black') }}>...</Text>
|
||||
```
|
||||
|
||||
## Media Queries, pseudo-classes, and pseudo-elements
|
||||
|
||||
Media Queries could be replaced with `mediaMatch`. This would have the added
|
||||
benefit of co-locating breakpoint-specific DOM and style changes. Perhaps Media
|
||||
Query data could be accessed on `this.content`?
|
||||
|
||||
Pseudo-classes like `:hover` and `:focus` can be handled with JavaScript.
|
||||
|
||||
Pseudo-elements should be avoided in general, but for particular cases like
|
||||
`::placeholder` it might be necessary to reimplement it in the `TextInput`
|
||||
component (see React Native's API).
|
||||
@@ -1,5 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<div id="react-root"></div>
|
||||
<script src="../dist/example.js"></script>
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
entry: {
|
||||
example: './example.js'
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
query: { cacheDirectory: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: 'example.js',
|
||||
path: '../dist'
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,42 @@
|
||||
import React, { Image, Swipeable, Text, TextInput, Touchable, View } from '../dist/react-native-web'
|
||||
import GridView from './GridView'
|
||||
import Heading from './Heading'
|
||||
import MediaQueryWidget from './MediaQueryWidget'
|
||||
import React, { Image, StyleSheet, ScrollView, Text, TextInput, Touchable, View } from '../../src'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
class Heading extends Component {
|
||||
static propTypes = {
|
||||
children: Text.propTypes.children,
|
||||
level: PropTypes.oneOf(['1', '2', '3']),
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'normal'])
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
level: '1',
|
||||
size: 'normal'
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, level, size } = this.props
|
||||
|
||||
return (
|
||||
<Text
|
||||
children={children}
|
||||
component={`h${level}`}
|
||||
style={headingStyles.size[size]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const headingStyles = {
|
||||
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 Component {
|
||||
export default class App extends React.Component {
|
||||
static propTypes = {
|
||||
mediaQuery: React.PropTypes.object,
|
||||
style: View.propTypes.style
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<Heading level='1' size='xlarge'>React Native for Web: examples</Heading>
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.state = {
|
||||
scrollEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
<Heading level='2' size='large'>Image</Heading>
|
||||
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={rootStyles}>
|
||||
<Heading size='xlarge'>React Native for Web</Heading>
|
||||
<Text>React Native Web takes the core components from <Text
|
||||
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>
|
||||
|
||||
<MediaQueryWidget mediaQuery={mediaQuery} />
|
||||
|
||||
<Heading size='large'>Image</Heading>
|
||||
<Image
|
||||
accessibilityLabel='accessible image'
|
||||
children={<Text>Inner content</Text>}
|
||||
@@ -79,22 +59,7 @@ class Example extends Component {
|
||||
testID='Example.image'
|
||||
/>
|
||||
|
||||
<Heading level='2' size='large'>Swipeable</Heading>
|
||||
<Swipeable
|
||||
onSwiped={(e) => { console.log('Swipeable.onSwiped', e) }}
|
||||
testID={'Example.swipeable'}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
alignSelf: 'center',
|
||||
width: '200px',
|
||||
height: '200px'
|
||||
}}
|
||||
/>
|
||||
</Swipeable>
|
||||
|
||||
<Heading level='2' size='large'>Text</Heading>
|
||||
<Heading size='large'>Text</Heading>
|
||||
<Text
|
||||
onPress={(e) => { console.log('Text.onPress', e) }}
|
||||
testID={'Example.text'}
|
||||
@@ -119,23 +84,31 @@ class Example extends 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) }}
|
||||
onChange={(e) => { console.log('TextInput.onChange', e) }}
|
||||
onChangeText={(e) => { console.log('TextInput.onChangeText', e) }}
|
||||
onFocus={(e) => { console.log('TextInput.onFocus', e) }}
|
||||
onSelectionChange={(e) => { console.log('TextInput.onSelectionChange', e) }}
|
||||
/>
|
||||
<TextInput secureTextEntry={true} />
|
||||
<TextInput secureTextEntry />
|
||||
<TextInput defaultValue='read only' editable={false} />
|
||||
<TextInput keyboardType='email-address' />
|
||||
<TextInput keyboardType='numeric' />
|
||||
<TextInput keyboardType='tel' />
|
||||
<TextInput keyboardType='url' />
|
||||
<TextInput keyboardType='search' />
|
||||
<TextInput defaultValue='default value' multiline />
|
||||
<TextInput keyboardType='phone-pad' />
|
||||
<TextInput keyboardType='url' selectTextOnFocus />
|
||||
<TextInput
|
||||
defaultValue='default value'
|
||||
maxNumberOfLines={10}
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
/>
|
||||
|
||||
<Heading level='2' size='large'>Touchable</Heading>
|
||||
<Heading size='large'>Touchable</Heading>
|
||||
<Touchable
|
||||
accessibilityLabel={'Touchable element'}
|
||||
activeHighlight='lightblue'
|
||||
activeOpacity={0.8}
|
||||
onLongPress={(e) => { console.log('Touchable.onLongPress', e) }}
|
||||
@@ -148,8 +121,8 @@ class Example extends 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 (
|
||||
@@ -160,7 +133,7 @@ class Example extends 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 (
|
||||
@@ -171,13 +144,13 @@ class Example extends 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}
|
||||
@@ -185,17 +158,70 @@ class Example extends 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
fontFamily: 'sans-serif',
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto'
|
||||
common: {
|
||||
marginVertical: 0,
|
||||
marginHorizontal: 'auto'
|
||||
},
|
||||
mqSmall: {
|
||||
maxWidth: '400px'
|
||||
},
|
||||
mqLarge: {
|
||||
maxWidth: '600px'
|
||||
}
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -205,7 +231,10 @@ const styles = {
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
borderWidth: '1px'
|
||||
borderWidth: 1
|
||||
},
|
||||
horizontalBox: {
|
||||
width: '50px'
|
||||
},
|
||||
boxFull: {
|
||||
width: '100%'
|
||||
@@ -213,7 +242,7 @@ const styles = {
|
||||
pointerEventsBox: {
|
||||
alignItems: 'center',
|
||||
borderWidth: '1px',
|
||||
flexGrow: '1',
|
||||
flexGrow: 1,
|
||||
height: '100px',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
@@ -222,7 +251,14 @@ const styles = {
|
||||
borderWidth: 1,
|
||||
height: '200px',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
scrollViewContainer: {
|
||||
height: '200px'
|
||||
},
|
||||
scrollViewStyle: {
|
||||
borderWidth: '1px'
|
||||
},
|
||||
scrollViewContentContainerStyle: {
|
||||
padding: '10px'
|
||||
}
|
||||
}
|
||||
|
||||
React.render(<Example />, document.getElementById('react-root'))
|
||||
})
|
||||
67
examples/components/GridView.js
Normal file
67
examples/components/GridView.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { StyleSheet, View } from '../../src'
|
||||
|
||||
const { Component, PropTypes } = React
|
||||
|
||||
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,
|
||||
margin: `0 calc(-0.5 * ${alley})`,
|
||||
padding: `0 ${gutter}`
|
||||
}
|
||||
|
||||
const newChildren = React.Children.map(children, (child) => {
|
||||
return child && React.cloneElement(child, {
|
||||
style: {
|
||||
...child.props.style,
|
||||
...styles.column,
|
||||
margin: `0 calc(0.5 * ${alley})`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='GridView' {...other} style={rootStyle}>
|
||||
<View style={contentContainerStyle}>
|
||||
{newChildren}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
31
examples/components/Heading.js
Normal file
31
examples/components/Heading.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text } from '../../src'
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const Heading = ({ children, size = 'normal' }) => (
|
||||
<Text
|
||||
accessibilityRole='heading'
|
||||
children={children}
|
||||
style={headingStyles.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.media}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaQueryWidget
|
||||
9
examples/index.html
Normal file
9
examples/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<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="/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()
|
||||
85
package.json
85
package.json
@@ -1,57 +1,70 @@
|
||||
{
|
||||
"name": "react-native-web",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.10",
|
||||
"description": "React Native for Web",
|
||||
"main": "dist/react-native-web.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublish": "NODE_ENV=production npm run build",
|
||||
"build": "rm -rf ./dist && webpack --config config/webpack.config.js --sort-assets-by --progress",
|
||||
"example": "cd example && webpack --config webpack.config.js",
|
||||
"lint": "eslint .",
|
||||
"specs": "NODE_ENV=test karma start config/karma.config.js",
|
||||
"specs:watch": "npm run specs -- --no-single-run",
|
||||
"start": "webpack-dev-server --config config/webpack.config.js --inline --hot --colors --quiet",
|
||||
"test": "npm run specs && npm run lint"
|
||||
"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 examples src",
|
||||
"prepublish": "NODE_ENV=publish 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": {
|
||||
"react": "^0.13.3",
|
||||
"react-swipeable": "^3.0.2",
|
||||
"react-tappable": "^0.6.0"
|
||||
"inline-style-prefixer": "^0.5.3",
|
||||
"lodash.debounce": "^3.1.1",
|
||||
"react-tappable": "^0.7.1",
|
||||
"react-textarea-autosize": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer-core": "^5.2.1",
|
||||
"babel-core": "^5.8.23",
|
||||
"babel-eslint": "^4.1.1",
|
||||
"babel-loader": "^5.3.2",
|
||||
"babel-runtime": "^5.8.20",
|
||||
"css-loader": "^0.17.0",
|
||||
"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",
|
||||
"extract-text-webpack-plugin": "^0.8.2",
|
||||
"karma": "^0.13.9",
|
||||
"karma-chrome-launcher": "^0.2.0",
|
||||
"karma-firefox-launcher": "^0.1.6",
|
||||
"karma-mocha": "^0.2.0",
|
||||
"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",
|
||||
"postcss-loader": "^0.4.4",
|
||||
"style-loader": "^0.12.3",
|
||||
"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",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/necolas/react-native-web.git"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"react"
|
||||
],
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-component",
|
||||
"react-native",
|
||||
"web"
|
||||
]
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,71 @@
|
||||
import CoreComponent from './modules/CoreComponent'
|
||||
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',
|
||||
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,
|
||||
type: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
component: 'div'
|
||||
}
|
||||
|
||||
static stylePropTypes = StylePropTypes;
|
||||
|
||||
render() {
|
||||
const {
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CoreComponent
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import restyle from './restyle'
|
||||
import stylePropTypes from './stylePropTypes'
|
||||
|
||||
class CoreComponent extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
component: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string
|
||||
]),
|
||||
style: PropTypes.object,
|
||||
testID: PropTypes.string
|
||||
}
|
||||
|
||||
static stylePropTypes = stylePropTypes;
|
||||
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
component: 'div'
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
component: Component,
|
||||
style,
|
||||
testID,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...other}
|
||||
{...restyle({ className, style })}
|
||||
data-testid={process.env.NODE_ENV === 'production' ? null : testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CoreComponent
|
||||
@@ -1,47 +0,0 @@
|
||||
export default function prefixStyles(style) {
|
||||
if (style.hasOwnProperty('flexBasis')) {
|
||||
style = {
|
||||
WebkitFlexBasis: style.flexBasis,
|
||||
msFlexBasis: style.flexBasis,
|
||||
...style
|
||||
}
|
||||
}
|
||||
|
||||
if (style.hasOwnProperty('flexGrow')) {
|
||||
style = {
|
||||
WebkitBoxFlex: style.flexGrow,
|
||||
WebkitFlexGrow: style.flexGrow,
|
||||
msFlexPositive: style.flexGrow,
|
||||
...style
|
||||
}
|
||||
}
|
||||
|
||||
if (style.hasOwnProperty('flexShrink')) {
|
||||
style = {
|
||||
WebkitFlexShrink: style.flexShrink,
|
||||
msFlexNegative: style.flexShrink,
|
||||
...style
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: adding `;` to the string value prevents React from automatically
|
||||
// adding a `px` suffix to the unitless value
|
||||
if (style.hasOwnProperty('order')) {
|
||||
style = {
|
||||
WebkitBoxOrdinalGroup: `${parseInt(style.order, 10) + 1};`,
|
||||
WebkitOrder: `${style.order}`,
|
||||
msFlexOrder: `${style.order}`,
|
||||
...style
|
||||
}
|
||||
}
|
||||
|
||||
if (style.hasOwnProperty('transform')) {
|
||||
style = {
|
||||
WebkitTransform: style.transform,
|
||||
msTransform: style.transform,
|
||||
...style
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import autoprefix from './autoprefix'
|
||||
import styles from '../../../modules/styles'
|
||||
|
||||
/**
|
||||
* Get the HTML class that corresponds to a style declaration
|
||||
* @param prop {string} prop name
|
||||
* @param style {Object} style
|
||||
* @return {string} class name
|
||||
*/
|
||||
function getSinglePurposeClassName(prop, style) {
|
||||
const className = `${prop}-${style[prop]}`
|
||||
if (style.hasOwnProperty(prop) && styles[className]) {
|
||||
return styles[className]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace inline styles with single purpose classes where possible
|
||||
* @param props {Object} React Element properties
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function stylingStrategy(props) {
|
||||
let className
|
||||
let style = {}
|
||||
|
||||
const classList = [ props.className ]
|
||||
for (const prop in props.style) {
|
||||
const styleClass = getSinglePurposeClassName(prop, props.style)
|
||||
if (styleClass) {
|
||||
classList.push(styleClass)
|
||||
} else {
|
||||
style[prop] = props.style[prop]
|
||||
}
|
||||
}
|
||||
|
||||
className = classList.join(' ')
|
||||
style = autoprefix(style)
|
||||
|
||||
return { className: className, style }
|
||||
}
|
||||
77
src/components/Image/__tests__/index-test.js
Normal file
77
src/components/Image/__tests__/index-test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
import Image from '../'
|
||||
|
||||
suite('components/Image', () => {
|
||||
test('default accessibility', () => {
|
||||
const dom = utils.renderToDOM(<Image />)
|
||||
assert.equal(dom.getAttribute('role'), 'img')
|
||||
})
|
||||
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Image accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
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 = utils.renderToDOM(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = dom.style.backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
|
||||
})
|
||||
|
||||
test('prop "onError"', function (done) {
|
||||
this.timeout(5000)
|
||||
utils.render(<Image
|
||||
onError={onError}
|
||||
source={{ uri: 'https://google.com/favicon.icox' }}
|
||||
/>)
|
||||
function onError(e) {
|
||||
assert.equal(e.nativeEvent.type, 'error')
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onLoad"', function (done) {
|
||||
this.timeout(5000)
|
||||
utils.render(<Image
|
||||
onLoad={onLoad}
|
||||
source={{ uri: 'https://google.com/favicon.ico' }}
|
||||
/>)
|
||||
function onLoad(e) {
|
||||
assert.equal(e.nativeEvent.type, 'load')
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onLoadEnd"')
|
||||
|
||||
test('prop "onLoadStart"')
|
||||
|
||||
test('prop "resizeMode"')
|
||||
|
||||
test('prop "source"')
|
||||
|
||||
test('prop "style"', () => {
|
||||
utils.assertProps.style(Image)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<Image testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
/* global window */
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import ImageStylePropTypes from './ImageStylePropTypes'
|
||||
import React, { PropTypes } from 'react'
|
||||
@@ -11,11 +12,14 @@ const STATUS_LOADING = 'LOADING'
|
||||
const STATUS_PENDING = 'PENDING'
|
||||
const STATUS_IDLE = 'IDLE'
|
||||
|
||||
const styles = {
|
||||
const imageStyleKeys = Object.keys(ImageStylePropTypes)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover'
|
||||
},
|
||||
img: {
|
||||
@@ -33,42 +37,42 @@ const styles = {
|
||||
top: 0
|
||||
},
|
||||
resizeMode: {
|
||||
clip: {
|
||||
backgroundSize: 'auto'
|
||||
},
|
||||
contain: {
|
||||
backgroundSize: 'contain'
|
||||
},
|
||||
cover: {
|
||||
backgroundSize: 'cover'
|
||||
},
|
||||
none: {
|
||||
backgroundSize: 'auto'
|
||||
},
|
||||
stretch: {
|
||||
backgroundSize: '100% 100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
defaultSource: PropTypes.object,
|
||||
onError: PropTypes.func,
|
||||
onLoad: PropTypes.func,
|
||||
onLoadEnd: PropTypes.func,
|
||||
onLoadStart: PropTypes.func,
|
||||
resizeMode: PropTypes.oneOf(['clip', 'contain', 'cover', 'stretch']),
|
||||
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
|
||||
source: PropTypes.object,
|
||||
style: PropTypes.shape(ImageStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
@@ -77,17 +81,13 @@ class Image extends React.Component {
|
||||
static stylePropTypes = ImageStylePropTypes
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
defaultSource: {},
|
||||
resizeMode: 'cover',
|
||||
source: {},
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
_cancelEvent(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
_createImageLoader() {
|
||||
const { source } = this.props
|
||||
|
||||
@@ -101,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
|
||||
}
|
||||
}
|
||||
@@ -113,8 +113,8 @@ class Image extends React.Component {
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.setState({ status: STATUS_ERRORED })
|
||||
if (onError) onError(event)
|
||||
this._onLoadEnd()
|
||||
if (onError) onError(event)
|
||||
}
|
||||
|
||||
_onLoad(e) {
|
||||
@@ -165,6 +165,7 @@ class Image extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
accessibilityLabel,
|
||||
accessible,
|
||||
children,
|
||||
defaultSource,
|
||||
resizeMode,
|
||||
@@ -176,7 +177,7 @@ class Image extends React.Component {
|
||||
const isLoaded = this.state.status === STATUS_LOADED
|
||||
const defaultImage = defaultSource.uri || null
|
||||
const displayImage = !isLoaded ? defaultImage : source.uri
|
||||
const resolvedStyle = pickProps(style, Object.keys(ImageStylePropTypes))
|
||||
const resolvedStyle = pickProps(style, imageStyleKeys)
|
||||
const backgroundImage = displayImage ? `url("${displayImage}")` : null
|
||||
|
||||
/**
|
||||
@@ -188,15 +189,15 @@ class Image extends React.Component {
|
||||
*/
|
||||
return (
|
||||
<View
|
||||
_className='Image'
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
aria-role='img'
|
||||
className={'Image'}
|
||||
component='div'
|
||||
accessibilityRole='img'
|
||||
accessible={accessible}
|
||||
style={{
|
||||
...(styles.initial),
|
||||
...styles.initial,
|
||||
...resolvedStyle,
|
||||
...(backgroundImage && { backgroundImage }),
|
||||
...(styles.resizeMode[resizeMode])
|
||||
...styles.resizeMode[resizeMode]
|
||||
}}
|
||||
testID={testID}
|
||||
>
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import Image from '.'
|
||||
import View from '../View'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
function render(component, node) {
|
||||
return node ? React.render(component, node) : ReactTestUtils.renderIntoDocument(component)
|
||||
}
|
||||
|
||||
function getImageDOM(props) {
|
||||
const result = ReactTestUtils.renderIntoDocument(<Image {...props} />)
|
||||
return React.findDOMNode(result)
|
||||
}
|
||||
|
||||
suite('Image', () => {
|
||||
test('defaults', () => {
|
||||
const result = shallowRender(<Image />)
|
||||
assert.equal(result.type, View)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const element = getImageDOM()
|
||||
const elementHasLabel = getImageDOM({ accessibilityLabel })
|
||||
|
||||
assert.equal(element.getAttribute('aria-label'), null)
|
||||
assert.equal(elementHasLabel.getAttribute('aria-label'), accessibilityLabel)
|
||||
})
|
||||
|
||||
test.skip('prop "children"', () => { })
|
||||
|
||||
test('prop "defaultSource"', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' }
|
||||
const elementHasdefaultSource = getImageDOM({ defaultSource })
|
||||
const backgroundImage = elementHasdefaultSource.style.backgroundImage
|
||||
|
||||
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
|
||||
})
|
||||
|
||||
test('prop "onError"', function (done) {
|
||||
this.timeout(5000)
|
||||
|
||||
function onError(e) {
|
||||
assert.equal(e.nativeEvent.type, 'error')
|
||||
done()
|
||||
}
|
||||
|
||||
render(<Image
|
||||
onError={onError}
|
||||
source={{ uri: 'https://google.com/favicon.icox' }}
|
||||
/>)
|
||||
})
|
||||
|
||||
test('prop "onLoad"', function (done) {
|
||||
this.timeout(5000)
|
||||
|
||||
function onLoad(e) {
|
||||
assert.equal(e.nativeEvent.type, 'load')
|
||||
done()
|
||||
}
|
||||
|
||||
render(<Image
|
||||
onLoad={onLoad}
|
||||
source={{ uri: 'https://google.com/favicon.ico' }}
|
||||
/>)
|
||||
})
|
||||
|
||||
test.skip('prop "onLoadEnd"', () => { })
|
||||
|
||||
test.skip('prop "onLoadStart"', () => { })
|
||||
|
||||
test.skip('prop "resizeMode"', () => { })
|
||||
|
||||
test.skip('prop "source"', () => { })
|
||||
|
||||
test('prop "style"', () => {
|
||||
const initial = shallowRender(<Image />)
|
||||
assert.deepEqual(
|
||||
initial.props.style,
|
||||
Image.defaultProps.style
|
||||
)
|
||||
|
||||
const unsupported = shallowRender(<Image style={{ unsupported: 'true' }} />)
|
||||
assert.deepEqual(
|
||||
unsupported.props.style.unsupported,
|
||||
null,
|
||||
'unsupported "style" properties must not be transferred'
|
||||
)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'Example.image'
|
||||
const elementHasTestID = getImageDOM({ testID })
|
||||
|
||||
assert.equal(
|
||||
elementHasTestID.getAttribute('data-testid'),
|
||||
testID
|
||||
)
|
||||
})
|
||||
})
|
||||
1
src/components/ListView/__tests__/index-test.js
Normal file
1
src/components/ListView/__tests__/index-test.js
Normal file
@@ -0,0 +1 @@
|
||||
/* eslint-env mocha */
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import ListView from '.'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
suite.skip('ListView', () => {
|
||||
test('prop "children"', () => {})
|
||||
})
|
||||
*/
|
||||
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)
|
||||
}
|
||||
11
src/components/ScrollView/__tests__/index-test.js
Normal file
11
src/components/ScrollView/__tests__/index-test.js
Normal file
@@ -0,0 +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,18 +0,0 @@
|
||||
/*
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import ScrollView from '.'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
suite.skip('ScrollView', () => {
|
||||
test('prop "children"', () => {})
|
||||
})
|
||||
*/
|
||||
@@ -1,2 +0,0 @@
|
||||
import Swipeable from 'react-swipeable'
|
||||
export default Swipeable
|
||||
@@ -13,11 +13,15 @@ export default {
|
||||
'letterSpacing',
|
||||
'lineHeight',
|
||||
'margin',
|
||||
'marginHorizontal',
|
||||
'marginVertical',
|
||||
'marginBottom',
|
||||
'marginLeft',
|
||||
'marginRight',
|
||||
'marginTop',
|
||||
'padding',
|
||||
'paddingHorizontal',
|
||||
'paddingVertical',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
'paddingRight',
|
||||
|
||||
55
src/components/Text/__tests__/index-test.js
Normal file
55
src/components/Text/__tests__/index-test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
import Text from '../'
|
||||
|
||||
suite('components/Text', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
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"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Text accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = utils.shallowRender(<Text>{children}</Text>)
|
||||
assert.equal(result.props.children, children)
|
||||
})
|
||||
|
||||
test('prop "numberOfLines"')
|
||||
|
||||
test('prop "onPress"', (done) => {
|
||||
const dom = utils.renderToDOM(<Text onPress={onPress} />)
|
||||
ReactTestUtils.Simulate.click(dom)
|
||||
function onPress(e) {
|
||||
assert.ok(e.nativeEvent)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
utils.assertProps.style(Text)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<Text testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,19 @@
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { PropTypes } from 'react'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
import TextStylePropTypes from './TextStylePropTypes'
|
||||
|
||||
const styles = {
|
||||
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: {
|
||||
@@ -18,12 +22,15 @@ const styles = {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class Text extends React.Component {
|
||||
static propTypes = {
|
||||
_className: PropTypes.string, // escape-hatch for code migrations
|
||||
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),
|
||||
@@ -33,7 +40,8 @@ class Text extends React.Component {
|
||||
static stylePropTypes = TextStylePropTypes
|
||||
|
||||
static defaultProps = {
|
||||
component: 'span',
|
||||
_className: '',
|
||||
accessible: true,
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
@@ -42,21 +50,28 @@ class Text extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, component, numberOfLines, style, testID } = this.props
|
||||
const resolvedStyle = pickProps(style, Object.keys(TextStylePropTypes))
|
||||
const {
|
||||
_className,
|
||||
numberOfLines,
|
||||
onPress,
|
||||
style,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
const className = `Text ${_className}`.trim()
|
||||
const resolvedStyle = pickProps(style, textStyleKeys)
|
||||
|
||||
return (
|
||||
<CoreComponent
|
||||
children={children}
|
||||
className={'Text'}
|
||||
component={component}
|
||||
{...other}
|
||||
className={className}
|
||||
component='span'
|
||||
onClick={this._onPress.bind(this)}
|
||||
style={{
|
||||
...(styles.initial),
|
||||
...styles.initial,
|
||||
...resolvedStyle,
|
||||
...(numberOfLines === 1 && styles.singleLineStyle)
|
||||
}}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import Text from '.'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
suite('Text', () => {
|
||||
test('defaults', () => {
|
||||
const result = ReactTestUtils.renderIntoDocument(<Text />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal((root.tagName).toLowerCase(), 'span')
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = shallowRender(<Text>{children}</Text>)
|
||||
|
||||
assert.equal(result.props.children, children)
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
const type = 'a'
|
||||
const result = ReactTestUtils.renderIntoDocument(<Text component={type} />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal(
|
||||
(root.tagName).toLowerCase(),
|
||||
type,
|
||||
'"component" did not produce the correct DOM node type'
|
||||
)
|
||||
})
|
||||
|
||||
test.skip('prop "numberOfLines"', () => {})
|
||||
|
||||
test('prop "onPress"', (done) => {
|
||||
const result = ReactTestUtils.renderIntoDocument(<Text onPress={onPress} />)
|
||||
const root = React.findDOMNode(result)
|
||||
ReactTestUtils.Simulate.click(root)
|
||||
|
||||
function onPress(e) {
|
||||
assert(true, 'the "onPress" callback was never called')
|
||||
assert.ok(e.nativeEvent)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
const initial = shallowRender(<Text />)
|
||||
assert.deepEqual(
|
||||
initial.props.style,
|
||||
Text.defaultProps.style
|
||||
)
|
||||
|
||||
const unsupported = shallowRender(<Text style={{ flexDirection: 'row' }} />)
|
||||
assert.deepEqual(
|
||||
unsupported.props.style.flexDirection,
|
||||
null,
|
||||
'unsupported "style" properties must not be transferred'
|
||||
)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'Example.text'
|
||||
const result = ReactTestUtils.renderIntoDocument(<Text testID={testID} />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal(
|
||||
root.getAttribute('data-testid'),
|
||||
testID
|
||||
)
|
||||
})
|
||||
})
|
||||
224
src/components/TextInput/__tests__/index-test.js
Normal file
224
src/components/TextInput/__tests__/index-test.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
import TextInput from '../'
|
||||
|
||||
suite('components/TextInput', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
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)
|
||||
// on
|
||||
dom = utils.renderToDOM(<TextInput autoComplete />)
|
||||
assert.equal(dom.getAttribute('autocomplete'), 'on')
|
||||
})
|
||||
|
||||
test('prop "autoFocus"', () => {
|
||||
// false
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.deepEqual(document.activeElement, document.body)
|
||||
// true
|
||||
dom = utils.renderToDOM(<TextInput autoFocus />)
|
||||
assert.deepEqual(document.activeElement, dom)
|
||||
})
|
||||
|
||||
utils.testIfDocumentFocused('prop "clearTextOnFocus"', () => {
|
||||
const defaultValue = 'defaultValue'
|
||||
// false
|
||||
let dom = utils.renderAndInject(<TextInput defaultValue={defaultValue} />)
|
||||
dom.focus()
|
||||
assert.equal(dom.value, defaultValue)
|
||||
// true
|
||||
dom = utils.renderAndInject(<TextInput clearTextOnFocus defaultValue={defaultValue} />)
|
||||
dom.focus()
|
||||
assert.equal(dom.value, '')
|
||||
})
|
||||
|
||||
test('prop "defaultValue"', () => {
|
||||
const defaultValue = 'defaultValue'
|
||||
const result = utils.shallowRender(<TextInput defaultValue={defaultValue} />)
|
||||
assert.equal(result.props.defaultValue, defaultValue)
|
||||
})
|
||||
|
||||
test('prop "editable"', () => {
|
||||
// true
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.getAttribute('readonly'), undefined)
|
||||
// false
|
||||
dom = utils.renderToDOM(<TextInput editable={false} />)
|
||||
assert.equal(dom.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)
|
||||
// email-address
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='email-address' />)
|
||||
assert.equal(dom.getAttribute('type'), 'email')
|
||||
// numeric
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='numeric' />)
|
||||
assert.equal(dom.getAttribute('type'), 'number')
|
||||
// phone-pad
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='phone-pad' />)
|
||||
assert.equal(dom.getAttribute('type'), 'tel')
|
||||
// url
|
||||
dom = utils.renderToDOM(<TextInput keyboardType='url' />)
|
||||
assert.equal(dom.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')
|
||||
})
|
||||
|
||||
test('prop "maxNumberOfLines"', () => {
|
||||
const style = { borderWidth: 0, fontSize: 20, lineHeight: 1 }
|
||||
const generateValue = () => {
|
||||
let str = ''
|
||||
while (str.length < 100) str += 'x'
|
||||
return str
|
||||
}
|
||||
|
||||
let dom = utils.renderAndInject(
|
||||
<TextInput
|
||||
maxNumberOfLines={3}
|
||||
multiline
|
||||
style={style}
|
||||
value={generateValue()}
|
||||
/>
|
||||
)
|
||||
const height = dom.getBoundingClientRect().height
|
||||
// need a range because of cross-browser differences
|
||||
assert.ok(height >= 60, height)
|
||||
assert.ok(height <= 66, height)
|
||||
})
|
||||
|
||||
test('prop "multiline"', () => {
|
||||
// false
|
||||
let dom = utils.renderToDOM(<TextInput />)
|
||||
assert.equal(dom.tagName, 'INPUT')
|
||||
// true
|
||||
dom = utils.renderToDOM(<TextInput multiline />)
|
||||
assert.equal(dom.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')
|
||||
// with multiline
|
||||
dom = utils.renderAndInject(<TextInput multiline numberOfLines={2} style={style} />)
|
||||
assert.equal(dom.tagName, 'TEXTAREA')
|
||||
const height = dom.getBoundingClientRect().height
|
||||
// need a range because of cross-browser differences
|
||||
assert.ok(height >= 40)
|
||||
assert.ok(height <= 46)
|
||||
})
|
||||
|
||||
test('prop "onBlur"', (done) => {
|
||||
const input = utils.renderToDOM(<TextInput onBlur={onBlur} />)
|
||||
ReactTestUtils.Simulate.blur(input)
|
||||
function onBlur(e) {
|
||||
assert.ok(e)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onChange"', (done) => {
|
||||
const input = utils.renderToDOM(<TextInput onChange={onChange} />)
|
||||
ReactTestUtils.Simulate.change(input)
|
||||
function onChange(e) {
|
||||
assert.ok(e)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onChangeText"', (done) => {
|
||||
const newText = 'newText'
|
||||
const input = utils.renderToDOM(<TextInput onChangeText={onChangeText} />)
|
||||
ReactTestUtils.Simulate.change(input, { target: { value: newText } })
|
||||
function onChangeText(text) {
|
||||
assert.equal(text, newText)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onFocus"', (done) => {
|
||||
const input = utils.renderToDOM(<TextInput onFocus={onFocus} />)
|
||||
ReactTestUtils.Simulate.focus(input)
|
||||
function onFocus(e) {
|
||||
assert.ok(e)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "onLayout"')
|
||||
|
||||
test('prop "onSelectionChange"', (done) => {
|
||||
const input = utils.renderAndInject(<TextInput defaultValue='12345' onSelectionChange={onSelectionChange} />)
|
||||
ReactTestUtils.Simulate.select(input, { target: { selectionStart: 0, selectionEnd: 3 } })
|
||||
function onSelectionChange(e) {
|
||||
assert.equal(e.selectionEnd, 3)
|
||||
assert.equal(e.selectionStart, 0)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "placeholder"')
|
||||
|
||||
test('prop "placeholderTextColor"')
|
||||
|
||||
test('prop "secureTextEntry"', () => {
|
||||
let dom = utils.renderToDOM(<TextInput secureTextEntry />)
|
||||
assert.equal(dom.getAttribute('type'), 'password')
|
||||
// ignored for multiline
|
||||
dom = utils.renderToDOM(<TextInput multiline secureTextEntry />)
|
||||
assert.equal(dom.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)
|
||||
// true
|
||||
dom = utils.renderAndInject(<TextInput defaultValue={text} selectTextOnFocus />)
|
||||
dom.focus()
|
||||
assert.equal(dom.selectionEnd, 4)
|
||||
assert.equal(dom.selectionStart, 0)
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
utils.assertProps.style(TextInput)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -1,96 +1,178 @@
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import TextInputStylePropTypes from './TextInputStylePropTypes'
|
||||
|
||||
const styles = {
|
||||
const textInputStyleKeys = Object.keys(TextInputStylePropTypes)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
appearance: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'black',
|
||||
borderWidth: '1px',
|
||||
boxSizing: 'border-box',
|
||||
color: 'inherit',
|
||||
font: 'inherit'
|
||||
font: 'inherit',
|
||||
padding: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class TextInput extends React.Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
autoComplete: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
clearTextOnFocus: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
keyboardType: PropTypes.oneOf(['default', 'email', 'numeric', 'search', 'tel', 'url']),
|
||||
keyboardType: PropTypes.oneOf(['default', 'email-address', 'numeric', 'phone-pad', 'url']),
|
||||
maxLength: PropTypes.number,
|
||||
maxNumberOfLines: PropTypes.number,
|
||||
multiline: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onChangeText: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onSelectionChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
placeholderTextColor: PropTypes.string,
|
||||
secureTextEntry: PropTypes.bool,
|
||||
selectTextOnFocus: PropTypes.bool,
|
||||
style: PropTypes.shape(TextInputStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
testID: CoreComponent.propTypes.testID,
|
||||
value: PropTypes.string
|
||||
}
|
||||
|
||||
static stylePropTypes = TextInputStylePropTypes
|
||||
|
||||
static defaultProps = {
|
||||
autoComplete: false,
|
||||
autoFocus: false,
|
||||
editable: true,
|
||||
keyboardType: 'default',
|
||||
multiline: false,
|
||||
numberOfLines: 2,
|
||||
secureTextEntry: false,
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
_onBlur(e) {
|
||||
if (this.props.onBlur) this.props.onBlur(e)
|
||||
const { onBlur } = this.props
|
||||
if (onBlur) onBlur(e)
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
if (this.props.onChangeText) this.props.onChangeText(e.target.value)
|
||||
if (this.props.onChange) this.props.onChange(e)
|
||||
const { onChange, onChangeText } = this.props
|
||||
if (onChangeText) onChangeText(e.target.value)
|
||||
if (onChange) onChange(e)
|
||||
}
|
||||
|
||||
_onFocus(e) {
|
||||
if (this.props.onFocus) this.props.onFocus(e)
|
||||
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
|
||||
const node = ReactDOM.findDOMNode(this)
|
||||
if (clearTextOnFocus) node.value = ''
|
||||
if (selectTextOnFocus) node.select()
|
||||
if (onFocus) onFocus(e)
|
||||
}
|
||||
|
||||
_onSelectionChange(e) {
|
||||
const { onSelectionChange } = this.props
|
||||
const { selectionDirection, selectionEnd, selectionStart } = e.target
|
||||
const event = {
|
||||
selectionDirection,
|
||||
selectionEnd,
|
||||
selectionStart,
|
||||
nativeEvent: e.nativeEvent
|
||||
}
|
||||
if (onSelectionChange) onSelectionChange(event)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
accessibilityLabel,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
editable,
|
||||
keyboardType,
|
||||
maxLength,
|
||||
maxNumberOfLines,
|
||||
multiline,
|
||||
numberOfLines,
|
||||
onBlur,
|
||||
onChange,
|
||||
onChangeText,
|
||||
onSelectionChange,
|
||||
placeholder,
|
||||
secureTextEntry,
|
||||
style,
|
||||
testID
|
||||
testID,
|
||||
value
|
||||
} = this.props
|
||||
|
||||
const resolvedStyle = pickProps(style, Object.keys(TextInputStylePropTypes))
|
||||
const type = secureTextEntry && 'password' || (keyboardType === 'default' ? '' : keyboardType)
|
||||
const resolvedStyle = pickProps(style, textInputStyleKeys)
|
||||
let type
|
||||
|
||||
switch (keyboardType) {
|
||||
case 'email-address':
|
||||
type = 'email'
|
||||
break
|
||||
case 'numeric':
|
||||
type = 'number'
|
||||
break
|
||||
case 'phone-pad':
|
||||
type = 'tel'
|
||||
break
|
||||
case 'url':
|
||||
type = 'url'
|
||||
break
|
||||
}
|
||||
|
||||
if (secureTextEntry) {
|
||||
type = 'password'
|
||||
}
|
||||
|
||||
const propsCommon = {
|
||||
accessibilityLabel,
|
||||
autoComplete: autoComplete && 'on',
|
||||
autoFocus,
|
||||
className: 'TextInput',
|
||||
defaultValue,
|
||||
maxLength,
|
||||
onBlur: onBlur && this._onBlur.bind(this),
|
||||
onChange: (onChange || onChangeText) && this._onChange.bind(this),
|
||||
onFocus: this._onFocus.bind(this),
|
||||
onSelect: onSelectionChange && this._onSelectionChange.bind(this),
|
||||
placeholder,
|
||||
readOnly: !editable,
|
||||
style: {
|
||||
...styles.initial,
|
||||
...resolvedStyle
|
||||
},
|
||||
testID,
|
||||
value
|
||||
}
|
||||
|
||||
const propsMultiline = {
|
||||
...propsCommon,
|
||||
component: TextareaAutosize,
|
||||
maxRows: maxNumberOfLines || numberOfLines,
|
||||
minRows: numberOfLines
|
||||
}
|
||||
|
||||
const propsSingleline = {
|
||||
...propsCommon,
|
||||
component: 'input',
|
||||
type
|
||||
}
|
||||
|
||||
const props = multiline ? propsMultiline : propsSingleline
|
||||
|
||||
return (
|
||||
<CoreComponent
|
||||
autoComplete={autoComplete}
|
||||
autoFocus={autoFocus}
|
||||
className={'TextInput'}
|
||||
component={multiline ? 'textarea' : 'input'}
|
||||
defaultValue={defaultValue || placeholder}
|
||||
onBlur={this._onBlur.bind(this)}
|
||||
onChange={this._onChange.bind(this)}
|
||||
onFocus={this._onFocus.bind(this)}
|
||||
readOnly={!editable}
|
||||
style={{
|
||||
...(styles.initial),
|
||||
...resolvedStyle
|
||||
}}
|
||||
testID={testID}
|
||||
type={multiline ? type : undefined}
|
||||
/>
|
||||
<CoreComponent {...props} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import TextInput from '.'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
suite.skip('TextInput', () => {
|
||||
test('prop "children"', () => {})
|
||||
})
|
||||
*/
|
||||
35
src/components/Touchable/__tests__/index-test.js
Normal file
35
src/components/Touchable/__tests__/index-test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
import Touchable from '../'
|
||||
|
||||
const children = <span style={{}}>children</span>
|
||||
const requiredProps = { children }
|
||||
|
||||
suite('components/Touchable', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} />)
|
||||
assert.deepEqual(result.props.children, children)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,15 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import Tappable from 'react-tappable'
|
||||
import View from '../View'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
...View.defaultProps.style,
|
||||
cursor: 'pointer',
|
||||
userSelect: undefined
|
||||
}
|
||||
})
|
||||
|
||||
class Touchable extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -7,32 +17,55 @@ class Touchable extends React.Component {
|
||||
this.state = {
|
||||
isActive: false
|
||||
}
|
||||
|
||||
this._onLongPress = this._onLongPress.bind(this)
|
||||
this._onPress = this._onPress.bind(this)
|
||||
this._onPressIn = this._onPressIn.bind(this)
|
||||
this._onPressOut = this._onPressOut.bind(this)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
activeHighlight: PropTypes.string,
|
||||
accessibilityLabel: View.propTypes.accessibilityLabel,
|
||||
accessibilityRole: View.propTypes.accessibilityRole,
|
||||
accessible: View.propTypes.accessible,
|
||||
activeOpacity: PropTypes.number,
|
||||
activeUnderlayColor: PropTypes.string,
|
||||
children: PropTypes.element,
|
||||
component: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.string
|
||||
]),
|
||||
delayLongPress: PropTypes.number,
|
||||
delayPressIn: PropTypes.number,
|
||||
delayPressOut: PropTypes.number,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
onPressIn: PropTypes.func,
|
||||
onPressOut: PropTypes.func
|
||||
onPressOut: PropTypes.func,
|
||||
style: View.propTypes.style
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
activeHighlight: 'transparent',
|
||||
accessibilityRole: 'button',
|
||||
activeOpacity: 1,
|
||||
component: 'div',
|
||||
activeUnderlayColor: 'transparent',
|
||||
delayLongPress: 1000,
|
||||
delayPressIn: 0,
|
||||
delayPressOut: 0
|
||||
delayPressOut: 0,
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
_getChildren() {
|
||||
const { activeOpacity, children } = this.props
|
||||
return React.cloneElement(React.Children.only(children), {
|
||||
style: {
|
||||
...children.props.style,
|
||||
...(this.state.isActive && { opacity: activeOpacity })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_onKeyEnter(e, callback) {
|
||||
var ENTER = 13
|
||||
if (e.keyCode === ENTER) {
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
_onLongPress(e) {
|
||||
@@ -53,33 +86,46 @@ class Touchable extends React.Component {
|
||||
if (this.props.onPressOut) this.props.onPressOut(e)
|
||||
}
|
||||
|
||||
_getChildren() {
|
||||
const { activeOpacity, children } = this.props
|
||||
return React.cloneElement(React.Children.only(children), {
|
||||
style: { ...children.props.style, ...(this.state.isActive ? { opacity: activeOpacity } : {}) }
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activeHighlight,
|
||||
component,
|
||||
delayLongPress
|
||||
accessibilityLabel,
|
||||
accessibilityRole,
|
||||
accessible,
|
||||
activeUnderlayColor,
|
||||
delayLongPress,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
/**
|
||||
* Creates a wrapping element that can receive beyboard focus. The
|
||||
* highlight is applied as a background color on this wrapper. The opacity
|
||||
* is set on the child element, allowing it to have its own background
|
||||
* color.
|
||||
*/
|
||||
return (
|
||||
<Tappable
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessible={accessible}
|
||||
children={this._getChildren()}
|
||||
component={component}
|
||||
onMouseDown={this._onPressIn.bind(this)}
|
||||
onMouseUp={this._onPressOut.bind(this)}
|
||||
onPress={this._onLongPress.bind(this)}
|
||||
onTap={this._onPress.bind(this)}
|
||||
onTouchEnd={this._onPressOut.bind(this)}
|
||||
onTouchStart={this._onPressIn.bind(this)}
|
||||
component={View}
|
||||
onKeyDown={(e) => { this._onKeyEnter(e, this._onPressIn) }}
|
||||
onKeyPress={this._onPress}
|
||||
onKeyUp={(e) => { this._onKeyEnter(e, this._onPressOut) }}
|
||||
onMouseDown={this._onPressIn}
|
||||
onMouseUp={this._onPressOut}
|
||||
onPress={this._onLongPress}
|
||||
onTap={this._onPress}
|
||||
onTouchEnd={this._onPressOut}
|
||||
onTouchStart={this._onPressIn}
|
||||
pressDelay={delayLongPress}
|
||||
pressMoveThreshold={5}
|
||||
style={{ backgroundColor: this.state.isActive ? activeHighlight : null }}
|
||||
style={{
|
||||
...styles.initial,
|
||||
...style,
|
||||
backgroundColor: this.state.isActive ? activeUnderlayColor : style.backgroundColor
|
||||
}}
|
||||
tabIndex='0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import Touchable from '.'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
suite.skip('Touchable', () => {
|
||||
test('prop "children"', () => {})
|
||||
})
|
||||
*/
|
||||
@@ -43,6 +43,7 @@ export default {
|
||||
'bottom',
|
||||
'boxShadow',
|
||||
'boxSizing',
|
||||
'cursor',
|
||||
'flexBasis',
|
||||
'flexDirection',
|
||||
'flexGrow',
|
||||
@@ -53,6 +54,8 @@ export default {
|
||||
'left',
|
||||
// margin
|
||||
'margin',
|
||||
'marginHorizontal',
|
||||
'marginVertical',
|
||||
'marginBottom',
|
||||
'marginLeft',
|
||||
'marginRight',
|
||||
@@ -69,6 +72,8 @@ export default {
|
||||
'overflowY',
|
||||
// padding
|
||||
'padding',
|
||||
'paddingHorizontal',
|
||||
'paddingVertical',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
'paddingRight',
|
||||
|
||||
54
src/components/View/__tests__/index-test.js
Normal file
54
src/components/View/__tests__/index-test.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
import View from '../'
|
||||
|
||||
suite('components/View', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<View accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityLiveRegion"', () => {
|
||||
const accessibilityLiveRegion = 'polite'
|
||||
const result = utils.shallowRender(<View accessibilityLiveRegion={accessibilityLiveRegion} />)
|
||||
assert.equal(result.props.accessibilityLiveRegion, accessibilityLiveRegion)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<View accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<View accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = utils.shallowRender(<View>{children}</View>)
|
||||
assert.equal(result.props.children, children)
|
||||
})
|
||||
|
||||
test('prop "pointerEvents"', () => {
|
||||
const result = utils.shallowRender(<View pointerEvents='box-only' />)
|
||||
assert.equal(result.props.style.pointerEvents, 'box-only')
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
utils.assertProps.style(View)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
const result = utils.shallowRender(<View testID={testID} />)
|
||||
assert.equal(result.props.testID, testID)
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,12 @@
|
||||
import { pickProps } from '../../modules/filterObjectProps'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { PropTypes } from 'react'
|
||||
import StyleSheet from '../../modules/StyleSheet'
|
||||
import ViewStylePropTypes from './ViewStylePropTypes'
|
||||
|
||||
const styles = {
|
||||
const viewStyleKeys = Object.keys(ViewStylePropTypes)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// https://github.com/facebook/css-layout#default-values
|
||||
initial: {
|
||||
alignItems: 'stretch',
|
||||
@@ -18,25 +21,24 @@ const styles = {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
textDecoration: 'none',
|
||||
// button reset
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
textAlign: 'inherit'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class View extends React.Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
_className: PropTypes.string, // escape-hatch for code migrations
|
||||
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'
|
||||
]),
|
||||
pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']),
|
||||
style: PropTypes.shape(ViewStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
}
|
||||
@@ -44,26 +46,32 @@ class View extends React.Component {
|
||||
static stylePropTypes = ViewStylePropTypes
|
||||
|
||||
static defaultProps = {
|
||||
component: 'div',
|
||||
_className: '',
|
||||
accessible: true,
|
||||
style: styles.initial
|
||||
}
|
||||
|
||||
render() {
|
||||
const { accessibilityLabel, pointerEvents, style, testID, ...other } = this.props
|
||||
const {
|
||||
_className,
|
||||
pointerEvents,
|
||||
style,
|
||||
...other
|
||||
} = this.props
|
||||
|
||||
const className = `View ${_className}`.trim()
|
||||
const pointerEventsStyle = pointerEvents && { pointerEvents }
|
||||
const resolvedStyle = pickProps(style, Object.keys(ViewStylePropTypes))
|
||||
const resolvedStyle = pickProps(style, viewStyleKeys)
|
||||
|
||||
return (
|
||||
<CoreComponent
|
||||
{...other}
|
||||
aria-label={accessibilityLabel}
|
||||
className={'View'}
|
||||
className={className}
|
||||
style={{
|
||||
...(styles.initial),
|
||||
...styles.initial,
|
||||
...resolvedStyle,
|
||||
...pointerEventsStyle
|
||||
}}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import assert from 'assert'
|
||||
import React from 'react/addons'
|
||||
|
||||
import View from '.'
|
||||
|
||||
const ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = React.addons.TestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
suite('View', () => {
|
||||
test('defaults', () => {
|
||||
const result = ReactTestUtils.renderIntoDocument(<View />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal((root.tagName).toLowerCase(), 'div')
|
||||
})
|
||||
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = ReactTestUtils.renderIntoDocument(<View accessibilityLabel={accessibilityLabel} />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal(root.getAttribute('aria-label'), accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const children = 'children'
|
||||
const result = shallowRender(<View>{children}</View>)
|
||||
|
||||
assert.equal(result.props.children, children)
|
||||
})
|
||||
|
||||
test('prop "component"', () => {
|
||||
const type = 'a'
|
||||
const result = ReactTestUtils.renderIntoDocument(<View component={type} />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal(
|
||||
(root.tagName).toLowerCase(),
|
||||
type,
|
||||
'"component" did not produce the correct DOM node type'
|
||||
)
|
||||
})
|
||||
|
||||
test('prop "pointerEvents"', () => {
|
||||
const result = shallowRender(<View pointerEvents='box-only' />)
|
||||
|
||||
assert.equal(
|
||||
result.props.style.pointerEvents,
|
||||
'box-only'
|
||||
)
|
||||
})
|
||||
|
||||
test('prop "style"', () => {
|
||||
const initial = shallowRender(<View />)
|
||||
assert.deepEqual(
|
||||
initial.props.style,
|
||||
View.defaultProps.style
|
||||
)
|
||||
|
||||
const unsupported = shallowRender(<View style={{ unsupported: 'true' }} />)
|
||||
assert.deepEqual(
|
||||
unsupported.props.style.unsupported,
|
||||
null,
|
||||
'unsupported "style" properties must not be transferred'
|
||||
)
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'Example.view'
|
||||
const result = ReactTestUtils.renderIntoDocument(<View testID={testID} />)
|
||||
const root = React.findDOMNode(result)
|
||||
|
||||
assert.equal(
|
||||
root.getAttribute('data-testid'),
|
||||
testID
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import StyleSheet from './modules/StyleSheet'
|
||||
|
||||
// components
|
||||
import Image from './components/Image'
|
||||
import ListView from './components/ListView'
|
||||
import ScrollView from './components/ScrollView'
|
||||
import Swipeable from './components/Swipeable'
|
||||
import Text from './components/Text'
|
||||
import TextInput from './components/TextInput'
|
||||
import Touchable from './components/Touchable'
|
||||
@@ -13,10 +14,12 @@ import View from './components/View'
|
||||
export default React
|
||||
|
||||
export {
|
||||
StyleSheet,
|
||||
|
||||
// components
|
||||
Image,
|
||||
ListView,
|
||||
ScrollView,
|
||||
Swipeable,
|
||||
Text,
|
||||
TextInput,
|
||||
Touchable,
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { PropTypes } from 'react'
|
||||
|
||||
const numberOrString = PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.string
|
||||
])
|
||||
|
||||
const { string } = PropTypes
|
||||
const { number, string } = PropTypes
|
||||
const numberOrString = PropTypes.oneOfType([ number, string ])
|
||||
|
||||
export default {
|
||||
alignContent: string,
|
||||
alignItems: string,
|
||||
alignSelf: string,
|
||||
appearance: string,
|
||||
backfaceVisibility: string,
|
||||
// background
|
||||
backgroundAttachment: string,
|
||||
backgroundClip: string,
|
||||
backgroundColor: string,
|
||||
@@ -21,25 +17,21 @@ export default {
|
||||
backgroundPosition: string,
|
||||
backgroundRepeat: string,
|
||||
backgroundSize: string,
|
||||
// border color
|
||||
borderColor: numberOrString,
|
||||
borderBottomColor: numberOrString,
|
||||
borderLeftColor: numberOrString,
|
||||
borderRightColor: numberOrString,
|
||||
borderTopColor: numberOrString,
|
||||
// border-radius
|
||||
borderColor: string,
|
||||
borderBottomColor: string,
|
||||
borderLeftColor: string,
|
||||
borderRightColor: string,
|
||||
borderTopColor: string,
|
||||
borderRadius: numberOrString,
|
||||
borderTopLeftRadius: numberOrString,
|
||||
borderTopRightRadius: numberOrString,
|
||||
borderBottomLeftRadius: numberOrString,
|
||||
borderBottomRightRadius: numberOrString,
|
||||
// border style
|
||||
borderStyle: numberOrString,
|
||||
borderBottomStyle: numberOrString,
|
||||
borderLeftStyle: numberOrString,
|
||||
borderRightStyle: numberOrString,
|
||||
borderTopStyle: numberOrString,
|
||||
// border width
|
||||
borderStyle: string,
|
||||
borderBottomStyle: string,
|
||||
borderLeftStyle: string,
|
||||
borderRightStyle: string,
|
||||
borderTopStyle: string,
|
||||
borderWidth: numberOrString,
|
||||
borderBottomWidth: numberOrString,
|
||||
borderLeftWidth: numberOrString,
|
||||
@@ -49,8 +41,10 @@ export default {
|
||||
boxSizing: string,
|
||||
clear: string,
|
||||
color: string,
|
||||
cursor: string,
|
||||
direction: string,
|
||||
display: string,
|
||||
flex: string,
|
||||
flexBasis: string,
|
||||
flexDirection: string,
|
||||
flexGrow: numberOrString,
|
||||
@@ -59,7 +53,7 @@ export default {
|
||||
float: string,
|
||||
font: string,
|
||||
fontFamily: string,
|
||||
fontSize: string,
|
||||
fontSize: numberOrString,
|
||||
fontStyle: string,
|
||||
fontWeight: string,
|
||||
height: numberOrString,
|
||||
@@ -67,13 +61,14 @@ export default {
|
||||
left: numberOrString,
|
||||
letterSpacing: string,
|
||||
lineHeight: numberOrString,
|
||||
// margin
|
||||
listStyle: string,
|
||||
margin: numberOrString,
|
||||
marginBottom: numberOrString,
|
||||
marginHorizontal: numberOrString,
|
||||
marginLeft: numberOrString,
|
||||
marginRight: numberOrString,
|
||||
marginTop: numberOrString,
|
||||
// min/max
|
||||
marginVertical: numberOrString,
|
||||
maxHeight: numberOrString,
|
||||
maxWidth: numberOrString,
|
||||
minHeight: numberOrString,
|
||||
@@ -83,16 +78,18 @@ export default {
|
||||
overflow: string,
|
||||
overflowX: string,
|
||||
overflowY: string,
|
||||
// padding
|
||||
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,
|
||||
99
src/modules/StyleSheet/Store.js
Normal file
99
src/modules/StyleSheet/Store.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import hyphenate from './hyphenate'
|
||||
import normalizeValue from './normalizeValue'
|
||||
import prefixer from './prefixer'
|
||||
|
||||
export default class Store {
|
||||
constructor(
|
||||
initialState:Object = {},
|
||||
options:Object = { obfuscateClassNames: false }
|
||||
) {
|
||||
this._counter = 0
|
||||
this._classNames = { ...initialState.classNames }
|
||||
this._declarations = { ...initialState.declarations }
|
||||
this._options = options
|
||||
}
|
||||
|
||||
get(property, value) {
|
||||
const normalizedValue = normalizeValue(property, value)
|
||||
const key = this._getDeclarationKey(property, normalizedValue)
|
||||
return this._classNames[key]
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
if (value != null) {
|
||||
const normalizedValue = normalizeValue(property, value)
|
||||
const values = this._getPropertyValues(property) || []
|
||||
if (values.indexOf(normalizedValue) === -1) {
|
||||
values.push(normalizedValue)
|
||||
this._setClassName(property, normalizedValue)
|
||||
this._setPropertyValues(property, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
const obfuscate = this._options.obfuscateClassNames
|
||||
|
||||
// sort the properties to ensure shorthands are first in the cascade
|
||||
const properties = Object.keys(this._declarations).sort()
|
||||
|
||||
// transform the class name to a valid CSS selector
|
||||
const getCssSelector = (property, value) => {
|
||||
let className = this.get(property, value)
|
||||
if (!obfuscate && className) {
|
||||
className = className.replace(/[:?.%\\$#]/g, '\\$&')
|
||||
}
|
||||
return className
|
||||
}
|
||||
|
||||
// transform the declarations into CSS rules with vendor-prefixes
|
||||
const buildCSSRules = (property, values) => {
|
||||
return values.reduce((cssRules, value) => {
|
||||
const declarations = prefixer.prefix({ [property]: value })
|
||||
const cssDeclarations = Object.keys(declarations).reduce((str, prop) => {
|
||||
str += `${hyphenate(prop)}:${value};`
|
||||
return str
|
||||
}, '')
|
||||
const selector = getCssSelector(property, value)
|
||||
|
||||
cssRules += `\n.${selector}{${cssDeclarations}}`
|
||||
|
||||
return cssRules
|
||||
}, '')
|
||||
}
|
||||
|
||||
const css = properties.reduce((css, property) => {
|
||||
const values = this._declarations[property]
|
||||
css += buildCSSRules(property, values)
|
||||
return css
|
||||
}, '')
|
||||
|
||||
return (`/* ${this._counter} unique declarations */${css}`)
|
||||
}
|
||||
|
||||
_getDeclarationKey(property, value) {
|
||||
return `${property}:${value}`
|
||||
}
|
||||
|
||||
_getPropertyValues(property) {
|
||||
return this._declarations[property]
|
||||
}
|
||||
|
||||
_setPropertyValues(property, values) {
|
||||
this._declarations[property] = values.map(value => normalizeValue(property, value))
|
||||
}
|
||||
|
||||
_setClassName(property, value) {
|
||||
const key = this._getDeclarationKey(property, value)
|
||||
const exists = !!this._classNames[key]
|
||||
if (!exists) {
|
||||
this._counter += 1
|
||||
if (this._options.obfuscateClassNames) {
|
||||
this._classNames[key] = `_s_${this._counter}`
|
||||
} else {
|
||||
const val = `${value}`.replace(/\s/g, '-')
|
||||
this._classNames[key] = `${property}:${val}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/modules/StyleSheet/__tests__/Store-test.js
Normal file
127
src/modules/StyleSheet/__tests__/Store-test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import Store from '../Store'
|
||||
|
||||
suite('modules/StyleSheet/Store', () => {
|
||||
suite('the constructor', () => {
|
||||
test('initialState', () => {
|
||||
const initialState = { classNames: { 'alignItems:center': '__classname__' } }
|
||||
const store = new Store(initialState)
|
||||
assert.deepEqual(store._classNames['alignItems:center'], '__classname__')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#get', () => {
|
||||
test('returns a declaration-specific className', () => {
|
||||
const initialState = {
|
||||
classNames: {
|
||||
'alignItems:center': '__expected__',
|
||||
'alignItems:flex-start': '__error__'
|
||||
}
|
||||
}
|
||||
const store = new Store(initialState)
|
||||
assert.deepEqual(store.get('alignItems', 'center'), '__expected__')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#set', () => {
|
||||
test('stores declarations', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('flexGrow', 0)
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._declarations, {
|
||||
alignItems: [ 'center' ],
|
||||
flexGrow: [ 0, 1, 2 ]
|
||||
})
|
||||
})
|
||||
|
||||
test('human-readable classNames', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('flexGrow', 0)
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'alignItems:center': 'alignItems:center',
|
||||
'flexGrow:0': 'flexGrow:0',
|
||||
'flexGrow:1': 'flexGrow:1',
|
||||
'flexGrow:2': 'flexGrow:2'
|
||||
})
|
||||
})
|
||||
|
||||
test('obfuscated classNames', () => {
|
||||
const store = new Store({}, { obfuscateClassNames: true })
|
||||
store.set('alignItems', 'center')
|
||||
store.set('flexGrow', 0)
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'alignItems:center': '_s_1',
|
||||
'flexGrow:0': '_s_2',
|
||||
'flexGrow:1': '_s_3',
|
||||
'flexGrow:2': '_s_4'
|
||||
})
|
||||
})
|
||||
|
||||
test('value normalization', () => {
|
||||
const store = new Store()
|
||||
store.set('flexGrow', 0)
|
||||
store.set('margin', 0)
|
||||
assert.deepEqual(store._declarations, {
|
||||
flexGrow: [ 0 ],
|
||||
margin: [ '0px' ]
|
||||
})
|
||||
assert.deepEqual(store._classNames, {
|
||||
'flexGrow:0': 'flexGrow:0',
|
||||
'margin:0px': 'margin:0px'
|
||||
})
|
||||
})
|
||||
|
||||
test('replaces space characters', () => {
|
||||
const store = new Store()
|
||||
store.set('margin', '0 auto')
|
||||
assert.deepEqual(store.get('margin', '0 auto'), 'margin:0-auto')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#toString', () => {
|
||||
test('human-readable style sheet', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('marginBottom', 0)
|
||||
store.set('margin', 1)
|
||||
store.set('margin', 2)
|
||||
store.set('margin', 3)
|
||||
|
||||
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;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
|
||||
test('obfuscated style sheet', () => {
|
||||
const store = new Store({}, { obfuscateClassNames: true })
|
||||
store.set('alignItems', 'center')
|
||||
store.set('marginBottom', 0)
|
||||
store.set('margin', 1)
|
||||
store.set('margin', 2)
|
||||
store.set('margin', 3)
|
||||
|
||||
const expected = '/* 5 unique declarations */\n' +
|
||||
'._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)
|
||||
})
|
||||
})
|
||||
33
src/modules/StyleSheet/__tests__/getStyleObjects-test.js
Normal file
33
src/modules/StyleSheet/__tests__/getStyleObjects-test.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import getStyleObjects from '../getStyleObjects'
|
||||
|
||||
const fixture = {
|
||||
rule: {
|
||||
margin: 0,
|
||||
padding: 0
|
||||
},
|
||||
nested: {
|
||||
auto: {
|
||||
backgroundSize: 'auto'
|
||||
},
|
||||
contain: {
|
||||
backgroundSize: 'contain'
|
||||
}
|
||||
},
|
||||
ignored: {
|
||||
pading: 0
|
||||
}
|
||||
}
|
||||
|
||||
suite('modules/StyleSheet/getStyleObjects', () => {
|
||||
test('returns only style objects', () => {
|
||||
const actual = getStyleObjects(fixture)
|
||||
assert.deepEqual(actual, [
|
||||
{ margin: 0, padding: 0 },
|
||||
{ backgroundSize: 'auto' },
|
||||
{ backgroundSize: 'contain' }
|
||||
])
|
||||
})
|
||||
})
|
||||
16
src/modules/StyleSheet/__tests__/hyphenate-test.js
Normal file
16
src/modules/StyleSheet/__tests__/hyphenate-test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
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('MozTransition'), '-moz-transition')
|
||||
assert.equal(hyphenate('msTransition'), '-ms-transition')
|
||||
assert.equal(hyphenate('WebkitTransition'), '-webkit-transition')
|
||||
})
|
||||
})
|
||||
37
src/modules/StyleSheet/__tests__/index-test.js
Normal file
37
src/modules/StyleSheet/__tests__/index-test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { resetCSS, predefinedCSS } from '../predefs'
|
||||
import assert from 'assert'
|
||||
import StyleSheet from '..'
|
||||
|
||||
const styles = { root: { borderWidth: 1 } }
|
||||
|
||||
suite('modules/StyleSheet', () => {
|
||||
setup(() => {
|
||||
StyleSheet.destroy()
|
||||
})
|
||||
|
||||
test('create', () => {
|
||||
assert.equal(StyleSheet.create(styles), styles)
|
||||
})
|
||||
|
||||
test('renderToString', () => {
|
||||
StyleSheet.create(styles)
|
||||
assert.equal(
|
||||
StyleSheet.renderToString(),
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 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 borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
|
||||
StyleSheet.create(styles)
|
||||
assert.deepEqual(StyleSheet.resolve(props), expected)
|
||||
})
|
||||
})
|
||||
15
src/modules/StyleSheet/__tests__/isObject-test.js
Normal file
15
src/modules/StyleSheet/__tests__/isObject-test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import isObject from '../isObject'
|
||||
|
||||
suite('modules/StyleSheet/isObject', () => {
|
||||
test('returns "true" for objects', () => {
|
||||
assert.ok(isObject({}) === true)
|
||||
})
|
||||
test('returns "false" for non-objects', () => {
|
||||
assert.ok(isObject(function () {}) === false)
|
||||
assert.ok(isObject([]) === false)
|
||||
assert.ok(isObject('') === false)
|
||||
})
|
||||
})
|
||||
16
src/modules/StyleSheet/__tests__/isStyleObject-test.js
Normal file
16
src/modules/StyleSheet/__tests__/isStyleObject-test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import isStyleObject from '../isStyleObject'
|
||||
|
||||
const style = { margin: 0 }
|
||||
const notStyle = { root: style }
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
13
src/modules/StyleSheet/__tests__/normalizeValue-test.js
Normal file
13
src/modules/StyleSheet/__tests__/normalizeValue-test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import normalizeValue from '../normalizeValue'
|
||||
|
||||
suite('modules/StyleSheet/normalizeValue', () => {
|
||||
test('normalizes property values requiring units', () => {
|
||||
assert.deepEqual(normalizeValue('margin', 0), '0px')
|
||||
})
|
||||
test('ignores unitless property values', () => {
|
||||
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
|
||||
})
|
||||
})
|
||||
51
src/modules/StyleSheet/expandStyle.js
Normal file
51
src/modules/StyleSheet/expandStyle.js
Normal file
@@ -0,0 +1,51 @@
|
||||
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' ]
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
22
src/modules/StyleSheet/getStyleObjects.js
Normal file
22
src/modules/StyleSheet/getStyleObjects.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import isObject from './isObject'
|
||||
import isStyleObject from './isStyleObject'
|
||||
|
||||
/**
|
||||
* Recursively check for objects that are style rules.
|
||||
*/
|
||||
const getStyleObjects = (styles: Object): Array => {
|
||||
const keys = Object.keys(styles)
|
||||
return keys.reduce((rules, key) => {
|
||||
const possibleRule = styles[key]
|
||||
if (isObject(possibleRule)) {
|
||||
if (isStyleObject(possibleRule)) {
|
||||
rules.push(possibleRule)
|
||||
} else {
|
||||
rules = rules.concat(getStyleObjects(possibleRule))
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default getStyleObjects
|
||||
1
src/modules/StyleSheet/hyphenate.js
Normal file
1
src/modules/StyleSheet/hyphenate.js
Normal file
@@ -0,0 +1 @@
|
||||
export default (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')
|
||||
88
src/modules/StyleSheet/index.js
Normal file
88
src/modules/StyleSheet/index.js
Normal file
@@ -0,0 +1,88 @@
|
||||
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
|
||||
* event classes
|
||||
*/
|
||||
const initialState = { classNames: predefinedClassNames }
|
||||
const options = { obfuscateClassNames: process.env.NODE_ENV === 'production' }
|
||||
const createStore = () => new Store(initialState, options)
|
||||
let store = createStore()
|
||||
|
||||
/**
|
||||
* Process all unique declarations
|
||||
*/
|
||||
const create = (styles: Object): Object => {
|
||||
const rules = getStyleObjects(styles)
|
||||
|
||||
rules.forEach((rule) => {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy existing styles
|
||||
*/
|
||||
const destroy = () => {
|
||||
store = createStore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the styles as a CSS style sheet
|
||||
*/
|
||||
const renderToString = () => {
|
||||
const css = store.toString()
|
||||
return `${resetCSS}\n${predefinedCSS}\n${css}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts React props and converts inline styles to single purpose classes
|
||||
* where possible.
|
||||
*/
|
||||
const resolve = ({ className = '', style = {} }) => {
|
||||
let _className
|
||||
let _style = {}
|
||||
const expandedStyle = expandStyle(style)
|
||||
|
||||
const classList = [ className ]
|
||||
for (const prop in expandedStyle) {
|
||||
if (!StylePropTypes[prop]) {
|
||||
continue
|
||||
}
|
||||
let styleClass = store.get(prop, expandedStyle[prop])
|
||||
|
||||
if (styleClass) {
|
||||
classList.push(styleClass)
|
||||
} else {
|
||||
_style[prop] = expandedStyle[prop]
|
||||
}
|
||||
}
|
||||
|
||||
_className = classList.join(' ')
|
||||
_style = prefixer.prefix(_style)
|
||||
|
||||
return { className: _className, style: _style }
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
destroy,
|
||||
renderToString,
|
||||
resolve
|
||||
}
|
||||
5
src/modules/StyleSheet/isObject.js
Normal file
5
src/modules/StyleSheet/isObject.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const isObject = (obj) => {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]'
|
||||
}
|
||||
|
||||
export default isObject
|
||||
9
src/modules/StyleSheet/isStyleObject.js
Normal file
9
src/modules/StyleSheet/isStyleObject.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { pickProps } from '../filterObjectProps'
|
||||
import StylePropTypes from '../StylePropTypes'
|
||||
|
||||
const isStyleObject = (obj) => {
|
||||
const declarations = pickProps(obj, Object.keys(StylePropTypes))
|
||||
return Object.keys(declarations).length > 0
|
||||
}
|
||||
|
||||
export default isStyleObject
|
||||
33
src/modules/StyleSheet/normalizeValue.js
Normal file
33
src/modules/StyleSheet/normalizeValue.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const unitlessNumbers = {
|
||||
boxFlex: true,
|
||||
boxFlexGroup: true,
|
||||
columnCount: true,
|
||||
flex: true,
|
||||
flexGrow: true,
|
||||
flexPositive: true,
|
||||
flexShrink: true,
|
||||
flexNegative: true,
|
||||
fontWeight: true,
|
||||
lineClamp: true,
|
||||
lineHeight: true,
|
||||
opacity: true,
|
||||
order: true,
|
||||
orphans: true,
|
||||
widows: true,
|
||||
zIndex: true,
|
||||
zoom: true,
|
||||
// SVG-related
|
||||
fillOpacity: true,
|
||||
strokeDashoffset: true,
|
||||
strokeOpacity: true,
|
||||
strokeWidth: true
|
||||
}
|
||||
|
||||
const normalizeValues = (property, value) => {
|
||||
if (!unitlessNumbers[property] && typeof value === 'number') {
|
||||
value = `${value}px`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export default normalizeValues
|
||||
24
src/modules/StyleSheet/predefs.js
Normal file
24
src/modules/StyleSheet/predefs.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Reset unwanted styles beyond the control of React inline styles
|
||||
*/
|
||||
export const resetCSS =
|
||||
`/* React Native Web */
|
||||
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}`
|
||||
|
||||
/**
|
||||
* Custom pointer event styles
|
||||
*/
|
||||
export const predefinedCSS =
|
||||
`/* pointer-events */
|
||||
._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': '_s_pe-a',
|
||||
'pointerEvents:box-none': '_s_pe-bn',
|
||||
'pointerEvents:box-only': '_s_pe-bo',
|
||||
'pointerEvents:none': '_s_pe-n'
|
||||
}
|
||||
3
src/modules/StyleSheet/prefixer.js
Normal file
3
src/modules/StyleSheet/prefixer.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Prefixer from 'inline-style-prefixer'
|
||||
const prefixer = new Prefixer()
|
||||
export default prefixer
|
||||
@@ -1,15 +1,9 @@
|
||||
import { omitProps, pickProps } from '.'
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { omitProps, pickProps } from '..'
|
||||
import assert from 'assert'
|
||||
|
||||
suite('pickProps', () => {
|
||||
test('interface', () => {
|
||||
assert.throws(
|
||||
() => { pickProps({}, true) },
|
||||
TypeError,
|
||||
'pickProps should throw if the second argument is not an array'
|
||||
)
|
||||
})
|
||||
|
||||
test('return value', () => {
|
||||
const obj = { a: 1, b: 2, c: { cc: { ccc: 3 } } }
|
||||
const props = [ 'a', 'b' ]
|
||||
@@ -21,14 +15,6 @@ suite('pickProps', () => {
|
||||
})
|
||||
|
||||
suite('omitProps', () => {
|
||||
test('interface', () => {
|
||||
assert.throws(
|
||||
() => { omitProps({}, true) },
|
||||
TypeError,
|
||||
'omitProps should throw if the second argument is not an array'
|
||||
)
|
||||
})
|
||||
|
||||
test('return value', () => {
|
||||
const obj = { a: 1, b: 2, c: { cc: { ccc: 3 } } }
|
||||
const props = [ 'a', 'b' ]
|
||||
@@ -1,12 +1,8 @@
|
||||
function filterProps(obj, props, excluded = false) {
|
||||
if (!Array.isArray(props)) {
|
||||
throw new TypeError('props is not an Array')
|
||||
}
|
||||
|
||||
function filterProps(obj, propKeys: Array, excluded = false) {
|
||||
const filtered = {}
|
||||
for (const prop in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
||||
const isMatch = props.indexOf(prop) > -1
|
||||
const isMatch = propKeys.indexOf(prop) > -1
|
||||
if (excluded && isMatch) {
|
||||
continue
|
||||
} else if (!excluded && !isMatch) {
|
||||
@@ -20,10 +16,10 @@ function filterProps(obj, props, excluded = false) {
|
||||
return filtered
|
||||
}
|
||||
|
||||
export function pickProps(obj, props) {
|
||||
return filterProps(obj, props)
|
||||
export function pickProps(obj, propKeys) {
|
||||
return filterProps(obj, propKeys)
|
||||
}
|
||||
|
||||
export function omitProps(obj, props) {
|
||||
return filterProps(obj, props, true)
|
||||
export function omitProps(obj, propKeys) {
|
||||
return filterProps(obj, propKeys, true)
|
||||
}
|
||||
|
||||
74
src/modules/specHelpers/index.js
Normal file
74
src/modules/specHelpers/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
export const assertProps = {
|
||||
style: function (Component, props) {
|
||||
let shallow
|
||||
// default styles
|
||||
shallow = shallowRender(<Component {...props} />)
|
||||
assert.deepEqual(
|
||||
shallow.props.style,
|
||||
Component.defaultProps.style
|
||||
)
|
||||
// filtering of unsupported styles
|
||||
const styleToFilter = { unsupported: 'unsupported' }
|
||||
shallow = shallowRender(<Component {...props} style={styleToFilter} />)
|
||||
assert.deepEqual(
|
||||
shallow.props.style.unsupported,
|
||||
undefined,
|
||||
'unsupported "style" properties must not be transferred'
|
||||
)
|
||||
// merging of custom styles
|
||||
const styleToMerge = { margin: '10' }
|
||||
shallow = shallowRender(<Component {...props} style={styleToMerge} />)
|
||||
assert.deepEqual(
|
||||
shallow.props.style,
|
||||
{ ...Component.defaultProps.style, ...styleToMerge }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function render(element, container) {
|
||||
return container
|
||||
? ReactDOM.render(element, container)
|
||||
: ReactTestUtils.renderIntoDocument(element)
|
||||
}
|
||||
|
||||
export function renderAndInject(element) {
|
||||
const id = '_renderAndInject'
|
||||
let div = document.getElementById(id)
|
||||
|
||||
if (!div) {
|
||||
div = document.createElement('div')
|
||||
div.id = id
|
||||
document.body.appendChild(div)
|
||||
} else {
|
||||
div.innerHTML = ''
|
||||
}
|
||||
|
||||
const result = render(element, div)
|
||||
return ReactDOM.findDOMNode(result)
|
||||
}
|
||||
|
||||
export function renderToDOM(element, container) {
|
||||
const result = render(element, container)
|
||||
return ReactDOM.findDOMNode(result)
|
||||
}
|
||||
|
||||
export function shallowRender(component, context = {}) {
|
||||
const shallowRenderer = ReactTestUtils.createRenderer()
|
||||
shallowRenderer.render(component, context)
|
||||
return shallowRenderer.getRenderOutput()
|
||||
}
|
||||
|
||||
export function testIfDocumentFocused(message, fn) {
|
||||
if (document.hasFocus && document.hasFocus()) {
|
||||
test(message, fn)
|
||||
} else {
|
||||
test.skip(`${message} – WARNING: document is not focused`)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import styles from './styles.css'
|
||||
export default styles
|
||||
@@ -1,683 +0,0 @@
|
||||
/* align-content */
|
||||
|
||||
.alignContent-center { align-content: center; }
|
||||
.alignContent-flex-end { align-content: flex-end; }
|
||||
.alignContent-flex-start { align-content: flex-start; }
|
||||
.alignContent-stretch { align-content: stretch; }
|
||||
.alignContent-space-around { align-content: space-around; }
|
||||
.alignContent-space-between { align-content: space-between; }
|
||||
|
||||
/* align-items */
|
||||
|
||||
.alignItems-center { align-items: center; }
|
||||
.alignItems-flex-end { align-items: flex-end; }
|
||||
.alignItems-flex-start { align-items: flex-start; }
|
||||
.alignItems-stretch { align-items: stretch; }
|
||||
.alignItems-space-around { align-items: space-around; }
|
||||
.alignItems-space-between { align-items: space-between; }
|
||||
|
||||
/* align-self */
|
||||
|
||||
.alignSelf-auto { align-self: auto; }
|
||||
.alignSelf-baseline { align-self: baseline; }
|
||||
.alignSelf-center { align-self: center; }
|
||||
.alignSelf-flex-end { align-self: flex-end; }
|
||||
.alignSelf-flex-start { align-self: flex-start; }
|
||||
.alignSelf-stretch { align-self: stretch; }
|
||||
|
||||
/* appearance */
|
||||
|
||||
.appearance-none { appearance: none; }
|
||||
|
||||
/* background-attachment */
|
||||
|
||||
.backgroundAttachment-fixed { background-attachment: fixed; }
|
||||
.backgroundAttachment-inherit { background-attachment: inherit; }
|
||||
.backgroundAttachment-local { background-attachment: local; }
|
||||
.backgroundAttachment-scroll { background-attachment: scroll; }
|
||||
|
||||
/* background-clip */
|
||||
|
||||
.backgroundClip-border-box { background-clip: border-box; }
|
||||
.backgroundClip-content-box { background-clip: content-box; }
|
||||
.backgroundClip-inherit { background-clip: inherit; }
|
||||
.backgroundClip-padding-box { background-clip: padding-box; }
|
||||
|
||||
/* background-color */
|
||||
|
||||
.backgroundColor-\#000,
|
||||
.backgroundColor-black { background-color: black; }
|
||||
.backgroundColor-\#fff,
|
||||
.backgroundColor-white { background-color: white; }
|
||||
.backgroundColor-currentcolor,
|
||||
.backgroundColor-currentColor { background-color: currentcolor; }
|
||||
.backgroundColor-inherit { background-color: inherit; }
|
||||
.backgroundColor-transparent { background-color: transparent; }
|
||||
|
||||
/* background-image */
|
||||
|
||||
.backgroundImage { background-image: none; }
|
||||
|
||||
/* background-origin */
|
||||
|
||||
.backgroundOrigin-border-box { background-clip: border-box; }
|
||||
.backgroundOrigin-content-box { background-clip: content-box; }
|
||||
.backgroundOrigin-inherit { background-clip: inherit; }
|
||||
.backgroundOrigin-padding-box { background-clip: padding-box; }
|
||||
|
||||
/* background-position */
|
||||
|
||||
.backgroundPosition-bottom { background-position: bottom; }
|
||||
.backgroundPosition-center { background-position: center; }
|
||||
.backgroundPosition-left { background-position: left; }
|
||||
.backgroundPosition-right { background-position: right; }
|
||||
.backgroundPosition-top { background-position: top; }
|
||||
|
||||
/* background-repeat */
|
||||
|
||||
.backgroundRepeat-inherit { background-repeat: inherit; }
|
||||
.backgroundRepeat-no-repeat { background-repeat: no-repeat; }
|
||||
.backgroundRepeat-repeat { background-repeat: repeat; }
|
||||
.backgroundRepeat-repeat-x { background-repeat: repeat-x; }
|
||||
.backgroundRepeat-repeat-y { background-repeat: repeat-y; }
|
||||
.backgroundRepeat-round { background-repeat: round; }
|
||||
.backgroundRepeat-space { background-repeat: space; }
|
||||
|
||||
/* background-size */
|
||||
|
||||
.backgroundSize-auto { background-size: auto; }
|
||||
.backgroundSize-contain { background-size: contain; }
|
||||
.backgroundSize-cover { background-size: cover; }
|
||||
.backgroundSize-inherit { background-size: inherit; }
|
||||
|
||||
/* border-color */
|
||||
|
||||
.borderColor-\#fff,
|
||||
.borderColor-white { border-color: white; }
|
||||
.borderColor-currentcolor { border-color: currentcolor; }
|
||||
.borderColor-inherit { border-color: inherit; }
|
||||
.borderColor-transparent { border-color: transparent; }
|
||||
|
||||
/* border-bottom-color */
|
||||
|
||||
.borderBottomColor-\#fff,
|
||||
.borderBottomColor-white { border-bottom-color: white; }
|
||||
.borderBottomColor-currentcolor { border-bottom-color: currentcolor; }
|
||||
.borderBottomColor-inherit { border-bottom-color: inherit; }
|
||||
.borderBottomColor-transparent { border-bottom-color: transparent; }
|
||||
|
||||
/* border-left-color */
|
||||
|
||||
.borderLeftColor-\#fff,
|
||||
.borderLeftColor-white { border-left-color: white; }
|
||||
.borderLeftColor-currentcolor { border-left-color: currentcolor; }
|
||||
.borderLeftColor-inherit { border-left-color: inherit; }
|
||||
.borderLeftColor-transparent { border-left-color: transparent; }
|
||||
|
||||
/* border-right-color */
|
||||
|
||||
.borderRightColor-\#fff,
|
||||
.borderRightColor-white { border-right-color: white; }
|
||||
.borderRightColor-currentcolor { border-right-color: currentcolor; }
|
||||
.borderRightColor-inherit { border-right-color: inherit; }
|
||||
.borderRightColor-transparent { border-right-color: transparent; }
|
||||
|
||||
/* border-top-color */
|
||||
|
||||
.borderTopColor-\#fff,
|
||||
.borderTopColor-white { border-top-color: white; }
|
||||
.borderTopColor-currentcolor { border-top-color: currentcolor; }
|
||||
.borderTopColor-inherit { border-top-color: inherit; }
|
||||
.borderTopColor-transparent { border-top-color: transparent; }
|
||||
|
||||
/* border-style */
|
||||
|
||||
.borderStyle-dashed { border-style: dashed; }
|
||||
.borderStyle-dotted { border-style: dotted; }
|
||||
.borderStyle-inherit { border-style: inherit; }
|
||||
.borderStyle-none { border-style: none; }
|
||||
.borderStyle-solid { border-style: solid; }
|
||||
|
||||
/* border-bottom-style */
|
||||
|
||||
.borderBottomStyle-dashed { border-bottom-style: dashed; }
|
||||
.borderBottomStyle-dotted { border-bottom-style: dotted; }
|
||||
.borderBottomStyle-inherit { border-bottom-style: inherit; }
|
||||
.borderBottomStyle-none { border-bottom-style: none; }
|
||||
.borderBottomStyle-solid { border-bottom-style: solid; }
|
||||
|
||||
/* border-left-style */
|
||||
|
||||
.borderLeftStyle-dashed { border-left-style: dashed; }
|
||||
.borderLeftStyle-dotted { border-left-style: dotted; }
|
||||
.borderLeftStyle-inherit { border-left-style: inherit; }
|
||||
.borderLeftStyle-none { border-left-style: none; }
|
||||
.borderLeftStyle-solid { border-left-style: solid; }
|
||||
|
||||
/* border-right-style */
|
||||
|
||||
.borderRightStyle-dashed { border-right-style: dashed; }
|
||||
.borderRightStyle-dotted { border-right-style: dotted; }
|
||||
.borderRightStyle-inherit { border-right-style: inherit; }
|
||||
.borderRightStyle-none { border-right-style: none; }
|
||||
.borderRightStyle-solid { border-right-style: solid; }
|
||||
|
||||
/* border-top-style */
|
||||
|
||||
.borderTopStyle-dashed { border-top-style: dashed; }
|
||||
.borderTopStyle-dotted { border-top-style: dotted; }
|
||||
.borderTopStyle-inherit { border-top-style: inherit; }
|
||||
.borderTopStyle-none { border-top-style: none; }
|
||||
.borderTopStyle-solid { border-top-style: solid; }
|
||||
|
||||
/* border-width */
|
||||
|
||||
.borderWidth-0 { border-width: 0; }
|
||||
.borderWidth-1px { border-width: 1px; }
|
||||
.borderWidth-2px { border-width: 2px; }
|
||||
.borderWidth-3px { border-width: 3px; }
|
||||
.borderWidth-4px { border-width: 4px; }
|
||||
.borderWidth-5px { border-width: 5px; }
|
||||
|
||||
/* border-bottom-width */
|
||||
|
||||
.borderBottomWidth-0 { border-bottom-width: 0; }
|
||||
.borderBottomWidth-1px { border-bottom-width: 1px; }
|
||||
.borderBottomWidth-2px { border-bottom-width: 2px; }
|
||||
.borderBottomWidth-3px { border-bottom-width: 3px; }
|
||||
.borderBottomWidth-4px { border-bottom-width: 4px; }
|
||||
.borderBottomWidth-5px { border-bottom-width: 5px; }
|
||||
|
||||
/* border-left-width */
|
||||
|
||||
.borderLeftWidth-0 { border-left-width: 0; }
|
||||
.borderLeftWidth-1px { border-left-width: 1px; }
|
||||
.borderLeftWidth-2px { border-left-width: 2px; }
|
||||
.borderLeftWidth-3px { border-left-width: 3px; }
|
||||
.borderLeftWidth-4px { border-left-width: 4px; }
|
||||
.borderLeftWidth-5px { border-left-width: 5px; }
|
||||
|
||||
/* border-right-width */
|
||||
|
||||
.borderRightWidth-0 { border-right-width: 0; }
|
||||
.borderRightWidth-1px { border-right-width: 1px; }
|
||||
.borderRightWidth-2px { border-right-width: 2px; }
|
||||
.borderRightWidth-3px { border-right-width: 3px; }
|
||||
.borderRightWidth-4px { border-right-width: 4px; }
|
||||
.borderRightWidth-5px { border-right-width: 5px; }
|
||||
|
||||
/* border-top-width */
|
||||
|
||||
.borderTopWidth-0 { border-top-width: 0; }
|
||||
.borderTopWidth-1px { border-top-width: 1px; }
|
||||
.borderTopWidth-2px { border-top-width: 2px; }
|
||||
.borderTopWidth-3px { border-top-width: 3px; }
|
||||
.borderTopWidth-4px { border-top-width: 4px; }
|
||||
.borderTopWidth-5px { border-top-width: 5px; }
|
||||
|
||||
/* bottom */
|
||||
|
||||
.bottom-0 { bottom: 0; }
|
||||
.bottom-50% { bottom: 50%; }
|
||||
.bottom-100% { bottom: 100%; }
|
||||
|
||||
/* box-sizing */
|
||||
|
||||
.boxSizing-border-box { box-sizing: border-box; }
|
||||
.boxSizing-content-box { box-sizing: content-box; }
|
||||
.boxSizing-inherit { box-sizing: inherit; }
|
||||
.boxSizing-padding-box { box-sizing: padding-box; }
|
||||
|
||||
/* clear */
|
||||
|
||||
.clear-both { clear: both; }
|
||||
.clear-inherit { clear: inherit; }
|
||||
.clear-left { clear: left; }
|
||||
.clear-none { clear: none; }
|
||||
.clear-right { clear: right; }
|
||||
|
||||
/* color */
|
||||
|
||||
.color-#000,
|
||||
.color-black { color: black; }
|
||||
.color-\#fff,
|
||||
.color-white { color: white; }
|
||||
.color-inherit { color: inherit; }
|
||||
.color-transparent { color: transparent; }
|
||||
|
||||
/* direction */
|
||||
|
||||
.direction-inherit { direction: inherit; }
|
||||
.direction-ltr { direction: ltr; }
|
||||
.direction-rtl { direction: rtl; }
|
||||
|
||||
/* display */
|
||||
|
||||
.display-block { display: block; }
|
||||
.display-contents { display: contents; }
|
||||
.display-flex { display: flex; }
|
||||
.display-grid { display: grid; }
|
||||
.display-inherit { display: inherit; }
|
||||
.display-initial { display: initial; }
|
||||
.display-inline { display: inline; }
|
||||
.display-inline-block { display: inline-block; }
|
||||
.display-inline-flex { display: inline-flex; }
|
||||
.display-inline-grid { display: inline-grid; }
|
||||
.display-inline-table { display: inline-table; }
|
||||
.display-list-item { display: list-item; }
|
||||
.display-none { display: none; }
|
||||
.display-table { display: table; }
|
||||
.display-table-cell { display: table-cell; }
|
||||
.display-table-column { display: table-column; }
|
||||
.display-table-column-group { display: table-column-group; }
|
||||
.display-table-footer-group { display: table-footer-group; }
|
||||
.display-table-header-group { display: table-header-group; }
|
||||
.display-table-row { display: table-row; }
|
||||
.display-table-row-group { display: table-row-group; }
|
||||
.display-unset { display: unset; }
|
||||
|
||||
/* flex-basis */
|
||||
|
||||
.flexBasis-0 { flex-basis: 0%; }
|
||||
.flexBasis-auto { flex-basis: auto; }
|
||||
|
||||
/* flex-direction */
|
||||
|
||||
.flexDirection-column { flex-direction: column; }
|
||||
.flexDirection-column-reverse { flex-direction: column-reverse; }
|
||||
.flexDirection-row { flex-direction: row; }
|
||||
.flexDirection-row-reverse { flex-direction: row-reverse; }
|
||||
|
||||
/* flex-grow */
|
||||
|
||||
.flexGrow-0 { flex-grow: 0; }
|
||||
.flexGrow-1 { flex-grow: 1; }
|
||||
.flexGrow-2 { flex-grow: 2; }
|
||||
.flexGrow-3 { flex-grow: 3; }
|
||||
.flexGrow-4 { flex-grow: 4; }
|
||||
.flexGrow-5 { flex-grow: 5; }
|
||||
.flexGrow-6 { flex-grow: 6; }
|
||||
.flexGrow-7 { flex-grow: 7; }
|
||||
.flexGrow-8 { flex-grow: 8; }
|
||||
.flexGrow-9 { flex-grow: 9; }
|
||||
.flexGrow-10 { flex-grow: 10; }
|
||||
.flexGrow-11 { flex-grow: 11; }
|
||||
|
||||
/* flex-shrink */
|
||||
|
||||
.flexShrink-0 { flex-shrink: 0; }
|
||||
.flexShrink-1 { flex-shrink: 1; }
|
||||
.flexShrink-2 { flex-shrink: 2; }
|
||||
.flexShrink-3 { flex-shrink: 3; }
|
||||
.flexShrink-4 { flex-shrink: 4; }
|
||||
.flexShrink-5 { flex-shrink: 5; }
|
||||
.flexShrink-6 { flex-shrink: 6; }
|
||||
.flexShrink-7 { flex-shrink: 7; }
|
||||
.flexShrink-8 { flex-shrink: 8; }
|
||||
.flexShrink-9 { flex-shrink: 9; }
|
||||
.flexShrink-10 { flex-shrink: 10; }
|
||||
.flexShrink-11 { flex-shrink: 11; }
|
||||
|
||||
/* flex-wrap */
|
||||
|
||||
.flexWrap-nowrap { flex-wrap: nowrap; }
|
||||
.flexWrap-wrap { flex-wrap: wrap; }
|
||||
.flexWrap-wrap-reverse { flex-wrap: wrap-reverse; }
|
||||
|
||||
/* float */
|
||||
|
||||
.float-left { float: left; }
|
||||
.float-none { float: none; }
|
||||
.float-right { float: right; }
|
||||
|
||||
/* font */
|
||||
|
||||
.font-inherit { font: inherit; }
|
||||
|
||||
/* font-family */
|
||||
|
||||
.fontFamily-inherit { font-family: inherit; }
|
||||
.fontFamily-monospace { font-family: monospace; }
|
||||
.fontFamily-sans-serif { font-family: sans-serif; }
|
||||
.fontFamily-serif { font-family: serif; }
|
||||
|
||||
/* font-size */
|
||||
|
||||
.fontSize-100\% { font-size: 100%; }
|
||||
.fontSize-inherit { font-size: inherit; }
|
||||
|
||||
/* font-style */
|
||||
|
||||
.fontStyle-inherit { font-style: inherit; }
|
||||
.fontStyle-italic { font-style: italic; }
|
||||
.fontStyle-normal { font-style: normal; }
|
||||
.fontStyle-oblique { font-style: oblique; }
|
||||
|
||||
/* font-weight */
|
||||
|
||||
.fontWeight-100 { font-weight: 100; }
|
||||
.fontWeight-200 { font-weight: 200; }
|
||||
.fontWeight-300 { font-weight: 300; }
|
||||
.fontWeight-400 { font-weight: 400; }
|
||||
.fontWeight-500 { font-weight: 500; }
|
||||
.fontWeight-600 { font-weight: 600; }
|
||||
.fontWeight-700 { font-weight: 700; }
|
||||
.fontWeight-800 { font-weight: 800; }
|
||||
.fontWeight-900 { font-weight: 900; }
|
||||
.fontWeight-bold { font-weight: bold; }
|
||||
.fontWeight-bolder { font-weight: bolder; }
|
||||
.fontWeight-inherit { font-weight: inherit; }
|
||||
.fontWeight-lighter { font-weight: lighter; }
|
||||
.fontWeight-normal { font-weight: normal; }
|
||||
|
||||
/* height */
|
||||
|
||||
.height-0 { height: 0; }
|
||||
.height-10\% { height: 10%; }
|
||||
.height-12\.5\% { height: 12.5%; }
|
||||
.height-20\% { height: 20%; }
|
||||
.height-25\% { height: 25%; }
|
||||
.height-30\% { height: 30%; }
|
||||
.height-37\.5\% { height: 37.5%; }
|
||||
.height-40\% { height: 40%; }
|
||||
.height-50\% { height: 50%; }
|
||||
.height-60\% { height: 60%; }
|
||||
.height-62\.5\% { height: 62.5%; }
|
||||
.height-70\% { height: 70%; }
|
||||
.height-75\% { height: 75%; }
|
||||
.height-80\% { height: 80%; }
|
||||
.height-87\.5\% { height: 87.5%; }
|
||||
.height-90\% { height: 90%; }
|
||||
.height-100\% { height: 100%; }
|
||||
|
||||
/* justify-content */
|
||||
|
||||
.justifyContent-center { justify-content: center; }
|
||||
.justifyContent-flex-end { justify-content: flex-end; }
|
||||
.justifyContent-flex-start { justify-content: flex-start; }
|
||||
.justifyContent-space-around { justify-content: space-around; }
|
||||
.justifyContent-space-between { justify-content: space-between; }
|
||||
|
||||
/* left */
|
||||
|
||||
.left-0 { left: 0; }
|
||||
.left-50% { left: 50%; }
|
||||
.left-100% { left: 100%; }
|
||||
|
||||
/* line-height */
|
||||
|
||||
.lineHeight-inherit { line-height: inherit; }
|
||||
.lineHeight-normal { line-height: normal; }
|
||||
|
||||
/* list-style */
|
||||
|
||||
.listStyle-none { list-style: none; }
|
||||
|
||||
/* margin */
|
||||
|
||||
.margin-0 { margin: 0; }
|
||||
.margin-auto { margin: auto; }
|
||||
|
||||
/* margin-bottom */
|
||||
|
||||
.marginBottom-auto { margin-bottom: auto; }
|
||||
.marginBottom-0 { margin-bottom: 0; }
|
||||
.marginBottom-1em { margin-bottom: 1em; }
|
||||
.marginBottom-1rem { margin-bottom: 1rem; }
|
||||
|
||||
/* margin-left */
|
||||
|
||||
.marginLeft-auto { margin-left: auto; }
|
||||
.marginLeft-0 { margin-left: 0; }
|
||||
.marginLeft-1em { margin-left: 1em; }
|
||||
.marginLeft-1rem { margin-left: 1rem; }
|
||||
|
||||
/* margin-right */
|
||||
|
||||
.marginRight-auto { margin-right: auto; }
|
||||
.marginRight-0 { margin-right: 0; }
|
||||
.marginRight-1em { margin-right: 1em; }
|
||||
.marginRight-1rem { margin-right: 1rem; }
|
||||
|
||||
/* margin-top */
|
||||
|
||||
.marginTop-auto { margin-top: auto; }
|
||||
.marginTop-0 { margin-top: 0; }
|
||||
.marginTop-1em { margin-top: 1em; }
|
||||
.marginTop-1rem { margin-top: 1rem; }
|
||||
|
||||
/* max-height */
|
||||
|
||||
.maxHeight-100\% { max-height: 100%; }
|
||||
|
||||
/* max-width */
|
||||
|
||||
.maxWidth-100\% { max-width: 100%; }
|
||||
|
||||
/* min-height */
|
||||
|
||||
.minHeight-100\% { min-height: 100%; }
|
||||
|
||||
/* min-width */
|
||||
|
||||
.minWidth-100\% { min-width: 100%; }
|
||||
|
||||
/* opacity */
|
||||
|
||||
.opacity-0 { opacity: 0; }
|
||||
.opacity-0\.1 { opacity: 0.1; }
|
||||
.opacity-0\.2 { opacity: 0.2; }
|
||||
.opacity-0\.25 { opacity: 0.25; }
|
||||
.opacity-0\.3 { opacity: 0.3; }
|
||||
.opacity-0\.4 { opacity: 0.4; }
|
||||
.opacity-0\.5 { opacity: 0.5; }
|
||||
.opacity-0\.6 { opacity: 0.6; }
|
||||
.opacity-0\.7 { opacity: 0.7; }
|
||||
.opacity-0\.75 { opacity: 0.75; }
|
||||
.opacity-0\.8 { opacity: 0.8; }
|
||||
.opacity-0\.9 { opacity: 0.9; }
|
||||
.opacity-1 { opacity: 1; }
|
||||
|
||||
/* order */
|
||||
|
||||
.order--1 { order: -1; }
|
||||
.order-1 { order: 1; }
|
||||
.order-2 { order: 2; }
|
||||
.order-3 { order: 3; }
|
||||
.order-4 { order: 4; }
|
||||
.order-5 { order: 5; }
|
||||
.order-6 { order: 6; }
|
||||
.order-7 { order: 7; }
|
||||
.order-8 { order: 8; }
|
||||
.order-9 { order: 9; }
|
||||
.order-10 { order: 10; }
|
||||
.order-11 { order: 11; }
|
||||
|
||||
/* overflow */
|
||||
|
||||
.overflow-auto { overflow: auto; }
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.overflow-inherit { overflow: inherit; }
|
||||
.overflow-scroll { overflow: scroll; }
|
||||
.overflow-visible { overflow: visible; }
|
||||
|
||||
/* overflow-x */
|
||||
|
||||
.overflowX-auto { overflow-x: auto; }
|
||||
.overflowX-hidden { overflow-x: hidden; }
|
||||
.overflowX-inherit { overflow-x: inherit; }
|
||||
.overflowX-scroll { overflow-x: scroll; }
|
||||
.overflowX-visible { overflow-x: visible; }
|
||||
|
||||
/* overflow-y */
|
||||
|
||||
.overflowY-auto { overflow-y: auto; }
|
||||
.overflowY-hidden { overflow-y: hidden; }
|
||||
.overflowY-inherit { overflow-y: inherit; }
|
||||
.overflowY-scroll { overflow-y: scroll; }
|
||||
.overflowY-visible { overflow-y: visible; }
|
||||
|
||||
/* padding */
|
||||
|
||||
.padding-0 { padding: 0; }
|
||||
.padding-1em { padding: 1em; }
|
||||
.padding-1rem { padding: 1rem; }
|
||||
|
||||
/* padding-bottom */
|
||||
|
||||
.paddingBottom-0 { padding-bottom: 0; }
|
||||
.paddingBottom-1em { padding-bottom: 1em; }
|
||||
.paddingBottom-1rem { padding-bottom: 1rem; }
|
||||
|
||||
/* padding-left */
|
||||
|
||||
.paddingLeft-0 { padding-left: 0; }
|
||||
.paddingLeft-1em { padding-left: 1em; }
|
||||
.paddingLeft-1rem { padding-left: 1rem; }
|
||||
|
||||
/* padding-right */
|
||||
|
||||
.paddingRight-0 { padding-right: 0; }
|
||||
.paddingRight-1em { padding-right: 1em; }
|
||||
.paddingRight-1rem { padding-right: 1rem; }
|
||||
|
||||
/* padding-top */
|
||||
|
||||
.paddingTop-0 { padding-top: 0; }
|
||||
.paddingTop-1em { padding-top: 1em; }
|
||||
.paddingTop-1rem { padding-top: 1rem; }
|
||||
|
||||
/* pointer-events */
|
||||
|
||||
.pointerEvents-auto { pointer-events: auto; }
|
||||
.pointerEvents-none { pointer-events: none; }
|
||||
.pointerEvents-box-none { pointer-events: none; }
|
||||
.pointerEvents-box-none * { pointer-events: auto;}
|
||||
.pointerEvents-box-only { pointer-events: auto; }
|
||||
.pointerEvents-box-only * { pointer-events: none; }
|
||||
|
||||
/* position */
|
||||
|
||||
.position-absolute { position: absolute; }
|
||||
.position-fixed { position: fixed; }
|
||||
.position-relative { position: relative; }
|
||||
|
||||
/* right */
|
||||
|
||||
.right-0 { right: 0; }
|
||||
.right-50% { right: 50%; }
|
||||
.right-100% { right: 100%; }
|
||||
|
||||
/* text-align */
|
||||
|
||||
.textAlign-center { text-align: center; }
|
||||
.textAlign-end { text-align: end; }
|
||||
.textAlign-inherit { text-align: inherit; }
|
||||
.textAlign-left { text-align: left; }
|
||||
.textAlign-right { text-align: right; }
|
||||
.textAlign-justify { text-align: justify; }
|
||||
.textAlign-start { text-align: start; }
|
||||
|
||||
/* text-decoration */
|
||||
|
||||
.textDecoration-inherit { text-decoration: inherit; }
|
||||
.textDecoration-none { text-decoration: none; }
|
||||
.textDecoration-underline { text-decoration: underline; }
|
||||
|
||||
/* text-overflow */
|
||||
|
||||
.textOverflow-clip { text-overflow: clip; }
|
||||
.textOverflow-ellipsis { text-overflow: ellipsis; }
|
||||
|
||||
/* text-rendering */
|
||||
|
||||
.textRendering-auto { text-rendering: auto; }
|
||||
.textRendering-geometricPrecision { text-rendering: geometricPrecision; }
|
||||
.textRendering-inherit { text-rendering: inherit; }
|
||||
.textRendering-optimizeLegibility { text-rendering: optimizeLegibility; }
|
||||
.textRendering-optimizeSpeed { text-rendering: optimizeSpeed; }
|
||||
|
||||
/* text-transform */
|
||||
|
||||
.textTransform-capitalize { text-transform: capitalize; }
|
||||
.textTransform-lowercase { text-transform: lowercase; }
|
||||
.textTransform-none { text-transform: none; }
|
||||
.textTransform-uppercase { text-transform: uppercase; }
|
||||
|
||||
/* top */
|
||||
|
||||
.top-0 { top: 0; }
|
||||
.top-50% { top: 50%; }
|
||||
.top-100% { top: 100%; }
|
||||
|
||||
/* unicode-bidi */
|
||||
|
||||
.unicodeBidi-bidi-override { unicode-bidi: bidi-override; }
|
||||
.unicodeBidi-embed { unicode-bidi: embed; }
|
||||
.unicodeBidi-inherit { unicode-bidi: inherit; }
|
||||
.unicodeBidi-isolate { unicode-bidi: isolate; }
|
||||
.unicodeBidi-isolate-override { unicode-bidi: isolate-override; }
|
||||
.unicodeBidi-normal { unicode-bidi: normal; }
|
||||
.unicodeBidi-plaintext { unicode-bidi: plaintext; }
|
||||
|
||||
/* user-select */
|
||||
|
||||
.userSelect-all { user-select: all; }
|
||||
.userSelect-inherit { user-select: inherit; }
|
||||
.userSelect-none { user-select: none; }
|
||||
.userSelect-text { user-select: text; }
|
||||
|
||||
/* visibility */
|
||||
|
||||
.visibility-collapse { visibility: collapse; }
|
||||
.visibility-hidden { visibility: hidden; }
|
||||
.visibility-inherit { visibility: inherit; }
|
||||
.visibility-visible { visibility: visible; }
|
||||
|
||||
/* white-space */
|
||||
|
||||
.whiteSpace-normal { white-space: normal; }
|
||||
.whiteSpace-nowrap { white-space: nowrap; }
|
||||
.whiteSpace-pre { white-space: pre; }
|
||||
.whiteSpace-pre-line { white-space: pre-line; }
|
||||
.whiteSpace-pre-wrap { white-space: pre-wrap; }
|
||||
|
||||
/* width */
|
||||
|
||||
.width-0 { width: 0; }
|
||||
.width-10\% { width: 10%; }
|
||||
.width-12\.5\% { width: 12.5%; }
|
||||
.width-20\% { width: 20%; }
|
||||
.width-25\% { width: 25%; }
|
||||
.width-30\% { width: 30%; }
|
||||
.width-37\.5\% { width: 37.5%; }
|
||||
.width-40\% { width: 40%; }
|
||||
.width-50\% { width: 50%; }
|
||||
.width-60\% { width: 60%; }
|
||||
.width-62\.5\% { width: 62.5%; }
|
||||
.width-70\% { width: 70%; }
|
||||
.width-75\% { width: 75%; }
|
||||
.width-80\% { width: 80%; }
|
||||
.width-87\.5\% { width: 87.5%; }
|
||||
.width-90\% { width: 90%; }
|
||||
.width-100\% { width: 100%; }
|
||||
|
||||
/* word-wrap */
|
||||
|
||||
.wordWrap-break-word { word-wrap: break-word; }
|
||||
.wordWrap-normal { word-wrap: normal; }
|
||||
|
||||
/* z-index */
|
||||
|
||||
.zIndex--1 { z-index: -1; }
|
||||
.zIndex-0 { z-index: 0; }
|
||||
.zIndex-1 { z-index: 1; }
|
||||
.zIndex-2 { z-index: 2; }
|
||||
.zIndex-3 { z-index: 3; }
|
||||
.zIndex-4 { z-index: 4; }
|
||||
.zIndex-5 { z-index: 5; }
|
||||
.zIndex-6 { z-index: 6; }
|
||||
.zIndex-7 { z-index: 7; }
|
||||
.zIndex-8 { z-index: 8; }
|
||||
.zIndex-9 { z-index: 9; }
|
||||
.zIndex-10 { z-index: 10; }
|
||||
@@ -4,6 +4,5 @@
|
||||
*
|
||||
* See: https://github.com/webpack/docs/wiki/context
|
||||
*/
|
||||
const specContext = require.context('.', true, /.+\.spec\.jsx?$/)
|
||||
specContext.keys().forEach(specContext)
|
||||
module.exports = specContext
|
||||
var context = require.context('./src', true, /-test\.js$/)
|
||||
context.keys().forEach(context)
|
||||
Reference in New Issue
Block a user