diff --git a/src/enhance-props.ts b/src/enhance-props.ts index 4d5487f..5ef4748 100644 --- a/src/enhance-props.ts +++ b/src/enhance-props.ts @@ -3,7 +3,7 @@ import expandAliases from './expand-aliases' import * as cache from './cache' import * as styles from './styles' import { Without } from './types/box-types' -import { EnhancerProps } from './types/enhancers' +import { BoxPropValue, EnhancerProps } from './types/enhancers' type PreservedProps = Without, keyof EnhancerProps> @@ -12,25 +12,26 @@ interface EnhancedPropsResult { enhancedProps: PreservedProps } -function noAnd(s: string): string { - return s.replace(/&/g, '') -} +const SELECTORS_PROP = 'selectors' /** * Converts the CSS props to class names and inserts the styles. */ export default function enhanceProps( props: EnhancerProps & React.ComponentPropsWithoutRef, - selectorHead = '' + selectorHead = '', + parentProperty = '' ): EnhancedPropsResult { const propsMap = expandAliases(props) const preservedProps: PreservedProps = {} let className: string = props.className || '' for (const [property, value] of propsMap) { - if (value && typeof value === 'object') { + const isSelectorOrChildProp = property === SELECTORS_PROP || parentProperty === SELECTORS_PROP + // Only attempt to process objects for the `selectors` prop or the individual selectors below it + if (isObject(value) && isSelectorOrChildProp) { const prop = property === 'selectors' ? '' : property - const parsed = enhanceProps(value, noAnd(selectorHead + prop)) + const parsed = enhanceProps(value, noAnd(selectorHead + prop), property) className = `${className} ${parsed.className}` continue } @@ -67,3 +68,7 @@ export default function enhanceProps( return { className, enhancedProps: preservedProps } } + +const isObject = (value: BoxPropValue | object): value is object => value != null && typeof value === 'object' + +const noAnd = (value: string): string => value.replace(/&/g, '') diff --git a/test/box.tsx b/test/box.tsx index 17eeff8..1a1d40d 100644 --- a/test/box.tsx +++ b/test/box.tsx @@ -1,12 +1,12 @@ import test from 'ava' -import React from 'react' +import React, { CSSProperties } from 'react' import * as render from 'react-test-renderer' -import {shallow} from 'enzyme' +import { shallow } from 'enzyme' import * as sinon from 'sinon' import Box from '../src/box' import * as styles from '../src/styles' import allPropertiesComponent from '../tools/all-properties-component' -import {propNames} from '../src/enhancers' +import { propNames } from '../src/enhancers' test.afterEach.always(() => { styles.clear() @@ -58,7 +58,7 @@ test('is prop allows changing the component type', t => { }) test('ref gets forwarded', t => { - const node = {domNode: true} + const node = { domNode: true } const ref = sinon.spy() render.create(, { createNodeMock() { @@ -82,3 +82,29 @@ test('maintains the original className', t => { const component = shallow() t.true(component.hasClass('derp')) }) + +test('renders with style prop', t => { + const expected: CSSProperties = { backgroundColor: 'red' } + + const component = shallow() + + t.deepEqual(component.prop('style'), expected) +}) + +test('renders with arbitrary non-enhancer props', t => { + interface CustomComponentProps { + foo: string + baz: number + fizz: { + buzz: boolean + } + } + + const CustomComponent: React.FC = props => {JSON.stringify(props, undefined, 4)} + + const component = shallow() + + t.is(component.prop('foo'), 'bar') + t.is(component.prop('baz'), 123) + t.deepEqual(component.prop('fizz'), { buzz: true }) +}) diff --git a/test/enhance-props.ts b/test/enhance-props.ts index d06f640..5adbab1 100644 --- a/test/enhance-props.ts +++ b/test/enhance-props.ts @@ -9,19 +9,19 @@ test.afterEach.always(() => { }) test.serial('enhances a prop', t => { - const {className, enhancedProps} = enhanceProps({width: 10}) + const { className, enhancedProps } = enhanceProps({ width: 10 }) t.is(className, 'ub-w_10px') t.deepEqual(enhancedProps, {}) }) test.serial('expands aliases', t => { - const {className, enhancedProps} = enhanceProps({margin: 11}) + const { className, enhancedProps } = enhanceProps({ margin: 11 }) t.is(className, 'ub-mb_11px ub-ml_11px ub-mr_11px ub-mt_11px') t.deepEqual(enhancedProps, {}) }) test.serial('injects styles', t => { - enhanceProps({width: 12}) + enhanceProps({ width: 12 }) t.is( styles.getAll(), ` @@ -32,8 +32,8 @@ test.serial('injects styles', t => { }) test.serial('uses the cache', t => { - enhanceProps({width: 13}) - enhanceProps({width: 13}) + enhanceProps({ width: 13 }) + enhanceProps({ width: 13 }) t.is( styles.getAll(), ` @@ -45,32 +45,43 @@ test.serial('uses the cache', t => { }) test.serial('strips falsey enhancer props', t => { - const {className, enhancedProps} = enhanceProps({width: false}) + const { className, enhancedProps } = enhanceProps({ width: false }) t.is(className, '') t.deepEqual(enhancedProps, {}) }) test.serial('does not strip enhancer props with 0 values', t => { - const {className, enhancedProps} = enhanceProps({width: 0}) + const { className, enhancedProps } = enhanceProps({ width: 0 }) t.is(className, 'ub-w_0px') t.deepEqual(enhancedProps, {}) }) test.serial('passes through non-enhancer props', t => { - const {className, enhancedProps} = enhanceProps({disabled: true}) + const expected = { disabled: true, foo: 'bar', baz: 123, fizz: { buzz: true } } + + const { className, enhancedProps } = enhanceProps(expected) + t.is(className, '') - t.deepEqual(enhancedProps, {disabled: true}) + t.deepEqual(enhancedProps, expected) }) test.serial('passes through falsey non-enhancer props', t => { - const {className, enhancedProps} = enhanceProps({disabled: false}) + const { className, enhancedProps } = enhanceProps({ disabled: false }) t.is(className, '') - t.deepEqual(enhancedProps, {disabled: false}) + t.deepEqual(enhancedProps, { disabled: false }) }) test.serial('handles invalid values', t => { // @ts-ignore - const {className, enhancedProps} = enhanceProps({minWidth: true}) + const { className, enhancedProps } = enhanceProps({ minWidth: true }) t.is(className, '') t.deepEqual(enhancedProps, {}) }) + +test.serial('preserves style prop', t => { + const expected = { style: { backgroundColor: 'red' } } + + const { enhancedProps } = enhanceProps(expected) + + t.deepEqual(enhancedProps, expected) +}) diff --git a/tools/story.tsx b/tools/story.tsx index 12f8857..b9ed054 100644 --- a/tools/story.tsx +++ b/tools/story.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { CSSProperties } from 'react' import { default as Box, configureSafeHref } from '../src' import { storiesOf } from '@storybook/react' import allPropertiesComponent from './all-properties-component' @@ -209,11 +209,24 @@ storiesOf('Box', module) React ref )) - .add('props pass through', () => ( - - - - )) + .add('props pass through', () => { + interface CustomComponentProps { + foo: string + baz: number + fizz: { + buzz: boolean + } + } + + const CustomComponent: React.FC = props => {JSON.stringify(props, undefined, 4)} + + return ( + + + + + ) + }) .add('all properties', () => ( {allPropertiesComponent()} @@ -256,3 +269,7 @@ storiesOf('Box', module) ) }) + .add('style prop', () => { + const style: CSSProperties = { backgroundColor: 'red', width: 200 } + return {JSON.stringify(style, undefined, 4)} + })