From 77f72aa129fec8919bf266c8a6ed54da6aecda32 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sun, 10 Jul 2016 17:54:34 -0700 Subject: [PATCH] [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 --- docs/apis/StyleSheet.md | 65 +++++----- docs/guides/rendering.md | 2 +- examples/components/GridView.js | 28 ++--- examples/components/Heading.js | 2 +- examples/index.js | 10 +- .../__tests__/renderApplication-test.js | 8 +- src/apis/AppRegistry/renderApplication.js | 13 +- src/apis/StyleSheet/StyleSheetRegistry.js | 113 ------------------ .../__tests__/StyleSheetRegistry-test.js | 55 --------- .../__tests__/createReactStyleObject-test.js | 13 ++ .../StyleSheet/__tests__/hyphenate-test.js | 16 --- src/apis/StyleSheet/__tests__/index-test.js | 72 ++++++----- .../__tests__/normalizeValue-test.js | 1 + src/apis/StyleSheet/createReactStyleObject.js | 23 +++- src/apis/StyleSheet/expandStyle.js | 7 +- src/apis/StyleSheet/flattenStyle.js | 31 ----- src/apis/StyleSheet/hyphenate.js | 1 - src/apis/StyleSheet/index.js | 111 ++++++++--------- src/apis/StyleSheet/predefs.js | 58 +++++---- src/apis/UIManager/__tests__/index-test.js | 2 +- src/apis/UIManager/index.js | 6 +- src/components/TextInput/index.js | 2 +- src/components/View/__tests__/index-test.js | 3 +- src/modules/ReactNativePropRegistry/index.js | 45 +++++++ .../flattenStyle/__tests__/index-test.js} | 4 +- src/modules/flattenStyle/index.js | 47 ++++++++ 26 files changed, 333 insertions(+), 405 deletions(-) delete mode 100644 src/apis/StyleSheet/StyleSheetRegistry.js delete mode 100644 src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js create mode 100644 src/apis/StyleSheet/__tests__/createReactStyleObject-test.js delete mode 100644 src/apis/StyleSheet/__tests__/hyphenate-test.js delete mode 100644 src/apis/StyleSheet/flattenStyle.js delete mode 100644 src/apis/StyleSheet/hyphenate.js create mode 100644 src/modules/ReactNativePropRegistry/index.js rename src/{apis/StyleSheet/__tests__/flattenStyle-test.js => modules/flattenStyle/__tests__/index-test.js} (94%) create mode 100644 src/modules/flattenStyle/index.js 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;