Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useResponsiveProps: Fixes default breakpoint prop value assignment #281

Merged
merged 5 commits into from
Jan 10, 2020
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
6 changes: 5 additions & 1 deletion packages/atomic-layout-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export {
PropAliases,
PropAliasDeclaration,
} from './const/propAliases'
export { default as parsePropName } from './utils/strings/parsePropName'
export {
default as parsePropName,
ParsedProp,
ParsedBreakpoint,
} from './utils/strings/parsePropName'
export { default as parseTemplates } from './utils/templates/parseTemplates'
export {
default as generateComponents,
Expand Down
1 change: 1 addition & 0 deletions packages/atomic-layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"bundlesize:esm": "bundlesize -f lib/esm/index.js",
"cypress": "cypress open --env envName=dev",
"cypress:cli": "cypress run --spec=./examples/all.test.js --browser=chrome --env envName=ci",
"jest": "jest",
kettanaito marked this conversation as resolved.
Show resolved Hide resolved
"test": "yarn test:unit && yarn test:e2e",
"test:unit": "cross-env BABEL_ENV=test jest --runInBand",
"test:e2e": "yarn cypress:cli",
Expand Down
34 changes: 34 additions & 0 deletions packages/atomic-layout/src/hooks/useResponsiveComponent.spec.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', () => {
kettanaito marked this conversation as resolved.
Show resolved Hide resolved
expect(html).toContain('title="Title"')
})

it('should not have any responsive prop with other breakpoints', () => {
expect(html).not.toContain('alt')
})
})
})
76 changes: 55 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,75 @@ 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
}

/**
* Server-side responsive props matcher.
* Apply props with the default breakpoint on the server.
* Server assumes the default breakpoint is currently present.
*
* @TODO Resolve for non-default breakpoints.
* @see https://github.com/kettanaito/atomic-layout/issues/284
*/
const serverMatcher: MatcherFunction = (parsedProp) => {
const { breakpoint } = parsedProp
return breakpoint.isDefault && typeof window === 'undefined'
}

/**
* 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, serverMatcher),
)
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
1 change: 1 addition & 0 deletions packages/atomic-layout/tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": ["tslint-react", "../../tslint.json"],
"rules": {
"no-console": true,
"no-submodule-imports": false,
"jsx-boolean-value": ["never"],
"jsx-no-multiline-js": false,
"jsx-wrap-multiline": false
Expand Down