import React from 'react'; import type { GlobalToken } from '../theme/interface'; import { useToken } from '../theme/internal'; 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: Breakpoint, i: number) => { 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: string = 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 default function useResponsiveObserver() { const [, token] = useToken(); const responsiveMap: BreakpointMap = getResponsiveMap(validateBreakpoints(token)); // To avoid repeat create instance, we add `useMemo` here. return React.useMemo(() => { const subscribers = new Map(); let subUid = -1; let screens = {}; return { matchHandlers: {} as { [prop: string]: { mql: MediaQueryList; listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; }; }, 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(); }, unregister() { Object.keys(responsiveMap).forEach((screen: Breakpoint) => { const matchMediaQuery = responsiveMap[screen]; const handler = this.matchHandlers[matchMediaQuery]; handler?.mql.removeListener(handler?.listener); }); subscribers.clear(); }, register() { Object.keys(responsiveMap).forEach((screen: Breakpoint) => { const matchMediaQuery = responsiveMap[screen]; const listener = ({ matches }: { matches: boolean }) => { this.dispatch({ ...screens, [screen]: matches, }); }; const mql = window.matchMedia(matchMediaQuery); mql.addListener(listener); this.matchHandlers[matchMediaQuery] = { mql, listener, }; listener(mql); }); }, responsiveMap, }; }, [token]); }