[change] StyleSheet: news APIs and refactor

This fixes several issues with 'StyleSheet' and simplifies the
implementation.

1. The generated style sheet could render after an apps existing style
sheets, potentially overwriting certain 'html' and 'body' styles. To fix
this, the style sheet is now rendered first in the document head.

2. 'StyleSheet' didn't make it easy to render app shells on the server.
The prerendered style sheet would contain classnames that didn't apply
to the client-generated style sheet (in part because the class names
were not generated as a hash of the declaration). When the client
initialized, server-rendered parts of the page could become unstyled. To
fix this 'StyleSheet' uses inline styles by default and a few predefined
CSS rules where inline styles are not possible.

3. Even with the strategy of mapping declarations to unique CSS rules,
very large apps can produce very large style sheets. For example,
twitter.com would produce a gzipped style sheet ~30 KB. Issues related
to this are also alleviated by using inline styles.

4. 'StyleSheet' didn't really work unless you rendered an app using
'AppRegistry'. To fix this, 'StyleSheet' now handles injection of the
DOM style sheet.

Using inline styles doesn't appear to have any serious performance
problems compared to using single classes (ref #110).

Fix #90
Fix #106
This commit is contained in:
Nicolas Gallagher
2016-07-10 17:54:34 -07:00
parent 216885406f
commit 77f72aa129
26 changed files with 333 additions and 405 deletions

View File

@@ -15,17 +15,52 @@ Each key of the object passed to `create` must define a style object.
Flattens an array of styles into a single style object.
**renderToString**: function
**render**: function
Returns a string of CSS used to style the application.
Returns a React `<style>` element for use in server-side rendering.
## Properties
**absoluteFill**: number
A very common pattern is to create overlays with position absolute and zero positioning,
so `absoluteFill` can be used for convenience and to reduce duplication of these repeated
styles.
```js
<View style={StyleSheet.absoluteFill} />
```
**absoluteFillObject**: object
Sometimes you may want `absoluteFill` but with a couple tweaks - `absoluteFillObject` can be
used to create a customized entry in a `StyleSheet`, e.g.:
```js
const styles = StyleSheet.create({
wrapper: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
top: 10
}
})
```
**hairlineWidth**: number
## Example
```js
<View style={styles.container}>
<Text
children={'Title text'}
style={[
styles.title,
this.props.isActive && styles.activeTitle
]}
/>
</View>
const styles = StyleSheet.create({
container: {
borderRadius: 4,
@@ -41,29 +76,3 @@ const styles = StyleSheet.create({
}
})
```
Use styles:
```js
<View style={styles.container}>
<Text
style={[
styles.title,
this.props.isActive && styles.activeTitle
]}
/>
</View>
```
Or:
```js
<View style={styles.container}>
<Text
style={{
...styles.title,
...(this.props.isActive && styles.activeTitle)
}}
/>
</View>
```

View File

@@ -54,5 +54,5 @@ AppRegistry.runApplication('App', {
})
// prerender the app
const { html, style, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
const { html, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
```

View File

@@ -20,24 +20,24 @@ export default class GridView extends Component {
render() {
const { alley, children, gutter, style, ...other } = this.props
const rootStyle = {
...style,
...styles.root
}
const rootStyle = [
style,
styles.root
]
const contentContainerStyle = {
...styles.contentContainer,
marginHorizontal: `calc(-0.5 * ${alley})`,
paddingHorizontal: `${gutter}`
}
const contentContainerStyle = [
styles.contentContainer,
{ marginHorizontal: `calc(-0.5 * ${alley})` },
{ paddingHorizontal: `${gutter}` }
]
const newChildren = React.Children.map(children, (child) => {
return child && React.cloneElement(child, {
style: {
...child.props.style,
...styles.column,
marginHorizontal: `calc(0.5 * ${alley})`
}
style: [
child.props.style,
styles.column,
{ marginHorizontal: `calc(0.5 * ${alley})` }
]
})
})

View File

@@ -5,7 +5,7 @@ const Heading = ({ children, size = 'normal' }) => (
<Text
accessibilityRole='heading'
children={children}
style={{ ...styles.root, ...sizeStyles[size] }}
style={[ styles.root, sizeStyles[size] ]}
/>
)

View File

@@ -1,10 +1,10 @@
import { AppRegistry } from 'react-native'
import React from 'react'
import ReactNative, { AppRegistry } from 'react-native'
import App from './components/App'
import Game2048 from './2048/Game2048'
import TicTacToeApp from './TicTacToe/TicTacToe'
const rootTag = document.getElementById('react-root')
AppRegistry.registerComponent('App', () => App)
AppRegistry.runApplication('App', {
rootTag: document.getElementById('react-root')
})
AppRegistry.runApplication('App', { rootTag })
// ReactNative.render(<App />, rootTag)

View File

@@ -1,20 +1,16 @@
/* eslint-env mocha */
import assert from 'assert'
import React from 'react'
import { elementId } from '../../StyleSheet'
import { prerenderApplication } from '../renderApplication'
import React from 'react'
const component = () => <div />
suite('apis/AppRegistry/renderApplication', () => {
test('prerenderApplication', () => {
const { html, style, styleElement } = prerenderApplication(component, {})
const { html, styleElement } = prerenderApplication(component, {})
assert.ok(html.indexOf('<div ') > -1)
assert.ok(typeof style === 'string')
assert.equal(styleElement.type, 'style')
assert.equal(styleElement.props.id, elementId)
assert.equal(styleElement.props.dangerouslySetInnerHTML.__html, style)
})
})

View File

@@ -13,17 +13,9 @@ import ReactDOMServer from 'react-dom/server'
import ReactNativeApp from './ReactNativeApp'
import StyleSheet from '../../apis/StyleSheet'
const renderStyleSheetToString = () => StyleSheet.renderToString()
const styleAsElement = (style) => <style dangerouslySetInnerHTML={{ __html: style }} id={StyleSheet.elementId} />
const styleAsTagString = (style) => `<style id="${StyleSheet.elementId}">${style}</style>`
export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) {
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag)
// insert style sheet if needed
const styleElement = document.getElementById(StyleSheet.elementId)
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', styleAsTagString(renderStyleSheetToString())) }
const component = (
<ReactNativeApp
initialProps={initialProps}
@@ -42,7 +34,6 @@ export function prerenderApplication(RootComponent: Component, initialProps: Obj
/>
)
const html = ReactDOMServer.renderToString(component)
const style = renderStyleSheetToString()
const styleElement = styleAsElement(style)
return { html, style, styleElement }
const styleElement = StyleSheet.render()
return { html, styleElement }
}

View File

@@ -1,113 +0,0 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import createReactStyleObject from './createReactStyleObject'
import hyphenate from './hyphenate'
import { predefinedClassNames } from './predefs'
import prefixAll from 'inline-style-prefixer/static'
let stylesCache = {}
let uniqueID = 0
const getCacheKey = (prop, value) => `${prop}:${value}`
const createCssDeclarations = (style) => {
return Object.keys(style).map((prop) => {
const property = hyphenate(prop)
const value = style[prop]
if (Array.isArray(value)) {
return value.reduce((acc, curr) => {
acc += `${property}:${curr};`
return acc
}, '')
} else {
return `${property}:${value};`
}
}).sort().join('')
}
class StyleSheetRegistry {
/* for testing */
static _reset() {
stylesCache = {}
uniqueID = 0
}
static renderToString() {
let str = `/* ${uniqueID} unique declarations */`
return Object.keys(stylesCache).reduce((str, key) => {
const id = stylesCache[key].id
const style = stylesCache[key].style
const declarations = createCssDeclarations(style)
const rule = `\n.${id}{${declarations}}`
str += rule
return str
}, str)
}
static registerStyle(style: Object): number {
if (process.env.NODE_ENV !== 'production') {
Object.freeze(style)
}
const reactStyleObject = createReactStyleObject(style)
Object.keys(reactStyleObject).forEach((prop) => {
const value = reactStyleObject[prop]
const cacheKey = getCacheKey(prop, value)
const exists = stylesCache[cacheKey] && stylesCache[cacheKey].id
if (!exists) {
const id = ++uniqueID
// add new declaration to the store
stylesCache[cacheKey] = {
id: `__style${id}`,
style: prefixAll({ [prop]: value })
}
}
})
return style
}
static getStyleAsNativeProps(styleSheetObject, canUseCSS = false) {
const classList = []
const reactStyleObject = createReactStyleObject(styleSheetObject)
let style = {}
for (const prop in reactStyleObject) {
const value = reactStyleObject[prop]
const cacheKey = getCacheKey(prop, value)
let selector = stylesCache[cacheKey] && stylesCache[cacheKey].id || predefinedClassNames[cacheKey]
if (selector && canUseCSS) {
classList.push(selector)
} else {
style[prop] = reactStyleObject[prop]
}
}
/**
* React 15 removed undocumented support for fallback values in
* inline-styles. For now, pick the last value and regress browser support
* for CSS features like flexbox.
*/
const vendorPrefixedStyle = Object.keys(prefixAll(style)).reduce((acc, prop) => {
const value = style[prop]
acc[prop] = Array.isArray(value) ? value[value.length - 1] : value
return acc
}, {})
return {
className: classList.join(' '),
style: vendorPrefixedStyle
}
}
}
module.exports = StyleSheetRegistry

View File

@@ -1,55 +0,0 @@
/* eslint-env mocha */
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import assert from 'assert'
import StyleSheetRegistry from '../StyleSheetRegistry'
suite('apis/StyleSheet/StyleSheetRegistry', () => {
setup(() => {
StyleSheetRegistry._reset()
})
test('static renderToString', () => {
const style1 = { alignItems: 'center', opacity: 1 }
const style2 = { alignItems: 'center', opacity: 1 }
StyleSheetRegistry.registerStyle(style1)
StyleSheetRegistry.registerStyle(style2)
const actual = StyleSheetRegistry.renderToString()
const expected = `/* 2 unique declarations */
.__style1{-ms-flex-align:center;-webkit-align-items:center;-webkit-box-align:center;align-items:center;}
.__style2{opacity:1;}`
assert.equal(actual, expected)
})
test('static getStyleAsNativeProps', () => {
const style = { borderColorTop: 'white', opacity: 1 }
const style1 = { opacity: 1 }
StyleSheetRegistry.registerStyle(style1)
// canUseCSS = false
const actual1 = StyleSheetRegistry.getStyleAsNativeProps(style)
const expected1 = {
className: '',
style: { borderColorTop: 'white', opacity: 1 }
}
assert.deepEqual(actual1, expected1)
// canUseCSS = true
const actual2 = StyleSheetRegistry.getStyleAsNativeProps(style, true)
const expected2 = {
className: '__style1',
style: { borderColorTop: 'white' }
}
assert.deepEqual(actual2, expected2)
})
})

View File

@@ -0,0 +1,13 @@
/* eslint-env mocha */
import assert from 'assert'
import createReactStyleObject from '../createReactStyleObject'
suite('apis/StyleSheet/createReactStyleObject', () => {
test('converts ReactNative style to ReactDOM style', () => {
const reactNativeStyle = { display: 'flex', marginVertical: 0, opacity: 0 }
const expectedStyle = { display: 'flex', marginTop: '0px', marginBottom: '0px', opacity: 0 }
assert.deepEqual(createReactStyleObject(reactNativeStyle), expectedStyle)
})
})

View File

@@ -1,16 +0,0 @@
/* eslint-env mocha */
import assert from 'assert'
import hyphenate from '../hyphenate'
suite('apis/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')
})
})

View File

@@ -1,60 +1,70 @@
/* eslint-env mocha */
import { resetCSS, predefinedCSS } from '../predefs'
import assert from 'assert'
import { defaultStyles } from '../predefs'
import isPlainObject from 'lodash/isPlainObject'
import StyleSheet from '..'
const styles = { root: { opacity: 1 } }
suite('apis/StyleSheet', () => {
setup(() => {
StyleSheet._destroy()
StyleSheet._reset()
})
test('absoluteFill', () => {
assert(Number.isInteger(StyleSheet.absoluteFill) === true)
})
test('absoluteFillObject', () => {
assert.ok(isPlainObject(StyleSheet.absoluteFillObject) === true)
})
suite('create', () => {
test('returns styles object', () => {
assert.equal(StyleSheet.create(styles), styles)
test('replaces styles with numbers', () => {
const style = StyleSheet.create({ root: { opacity: 1 } })
assert(Number.isInteger(style.root) === true)
})
test('updates already-rendered style sheet', () => {
// setup
const div = document.createElement('div')
document.body.appendChild(div)
StyleSheet.create(styles)
div.innerHTML = `<style id='${StyleSheet.elementId}'>${StyleSheet.renderToString()}</style>`
// test
test('renders a style sheet in the browser', () => {
StyleSheet.create({ root: { color: 'red' } })
assert.equal(
document.getElementById(StyleSheet.elementId).textContent,
`${resetCSS}\n${predefinedCSS}\n` +
`/* 2 unique declarations */\n` +
`.__style1{opacity:1;}\n` +
'.__style2{color:red;}'
document.getElementById('__react-native-style').textContent,
defaultStyles
)
// teardown
document.body.removeChild(div)
})
})
test('renderToString', () => {
StyleSheet.create(styles)
test('flatten', () => {
assert(typeof StyleSheet.flatten === 'function')
})
test('hairlineWidth', () => {
assert(Number.isInteger(StyleSheet.hairlineWidth) === true)
})
test('render', () => {
assert.equal(
StyleSheet.renderToString(),
`${resetCSS}\n${predefinedCSS}\n` +
`/* 1 unique declarations */\n` +
'.__style1{opacity:1;}'
StyleSheet.render().props.dangerouslySetInnerHTML.__html,
defaultStyles
)
})
test('resolve', () => {
assert.deepEqual(
StyleSheet.resolve({ className: 'test', style: styles.root }),
{
StyleSheet.resolve({
className: 'test',
style: { opacity: 1 }
style: {
display: 'flex',
opacity: 1,
pointerEvents: 'box-none'
}
}),
{
className: 'test __style_df __style_pebn',
style: {
display: 'flex',
opacity: 1,
pointerEvents: 'box-none'
}
}
)
})

View File

@@ -9,5 +9,6 @@ suite('apis/StyleSheet/normalizeValue', () => {
})
test('ignores unitless property values', () => {
assert.deepEqual(normalizeValue('flexGrow', 1), 1)
assert.deepEqual(normalizeValue('scale', 2), 2)
})
})

View File

@@ -1,7 +1,22 @@
import expandStyle from './expandStyle'
import flattenStyle from '../StyleSheet/flattenStyle'
import processTransform from '../StyleSheet/processTransform'
import flattenStyle from '../../modules/flattenStyle'
import prefixAll from 'inline-style-prefixer/static'
import processTransform from './processTransform'
const createReactStyleObject = (style) => processTransform(expandStyle(flattenStyle(style)))
const addVendorPrefixes = (style) => {
let prefixedStyles = prefixAll(style)
// React@15 removed undocumented support for fallback values in
// inline-styles. Revert array values to the standard CSS value
for (const prop in prefixedStyles) {
const value = prefixedStyles[prop]
if (Array.isArray(value)) {
prefixedStyles[prop] = value[value.length - 1]
}
}
return prefixedStyles
}
module.exports = createReactStyleObject
const _createReactDOMStyleObject = (reactNativeStyle) => processTransform(expandStyle(flattenStyle(reactNativeStyle)))
const createReactDOMStyleObject = (reactNativeStyle) => addVendorPrefixes(_createReactDOMStyleObject(reactNativeStyle))
module.exports = createReactDOMStyleObject

View File

@@ -11,6 +11,7 @@
import normalizeValue from './normalizeValue'
const emptyObject = {}
const styleShortFormProperties = {
borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ],
borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ],
@@ -43,8 +44,8 @@ const createStyleReducer = (originalStyle) => {
// React Native treats `flex:1` like `flex:1 1 auto`
if (prop === 'flex') {
style.flexGrow = value
if (style.flexShrink == null) { style.flexShrink = 1 }
if (style.flexBasis == null) { style.flexBasis = 'auto' }
style.flexShrink = 1
style.flexBasis = 'auto'
// React Native accepts 'center' as a value
} else if (prop === 'textAlignVertical') {
style.verticalAlign = (value === 'center' ? 'middle' : value)
@@ -63,7 +64,7 @@ const createStyleReducer = (originalStyle) => {
}
}
const expandStyle = (style) => {
const expandStyle = (style = emptyObject) => {
const sortedStyleProps = alphaSort(Object.keys(style))
const styleReducer = createStyleReducer(style)
return sortedStyleProps.reduce(styleReducer, {})

View File

@@ -1,31 +0,0 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* @flow
*/
import invariant from 'fbjs/lib/invariant'
module.exports = function flattenStyle(style): ?Object {
if (!style) {
return undefined
}
invariant(style !== true, 'style may be false but not true')
if (!Array.isArray(style)) {
return style
}
const result = {}
for (let i = 0; i < style.length; ++i) {
const computedStyle = flattenStyle(style[i])
if (computedStyle) {
for (const key in computedStyle) {
result[key] = computedStyle[key]
}
}
}
return result
}

View File

@@ -1 +0,0 @@
module.exports = (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')

View File

@@ -1,75 +1,76 @@
import { resetCSS, predefinedCSS } from './predefs'
import flattenStyle from './flattenStyle'
import StyleSheetRegistry from './StyleSheetRegistry'
import createReactStyleObject from './createReactStyleObject'
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
import flattenStyle from '../../modules/flattenStyle'
import React from 'react'
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry'
import StyleSheetValidation from './StyleSheetValidation'
import { defaultStyles, mapStyleToClassName } from './predefs'
const ELEMENT_ID = 'react-stylesheet'
let isRendered = false
let lastStyleSheet = ''
let styleElement
const STYLE_SHEET_ID = '__react-native-style'
/**
* Initialize the store with pointer-event styles mapping to our custom pointer
* event classes
*/
/**
* Destroy existing styles
*/
const _destroy = () => {
isRendered = false
StyleSheetRegistry._reset()
const _injectStyleSheet = () => {
// check if the server rendered the style sheet
styleElement = document.getElementById(STYLE_SHEET_ID)
// if not, inject the style sheet
if (!styleElement) { document.head.insertAdjacentHTML('afterbegin', renderToString()) }
isRendered = true
}
const _reset = () => {
if (styleElement) { document.head.removeChild(styleElement) }
styleElement = null
isRendered = false
}
const absoluteFillObject = { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }
const absoluteFill = ReactNativePropRegistry.register(absoluteFillObject)
const create = (styles: Object): Object => {
for (const key in styles) {
StyleSheetValidation.validateStyle(key, styles)
StyleSheetRegistry.registerStyle(styles[key])
if (!isRendered && ExecutionEnvironment.canUseDOM) {
_injectStyleSheet()
}
// update the style sheet in place
if (isRendered) {
const stylesheet = document.getElementById(ELEMENT_ID)
if (stylesheet) {
const newStyleSheet = renderToString()
if (lastStyleSheet !== newStyleSheet) {
stylesheet.textContent = newStyleSheet
lastStyleSheet = newStyleSheet
}
} else if (process.env.NODE_ENV !== 'production') {
console.error(`ReactNative: cannot find "${ELEMENT_ID}" element`)
const result = {}
for (let key in styles) {
StyleSheetValidation.validateStyle(key, styles)
result[key] = ReactNativePropRegistry.register(styles[key])
}
return result
}
const render = () => <style dangerouslySetInnerHTML={{ __html: defaultStyles }} id={STYLE_SHEET_ID} />
const renderToString = () => `<style id="${STYLE_SHEET_ID}">${defaultStyles}</style>`
/**
* Accepts React props and converts style declarations to classNames when necessary
*/
const resolve = (props) => {
let className = props.className || ''
let style = createReactStyleObject(props.style)
for (const prop in style) {
const value = style[prop]
const replacementClassName = mapStyleToClassName(prop, value)
if (replacementClassName) {
className += ` ${replacementClassName}`
// delete style[prop]
}
}
return styles
}
/**
* Render the styles as a CSS style sheet
*/
const renderToString = () => {
const css = StyleSheetRegistry.renderToString()
isRendered = true
return `${resetCSS}\n${predefinedCSS}\n${css}`
}
/**
* Accepts React props and converts inline styles to single purpose classes
* where possible.
*/
const resolve = ({ className, style = {} }) => {
const props = StyleSheetRegistry.getStyleAsNativeProps(style, isRendered)
return {
...props,
className: className ? `${props.className} ${className}`.trim() : props.className
}
return { className, style }
}
module.exports = {
_destroy,
_reset,
absoluteFill,
absoluteFillObject,
create,
elementId: ELEMENT_ID,
hairlineWidth: 1,
flatten: flattenStyle,
renderToString,
/* @platform web */
render,
/* @platform web */
resolve
}

View File

@@ -1,24 +1,38 @@
/**
* Reset unwanted styles beyond the control of React inline styles
*/
export const resetCSS =
`/* React Native for Web */
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}
body {margin:0}
button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display:none}`
const DISPLAY_FLEX_CLASSNAME = '__style_df'
const POINTER_EVENTS_AUTO_CLASSNAME = '__style_pea'
const POINTER_EVENTS_BOX_NONE_CLASSNAME = '__style_pebn'
const POINTER_EVENTS_BOX_ONLY_CLASSNAME = '__style_pebo'
const POINTER_EVENTS_NONE_CLASSNAME = '__style_pen'
/**
* Custom pointer event styles
*/
export const predefinedCSS =
`/* pointer-events */
.__style_pea, .__style_pebo, .__style_pebn * {pointer-events:auto}
.__style_pen, .__style_pebo *, .__style_pebn {pointer-events:none}`
export const predefinedClassNames = {
'pointerEvents:auto': '__style_pea',
'pointerEvents:box-none': '__style_pebn',
'pointerEvents:box-only': '__style_pebo',
'pointerEvents:none': '__style_pen'
const styleAsClassName = {
display: {
'flex': DISPLAY_FLEX_CLASSNAME
},
pointerEvents: {
'auto': POINTER_EVENTS_AUTO_CLASSNAME,
'box-none': POINTER_EVENTS_BOX_NONE_CLASSNAME,
'box-only': POINTER_EVENTS_BOX_ONLY_CLASSNAME,
'none': POINTER_EVENTS_NONE_CLASSNAME
}
}
export const mapStyleToClassName = (prop, value) => {
return styleAsClassName[prop] && styleAsClassName[prop][value]
}
// reset unwanted styles beyond the control of React inline styles
const resetCSS =
'/* React Native */\n' +
'html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}\n' +
'body {margin:0}\n' +
'button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}\n' +
'input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display:none}'
const helperCSS =
// vendor prefix 'display:flex' until React supports fallback values for inline styles
`.${DISPLAY_FLEX_CLASSNAME} {display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}\n` +
// implement React Native's pointer event values
`.${POINTER_EVENTS_AUTO_CLASSNAME}, .${POINTER_EVENTS_BOX_ONLY_CLASSNAME}, .${POINTER_EVENTS_BOX_NONE_CLASSNAME} * {pointer-events:auto}\n` +
`.${POINTER_EVENTS_NONE_CLASSNAME}, .${POINTER_EVENTS_BOX_ONLY_CLASSNAME} *, .${POINTER_EVENTS_NONE_CLASSNAME} {pointer-events:none}`
export const defaultStyles = `${resetCSS}\n${helperCSS}`

View File

@@ -109,7 +109,7 @@ suite('apis/UIManager', () => {
assert.equal(node.getAttribute('class'), 'existing extra')
})
test('adds new style to existing style', () => {
test('adds correct DOM styles to existing style', () => {
const node = createNode({ color: 'red' })
const props = { style: { marginVertical: 0, opacity: 0 } }
UIManager.updateView(node, props, componentStub)

View File

@@ -35,7 +35,6 @@ const UIManager = {
updateView(node, props, component /* only needed to surpress React errors in __DEV__ */) {
for (const prop in props) {
let nativeProp
const value = props[prop]
switch (prop) {
@@ -48,12 +47,13 @@ const UIManager = {
)
break
case 'class':
case 'className':
nativeProp = 'class'
case 'className': {
const nativeProp = 'class'
// prevent class names managed by React Native from being replaced
const className = node.getAttribute(nativeProp) + ' ' + value
node.setAttribute(nativeProp, className)
break
}
case 'text':
case 'value':
// native platforms use `text` prop to replace text input value

View File

@@ -135,7 +135,7 @@ class TextInput extends Component {
onFocus: this._handleFocus,
onSelect: onSelectionChange && this._handleSelectionChange,
readOnly: !editable,
style: { ...styles.input, ...textStyles, outline: style.outline },
style: [ styles.input, textStyles, { outline: style.outline } ],
value
}

View File

@@ -1,6 +1,7 @@
/* eslint-env mocha */
import assert from 'assert'
import includes from 'lodash/includes'
import React from 'react'
import { shallow } from 'enzyme'
import View from '../'
@@ -14,7 +15,7 @@ suite('components/View', () => {
test('prop "pointerEvents"', () => {
const view = shallow(<View pointerEvents='box-only' />)
assert.equal(view.prop('className'), '__style_pebo')
assert.ok(includes(view.prop('className'), '__style_pebo') === true)
})
test('prop "style"', () => {

View File

@@ -0,0 +1,45 @@
/* eslint-disable */
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativePropRegistry
* @flow
*/
'use strict';
const emptyObject = {};
const objects = {};
let uniqueID = 1;
class ReactNativePropRegistry {
static register(object: Object): number {
let id = ++uniqueID;
if (process.env.NODE_ENV !== 'production') {
Object.freeze(object);
}
objects[id] = object;
return id;
}
static getByID(id: number): Object {
if (!id) {
// Used in the style={[condition && id]} pattern,
// we want it to be a no-op when the value is false or null
return emptyObject;
}
const object = objects[id];
if (!object) {
console.warn('Invalid style with id `' + id + '`. Skipping ...');
return emptyObject;
}
return object;
}
}
module.exports = ReactNativePropRegistry;

View File

@@ -10,9 +10,9 @@
*/
import assert from 'assert'
import flattenStyle from '../flattenStyle'
import flattenStyle from '..'
suite('apis/StyleSheet/flattenStyle', () => {
suite('modules/flattenStyle', () => {
test('should merge style objects', () => {
const style1 = {opacity: 1}
const style2 = {order: 2}

View File

@@ -0,0 +1,47 @@
/* eslint-disable */
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule flattenStyle
* @flow
*/
'use strict';
var ReactNativePropRegistry = require('../ReactNativePropRegistry');
var invariant = require('fbjs/lib/invariant');
function getStyle(style) {
if (typeof style === 'number') {
return ReactNativePropRegistry.getByID(style);
}
return style;
}
function flattenStyle(style: ?StyleObj): ?Object {
if (!style) {
return undefined;
}
invariant(style !== true, 'style may be false but not true');
if (!Array.isArray(style)) {
return getStyle(style);
}
var result = {};
for (var i = 0, styleLength = style.length; i < styleLength; ++i) {
var computedStyle = flattenStyle(style[i]);
if (computedStyle) {
for (var key in computedStyle) {
result[key] = computedStyle[key];
}
}
}
return result;
}
module.exports = flattenStyle;