From 924dc36d4a1e04f16313f5e63351f9688027f27c Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sun, 20 Mar 2016 10:24:14 -0700 Subject: [PATCH] [fix] refactor StyleSheet **Problem** StyleSheet's implementation was overly complex. It required `flattenStyle` to use `expandStyle`, and couldn't support mapping React Native style props to CSS properties without also exposing those CSS properties in the API. **Response** - `flattenStyle` is concerned only with flattening style objects. - `StyleSheetRegistry` is responsible for registering styles, mapping the React Native style prop to DOM props, and generating the CSS for the backing style element. - `StyleSheetRegistry` uses a simpler approach to caching styles and generating style sheet strings. It also drops the unobfuscated class names from development mode, as the React Dev Tools can provide a better debugging experience (pending a fix to allow props/styles to be changed from the dev tools). - `StyleSheet` will fall back to inline styles if it doesn't think a style sheet has been rendered into the document. The relationship is currently only implicit. This should be revisited. - `StyleSheet` exports `renderToString` as part of the documented API. - Fix processing of `transformMatrix` and add tests for `processTransform`. - Fix `input[type=search]` rendering in Safari by using `display:none` on its pseudo-elements. - Add support for `textDecorationLine` and `textAlignVertical`. - Note the `View` hack to conditionally apply the `flex-shrink:0` reset from css-layout. This is required because React Native's approach to resolving `style` is to give precendence to long-hand styles (e.g., `flexShrink`) over short-hand styles (e.g., `flex`). This means the `View` reset overrides any `flex:1` declaration. To get around this, `flexShrink` is only set in `View` if `flex` is not set. --- docs/apis/StyleSheet.md | 10 +- src/apis/AppRegistry/renderApplication.js | 2 +- src/apis/StyleSheet/Store.js | 99 --------------- src/apis/StyleSheet/StyleSheetRegistry.js | 84 ++++++++++--- src/apis/StyleSheet/StyleSheetValidation.js | 3 +- src/apis/StyleSheet/__tests__/Store-test.js | 116 ------------------ .../__tests__/StyleSheetRegistry-test.js | 55 +++++++++ src/apis/StyleSheet/__tests__/index-test.js | 51 +++----- .../__tests__/processTransform-test.js | 31 +++++ src/apis/StyleSheet/expandStyle.js | 22 ++-- src/apis/StyleSheet/flattenStyle.js | 5 +- src/apis/StyleSheet/index.js | 37 +++--- src/apis/StyleSheet/predefs.js | 16 +-- src/apis/StyleSheet/processTransform.js | 3 +- src/components/Text/TextStylePropTypes.js | 13 +- src/components/Text/index.js | 2 +- src/components/View/index.js | 12 +- 17 files changed, 241 insertions(+), 320 deletions(-) delete mode 100644 src/apis/StyleSheet/Store.js delete mode 100644 src/apis/StyleSheet/__tests__/Store-test.js create mode 100644 src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js create mode 100644 src/apis/StyleSheet/__tests__/processTransform-test.js diff --git a/docs/apis/StyleSheet.md b/docs/apis/StyleSheet.md index c85a64e0..654a151a 100644 --- a/docs/apis/StyleSheet.md +++ b/docs/apis/StyleSheet.md @@ -11,12 +11,18 @@ outside of the render loop and are applied as inline styles. Read more about to Each key of the object passed to `create` must define a style object. +**flatten**: function + +Flattens an array of styles into a single style object. + +**renderToString**: function + +Returns a string of CSS used to style the application. + ## Properties **hairlineWidth**: number -**flatten**: function - ## Example ```js diff --git a/src/apis/AppRegistry/renderApplication.js b/src/apis/AppRegistry/renderApplication.js index ada932d4..39fadd05 100644 --- a/src/apis/AppRegistry/renderApplication.js +++ b/src/apis/AppRegistry/renderApplication.js @@ -13,7 +13,7 @@ import ReactDOMServer from 'react-dom/server' import ReactNativeApp from './ReactNativeApp' import StyleSheet from '../../apis/StyleSheet' -const renderStyleSheetToString = () => StyleSheet._renderToString() +const renderStyleSheetToString = () => StyleSheet.renderToString() const styleAsElement = (style) => ` diff --git a/src/apis/StyleSheet/Store.js b/src/apis/StyleSheet/Store.js deleted file mode 100644 index 72d12b90..00000000 --- a/src/apis/StyleSheet/Store.js +++ /dev/null @@ -1,99 +0,0 @@ -import prefixAll from 'inline-style-prefix-all' -import hyphenate from './hyphenate' - -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 key = this._getDeclarationKey(property, value) - return this._classNames[key] - } - - set(property, value) { - if (value != null) { - const values = this._getPropertyValues(property) || [] - if (values.indexOf(value) === -1) { - values.push(value) - this._setClassName(property, value) - 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 = prefixAll({ [property]: value }) - const cssDeclarations = Object.keys(declarations).reduce((str, prop) => { - const value = declarations[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 => 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}` - } - } - } -} - -module.exports = Store diff --git a/src/apis/StyleSheet/StyleSheetRegistry.js b/src/apis/StyleSheet/StyleSheetRegistry.js index 90e00ef1..97b01d87 100644 --- a/src/apis/StyleSheet/StyleSheetRegistry.js +++ b/src/apis/StyleSheet/StyleSheetRegistry.js @@ -7,42 +7,94 @@ */ import prefixAll from 'inline-style-prefix-all' +import hyphenate from './hyphenate' +import expandStyle from './expandStyle' import flattenStyle from './flattenStyle' import processTransform from './processTransform' +import { predefinedClassNames } from './predefs' + +let stylesCache = {} +let uniqueID = 0 + +const getCacheKey = (prop, value) => `${prop}:${value}` + +const normalizeStyle = (style) => { + return processTransform(expandStyle(flattenStyle(style))) +} + +const createCssDeclarations = (style) => { + return Object.keys(style).map((prop) => { + const property = hyphenate(prop) + const value = style[prop] + return `${property}:${value};` + }).sort().join('') +} class StyleSheetRegistry { - static registerStyle(style: Object, store): number { + /* 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 normalizedStyle = processTransform(flattenStyle(style)) + const normalizedStyle = normalizeStyle(style) + Object.keys(normalizedStyle).forEach((prop) => { - // add each declaration to the store - store.set(prop, normalizedStyle[prop]) + const value = normalizedStyle[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(style, store) { - let _className - let _style = {} + static getStyleAsNativeProps(styleSheetObject, canUseCSS = false) { const classList = [] - const normalizedStyle = processTransform(flattenStyle(style)) + const normalizedStyle = normalizeStyle(styleSheetObject) + let style = {} for (const prop in normalizedStyle) { - let styleClass = store.get(prop, normalizedStyle[prop]) + const value = normalizedStyle[prop] + const cacheKey = getCacheKey(prop, value) + let selector = stylesCache[cacheKey] && stylesCache[cacheKey].id || predefinedClassNames[cacheKey] - if (styleClass) { - classList.push(styleClass) + if (selector && canUseCSS) { + classList.push(selector) } else { - _style[prop] = normalizedStyle[prop] + style[prop] = normalizedStyle[prop] } } - _className = classList.join(' ') - _style = prefixAll(_style) - - return { className: _className, style: _style } + return { + className: classList.join(' '), + style: prefixAll(style) + } } } diff --git a/src/apis/StyleSheet/StyleSheetValidation.js b/src/apis/StyleSheet/StyleSheetValidation.js index d792f26d..86655265 100644 --- a/src/apis/StyleSheet/StyleSheetValidation.js +++ b/src/apis/StyleSheet/StyleSheetValidation.js @@ -63,8 +63,7 @@ StyleSheetValidation.addValidStylePropTypes({ direction: PropTypes.string, /* @private */ float: PropTypes.oneOf([ 'left', 'none', 'right' ]), font: PropTypes.string, /* @private */ - listStyle: PropTypes.string, - verticalAlign: PropTypes.string + listStyle: PropTypes.string }) module.exports = StyleSheetValidation diff --git a/src/apis/StyleSheet/__tests__/Store-test.js b/src/apis/StyleSheet/__tests__/Store-test.js deleted file mode 100644 index f2ca4c78..00000000 --- a/src/apis/StyleSheet/__tests__/Store-test.js +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-env mocha */ - -import assert from 'assert' -import Store from '../Store' - -suite('apis/StyleSheet/Store', () => { - suite('the constructor', () => { - test('initialState', () => { - const initialState = { classNames: { 'textAlign:center': '__classname__' } } - const store = new Store(initialState) - assert.deepEqual(store._classNames['textAlign:center'], '__classname__') - }) - }) - - suite('#get', () => { - test('returns a declaration-specific className', () => { - const initialState = { - classNames: { - 'textAlign:center': '__expected__', - 'textAlign:left': '__error__' - } - } - const store = new Store(initialState) - assert.deepEqual(store.get('textAlign', 'center'), '__expected__') - }) - }) - - suite('#set', () => { - test('stores declarations', () => { - const store = new Store() - store.set('textAlign', 'center') - store.set('marginTop', 0) - store.set('marginTop', 1) - store.set('marginTop', 2) - assert.deepEqual(store._declarations, { - textAlign: [ 'center' ], - marginTop: [ 0, 1, 2 ] - }) - }) - - test('human-readable classNames', () => { - const store = new Store() - store.set('textAlign', 'center') - store.set('marginTop', 0) - store.set('marginTop', 1) - store.set('marginTop', 2) - assert.deepEqual(store._classNames, { - 'textAlign:center': 'textAlign:center', - 'marginTop:0': 'marginTop:0', - 'marginTop:1': 'marginTop:1', - 'marginTop:2': 'marginTop:2' - }) - }) - - test('obfuscated classNames', () => { - const store = new Store({}, { obfuscateClassNames: true }) - store.set('textAlign', 'center') - store.set('marginTop', 0) - store.set('marginTop', 1) - store.set('marginTop', 2) - assert.deepEqual(store._classNames, { - 'textAlign:center': '_s_1', - 'marginTop:0': '_s_2', - 'marginTop:1': '_s_3', - 'marginTop:2': '_s_4' - }) - }) - - test('replaces space characters', () => { - const store = new Store() - - store.set('backgroundPosition', 'top left') - assert.equal(store.get('backgroundPosition', 'top left'), 'backgroundPosition\:top-left') - }) - }) - - suite('#toString', () => { - test('human-readable style sheet', () => { - const store = new Store() - store.set('textAlign', 'center') - store.set('backgroundColor', 'rgba(0,0,0,0)') - store.set('color', '#fff') - store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif') - store.set('marginBottom', '0px') - store.set('width', '100%') - - const expected = '/* 6 unique declarations */\n' + - '.backgroundColor\\:rgba\\(0\\,0\\,0\\,0\\){background-color:rgba(0,0,0,0);}\n' + - '.color\\:\\#fff{color:#fff;}\n' + - '.fontFamily\\:\\"Helvetica-Neue\\"\\,-Arial\\,-sans-serif{font-family:"Helvetica Neue", Arial, sans-serif;}\n' + - '.marginBottom\\:0px{margin-bottom:0px;}\n' + - '.textAlign\\:center{text-align:center;}\n' + - '.width\\:100\\%{width:100%;}' - - assert.equal(store.toString(), expected) - }) - - test('obfuscated style sheet', () => { - const store = new Store({}, { obfuscateClassNames: true }) - store.set('textAlign', 'center') - store.set('marginBottom', '0px') - store.set('margin', '1px') - store.set('margin', '2px') - store.set('margin', '3px') - - const expected = '/* 5 unique declarations */\n' + - '._s_3{margin:1px;}\n' + - '._s_4{margin:2px;}\n' + - '._s_5{margin:3px;}\n' + - '._s_2{margin-bottom:0px;}\n' + - '._s_1{text-align:center;}' - - assert.equal(store.toString(), expected) - }) - }) -}) diff --git a/src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js b/src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js new file mode 100644 index 00000000..44581302 --- /dev/null +++ b/src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js @@ -0,0 +1,55 @@ +/* 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__/index-test.js b/src/apis/StyleSheet/__tests__/index-test.js index cfbbad37..fa8de14a 100644 --- a/src/apis/StyleSheet/__tests__/index-test.js +++ b/src/apis/StyleSheet/__tests__/index-test.js @@ -4,7 +4,7 @@ import { resetCSS, predefinedCSS } from '../predefs' import assert from 'assert' import StyleSheet from '..' -const styles = { root: { borderWidth: 1 } } +const styles = { root: { opacity: 1 } } suite('apis/StyleSheet', () => { setup(() => { @@ -12,55 +12,40 @@ suite('apis/StyleSheet', () => { }) suite('create', () => { - const div = document.createElement('div') - - setup(() => { - document.body.appendChild(div) - StyleSheet.create(styles) - div.innerHTML = `` - }) - - teardown(() => { - document.body.removeChild(div) - }) - test('returns styles object', () => { assert.equal(StyleSheet.create(styles), styles) }) test('updates already-rendered style sheet', () => { - StyleSheet.create({ root: { color: 'red' } }) + // setup + const div = document.createElement('div') + document.body.appendChild(div) + StyleSheet.create(styles) + div.innerHTML = `` + // test + StyleSheet.create({ root: { color: 'red' } }) assert.equal( document.getElementById(StyleSheet.elementId).textContent, `${resetCSS}\n${predefinedCSS}\n` + - `/* 5 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;}\n` + - `.color\\:red{color:red;}` + `/* 2 unique declarations */\n` + + `.__style1{opacity:1;}\n` + + `.__style2{color:red;}` ) + + // teardown + document.body.removeChild(div) }) }) - test('resolve', () => { - const props = { style: styles.root } - const expected = { className: 'borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} } + test('renderToString', () => { StyleSheet.create(styles) - assert.deepEqual(StyleSheet.resolve(props), expected) - }) - test('_renderToString', () => { - StyleSheet.create(styles) assert.equal( - StyleSheet._renderToString(), + 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;}` + `/* 1 unique declarations */\n` + + `.__style1{opacity:1;}` ) }) }) diff --git a/src/apis/StyleSheet/__tests__/processTransform-test.js b/src/apis/StyleSheet/__tests__/processTransform-test.js new file mode 100644 index 00000000..35d5966e --- /dev/null +++ b/src/apis/StyleSheet/__tests__/processTransform-test.js @@ -0,0 +1,31 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import processTransform from '../processTransform' + +suite('apis/StyleSheet/processTransform', () => { + test('transform', () => { + const style = { + transform: [ + { scaleX: 20 }, + { rotate: '20deg' } + ] + } + + assert.deepEqual( + processTransform(style), + { transform: 'scaleX(20) rotate(20deg)' } + ) + }) + + test('transformMatrix', () => { + const style = { + transformMatrix: [ 1, 2, 3, 4, 5, 6 ] + } + + assert.deepEqual( + processTransform(style), + { transform: 'matrix3d(1,2,3,4,5,6)' } + ) + }) +}) diff --git a/src/apis/StyleSheet/expandStyle.js b/src/apis/StyleSheet/expandStyle.js index 2287b2ff..7899d327 100644 --- a/src/apis/StyleSheet/expandStyle.js +++ b/src/apis/StyleSheet/expandStyle.js @@ -8,16 +8,19 @@ const styleShortHands = { margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ], marginHorizontal: [ 'marginRight', 'marginLeft' ], marginVertical: [ 'marginTop', 'marginBottom' ], + overflow: [ 'overflowX', 'overflowY' ], padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ], paddingHorizontal: [ 'paddingRight', 'paddingLeft' ], paddingVertical: [ 'paddingTop', 'paddingBottom' ], + textDecorationLine: [ 'textDecoration' ], writingDirection: [ 'direction' ] } /** - * Alpha-sort properties, apart from shorthands which appear before the - * properties they expand into. This ensures that more specific styles override - * the shorthands, whatever the order in which they were originally declared. + * Alpha-sort properties, apart from shorthands – they must appear before the + * longhand properties that they expand into. This lets more specific styles + * override less specific styles, whatever the order in which they were + * originally declared. */ const sortProps = (propsArray) => propsArray.sort((a, b) => { const expandedA = styleShortHands[a] @@ -41,14 +44,17 @@ const expandStyle = (style) => { const expandedProps = styleShortHands[key] const value = normalizeValue(key, style[key]) - if (expandedProps) { - expandedProps.forEach((prop, i) => { - resolvedStyle[expandedProps[i]] = value - }) - } else if (key === 'flex') { + // React Native treats `flex:1` like `flex:1 1 auto` + if (key === 'flex') { resolvedStyle.flexGrow = value resolvedStyle.flexShrink = 1 resolvedStyle.flexBasis = 'auto' + } else if (key === 'textAlignVertical') { + resolvedStyle.verticalAlign = (value === 'center' ? 'middle' : value) + } else if (expandedProps) { + expandedProps.forEach((prop, i) => { + resolvedStyle[expandedProps[i]] = value + }) } else { resolvedStyle[key] = value } diff --git a/src/apis/StyleSheet/flattenStyle.js b/src/apis/StyleSheet/flattenStyle.js index 1e89d53e..4794b6ac 100644 --- a/src/apis/StyleSheet/flattenStyle.js +++ b/src/apis/StyleSheet/flattenStyle.js @@ -6,7 +6,6 @@ * @flow */ import invariant from 'fbjs/lib/invariant' -import expandStyle from './expandStyle' module.exports = function flattenStyle(style): ?Object { if (!style) { @@ -16,9 +15,7 @@ module.exports = function flattenStyle(style): ?Object { invariant(style !== true, 'style may be false but not true') if (!Array.isArray(style)) { - // we must expand styles during the flattening because expanded styles - // override shorthands - return expandStyle(style) + return style } const result = {} diff --git a/src/apis/StyleSheet/index.js b/src/apis/StyleSheet/index.js index dd7d22d9..c17c80c1 100644 --- a/src/apis/StyleSheet/index.js +++ b/src/apis/StyleSheet/index.js @@ -1,6 +1,5 @@ -import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs' +import { resetCSS, predefinedCSS } from './predefs' import flattenStyle from './flattenStyle' -import Store from './Store' import StyleSheetRegistry from './StyleSheetRegistry' import StyleSheetValidation from './StyleSheetValidation' @@ -12,65 +11,61 @@ let lastStyleSheet = '' * 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() /** * Destroy existing styles */ const _destroy = () => { - store = createStore() isRendered = false -} - -/** - * Render the styles as a CSS style sheet - */ -const _renderToString = () => { - const css = store.toString() - isRendered = true - return `${resetCSS}\n${predefinedCSS}\n${css}` + StyleSheetRegistry._reset() } const create = (styles: Object): Object => { for (const key in styles) { StyleSheetValidation.validateStyle(key, styles) - StyleSheetRegistry.registerStyle(styles[key], store) + StyleSheetRegistry.registerStyle(styles[key]) } // update the style sheet in place if (isRendered) { const stylesheet = document.getElementById(ELEMENT_ID) if (stylesheet) { - const newStyleSheet = _renderToString() + const newStyleSheet = renderToString() if (lastStyleSheet !== newStyleSheet) { stylesheet.textContent = newStyleSheet lastStyleSheet = newStyleSheet } } else if (process.env.NODE_ENV !== 'production') { - console.error('ReactNative: cannot find "react-stylesheet" element') + console.error(`ReactNative: cannot find "${ELEMENT_ID}" element`) } } 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 = ({ style = {} }) => { - return StyleSheetRegistry.getStyleAsNativeProps(style, store) + return StyleSheetRegistry.getStyleAsNativeProps(style, isRendered) } module.exports = { _destroy, - _renderToString, create, elementId: ELEMENT_ID, hairlineWidth: 1, flatten: flattenStyle, + renderToString, resolve } diff --git a/src/apis/StyleSheet/predefs.js b/src/apis/StyleSheet/predefs.js index 4913716f..9506c54a 100644 --- a/src/apis/StyleSheet/predefs.js +++ b/src/apis/StyleSheet/predefs.js @@ -2,23 +2,23 @@ * Reset unwanted styles beyond the control of React inline styles */ export const resetCSS = -`/* React Native Web */ +`/* 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 {-webkit-appearance:none}` +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display: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}` +.__style_pea, .__style_pebo, .__style_pebn * {pointer-events:auto} +.__style_pen, .__style_pebo *, .__style_pebn {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' + 'pointerEvents:auto': '__style_pea', + 'pointerEvents:box-none': '__style_pebn', + 'pointerEvents:box-only': '__style_pebo', + 'pointerEvents:none': '__style_pen' } diff --git a/src/apis/StyleSheet/processTransform.js b/src/apis/StyleSheet/processTransform.js index 177e3188..41f33208 100644 --- a/src/apis/StyleSheet/processTransform.js +++ b/src/apis/StyleSheet/processTransform.js @@ -15,7 +15,8 @@ const processTransform = (style) => { if (style.transform) { style.transform = style.transform.map(mapTransform).join(' ') } else if (style.transformMatrix) { - style.transformMatrix = convertTransformMatrix(style.transformMatrix) + style.transform = convertTransformMatrix(style.transformMatrix) + delete style.transformMatrix } } return style diff --git a/src/components/Text/TextStylePropTypes.js b/src/components/Text/TextStylePropTypes.js index 5be12d90..71f464a2 100644 --- a/src/components/Text/TextStylePropTypes.js +++ b/src/components/Text/TextStylePropTypes.js @@ -15,14 +15,17 @@ module.exports = { letterSpacing: numberOrString, lineHeight: numberOrString, textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]), - /** - * @platform web - */ - textDecoration: string, + textAlignVertical: oneOf([ 'auto', 'bottom', 'center', 'top' ]), + textDecorationLine: string, + /* @platform web */ textOverflow: string, + /* @platform web */ textShadow: string, + /* @platform web */ textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]), + /* @platform web */ whiteSpace: string, + /* @platform web */ wordWrap: string, - writingDirection: string + writingDirection: oneOf([ 'auto', 'ltr', 'rtl' ]) } diff --git a/src/components/Text/index.js b/src/components/Text/index.js index be52666f..afe110bb 100644 --- a/src/components/Text/index.js +++ b/src/components/Text/index.js @@ -56,7 +56,7 @@ const styles = StyleSheet.create({ font: 'inherit', margin: 0, padding: 0, - textDecoration: 'none', + textDecorationLine: 'none', wordWrap: 'break-word' }, singleLineStyle: { diff --git a/src/components/View/index.js b/src/components/View/index.js index 89a12f88..4f95cba5 100644 --- a/src/components/View/index.js +++ b/src/components/View/index.js @@ -40,7 +40,8 @@ class View extends Component { }; static defaultProps = { - accessible: true + accessible: true, + style: {} }; constructor(props, context) { @@ -55,6 +56,7 @@ class View extends Component { ...other } = this.props + const flattenedStyle = StyleSheet.flatten(style) const pointerEventsStyle = pointerEvents && { pointerEvents } return ( @@ -73,6 +75,8 @@ class View extends Component { style={[ styles.initial, style, + // 'View' needs to use 'flexShrink' in its reset when there is no 'flex' style provided + flattenedStyle.flex == null && styles.flexReset, pointerEventsStyle ]} /> @@ -104,7 +108,6 @@ const styles = StyleSheet.create({ display: 'flex', flexBasis: 'auto', flexDirection: 'column', - flexShrink: 0, margin: 0, padding: 0, position: 'relative', @@ -113,13 +116,16 @@ const styles = StyleSheet.create({ color: 'inherit', font: 'inherit', textAlign: 'inherit', - textDecoration: 'none', + textDecorationLine: 'none', // list reset listStyle: 'none', // fix flexbox bugs maxWidth: '100%', minHeight: 0, minWidth: 0 + }, + flexReset: { + flexShrink: 0 } })