diff --git a/docs/apis/StyleSheet.md b/docs/apis/StyleSheet.md index 654a151a..14ee202e 100644 --- a/docs/apis/StyleSheet.md +++ b/docs/apis/StyleSheet.md @@ -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 `` - 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 = ( ) const html = ReactDOMServer.renderToString(component) - const style = renderStyleSheetToString() - const styleElement = styleAsElement(style) - return { html, style, styleElement } + const styleElement = StyleSheet.render() + return { html, styleElement } } diff --git a/src/apis/StyleSheet/StyleSheetRegistry.js b/src/apis/StyleSheet/StyleSheetRegistry.js deleted file mode 100644 index 984c9697..00000000 --- a/src/apis/StyleSheet/StyleSheetRegistry.js +++ /dev/null @@ -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 diff --git a/src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js b/src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js deleted file mode 100644 index 44581302..00000000 --- a/src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js +++ /dev/null @@ -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) - }) -}) diff --git a/src/apis/StyleSheet/__tests__/createReactStyleObject-test.js b/src/apis/StyleSheet/__tests__/createReactStyleObject-test.js new file mode 100644 index 00000000..f876432b --- /dev/null +++ b/src/apis/StyleSheet/__tests__/createReactStyleObject-test.js @@ -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) + }) +}) diff --git a/src/apis/StyleSheet/__tests__/hyphenate-test.js b/src/apis/StyleSheet/__tests__/hyphenate-test.js deleted file mode 100644 index ca2f1e31..00000000 --- a/src/apis/StyleSheet/__tests__/hyphenate-test.js +++ /dev/null @@ -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') - }) -}) diff --git a/src/apis/StyleSheet/__tests__/index-test.js b/src/apis/StyleSheet/__tests__/index-test.js index 9c67d128..f3601cf5 100644 --- a/src/apis/StyleSheet/__tests__/index-test.js +++ b/src/apis/StyleSheet/__tests__/index-test.js @@ -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 = `` - - // 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' + } } ) }) diff --git a/src/apis/StyleSheet/__tests__/normalizeValue-test.js b/src/apis/StyleSheet/__tests__/normalizeValue-test.js index 9eb6a489..42d91cf2 100644 --- a/src/apis/StyleSheet/__tests__/normalizeValue-test.js +++ b/src/apis/StyleSheet/__tests__/normalizeValue-test.js @@ -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) }) }) diff --git a/src/apis/StyleSheet/createReactStyleObject.js b/src/apis/StyleSheet/createReactStyleObject.js index 4e4e04dd..26a52dd5 100644 --- a/src/apis/StyleSheet/createReactStyleObject.js +++ b/src/apis/StyleSheet/createReactStyleObject.js @@ -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 diff --git a/src/apis/StyleSheet/expandStyle.js b/src/apis/StyleSheet/expandStyle.js index 0b9e454a..f382d252 100644 --- a/src/apis/StyleSheet/expandStyle.js +++ b/src/apis/StyleSheet/expandStyle.js @@ -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, {}) diff --git a/src/apis/StyleSheet/flattenStyle.js b/src/apis/StyleSheet/flattenStyle.js deleted file mode 100644 index 4794b6ac..00000000 --- a/src/apis/StyleSheet/flattenStyle.js +++ /dev/null @@ -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 -} diff --git a/src/apis/StyleSheet/hyphenate.js b/src/apis/StyleSheet/hyphenate.js deleted file mode 100644 index e68b8266..00000000 --- a/src/apis/StyleSheet/hyphenate.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-') diff --git a/src/apis/StyleSheet/index.js b/src/apis/StyleSheet/index.js index a8db0945..9f81e6a9 100644 --- a/src/apis/StyleSheet/index.js +++ b/src/apis/StyleSheet/index.js @@ -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 = () => ` + +/** + * 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 } diff --git a/src/apis/StyleSheet/predefs.js b/src/apis/StyleSheet/predefs.js index 9506c54a..4fa67ddf 100644 --- a/src/apis/StyleSheet/predefs.js +++ b/src/apis/StyleSheet/predefs.js @@ -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}` diff --git a/src/apis/UIManager/__tests__/index-test.js b/src/apis/UIManager/__tests__/index-test.js index 47d8d971..1bd2ac48 100644 --- a/src/apis/UIManager/__tests__/index-test.js +++ b/src/apis/UIManager/__tests__/index-test.js @@ -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) diff --git a/src/apis/UIManager/index.js b/src/apis/UIManager/index.js index 1e5d1013..5f07e4bf 100644 --- a/src/apis/UIManager/index.js +++ b/src/apis/UIManager/index.js @@ -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 diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index e0968420..10fcefbe 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -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 } diff --git a/src/components/View/__tests__/index-test.js b/src/components/View/__tests__/index-test.js index a7e2f9ed..37a44465 100644 --- a/src/components/View/__tests__/index-test.js +++ b/src/components/View/__tests__/index-test.js @@ -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() - assert.equal(view.prop('className'), '__style_pebo') + assert.ok(includes(view.prop('className'), '__style_pebo') === true) }) test('prop "style"', () => { diff --git a/src/modules/ReactNativePropRegistry/index.js b/src/modules/ReactNativePropRegistry/index.js new file mode 100644 index 00000000..b60ba5f0 --- /dev/null +++ b/src/modules/ReactNativePropRegistry/index.js @@ -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; diff --git a/src/apis/StyleSheet/__tests__/flattenStyle-test.js b/src/modules/flattenStyle/__tests__/index-test.js similarity index 94% rename from src/apis/StyleSheet/__tests__/flattenStyle-test.js rename to src/modules/flattenStyle/__tests__/index-test.js index 5d5576d6..830d5f3b 100644 --- a/src/apis/StyleSheet/__tests__/flattenStyle-test.js +++ b/src/modules/flattenStyle/__tests__/index-test.js @@ -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} diff --git a/src/modules/flattenStyle/index.js b/src/modules/flattenStyle/index.js new file mode 100644 index 00000000..7b9265c4 --- /dev/null +++ b/src/modules/flattenStyle/index.js @@ -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;