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

feat(useMediaQuery): add disable as an option #1572

Merged
merged 1 commit into from
Sep 21, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -165,7 +167,7 @@ const Playground = () => {
}}
right
>
Change
Switch
</Button>
<MediaQuery when={query}>
<Code>when</Code>
Expand Down Expand Up @@ -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)')
...
})
```
18 changes: 15 additions & 3 deletions packages/dnb-eufemia/src/shared/MediaQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export type MediaQueryProperties = {
*/
not?: boolean

/**
* If set to true, no MediaQuery will be used.
*/
disabled?: boolean

/**
* For debugging
*/
Expand All @@ -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

Expand Down Expand Up @@ -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
}

Expand All @@ -142,7 +154,7 @@ export function makeMediaQueryList(
const mediaQueryList = window.matchMedia(mediaQueryString)

if (log) {
console.log('mediaQueryString', mediaQueryString)
console.log('MediaQuery:', mediaQueryString)
}

return mediaQueryList
Expand Down
4 changes: 2 additions & 2 deletions packages/dnb-eufemia/src/shared/__tests__/MediaQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ describe('MediaQuery', () => {
})

afterEach(() => {
matchMedia.clear()
matchMedia?.clear()
})

afterAll(() => {
matchMedia.destroy()
matchMedia?.destroy()
})

it('should match for query with medium width', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -14,11 +16,11 @@ export function mockMediaQuery() {
})

afterEach(() => {
matchMedia.clear()
matchMedia?.clear()
})

afterAll(() => {
matchMedia.destroy()
matchMedia?.destroy()
})

return matchMedia
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@
*/

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'),
isMatchMediaSupported: jest.fn(),
}))

describe('useMediaQuery', () => {
let matchMedia
let matchMedia: MatchMediaMock

beforeAll(() => {
matchMedia = new MatchMediaMock()
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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)
})
})
22 changes: 15 additions & 7 deletions packages/dnb-eufemia/src/shared/useMediaQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -30,7 +35,11 @@ export default function useMediaQuery(props: MediaQueryProps) {
const [match, matchUpdate] = React.useState(matches)

const listenerRef = React.useRef<MediaQueryListener>()
React.useEffect(() => {
React.useLayoutEffect(() => {
if (disabled) {
return // stop here
}

if (typeof listenerRef.current === 'function') {
listenerRef.current()

Expand All @@ -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
}