From 29b6e0dcde902feeb17e631f84524bf1e0f3b3f9 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 2 Jan 2020 15:07:21 +0100 Subject: [PATCH 1/5] @atomic-layout/core: Exports "ParsedProp" and "ParsedBreakpoint" --- packages/atomic-layout-core/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/atomic-layout-core/src/index.ts b/packages/atomic-layout-core/src/index.ts index 16a58548..329f4f55 100644 --- a/packages/atomic-layout-core/src/index.ts +++ b/packages/atomic-layout-core/src/index.ts @@ -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, From e52dc31895cce9486326ff451b7fba304dad7533 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 2 Jan 2020 15:07:39 +0100 Subject: [PATCH 2/5] atomic-layout: Disables submodule import tslint rule --- packages/atomic-layout/tslint.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/atomic-layout/tslint.json b/packages/atomic-layout/tslint.json index b584b11c..895ca9c8 100644 --- a/packages/atomic-layout/tslint.json +++ b/packages/atomic-layout/tslint.json @@ -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 From 778d54cc5b5c62d3783fb1a1afa78ddbe7b8be12 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 2 Jan 2020 15:08:01 +0100 Subject: [PATCH 3/5] atomic-layout: Adds shorthand "jest" command --- packages/atomic-layout/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/atomic-layout/package.json b/packages/atomic-layout/package.json index 482874a9..ce2b9143 100644 --- a/packages/atomic-layout/package.json +++ b/packages/atomic-layout/package.json @@ -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", "test": "yarn test:unit && yarn test:e2e", "test:unit": "cross-env BABEL_ENV=test jest --runInBand", "test:e2e": "yarn cypress:cli", From db1835149f5260a9fa91a629a55494e2bc8b9a61 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 2 Jan 2020 15:08:32 +0100 Subject: [PATCH 4/5] useResponsiveProps: Fixes default breakpoint props assignment on SSR --- .../src/hooks/useResponsiveComponent.spec.tsx | 34 ++++++++++ .../src/hooks/useResponsiveProps.ts | 65 +++++++++++++------ 2 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 packages/atomic-layout/src/hooks/useResponsiveComponent.spec.tsx diff --git a/packages/atomic-layout/src/hooks/useResponsiveComponent.spec.tsx b/packages/atomic-layout/src/hooks/useResponsiveComponent.spec.tsx new file mode 100644 index 00000000..e692f85b --- /dev/null +++ b/packages/atomic-layout/src/hooks/useResponsiveComponent.spec.tsx @@ -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 +}) + +describe('useResponsiveComponent', () => { + describe('given rendered on a server', () => { + let html: ReturnType + + beforeAll(() => { + html = renderToString( + , + ) + }) + + 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') + }) + }) +}) diff --git a/packages/atomic-layout/src/hooks/useResponsiveProps.ts b/packages/atomic-layout/src/hooks/useResponsiveProps.ts index 198ebc3c..13c0aa65 100644 --- a/packages/atomic-layout/src/hooks/useResponsiveProps.ts +++ b/packages/atomic-layout/src/hooks/useResponsiveProps.ts @@ -3,10 +3,48 @@ 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 = ( + props: Record, + matcher: MatcherFunction = defaultMatcher, +) => { + return Object.keys(props) + .map(parsePropName) + .filter(matcher) + .reduce( + (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. @@ -14,30 +52,15 @@ import useBreakpointChange from './useBreakpointChange' const useResponsiveProps = >( responsiveProps: ResponsiveProps, ): Partial => { - const [props, setProps] = useState() + const [props, setProps] = useState( + filterProps(responsiveProps, ({ breakpoint }) => { + return breakpoint.isDefault && typeof window === 'undefined' + }), + ) const [breakpointName, setBreakpointName] = useState() 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( - (acc, { originPropName, purePropName }) => ({ - ...acc, - [purePropName]: inputProps[originPropName], - }), - {} as ResponsiveProps, - ) - - return nextProps + return filterProps(inputProps) } // Store the current breakpoint name in the state. From c5754e953546dd570e9ef191d26489aa18051015 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 2 Jan 2020 15:37:12 +0100 Subject: [PATCH 5/5] useResponsiveProps: Separates server-side matcher --- .../src/hooks/useResponsiveProps.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/atomic-layout/src/hooks/useResponsiveProps.ts b/packages/atomic-layout/src/hooks/useResponsiveProps.ts index 13c0aa65..0aed1382 100644 --- a/packages/atomic-layout/src/hooks/useResponsiveProps.ts +++ b/packages/atomic-layout/src/hooks/useResponsiveProps.ts @@ -25,6 +25,19 @@ const defaultMatcher: MatcherFunction = (parsedProp) => { 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. @@ -53,9 +66,7 @@ const useResponsiveProps = >( responsiveProps: ResponsiveProps, ): Partial => { const [props, setProps] = useState( - filterProps(responsiveProps, ({ breakpoint }) => { - return breakpoint.isDefault && typeof window === 'undefined' - }), + filterProps(responsiveProps, serverMatcher), ) const [breakpointName, setBreakpointName] = useState()