Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/enhance-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<React.ComponentProps<any>, keyof EnhancerProps>

Expand All @@ -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<any>,
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
}
Expand Down Expand Up @@ -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, '')
34 changes: 30 additions & 4 deletions test/box.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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(<Box ref={ref} />, {
createNodeMock() {
Expand All @@ -82,3 +82,29 @@ test('maintains the original className', t => {
const component = shallow(<Box className="derp" margin="10px" />)
t.true(component.hasClass('derp'))
})

test('renders with style prop', t => {
const expected: CSSProperties = { backgroundColor: 'red' }

const component = shallow(<Box style={expected} />)

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<CustomComponentProps> = props => <code>{JSON.stringify(props, undefined, 4)}</code>

const component = shallow(<Box is={CustomComponent} foo="bar" baz={123} fizz={{ buzz: true }} />)

t.is(component.prop('foo'), 'bar')
t.is(component.prop('baz'), 123)
t.deepEqual(component.prop('fizz'), { buzz: true })
})
35 changes: 23 additions & 12 deletions test/enhance-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
`
Expand All @@ -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(),
`
Expand All @@ -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)
})
29 changes: 23 additions & 6 deletions tools/story.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -209,11 +209,24 @@ storiesOf('Box', module)
<Box ref={reactRef}>React ref</Box>
</Box>
))
.add('props pass through', () => (
<Box>
<Box is="input" type="file" />
</Box>
))
.add('props pass through', () => {
interface CustomComponentProps {
foo: string
baz: number
fizz: {
buzz: boolean
}
}

const CustomComponent: React.FC<CustomComponentProps> = props => <code>{JSON.stringify(props, undefined, 4)}</code>

return (
<Box display="flex" flexDirection="column">
<Box is="input" type="file" />
<Box is={CustomComponent} foo="bar" baz={123} fizz={{ buzz: true }} />
</Box>
)
})
.add('all properties', () => (
<Box>
{allPropertiesComponent()}
Expand Down Expand Up @@ -256,3 +269,7 @@ storiesOf('Box', module)
</Box>
)
})
.add('style prop', () => {
const style: CSSProperties = { backgroundColor: 'red', width: 200 }
return <Box style={style}>{JSON.stringify(style, undefined, 4)}</Box>
})