Skip to content
Open
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
1 change: 1 addition & 0 deletions examples/all.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('Components', () => {
require('./components/Composition/rendering/responsive-props/InclusiveNotch.test')
require('./components/Composition/rendering/responsive-props/BreakpointSpecific.test')
require('./components/Composition/rendering/responsive-props/BreakpointEdges.test')
require('./components/Composition/rendering/responsive-props/Down.test')
})
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'
import { Box } from 'atomic-layout'

const DownResponsiveProps = () => (
<Box
padding={10}
paddingSmDown={20}
paddingMdDown={30}
paddingLgDown={40}
paddingXlDown={50}
>
Content
</Box>
)

export default DownResponsiveProps
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
it('Down', () => {
cy.loadStory(
['components', 'composition', 'rendering', 'responsive-props'],
['down'],
)

const assertPadding = (value) => {
return cy.get('#element').should('have.css', 'padding', value)
}

assertPadding('10px')
cy.setBreakpoint('sm').then(() => assertPadding('20px'))
cy.setBreakpoint('md').then(() => assertPadding('30px'))
cy.setBreakpoint('lg').then(() => assertPadding('40px'))
cy.setBreakpoint('xl').then(() => assertPadding('50px'))
})
2 changes: 2 additions & 0 deletions examples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ import BreakpointEdges from './components/Composition/rendering/responsive-props
import MobileFirstResponsiveProps from './components/Composition/rendering/responsive-props/MobileFirst'
import BreakpointSpecificResponsiveProps from './components/Composition/rendering/responsive-props/BreakpointSpecific'
import InclusiveNotchResponsiveProps from './components/Composition/rendering/responsive-props/InclusiveNotch'
import DownResponsiveProps from './components/Composition/rendering/responsive-props/Down'
storiesOf('Components|Composition/Rendering/Responsive props', module)
.add('Breakpoint edges', BreakpointEdges)
.add('Mobile-first', MobileFirstResponsiveProps)
.add('Breakpoint-specific', BreakpointSpecificResponsiveProps)
.add('Inclusive-notch', InclusiveNotchResponsiveProps)
.add('Down', DownResponsiveProps)

/**
* Only
Expand Down
2 changes: 1 addition & 1 deletion src/const/propAliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sanitizeTemplateArea from '@utils/strings/sanitizeTemplateArea'

type ValueTransformer<I, R> = (val: I) => R

interface PropAliasDeclaration {
export interface PropAliasDeclaration {
props: string[]
transformValue?: ValueTransformer<Numeric, string>
}
Expand Down
53 changes: 53 additions & 0 deletions src/utils/styles/applyStyles/applyStyles.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import applyStyles from './applyStyles'

describe('applyStyles', () => {
describe('given arbitrary component props', () => {
const props = {
unknownProp: '20px',
padding: null,
margin: undefined,

gap: 10,
templateCols: '250px',
flexDirection: 'column',
justifyContentMd: 'flex-start',
placeLgDown: 'baseline',
alignItemsSmOnly: 'center',
}
const css = applyStyles(props)

it('should ignore unknown props', () => {
expect(css).not.toContain('uknownProp')
})

it('should ignore known props with falsy values', () => {
expect(css).not.toContain('padding')
expect(css).not.toContain('margin')
})

it('should produce CSS properties based on known props', () => {
expect(css).toContain('flex-direction:column')
})

it('should apply custom transformation function to known props', () => {
expect(css).toContain('grid-gap:10px')
expect(css).toContain('grid-template-columns:250px')
})

it('should wrap responsive prop with "up" behavior in media query', () => {
expect(css).toContain(
'@media (min-width:768px) {justify-content:flex-start;}',
)
})

it('should wrap responsive prop with "down" behavior in media query', () => {
expect(css).toContain('@media (max-width:1199px) {place-self:baseline;}')
})

it('should wrap responsive prop with "only" behavior in media query', () => {
expect(css).toContain(
'@media (min-width:576px) and (max-width:767px) {align-items:center;}',
)
})
})
})
80 changes: 63 additions & 17 deletions src/utils/styles/applyStyles/applyStyles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Layout from '@src/Layout'
import { BreakpointBehavior } from '@const/defaultOptions'
import propAliases from '@const/propAliases'
import { BreakpointBehavior, Breakpoint } from '@const/defaultOptions'
import propAliases, { PropAliasDeclaration } from '@const/propAliases'
import parsePropName, { Props } from '@utils/strings/parsePropName'
import isset from '@utils/functions/isset'
import createMediaQuery from '../createMediaQuery'
Expand Down Expand Up @@ -28,30 +28,76 @@ const createStyleString = (
: styleProps
}

interface PropAliasGroup {
propAlias: PropAliasDeclaration
records: Array<{
propValue: any
breakpoint: Breakpoint
behavior: BreakpointBehavior
}>
}

/**
* Produces a CSS string based on the given component props.
* Takes only known prop aliases, ignores all the other props.
*/
export default function applyStyles(pristineProps: Props): string {
return (
Object.keys(pristineProps)
// Parse each prop to include "breakpoint" and "behavior"
.map(parsePropName)
// Filter out props that are not included in prop aliases
.filter(({ purePropName }) => propAliases.hasOwnProperty(purePropName))
// Filter out props with "undefined" or "null" as value
.filter(({ originPropName }) => isset(pristineProps[originPropName]))
// Map each prop to a CSS string
.map(({ purePropName, originPropName, breakpoint, behavior }) => {
const { props, transformValue } = propAliases[purePropName]
const propValue = pristineProps[originPropName]
// First, split pritstine component's props into prop alias groups.
// This allows to operate with each prop alias with all its records at once.
const propAliasGroups = Object.entries(pristineProps)
// Filter out props with "undefined" or "null" as a value.
.filter(([_, propValue]) => isset(propValue))
.reduce<Record<string, PropAliasGroup>>(
(groups, [pristinePropName, pristinePropValue]) => {
const { purePropName, breakpoint, behavior } = parsePropName(
pristinePropName,
)
const propAlias = propAliases[purePropName]

// Filter out props that are not in the known prop aliases.
if (!propAlias) {
return groups
}

const prevRecords = groups[purePropName]
? groups[purePropName].records
: []
const nextRecords = prevRecords.concat({
breakpoint,
behavior,
propValue: pristinePropValue,
})
const groupItem: PropAliasGroup = {
propAlias,
records: nextRecords,
}

return {
...groups,
[purePropName]: groupItem,
}
},
{},
)

return Object.entries(propAliasGroups)
.reduce<string[]>((css, [_, propAliasGroup]) => {
const { propAlias, records } = propAliasGroup
const { props, transformValue } = propAlias

const styles = records.map(({ breakpoint, behavior, propValue }) => {
const transformedPropValue = transformValue
? transformValue(propValue)
: propValue

return createStyleString(
props,
transformedPropValue,
breakpoint,
behavior,
)
})
.join(' ')
)

return css.concat(styles)
}, [])
.join(' ')
}