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