From 826e604e0d63ff54fe86ca2fbfd715e1881bc4ee Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 9 May 2022 13:37:21 +0000 Subject: [PATCH 1/5] feat: add `use-media` fork to support react 18 --- packages/use-media/.eslintrc.cjs | 10 ++++ packages/use-media/.npmignore | 5 ++ packages/use-media/README.md | 51 +++++++++++++++++++ packages/use-media/package.json | 31 +++++++++++ packages/use-media/src/index.ts | 1 + packages/use-media/src/types.ts | 4 ++ packages/use-media/src/useMedia.ts | 50 ++++++++++++++++++ .../use-media/src/utilities/camelToHyphen.ts | 5 ++ packages/use-media/src/utilities/index.ts | 3 ++ packages/use-media/src/utilities/noop.ts | 1 + .../src/utilities/queryObjectToString.ts | 30 +++++++++++ 11 files changed, 191 insertions(+) create mode 100644 packages/use-media/.eslintrc.cjs create mode 100644 packages/use-media/.npmignore create mode 100644 packages/use-media/README.md create mode 100644 packages/use-media/package.json create mode 100644 packages/use-media/src/index.ts create mode 100644 packages/use-media/src/types.ts create mode 100644 packages/use-media/src/useMedia.ts create mode 100644 packages/use-media/src/utilities/camelToHyphen.ts create mode 100644 packages/use-media/src/utilities/index.ts create mode 100644 packages/use-media/src/utilities/noop.ts create mode 100644 packages/use-media/src/utilities/queryObjectToString.ts diff --git a/packages/use-media/.eslintrc.cjs b/packages/use-media/.eslintrc.cjs new file mode 100644 index 000000000..a2dbbe3d7 --- /dev/null +++ b/packages/use-media/.eslintrc.cjs @@ -0,0 +1,10 @@ +const { join } = require('path') + +module.exports = { + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { packageDir: [__dirname, join(__dirname, '../../')] }, + ], + }, +} diff --git a/packages/use-media/.npmignore b/packages/use-media/.npmignore new file mode 100644 index 000000000..5600eef5f --- /dev/null +++ b/packages/use-media/.npmignore @@ -0,0 +1,5 @@ +**/__tests__/** +examples/ +src +.eslintrc.cjs +!.npmignore diff --git a/packages/use-media/README.md b/packages/use-media/README.md new file mode 100644 index 000000000..e2002ad42 --- /dev/null +++ b/packages/use-media/README.md @@ -0,0 +1,51 @@ +# `@scaleway/use-media` + +## A small hook to track CSS media query state + +This library has been forked from [use-media](https://github.com/streamich/use-media), many thanks to the original author, [Vadim Dalecky](https://github.com/streamich). + +## Install + +```bash +$ pnpm add @scaleway/use-media +``` + +## Usage + +### With `useEffect` + +```tsx +import { useMedia } from '@scaleway/use-media' + +const App = () => { + // Accepts an object of features to test + const isWide = useMedia({ minWidth: '1000px' }); + // Or a regular media query string + const reduceMotion = useMedia('(prefers-reduced-motion: reduce)'); + + return ( +
+ Screen is wide: {isWide ? '😃' : '😢'} +
+ ); +} +``` + +### With `useLayoutEffect` + +```tsx +import { useMediaLayout } from '@scaleway/use-media' + +const App = () => { + // Accepts an object of features to test + const isWide = useMediaLayout({ minWidth: '1000px' }); + // Or a regular media query string + const reduceMotion = useMediaLayout('(prefers-reduced-motion: reduce)'); + + return ( +
+ Screen is wide: {isWide ? '😃' : '😢'} +
+ ); +} +``` diff --git a/packages/use-media/package.json b/packages/use-media/package.json new file mode 100644 index 000000000..7f513a7f5 --- /dev/null +++ b/packages/use-media/package.json @@ -0,0 +1,31 @@ +{ + "name": "@scaleway/use-media", + "version": "1.0.0", + "description": "A small hook to track CSS media query state", + "keywords": [ + "react", + "reactjs", + "hooks", + "media", + "media queries" + ], + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "browser": { + "dist/index.js": "./dist/index.browser.js" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/scaleway/scaleway-lib", + "directory": "packages/use-media" + }, + "license": "MIT", + "peerDependencies": { + "react": "17.x || 18.x" + } +} diff --git a/packages/use-media/src/index.ts b/packages/use-media/src/index.ts new file mode 100644 index 000000000..359995379 --- /dev/null +++ b/packages/use-media/src/index.ts @@ -0,0 +1 @@ +export { useMedia, useMediaLayout } from './useMedia' diff --git a/packages/use-media/src/types.ts b/packages/use-media/src/types.ts new file mode 100644 index 000000000..1a66d432d --- /dev/null +++ b/packages/use-media/src/types.ts @@ -0,0 +1,4 @@ +import { DependencyList, EffectCallback } from 'react' + +export type Effect = (effect: EffectCallback, deps?: DependencyList) => void +export type MediaQueryObject = { [key: string]: string | number | boolean } diff --git a/packages/use-media/src/useMedia.ts b/packages/use-media/src/useMedia.ts new file mode 100644 index 000000000..f4f5b95bc --- /dev/null +++ b/packages/use-media/src/useMedia.ts @@ -0,0 +1,50 @@ +import { useEffect, useLayoutEffect, useState } from 'react' +import { Effect, MediaQueryObject } from './types' +import { noop, queryObjectToString } from './utilities' + +export const mockMediaQueryList: MediaQueryList = { + addEventListener: noop, + addListener: noop, + dispatchEvent: () => true, + matches: false, + media: '', + onchange: noop, + removeEventListener: noop, + removeListener: noop, +} + +const createUseMedia = + (effect: Effect) => + (rawQuery: string | MediaQueryObject, defaultState = false) => { + const [state, setState] = useState(defaultState) + const query = queryObjectToString(rawQuery) + + effect(() => { + let mounted = true + const mediaQueryList: MediaQueryList = + typeof window === 'undefined' + ? mockMediaQueryList + : window.matchMedia(query) + + const onChange = () => { + if (!mounted) { + return + } + + setState(Boolean(mediaQueryList.matches)) + } + + mediaQueryList.addListener(onChange) + setState(mediaQueryList.matches) + + return () => { + mounted = false + mediaQueryList.removeListener(onChange) + } + }, [query]) + + return state + } + +export const useMedia = createUseMedia(useEffect) +export const useMediaLayout = createUseMedia(useLayoutEffect) diff --git a/packages/use-media/src/utilities/camelToHyphen.ts b/packages/use-media/src/utilities/camelToHyphen.ts new file mode 100644 index 000000000..87c9a168b --- /dev/null +++ b/packages/use-media/src/utilities/camelToHyphen.ts @@ -0,0 +1,5 @@ +export default function camelToHyphen(camelString: string) { + return camelString + .replace(/[A-Z]/g, string => `-${string.toLowerCase()}`) + .toLowerCase() +} diff --git a/packages/use-media/src/utilities/index.ts b/packages/use-media/src/utilities/index.ts new file mode 100644 index 000000000..43aeb6a68 --- /dev/null +++ b/packages/use-media/src/utilities/index.ts @@ -0,0 +1,3 @@ +export { default as camelToHyphen } from './camelToHyphen' +export { default as queryObjectToString } from './queryObjectToString' +export { default as noop } from './noop' diff --git a/packages/use-media/src/utilities/noop.ts b/packages/use-media/src/utilities/noop.ts new file mode 100644 index 000000000..ca6a74471 --- /dev/null +++ b/packages/use-media/src/utilities/noop.ts @@ -0,0 +1 @@ +export default function noop() {} diff --git a/packages/use-media/src/utilities/queryObjectToString.ts b/packages/use-media/src/utilities/queryObjectToString.ts new file mode 100644 index 000000000..ee9aa3332 --- /dev/null +++ b/packages/use-media/src/utilities/queryObjectToString.ts @@ -0,0 +1,30 @@ +import { MediaQueryObject } from '../types' +import camelToHyphen from './camelToHyphen' + +const QUERY_COMBINATOR = ' and ' + +export default function queryObjectToString(query: string | MediaQueryObject) { + if (typeof query === 'string') { + return query + } + + return Object.entries(query) + .map(([feature, value]) => { + const convertedFeature = camelToHyphen(feature) + let convertedValue = value + + if (typeof convertedValue === 'boolean') { + return convertedValue ? convertedFeature : `not ${convertedFeature}` + } + + if ( + typeof convertedValue === 'number' && + /[height|width]$/.test(convertedFeature) + ) { + convertedValue = `${convertedValue}px` + } + + return `(${convertedFeature}: ${convertedValue})` + }) + .join(QUERY_COMBINATOR) +} From a563efe282b20250593e88bcafcc0c6f9b476b14 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 10 May 2022 08:08:17 +0000 Subject: [PATCH 2/5] feat: add simple tests --- packages/use-media/src/__tests__/useMedia.tsx | 18 ++++++++++++++++++ packages/use-media/src/useMedia.ts | 7 ++++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 packages/use-media/src/__tests__/useMedia.tsx diff --git a/packages/use-media/src/__tests__/useMedia.tsx b/packages/use-media/src/__tests__/useMedia.tsx new file mode 100644 index 000000000..25cbc3a77 --- /dev/null +++ b/packages/use-media/src/__tests__/useMedia.tsx @@ -0,0 +1,18 @@ +import { renderHook } from '@testing-library/react-hooks' +import { useMedia } from '..' + +describe('useMedia hook', () => { + it('useMedia should return the result of a query with an object', () => { + const { result } = renderHook(() => useMedia({ minWidth: '1000px' })) + + expect(result.current).toBe(false) + }) + + it('useMedia should return the result of a query with a string', () => { + const { result } = renderHook(() => + useMedia('screen and (min-width: 1000px)'), + ) + + expect(result.current).toBe(false) + }) +}) diff --git a/packages/use-media/src/useMedia.ts b/packages/use-media/src/useMedia.ts index f4f5b95bc..163f4b705 100644 --- a/packages/use-media/src/useMedia.ts +++ b/packages/use-media/src/useMedia.ts @@ -22,7 +22,8 @@ const createUseMedia = effect(() => { let mounted = true const mediaQueryList: MediaQueryList = - typeof window === 'undefined' + typeof window === 'undefined' || + typeof window.matchMedia === 'undefined' ? mockMediaQueryList : window.matchMedia(query) @@ -34,12 +35,12 @@ const createUseMedia = setState(Boolean(mediaQueryList.matches)) } - mediaQueryList.addListener(onChange) + mediaQueryList.addEventListener('change', onChange) setState(mediaQueryList.matches) return () => { mounted = false - mediaQueryList.removeListener(onChange) + mediaQueryList.removeEventListener('change', onChange) } }, [query]) From 861a63d12ee5422d1ed6b64859f8812f0dbf388a Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 10 May 2022 10:16:32 +0000 Subject: [PATCH 3/5] refactor: remove MediaQueryObject to only accept strings query --- packages/use-media/src/__tests__/useMedia.tsx | 6 ---- packages/use-media/src/types.ts | 1 - packages/use-media/src/useMedia.ts | 7 ++--- packages/use-media/src/utilities/index.ts | 1 - .../src/utilities/queryObjectToString.ts | 30 ------------------- 5 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 packages/use-media/src/utilities/queryObjectToString.ts diff --git a/packages/use-media/src/__tests__/useMedia.tsx b/packages/use-media/src/__tests__/useMedia.tsx index 25cbc3a77..a38aa52a6 100644 --- a/packages/use-media/src/__tests__/useMedia.tsx +++ b/packages/use-media/src/__tests__/useMedia.tsx @@ -2,12 +2,6 @@ import { renderHook } from '@testing-library/react-hooks' import { useMedia } from '..' describe('useMedia hook', () => { - it('useMedia should return the result of a query with an object', () => { - const { result } = renderHook(() => useMedia({ minWidth: '1000px' })) - - expect(result.current).toBe(false) - }) - it('useMedia should return the result of a query with a string', () => { const { result } = renderHook(() => useMedia('screen and (min-width: 1000px)'), diff --git a/packages/use-media/src/types.ts b/packages/use-media/src/types.ts index 1a66d432d..d028ef8c5 100644 --- a/packages/use-media/src/types.ts +++ b/packages/use-media/src/types.ts @@ -1,4 +1,3 @@ import { DependencyList, EffectCallback } from 'react' export type Effect = (effect: EffectCallback, deps?: DependencyList) => void -export type MediaQueryObject = { [key: string]: string | number | boolean } diff --git a/packages/use-media/src/useMedia.ts b/packages/use-media/src/useMedia.ts index 163f4b705..ce4d32968 100644 --- a/packages/use-media/src/useMedia.ts +++ b/packages/use-media/src/useMedia.ts @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect, useState } from 'react' -import { Effect, MediaQueryObject } from './types' -import { noop, queryObjectToString } from './utilities' +import { Effect } from './types' +import { noop } from './utilities' export const mockMediaQueryList: MediaQueryList = { addEventListener: noop, @@ -15,9 +15,8 @@ export const mockMediaQueryList: MediaQueryList = { const createUseMedia = (effect: Effect) => - (rawQuery: string | MediaQueryObject, defaultState = false) => { + (query: string, defaultState = false) => { const [state, setState] = useState(defaultState) - const query = queryObjectToString(rawQuery) effect(() => { let mounted = true diff --git a/packages/use-media/src/utilities/index.ts b/packages/use-media/src/utilities/index.ts index 43aeb6a68..a2d8b95e9 100644 --- a/packages/use-media/src/utilities/index.ts +++ b/packages/use-media/src/utilities/index.ts @@ -1,3 +1,2 @@ export { default as camelToHyphen } from './camelToHyphen' -export { default as queryObjectToString } from './queryObjectToString' export { default as noop } from './noop' diff --git a/packages/use-media/src/utilities/queryObjectToString.ts b/packages/use-media/src/utilities/queryObjectToString.ts deleted file mode 100644 index ee9aa3332..000000000 --- a/packages/use-media/src/utilities/queryObjectToString.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MediaQueryObject } from '../types' -import camelToHyphen from './camelToHyphen' - -const QUERY_COMBINATOR = ' and ' - -export default function queryObjectToString(query: string | MediaQueryObject) { - if (typeof query === 'string') { - return query - } - - return Object.entries(query) - .map(([feature, value]) => { - const convertedFeature = camelToHyphen(feature) - let convertedValue = value - - if (typeof convertedValue === 'boolean') { - return convertedValue ? convertedFeature : `not ${convertedFeature}` - } - - if ( - typeof convertedValue === 'number' && - /[height|width]$/.test(convertedFeature) - ) { - convertedValue = `${convertedValue}px` - } - - return `(${convertedFeature}: ${convertedValue})` - }) - .join(QUERY_COMBINATOR) -} From 688959b2c6a0b5d7ce72138326c1733721656a49 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 6 Jun 2022 12:42:03 +0000 Subject: [PATCH 4/5] fix: resolve review --- packages/use-media/src/__tests__/useMedia.tsx | 58 ++++++++++++++++++- packages/use-media/src/useMedia.ts | 5 +- .../use-media/src/utilities/camelToHyphen.ts | 5 -- packages/use-media/src/utilities/index.ts | 2 - packages/use-media/src/utilities/noop.ts | 1 - 5 files changed, 60 insertions(+), 11 deletions(-) delete mode 100644 packages/use-media/src/utilities/camelToHyphen.ts delete mode 100644 packages/use-media/src/utilities/index.ts delete mode 100644 packages/use-media/src/utilities/noop.ts diff --git a/packages/use-media/src/__tests__/useMedia.tsx b/packages/use-media/src/__tests__/useMedia.tsx index a38aa52a6..c2ff3bb8a 100644 --- a/packages/use-media/src/__tests__/useMedia.tsx +++ b/packages/use-media/src/__tests__/useMedia.tsx @@ -2,11 +2,67 @@ import { renderHook } from '@testing-library/react-hooks' import { useMedia } from '..' describe('useMedia hook', () => { - it('useMedia should return the result of a query with a string', () => { + it('should return the result of a query with a string', () => { const { result } = renderHook(() => useMedia('screen and (min-width: 1000px)'), ) expect(result.current).toBe(false) }) + + it('should call onChange', () => { + const mockAddEventListener = (_event: string, callback: () => void) => + callback() + + Object.defineProperty(window, 'matchMedia', { + value: jest.fn().mockImplementation((query: string) => ({ + addEventListener: mockAddEventListener, + addListener: jest.fn(), + dispatchEvent: jest.fn(), + matches: false, + media: query, + onchange: null, + removeEventListener: jest.fn(), + removeListener: jest.fn(), + })), + writable: true, + }) + + const { result } = renderHook(() => + useMedia('screen and (min-width: 1000px)'), + ) + + expect(result.current).toBe(false) + }) + + it('should not call onChange when unmounted', () => { + let callback: () => void + + const mockAddEventListener = (_event: string, callbackFn: () => void) => { + callback = callbackFn + } + + Object.defineProperty(window, 'matchMedia', { + value: jest.fn().mockImplementation((query: string) => ({ + addEventListener: mockAddEventListener, + addListener: jest.fn(), + dispatchEvent: jest.fn(), + matches: false, + media: query, + onchange: null, + removeEventListener: jest.fn(), + removeListener: jest.fn(), + })), + writable: true, + }) + + const { result, unmount } = renderHook(() => + useMedia('screen and (min-width: 1000px)'), + ) + + unmount() + callback?.() + + expect(result.current).toBe(false) + }) }) diff --git a/packages/use-media/src/useMedia.ts b/packages/use-media/src/useMedia.ts index ce4d32968..0c5b75a93 100644 --- a/packages/use-media/src/useMedia.ts +++ b/packages/use-media/src/useMedia.ts @@ -1,11 +1,12 @@ import { useEffect, useLayoutEffect, useState } from 'react' import { Effect } from './types' -import { noop } from './utilities' + +function noop() {} export const mockMediaQueryList: MediaQueryList = { addEventListener: noop, addListener: noop, - dispatchEvent: () => true, + dispatchEvent: /* istanbul ignore next */ () => true, matches: false, media: '', onchange: noop, diff --git a/packages/use-media/src/utilities/camelToHyphen.ts b/packages/use-media/src/utilities/camelToHyphen.ts deleted file mode 100644 index 87c9a168b..000000000 --- a/packages/use-media/src/utilities/camelToHyphen.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function camelToHyphen(camelString: string) { - return camelString - .replace(/[A-Z]/g, string => `-${string.toLowerCase()}`) - .toLowerCase() -} diff --git a/packages/use-media/src/utilities/index.ts b/packages/use-media/src/utilities/index.ts deleted file mode 100644 index a2d8b95e9..000000000 --- a/packages/use-media/src/utilities/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as camelToHyphen } from './camelToHyphen' -export { default as noop } from './noop' diff --git a/packages/use-media/src/utilities/noop.ts b/packages/use-media/src/utilities/noop.ts deleted file mode 100644 index ca6a74471..000000000 --- a/packages/use-media/src/utilities/noop.ts +++ /dev/null @@ -1 +0,0 @@ -export default function noop() {} From 722f6dc9a1a3f7b4ad3ed1893d99f5290e61f6ff Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 6 Jun 2022 12:45:02 +0000 Subject: [PATCH 5/5] fix: tsc error --- packages/use-media/src/__tests__/useMedia.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/use-media/src/__tests__/useMedia.tsx b/packages/use-media/src/__tests__/useMedia.tsx index c2ff3bb8a..c6adae355 100644 --- a/packages/use-media/src/__tests__/useMedia.tsx +++ b/packages/use-media/src/__tests__/useMedia.tsx @@ -61,7 +61,8 @@ describe('useMedia hook', () => { ) unmount() - callback?.() + // @ts-expect-error variable is assigned inside mockAddEventListener + callback() expect(result.current).toBe(false) })