From 46ca41ad71ccb7c645418b510d33eab8b6069419 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 21 Sep 2022 12:18:46 +0200 Subject: [PATCH] feat(useMediaQuery): add disable as an option --- .../docs/uilib/usage/layout/media-queries.md | 19 ++++++- .../dnb-eufemia/src/shared/MediaQueryUtils.ts | 18 +++++-- .../src/shared/__tests__/MediaQuery.test.tsx | 4 +- ...ediaQueryMocker.js => MediaQueryMocker.ts} | 8 +-- ...iaQuery.test.js => useMediaQuery.test.tsx} | 51 ++++++++++++++++--- .../dnb-eufemia/src/shared/useMediaQuery.tsx | 22 +++++--- 6 files changed, 100 insertions(+), 22 deletions(-) rename packages/dnb-eufemia/src/shared/__tests__/helpers/{MediaQueryMocker.js => MediaQueryMocker.ts} (66%) rename packages/dnb-eufemia/src/shared/__tests__/{useMediaQuery.test.js => useMediaQuery.test.tsx} (74%) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/usage/layout/media-queries.md b/packages/dnb-design-system-portal/src/docs/uilib/usage/layout/media-queries.md index 6f6f053a5de..a27b1fb62b8 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/usage/layout/media-queries.md +++ b/packages/dnb-design-system-portal/src/docs/uilib/usage/layout/media-queries.md @@ -125,6 +125,8 @@ function Component() { } ``` +You can also disable the usage of `window.matchMedia` temporally by providing `disabled: true` as an option. + ### Live example This example uses the `not` property to reverse the behavior. @@ -165,7 +167,7 @@ const Playground = () => { }} right > - Change + Switch when @@ -252,3 +254,18 @@ You can re-use the SASS mixins from Eufemia: ``` Based of the findings of [this article](https://zellwk.com/blog/media-query-units/) and [this webkit bug](https://bugs.webkit.org/show_bug.cgi?id=156684) Eufemia recommends to use `em` units for media query usage to meet the best overall browser support. Read [more about units](/uilib/usage/best-practices/for-styling#units). + +## How to deal with Jest + +You can mock `window.matchMedia` with e.g. [jest-matchmedia-mock](https://www.npmjs.com/package/jest-matchmedia-mock). + +```js +import MatchMediaMock from 'jest-matchmedia-mock' + +const matchMedia = new MatchMediaMock() + +it('your test', () => { + matchMedia.useMediaQuery('(min-width: 50em) and (max-width: 60em)') + ... +}) +``` diff --git a/packages/dnb-eufemia/src/shared/MediaQueryUtils.ts b/packages/dnb-eufemia/src/shared/MediaQueryUtils.ts index 9fdee046244..76b9825f6ef 100644 --- a/packages/dnb-eufemia/src/shared/MediaQueryUtils.ts +++ b/packages/dnb-eufemia/src/shared/MediaQueryUtils.ts @@ -51,6 +51,11 @@ export type MediaQueryProperties = { */ not?: boolean + /** + * If set to true, no MediaQuery will be used. + */ + disabled?: boolean + /** * For debugging */ @@ -64,6 +69,7 @@ export type MediaQueryProps = { * If set to true, it will match and return the given children during SSR. */ matchOnSSR?: boolean + children?: React.ReactNode } & MediaQueryProperties @@ -127,10 +133,16 @@ export const isMatchMediaSupported = (): boolean => * @returns MediaQueryList type */ export function makeMediaQueryList( - { query, when, not = null, log = false }: MediaQueryProperties = {}, + { + query, + when, + not = null, + log = false, + disabled = false, + }: MediaQueryProperties = {}, breakpoints: MediaQueryBreakpoints = null ): MediaQueryList { - if (!isMatchMediaSupported()) { + if (disabled || !isMatchMediaSupported()) { return null } @@ -142,7 +154,7 @@ export function makeMediaQueryList( const mediaQueryList = window.matchMedia(mediaQueryString) if (log) { - console.log('mediaQueryString', mediaQueryString) + console.log('MediaQuery:', mediaQueryString) } return mediaQueryList diff --git a/packages/dnb-eufemia/src/shared/__tests__/MediaQuery.test.tsx b/packages/dnb-eufemia/src/shared/__tests__/MediaQuery.test.tsx index c64d3d0da5e..abbec607d50 100644 --- a/packages/dnb-eufemia/src/shared/__tests__/MediaQuery.test.tsx +++ b/packages/dnb-eufemia/src/shared/__tests__/MediaQuery.test.tsx @@ -32,11 +32,11 @@ describe('MediaQuery', () => { }) afterEach(() => { - matchMedia.clear() + matchMedia?.clear() }) afterAll(() => { - matchMedia.destroy() + matchMedia?.destroy() }) it('should match for query with medium width', () => { diff --git a/packages/dnb-eufemia/src/shared/__tests__/helpers/MediaQueryMocker.js b/packages/dnb-eufemia/src/shared/__tests__/helpers/MediaQueryMocker.ts similarity index 66% rename from packages/dnb-eufemia/src/shared/__tests__/helpers/MediaQueryMocker.js rename to packages/dnb-eufemia/src/shared/__tests__/helpers/MediaQueryMocker.ts index cff38862aad..46ddd2e6095 100644 --- a/packages/dnb-eufemia/src/shared/__tests__/helpers/MediaQueryMocker.js +++ b/packages/dnb-eufemia/src/shared/__tests__/helpers/MediaQueryMocker.ts @@ -1,6 +1,8 @@ -import { isMatchMediaSupported } from '../../MediaQueryUtils' +import { isMatchMediaSupported as _isMatchMediaSupported } from '../../MediaQueryUtils' import MatchMediaMock from 'jest-matchmedia-mock' +const isMatchMediaSupported = _isMatchMediaSupported as jest.Mock + jest.mock('../../MediaQueryUtils', () => ({ ...jest.requireActual('../../MediaQueryUtils'), isMatchMediaSupported: jest.fn(), @@ -14,11 +16,11 @@ export function mockMediaQuery() { }) afterEach(() => { - matchMedia.clear() + matchMedia?.clear() }) afterAll(() => { - matchMedia.destroy() + matchMedia?.destroy() }) return matchMedia diff --git a/packages/dnb-eufemia/src/shared/__tests__/useMediaQuery.test.js b/packages/dnb-eufemia/src/shared/__tests__/useMediaQuery.test.tsx similarity index 74% rename from packages/dnb-eufemia/src/shared/__tests__/useMediaQuery.test.js rename to packages/dnb-eufemia/src/shared/__tests__/useMediaQuery.test.tsx index 2c4ff1ab6d6..1f84be4ab8b 100644 --- a/packages/dnb-eufemia/src/shared/__tests__/useMediaQuery.test.js +++ b/packages/dnb-eufemia/src/shared/__tests__/useMediaQuery.test.tsx @@ -4,11 +4,17 @@ */ import React from 'react' +import { renderHook } from '@testing-library/react-hooks' import { mount } from '../../core/jest/jestSetup' import MatchMediaMock from 'jest-matchmedia-mock' import useMediaQuery from '../useMediaQuery' import Provider from '../Provider' -import { isMatchMediaSupported } from '../MediaQueryUtils' +import { + isMatchMediaSupported as _isMatchMediaSupported, + MediaQueryProps, +} from '../MediaQueryUtils' + +const isMatchMediaSupported = _isMatchMediaSupported as jest.Mock jest.mock('../MediaQueryUtils', () => ({ ...jest.requireActual('../MediaQueryUtils'), @@ -16,7 +22,7 @@ jest.mock('../MediaQueryUtils', () => ({ })) describe('useMediaQuery', () => { - let matchMedia + let matchMedia: MatchMediaMock beforeAll(() => { matchMedia = new MatchMediaMock() @@ -27,16 +33,16 @@ describe('useMediaQuery', () => { }) afterEach(() => { - matchMedia.clear() + matchMedia?.clear() }) afterAll(() => { - matchMedia.destroy() + matchMedia?.destroy() }) - const RenderMediaQueryHook = (props) => { + const RenderMediaQueryHook = (props: MediaQueryProps) => { const match = useMediaQuery(props) - return match ? props.children : null + return <>{match ? props.children : null} } it('should have valid strings inside render', () => { @@ -149,4 +155,37 @@ describe('useMediaQuery', () => { expect(match1Handler).toHaveBeenCalledWith(true) expect(match2Handler).toHaveBeenCalledWith(false) }) + + it('can be disabled', () => { + jest + .spyOn(window, 'matchMedia') + .mockImplementationOnce(jest.fn(window.matchMedia)) + + matchMedia.useMediaQuery('(min-width: 0) and (max-width: 72em)') + + const when = { min: '0', max: 'x-large' } + + const { result: resultA } = renderHook(() => + useMediaQuery({ + when, + }) + ) + + expect(window.matchMedia).toBeCalledTimes(2) + expect(resultA.current).toBe(true) + + jest + .spyOn(window, 'matchMedia') + .mockImplementationOnce(jest.fn(window.matchMedia)) + + const { result: resultB } = renderHook(() => + useMediaQuery({ + disabled: true, + when, + }) + ) + + expect(window.matchMedia).toBeCalledTimes(2) + expect(resultB.current).toBe(false) + }) }) diff --git a/packages/dnb-eufemia/src/shared/useMediaQuery.tsx b/packages/dnb-eufemia/src/shared/useMediaQuery.tsx index aaa4d83a793..6493f5b0454 100644 --- a/packages/dnb-eufemia/src/shared/useMediaQuery.tsx +++ b/packages/dnb-eufemia/src/shared/useMediaQuery.tsx @@ -15,10 +15,15 @@ export type { MediaQueryProps } export default function useMediaQuery(props: MediaQueryProps) { const context = React.useContext(Context) - const { query, when, not, matchOnSSR } = props - let [matches] = React.useState(() => - !isMatchMediaSupported() && isTrue(matchOnSSR) ? true : false - ) + const { query, when, not, matchOnSSR, disabled } = props + + let matches = React.useMemo(() => { + if (disabled) { + return false // stop here + } + + return isTrue(matchOnSSR) && !isMatchMediaSupported() + }, [disabled, matchOnSSR]) const mediaQueryList = React.useRef( makeMediaQueryList(props, context.breakpoints) @@ -30,7 +35,11 @@ export default function useMediaQuery(props: MediaQueryProps) { const [match, matchUpdate] = React.useState(matches) const listenerRef = React.useRef() - React.useEffect(() => { + React.useLayoutEffect(() => { + if (disabled) { + return // stop here + } + if (typeof listenerRef.current === 'function') { listenerRef.current() @@ -47,8 +56,7 @@ export default function useMediaQuery(props: MediaQueryProps) { ) return listenerRef.current - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [query, when, not]) + }, [query, when, not, disabled]) // eslint-disable-line react-hooks/exhaustive-deps return match }