import React from 'react'; import type { GlobalToken } from '../theme/internal'; import { useToken } from '../theme/internal'; import { addMediaQueryListener, removeMediaQueryListener } from './mediaQueryUtil'; export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'; export type BreakpointMap = Record; export type ScreenMap = Partial>; export type ScreenSizeMap = Partial>; export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']; type SubscribeFunc = (screens: ScreenMap) => void; const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({ xs: `(max-width: ${token.screenXSMax}px)`, sm: `(min-width: ${token.screenSM}px)`, md: `(min-width: ${token.screenMD}px)`, lg: `(min-width: ${token.screenLG}px)`, xl: `(min-width: ${token.screenXL}px)`, xxl: `(min-width: ${token.screenXXL}px)`, }); /** * Ensures that the breakpoints token are valid, in good order * For each breakpoint : screenMin <= screen <= screenMax and screenMax <= nextScreenMin */ const validateBreakpoints = (token: GlobalToken) => { const indexableToken: any = token; const revBreakpoints = [...responsiveArray].reverse(); revBreakpoints.forEach((breakpoint, i) => { const breakpointUpper = breakpoint.toUpperCase(); const screenMin = `screen${breakpointUpper}Min`; const screen = `screen${breakpointUpper}`; if (!(indexableToken[screenMin] <= indexableToken[screen])) { throw new Error( `${screenMin}<=${screen} fails : !(${indexableToken[screenMin]}<=${indexableToken[screen]})`, ); } if (i < revBreakpoints.length - 1) { const screenMax = `screen${breakpointUpper}Max`; if (!(indexableToken[screen] <= indexableToken[screenMax])) { throw new Error( `${screen}<=${screenMax} fails : !(${indexableToken[screen]}<=${indexableToken[screenMax]})`, ); } const nextBreakpointUpperMin = revBreakpoints[i + 1].toUpperCase(); const nextScreenMin = `screen${nextBreakpointUpperMin}Min`; if (!(indexableToken[screenMax] <= indexableToken[nextScreenMin])) { throw new Error( `${screenMax}<=${nextScreenMin} fails : !(${indexableToken[screenMax]}<=${indexableToken[nextScreenMin]})`, ); } } }); return token; }; export const matchScreen = (screens: ScreenMap, screenSizes?: ScreenSizeMap) => { if (!screenSizes) { return; } for (const breakpoint of responsiveArray) { if (screens[breakpoint] && screenSizes?.[breakpoint] !== undefined) { return screenSizes[breakpoint]; } } }; interface ResponsiveObserverType { responsiveMap: BreakpointMap; dispatch: (map: ScreenMap) => boolean; subscribe: (func: SubscribeFunc) => number; unsubscribe: (token: number) => void; register: () => void; unregister: () => void; matchHandlers: Record< PropertyKey, { mql: MediaQueryList; listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void; } >; } const useResponsiveObserver = () => { const [, token] = useToken(); const responsiveMap = getResponsiveMap(validateBreakpoints(token)); // To avoid repeat create instance, we add `useMemo` here. return React.useMemo(() => { const subscribers = new Map(); let subUid = -1; let screens: Partial> = {}; return { responsiveMap, matchHandlers: {}, dispatch(pointMap: ScreenMap) { screens = pointMap; subscribers.forEach((func) => func(screens)); return subscribers.size >= 1; }, subscribe(func: SubscribeFunc): number { if (!subscribers.size) { this.register(); } subUid += 1; subscribers.set(subUid, func); func(screens); return subUid; }, unsubscribe(paramToken: number) { subscribers.delete(paramToken); if (!subscribers.size) { this.unregister(); } }, register() { Object.entries(responsiveMap).forEach(([screen, mediaQuery]) => { const listener = ({ matches }: { matches: boolean }) => { this.dispatch({ ...screens, [screen]: matches }); }; const mql = window.matchMedia(mediaQuery); addMediaQueryListener(mql, listener); this.matchHandlers[mediaQuery] = { mql, listener }; listener(mql); }); }, unregister() { Object.values(responsiveMap).forEach((mediaQuery) => { const handler = this.matchHandlers[mediaQuery]; removeMediaQueryListener(handler?.mql, handler?.listener); }); subscribers.clear(); }, }; }, [token]); }; export default useResponsiveObserver;