diff --git a/src/components/CoreComponent/index.js b/src/components/CoreComponent/index.js
new file mode 100644
index 00000000..f1ce55a5
--- /dev/null
+++ b/src/components/CoreComponent/index.js
@@ -0,0 +1,3 @@
+import CoreComponent from './modules/CoreComponent'
+
+export default CoreComponent
diff --git a/src/components/CoreComponent/modules/CoreComponent.js b/src/components/CoreComponent/modules/CoreComponent.js
new file mode 100644
index 00000000..6a319d66
--- /dev/null
+++ b/src/components/CoreComponent/modules/CoreComponent.js
@@ -0,0 +1,42 @@
+import React, { PropTypes } from 'react'
+import restyle from './restyle'
+import stylePropTypes from './stylePropTypes'
+
+class CoreComponent extends React.Component {
+ static propTypes = {
+ className: PropTypes.string,
+ component: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.string
+ ]),
+ style: PropTypes.object,
+ testID: PropTypes.string
+ }
+
+ static stylePropTypes = stylePropTypes;
+
+ static defaultProps = {
+ className: '',
+ component: 'div'
+ }
+
+ render() {
+ const {
+ className,
+ component: Component,
+ style,
+ testID,
+ ...other
+ } = this.props
+
+ return (
+
+ )
+ }
+}
+
+export default CoreComponent
diff --git a/src/components/CoreComponent/modules/autoprefix.js b/src/components/CoreComponent/modules/autoprefix.js
new file mode 100644
index 00000000..6216a7a3
--- /dev/null
+++ b/src/components/CoreComponent/modules/autoprefix.js
@@ -0,0 +1,47 @@
+export default function prefixStyles(style) {
+ if (style.hasOwnProperty('flexBasis')) {
+ style = {
+ WebkitFlexBasis: style.flexBasis,
+ msFlexBasis: style.flexBasis,
+ ...style
+ }
+ }
+
+ if (style.hasOwnProperty('flexGrow')) {
+ style = {
+ WebkitBoxFlex: style.flexGrow,
+ WebkitFlexGrow: style.flexGrow,
+ msFlexPositive: style.flexGrow,
+ ...style
+ }
+ }
+
+ if (style.hasOwnProperty('flexShrink')) {
+ style = {
+ WebkitFlexShrink: style.flexShrink,
+ msFlexNegative: style.flexShrink,
+ ...style
+ }
+ }
+
+ // NOTE: adding `;` to the string value prevents React from automatically
+ // adding a `px` suffix to the unitless value
+ if (style.hasOwnProperty('order')) {
+ style = {
+ WebkitBoxOrdinalGroup: `${parseInt(style.order, 10) + 1};`,
+ WebkitOrder: `${style.order}`,
+ msFlexOrder: `${style.order}`,
+ ...style
+ }
+ }
+
+ if (style.hasOwnProperty('transform')) {
+ style = {
+ WebkitTransform: style.transform,
+ msTransform: style.transform,
+ ...style
+ }
+ }
+
+ return style
+}
diff --git a/src/components/CoreComponent/modules/restyle.js b/src/components/CoreComponent/modules/restyle.js
new file mode 100644
index 00000000..f60acbfc
--- /dev/null
+++ b/src/components/CoreComponent/modules/restyle.js
@@ -0,0 +1,40 @@
+import autoprefix from './autoprefix'
+import styles from '../../../modules/styles'
+
+/**
+ * Get the HTML class that corresponds to a style declaration
+ * @param prop {string} prop name
+ * @param style {Object} style
+ * @return {string} class name
+ */
+function getSinglePurposeClassName(prop, style) {
+ const className = `${prop}-${style[prop]}`
+ if (style.hasOwnProperty(prop) && styles[className]) {
+ return styles[className]
+ }
+}
+
+/**
+ * Replace inline styles with single purpose classes where possible
+ * @param props {Object} React Element properties
+ * @return {Object}
+ */
+export default function stylingStrategy(props) {
+ let className
+ let style = {}
+
+ const classList = [ props.className ]
+ for (const prop in props.style) {
+ const styleClass = getSinglePurposeClassName(prop, props.style)
+ if (styleClass) {
+ classList.push(styleClass)
+ } else {
+ style[prop] = props.style[prop]
+ }
+ }
+
+ className = classList.join(' ')
+ style = autoprefix(style)
+
+ return { className: className, style }
+}
diff --git a/src/components/CoreComponent/modules/stylePropTypes.js b/src/components/CoreComponent/modules/stylePropTypes.js
new file mode 100644
index 00000000..6c18a6b8
--- /dev/null
+++ b/src/components/CoreComponent/modules/stylePropTypes.js
@@ -0,0 +1,104 @@
+import { PropTypes } from 'react'
+
+const numberOrString = PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string
+])
+
+const { string } = PropTypes
+
+export default {
+ alignContent: string,
+ alignItems: string,
+ alignSelf: string,
+ backfaceVisibility: string,
+ // background
+ backgroundAttachment: string,
+ backgroundClip: string,
+ backgroundColor: string,
+ backgroundImage: string,
+ backgroundOrigin: string,
+ backgroundPosition: string,
+ backgroundRepeat: string,
+ backgroundSize: string,
+ // border color
+ borderColor: numberOrString,
+ borderBottomColor: numberOrString,
+ borderLeftColor: numberOrString,
+ borderRightColor: numberOrString,
+ borderTopColor: numberOrString,
+ // border-radius
+ borderRadius: numberOrString,
+ borderTopLeftRadius: numberOrString,
+ borderTopRightRadius: numberOrString,
+ borderBottomLeftRadius: numberOrString,
+ borderBottomRightRadius: numberOrString,
+ // border style
+ borderStyle: numberOrString,
+ borderBottomStyle: numberOrString,
+ borderLeftStyle: numberOrString,
+ borderRightStyle: numberOrString,
+ borderTopStyle: numberOrString,
+ // border width
+ borderWidth: numberOrString,
+ borderBottomWidth: numberOrString,
+ borderLeftWidth: numberOrString,
+ borderRightWidth: numberOrString,
+ borderTopWidth: numberOrString,
+ bottom: numberOrString,
+ boxSizing: string,
+ clear: string,
+ color: string,
+ direction: string,
+ display: string,
+ flexBasis: string,
+ flexDirection: string,
+ flexGrow: numberOrString,
+ flexShrink: numberOrString,
+ flexWrap: string,
+ float: string,
+ font: string,
+ fontFamily: string,
+ fontSize: string,
+ fontStyle: string,
+ fontWeight: string,
+ height: numberOrString,
+ justifyContent: string,
+ left: numberOrString,
+ letterSpacing: string,
+ lineHeight: numberOrString,
+ // margin
+ margin: numberOrString,
+ marginBottom: numberOrString,
+ marginLeft: numberOrString,
+ marginRight: numberOrString,
+ marginTop: numberOrString,
+ // min/max
+ maxHeight: numberOrString,
+ maxWidth: numberOrString,
+ minHeight: numberOrString,
+ minWidth: numberOrString,
+ opacity: numberOrString,
+ order: numberOrString,
+ overflow: string,
+ overflowX: string,
+ overflowY: string,
+ // padding
+ padding: numberOrString,
+ paddingBottom: numberOrString,
+ paddingLeft: numberOrString,
+ paddingRight: numberOrString,
+ paddingTop: numberOrString,
+ position: string,
+ right: numberOrString,
+ textAlign: string,
+ textDecoration: string,
+ textTransform: string,
+ top: numberOrString,
+ userSelect: string,
+ visibility: string,
+ whiteSpace: string,
+ width: numberOrString,
+ wordWrap: string,
+ zIndex: numberOrString
+}
diff --git a/src/components/View/ViewStylePropTypes.js b/src/components/View/ViewStylePropTypes.js
new file mode 100644
index 00000000..ce80621a
--- /dev/null
+++ b/src/components/View/ViewStylePropTypes.js
@@ -0,0 +1,85 @@
+import { pickProps } from '../../modules/filterObjectProps'
+import CoreComponent from '../CoreComponent'
+
+export default {
+ ...pickProps(CoreComponent.stylePropTypes, [
+ 'alignContent',
+ 'alignItems',
+ 'alignSelf',
+ 'backfaceVisibility',
+ // background
+ 'backgroundAttachment',
+ 'backgroundClip',
+ 'backgroundColor',
+ 'backgroundImage',
+ 'backgroundPosition',
+ 'backgroundOrigin',
+ 'backgroundRepeat',
+ 'backgroundSize',
+ // border-color
+ 'borderColor',
+ 'borderTopColor',
+ 'borderRightColor',
+ 'borderBottomColor',
+ 'borderLeftColor',
+ // border-radius
+ 'borderRadius',
+ 'borderTopLeftRadius',
+ 'borderTopRightRadius',
+ 'borderBottomLeftRadius',
+ 'borderBottomRightRadius',
+ // border style
+ 'borderStyle',
+ 'borderBottomStyle',
+ 'borderLeftStyle',
+ 'borderRightStyle',
+ 'borderTopStyle',
+ // border width
+ 'borderWidth',
+ 'borderBottomWidth',
+ 'borderLeftWidth',
+ 'borderRightWidth',
+ 'borderTopWidth',
+ 'bottom',
+ 'boxShadow',
+ 'boxSizing',
+ 'flexBasis',
+ 'flexDirection',
+ 'flexGrow',
+ 'flexShrink',
+ 'flexWrap',
+ 'height',
+ 'justifyContent',
+ 'left',
+ // margin
+ 'margin',
+ 'marginBottom',
+ 'marginLeft',
+ 'marginRight',
+ 'marginTop',
+ // max/min
+ 'maxHeight',
+ 'maxWidth',
+ 'minHeight',
+ 'minWidth',
+ 'opacity',
+ 'order',
+ 'overflow',
+ 'overflowX',
+ 'overflowY',
+ // padding
+ 'padding',
+ 'paddingBottom',
+ 'paddingLeft',
+ 'paddingRight',
+ 'paddingTop',
+ 'position',
+ 'right',
+ 'top',
+ 'transform',
+ 'userSelect',
+ 'visibility',
+ 'width',
+ 'zIndex'
+ ])
+}
diff --git a/src/components/View/index.js b/src/components/View/index.js
new file mode 100644
index 00000000..dbb53a0a
--- /dev/null
+++ b/src/components/View/index.js
@@ -0,0 +1,72 @@
+import { pickProps } from '../../modules/filterObjectProps'
+import CoreComponent from '../CoreComponent'
+import React, { PropTypes } from 'react'
+import ViewStylePropTypes from './ViewStylePropTypes'
+
+const styles = {
+ // https://github.com/facebook/css-layout#default-values
+ initial: {
+ alignItems: 'stretch',
+ borderWidth: 0,
+ borderStyle: 'solid',
+ boxSizing: 'border-box',
+ display: 'flex',
+ flexBasis: 'auto',
+ flexDirection: 'column',
+ flexShrink: 0,
+ listStyle: 'none',
+ margin: 0,
+ padding: 0,
+ position: 'relative',
+ // button reset
+ backgroundColor: 'transparent',
+ color: 'inherit',
+ font: 'inherit',
+ textAlign: 'inherit'
+ }
+}
+
+class View extends React.Component {
+ static propTypes = {
+ accessibilityLabel: PropTypes.string,
+ children: PropTypes.any,
+ component: CoreComponent.propTypes.component,
+ pointerEvents: PropTypes.oneOf([
+ 'auto',
+ 'box-none',
+ 'box-only',
+ 'none'
+ ]),
+ style: PropTypes.shape(ViewStylePropTypes),
+ testID: CoreComponent.propTypes.testID
+ }
+
+ static stylePropTypes = ViewStylePropTypes
+
+ static defaultProps = {
+ component: 'div',
+ style: styles.initial
+ }
+
+ render() {
+ const { accessibilityLabel, pointerEvents, style, testID, ...other } = this.props
+ const pointerEventsStyle = pointerEvents && { pointerEvents }
+ const resolvedStyle = pickProps(style, Object.keys(ViewStylePropTypes))
+
+ return (
+
+ )
+ }
+}
+
+export default View
diff --git a/src/modules/View/index.spec.js b/src/components/View/index.spec.js
similarity index 72%
rename from src/modules/View/index.spec.js
rename to src/components/View/index.spec.js
index f9957fd3..d0e2954e 100644
--- a/src/modules/View/index.spec.js
+++ b/src/components/View/index.spec.js
@@ -1,7 +1,6 @@
import assert from 'assert'
import React from 'react/addons'
-import { ViewDefaultStyle } from './ViewStylePropTypes'
import View from '.'
const ReactTestUtils = React.addons.TestUtils
@@ -20,6 +19,14 @@ suite('View', () => {
assert.equal((root.tagName).toLowerCase(), 'div')
})
+ test('prop "accessibilityLabel"', () => {
+ const accessibilityLabel = 'accessibilityLabel'
+ const result = ReactTestUtils.renderIntoDocument()
+ const root = React.findDOMNode(result)
+
+ assert.equal(root.getAttribute('aria-label'), accessibilityLabel)
+ })
+
test('prop "children"', () => {
const children = 'children'
const result = shallowRender({children})
@@ -27,16 +34,6 @@ suite('View', () => {
assert.equal(result.props.children, children)
})
- test('prop "className"', () => {
- const className = 'className'
- const result = shallowRender()
-
- assert.ok(
- (result.props.className).indexOf(className) > -1,
- '"className" was not transferred'
- )
- })
-
test('prop "component"', () => {
const type = 'a'
const result = ReactTestUtils.renderIntoDocument()
@@ -62,8 +59,7 @@ suite('View', () => {
const initial = shallowRender()
assert.deepEqual(
initial.props.style,
- ViewDefaultStyle,
- 'default "style" is incorrect'
+ View.defaultProps.style
)
const unsupported = shallowRender()
@@ -73,4 +69,15 @@ suite('View', () => {
'unsupported "style" properties must not be transferred'
)
})
+
+ test('prop "testID"', () => {
+ const testID = 'Example.view'
+ const result = ReactTestUtils.renderIntoDocument()
+ const root = React.findDOMNode(result)
+
+ assert.equal(
+ root.getAttribute('data-testid'),
+ testID
+ )
+ })
})
diff --git a/src/modules/View/ViewStylePropTypes.js b/src/modules/View/ViewStylePropTypes.js
deleted file mode 100644
index f8f5c2a4..00000000
--- a/src/modules/View/ViewStylePropTypes.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { PropTypes } from 'react'
-import { StylePropTypes } from '../react-native-web-style'
-
-export default {
- ...StylePropTypes.BackgroundPropTypes,
- ...StylePropTypes.BorderThemePropTypes,
- ...StylePropTypes.LayoutPropTypes,
- boxShadow: PropTypes.string,
- opacity: PropTypes.number,
- transform: PropTypes.string
-}
-
-// https://github.com/facebook/css-layout#default-values
-export const ViewDefaultStyle = {
- alignItems: 'stretch',
- borderWidth: 0,
- borderStyle: 'solid',
- boxSizing: 'border-box',
- display: 'flex',
- flexBasis: 'auto',
- flexDirection: 'column',
- flexShrink: 0,
- listStyle: 'none',
- margin: 0,
- padding: 0,
- position: 'relative'
-}
diff --git a/src/modules/View/index.js b/src/modules/View/index.js
deleted file mode 100644
index 0499390a..00000000
--- a/src/modules/View/index.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { pickProps } from '../filterObjectProps'
-import { WebStyleComponent } from '../react-native-web-style'
-import React, { PropTypes } from 'react'
-import ViewStylePropTypes, { ViewDefaultStyle } from './ViewStylePropTypes'
-
-class View extends React.Component {
- static propTypes = {
- children: PropTypes.any,
- className: PropTypes.string,
- component: PropTypes.oneOfType([
- PropTypes.func,
- PropTypes.string
- ]),
- pointerEvents: PropTypes.oneOf([
- 'auto',
- 'box-none',
- 'box-only',
- 'none'
- ]),
- style: PropTypes.shape(ViewStylePropTypes)
- }
-
- static defaultProps = {
- className: '',
- component: 'div'
- }
-
- render() {
- const { className, pointerEvents, style, ...other } = this.props
- const filteredStyle = pickProps(style, Object.keys(ViewStylePropTypes))
- const pointerEventsStyle = pointerEvents && { pointerEvents }
- const mergedStyle = {
- ...ViewDefaultStyle,
- ...filteredStyle,
- ...pointerEventsStyle
- }
-
- return (
-
- )
- }
-}
-
-export default View