Skip to content

Commit

Permalink
useResponsiveProps: Fixes default breakpoint props assignment on SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Jan 2, 2020
1 parent a2f3d76 commit 081f909
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 21 deletions.
34 changes: 34 additions & 0 deletions packages/atomic-layout/src/hooks/useResponsiveComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @jest-environment node
*/
import React from 'react'
import { renderToString } from 'react-dom/server'
import useResponsiveComponent from './useResponsiveComponent'

const Component = useResponsiveComponent((props) => {
return <img {...props} />
})

describe('useResponsiveComponent', () => {
describe('given rendered on a server', () => {
let html: ReturnType<typeof renderToString>

beforeAll(() => {
html = renderToString(
<Component src="image.png" altMd="Image" titleLgDown="Title" />,
)
})

it('should have responsive prop with default breakpoint', () => {
expect(html).toContain('src="image.png"')
})

it.skip('should have responsive prop with "down" behavior', () => {
expect(html).toContain('title="Title"')
})

it('should not have any responsive prop with other breakpoints', () => {
expect(html).not.toContain('alt')
})
})
})
65 changes: 44 additions & 21 deletions packages/atomic-layout/src/hooks/useResponsiveProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,64 @@ import {
Numeric,
Layout,
createMediaQuery,
ParsedProp,
parsePropName,
} from '@atomic-layout/core'
import useBreakpointChange from './useBreakpointChange'

type MatcherFunction = (parsedProp: ParsedProp) => boolean

/**
* Default responsive props matcher.
* Creates a media query based on the given prop's breakpoint
* and uses native "window.matchMedia" to assert the match.
*/
const defaultMatcher: MatcherFunction = (parsedProp) => {
const { breakpoint, behavior } = parsedProp
const mediaQuery = createMediaQuery(
Layout.breakpoints[breakpoint.name],
behavior,
)

return matchMedia(mediaQuery).matches
}

/**
* Filters given responsive props against the browser state.
* Accepts an optional matcher function to operate on a server.
*/
const filterProps = <R>(
props: Record<string, any>,
matcher: MatcherFunction = defaultMatcher,
) => {
return Object.keys(props)
.map(parsePropName)
.filter(matcher)
.reduce<R>(
(acc, { originPropName, purePropName }) => ({
...acc,
[purePropName]: props[originPropName],
}),
{} as R,
)
}

/**
* Accepts an object of responsive props and returns
* an object of props relative to the current viewport.
*/
const useResponsiveProps = <ResponsiveProps extends Record<string, Numeric>>(
responsiveProps: ResponsiveProps,
): Partial<ResponsiveProps> => {
const [props, setProps] = useState<ResponsiveProps>()
const [props, setProps] = useState<ResponsiveProps>(
filterProps(responsiveProps, ({ breakpoint }) => {
return breakpoint.isDefault && typeof window === 'undefined'
}),
)
const [breakpointName, setBreakpointName] = useState<string>()

const resolveProps = (inputProps: ResponsiveProps) => {
const nextProps = Object.keys(inputProps)
.map(parsePropName)
.filter(({ breakpoint, behavior }) => {
const mediaQuery = createMediaQuery(
Layout.breakpoints[breakpoint.name],
behavior,
)
const { matches } = matchMedia(mediaQuery)

return matches
})
.reduce<ResponsiveProps>(
(acc, { originPropName, purePropName }) => ({
...acc,
[purePropName]: inputProps[originPropName],
}),
{} as ResponsiveProps,
)

return nextProps
return filterProps<ResponsiveProps>(inputProps)
}

// Store the current breakpoint name in the state.
Expand Down

0 comments on commit 081f909

Please sign in to comment.