mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-23 23:06:24 +00:00
[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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) => <style dangerouslySetInnerHTML={{ __html: style }} id={StyleSheet.elementId} />
|
||||
const styleAsTagString = (style) => `<style id="${StyleSheet.elementId}">${style}</style>`
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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 = `<style id='${StyleSheet.elementId}'>${StyleSheet._renderToString()}</style>`
|
||||
})
|
||||
|
||||
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 = `<style id='${StyleSheet.elementId}'>${StyleSheet.renderToString()}</style>`
|
||||
|
||||
// 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;}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)' }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' ])
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ const styles = StyleSheet.create({
|
||||
font: 'inherit',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
textDecoration: 'none',
|
||||
textDecorationLine: 'none',
|
||||
wordWrap: 'break-word'
|
||||
},
|
||||
singleLineStyle: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user