From 4631fd5b27eac9639660dfb7cacec77bbfe331ac Mon Sep 17 00:00:00 2001 From: unix Date: Wed, 13 May 2020 00:35:52 +0800 Subject: [PATCH] feat(utils-shared): add hooks for media query --- components/utils-shared/index.ts | 2 + components/utils-shared/use-media-query.ts | 92 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 components/utils-shared/use-media-query.ts diff --git a/components/utils-shared/index.ts b/components/utils-shared/index.ts index 6c50528..0fe42ee 100644 --- a/components/utils-shared/index.ts +++ b/components/utils-shared/index.ts @@ -2,10 +2,12 @@ import { default as useBodyScroll } from './use-body-scroll' import { default as useClipboard } from './use-clipboard' import { default as useCurrentState } from './use-current-state' import { default as useClickAway } from './use-click-away' +import { default as useMediaQuery } from './use-media-query' export default { useBodyScroll, useClipboard, useCurrentState, useClickAway, + useMediaQuery, } diff --git a/components/utils-shared/use-media-query.ts b/components/utils-shared/use-media-query.ts new file mode 100644 index 0000000..5a20a9b --- /dev/null +++ b/components/utils-shared/use-media-query.ts @@ -0,0 +1,92 @@ +import { useEffect, useMemo, useState } from 'react' +import useTheme from '../styles/use-theme' +import { tuple } from '../utils/prop-types' +import { BreakpointsItem, ZeitUIThemesBreakpoints } from '../styles/themes' + +const breakpoints = tuple('xs', 'sm', 'md', 'lg', 'xl', 'mobile') +export type ResponsiveBreakpoint = typeof breakpoints[number] + +const matchType = tuple('up', 'down', 'default') +export type ResponsiveMatchType = typeof matchType[number] +export type ResponsiveOptions = { + match?: ResponsiveMatchType + ssrMatchMedia?: (query: string) => { matches: boolean } +} + +const defaultResponsiveOptions = { + match: 'default' as ResponsiveMatchType, +} + +const makeQueries = ( + bp: ZeitUIThemesBreakpoints, + up: boolean, + down: boolean, +): { + [key in ResponsiveBreakpoint]: string +} => { + const queryString = (item: BreakpointsItem) => { + const upQuery = `(min-width: ${item.min})` + const downQuery = `(max-width: ${item.max})` + return up ? upQuery : down ? downQuery : `${upQuery} and ${downQuery}` + } + const xs = queryString(bp.xs) + return { + xs: xs, + mobile: xs, + sm: queryString(bp.sm), + md: queryString(bp.md), + lg: queryString(bp.lg), + xl: queryString(bp.xl), + } +} + +const useMediaQuery = ( + breakpoint: ResponsiveBreakpoint, + options: ResponsiveOptions = defaultResponsiveOptions, +) => { + const { match: matchType = 'default', ssrMatchMedia = null } = options + const supportMedia = typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined' + + const theme = useTheme() + const mediaQueries: { + [key in ResponsiveBreakpoint]: string + } = useMemo(() => { + const up = matchType === 'up' + const down = matchType === 'down' + return makeQueries(theme.breakpoints, up, down) + }, [theme.breakpoints, options]) + const query = useMemo(() => mediaQueries[breakpoint], [mediaQueries, breakpoint]) + const matchQuery = (q: string) => window.matchMedia(q) + + /** + * Do nothing in the server-side rendering. + * If server match query fucntion is simulated, return user-defined value first. + */ + const [state, setState] = useState(() => { + if (supportMedia) return matchQuery(query).matches + if (ssrMatchMedia && typeof ssrMatchMedia === 'function') { + return ssrMatchMedia(query).matches + } + return false + }) + + useEffect(() => { + if (!supportMedia) return + const queryList = matchQuery(query) + const update = () => setState(matchQuery(query).matches) + update() + + /** + * addListener is deprecated. EventTarget.addEventListener is recommended. + * But in some old browsers, MediaQueryList does not inherit from EventTarget. + */ + queryList.addListener(update) + return () => { + queryList.removeListener(update) + } + }, [supportMedia]) + + return state +} + +export default useMediaQuery