// Not fixing react-hooks errors as it would require a complete refactor /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/rules-of-hooks */ import React, { useEffect, useMemo, useState, type ReactNode } from 'react' type Props = Record type GenericChildrenType = { children?: ReactNode } export const subtractOnePixel = (breakpoint: string): string => `${parseInt(breakpoint, 10) - 1}px` export const useMediaQueries = ( propQueries: Props = {}, ): { queries: { [key: string]: boolean isSmall: boolean isMedium: boolean isLarge: boolean isMediumOrSmaller: boolean isMediumOrLarger: boolean } components: { [key: string]: (props: GenericChildrenType) => JSX.Element SmallOnly: (props: GenericChildrenType) => JSX.Element MediumOnly: (props: GenericChildrenType) => JSX.Element LargeOnly: (props: GenericChildrenType) => JSX.Element MediumOrSmaller: (props: GenericChildrenType) => JSX.Element MediumOrLarger: (props: GenericChildrenType) => JSX.Element } } => { if (typeof window === 'undefined') { return { queries: { isSmall: false, isMedium: false, isLarge: false, isMediumOrSmaller: false, isMediumOrLarger: true, }, components: { SmallOnly: () => <>, MediumOnly: () => <>, LargeOnly: () => <>, MediumOrSmaller: () => <>, MediumOrLarger: (props: HelperComponentProps) => <>{props.children}, }, } } // Ensure these match the tokens in @kaizen/design-tokens const breakpoints = { medium: '768px', large: '1080px' } // The `addEventListener` calls blow up legacy Edge (<= v18/pre chromium), // so we disable the functionality of updating after page load. const isLegacyEdge = /Edge/.exec(navigator.userAgent) const isUnsupportedSafari = window.matchMedia('').addEventListener === undefined // --------------------------------------- // Create Kaizen breakpoint matches for initial state // --------------------------------------- const smallMatchMedia = useMemo( () => window.matchMedia(`(max-width: ${subtractOnePixel(breakpoints.medium)})`), [breakpoints.medium], ) const mediumMatchMedia = useMemo( () => window.matchMedia( `(min-width: ${breakpoints.medium}) and (max-width: ${subtractOnePixel( breakpoints.large, )})`, ), [breakpoints.large], ) const largeMatchMedia = useMemo( () => window.matchMedia(`(min-width: ${breakpoints.large})`), [breakpoints.large], ) const isSmall = smallMatchMedia.matches || false const isMedium = mediumMatchMedia.matches || false const isLarge = largeMatchMedia.matches || false const [kaizenMatches, setKaizenMatches] = useState<{ isSmall: boolean isMedium: boolean isLarge: boolean isMediumOrSmaller: boolean isMediumOrLarger: boolean }>({ isSmall, isMedium, isLarge, isMediumOrSmaller: !isLarge, isMediumOrLarger: !isSmall, // Note: ^the last two will need an adjustment in logic if/when we add more breakpoints }) // --------------------------------------- // Create an event listener based on the small and large breakpoints and update state whenever one of those changes // --------------------------------------- useEffect(() => { if (isLegacyEdge || isUnsupportedSafari) { return } const updateMatches = (): void => { const isSmallAfterUpdate = smallMatchMedia.matches || false const isMediumAfterUpdate = mediumMatchMedia.matches || false const isLargeAfterUpdate = largeMatchMedia.matches || false setKaizenMatches({ isSmall: isSmallAfterUpdate, isMedium: isMediumAfterUpdate, isLarge: isLargeAfterUpdate, isMediumOrSmaller: !isLargeAfterUpdate, isMediumOrLarger: !isSmallAfterUpdate, }) } smallMatchMedia.addEventListener('change', updateMatches, true) largeMatchMedia.addEventListener('change', updateMatches, true) return () => { smallMatchMedia.removeEventListener('change', updateMatches) largeMatchMedia.removeEventListener('change', updateMatches) } }, []) // --------------------------------------- // Create custom queries matches for initial state // --------------------------------------- const customQueryMatchMedias = new Map() const customQueryMatches: Record = {} new Map(Object.entries(propQueries)).forEach((queryString, queryName) => { const matchMedia = window.matchMedia(queryString) customQueryMatches[queryName] = matchMedia.matches || false // Store this matchMedia so that we can loop over it later and add event listeners customQueryMatchMedias.set(queryName, matchMedia) }) const [customMatches, setCustomMatches] = useState>(customQueryMatches) // --------------------------------------- // Create an event listener for each custom query // --------------------------------------- useEffect(() => { if (isLegacyEdge || isUnsupportedSafari) { return } const updateCustomMatches = (matchMedia: MediaQueryList, queryName: string): void => setCustomMatches({ ...customQueryMatches, [queryName]: matchMedia.matches || false, }) const allEventListeners = new Map() customQueryMatchMedias.forEach((matchMedia, queryName) => { // Store this method so that we can reference it in the cleanup function below const updateCustomMatchesForQuery = (): void => { updateCustomMatches(matchMedia, queryName) } allEventListeners.set(matchMedia, updateCustomMatchesForQuery) // Add the event listener matchMedia.addEventListener('change', updateCustomMatchesForQuery, true) }) return () => { allEventListeners.forEach((eventListener, matchMedia) => { matchMedia.removeEventListener('change', eventListener) }) } }, []) // --------------------------------------- // Create Kaizen helper components // --------------------------------------- type HelperComponentProps = { children?: ReactNode } const kaizenComponents = { SmallOnly: (props: HelperComponentProps) => <>{kaizenMatches.isSmall && props.children}, MediumOnly: (props: HelperComponentProps) => <>{kaizenMatches.isMedium && props.children}, LargeOnly: (props: HelperComponentProps) => <>{kaizenMatches.isLarge && props.children}, MediumOrSmaller: (props: HelperComponentProps) => ( <>{kaizenMatches.isMediumOrSmaller && props.children} ), MediumOrLarger: (props: HelperComponentProps) => ( <>{kaizenMatches.isMediumOrLarger && props.children} ), } // --------------------------------------- // Create custom query helper components // --------------------------------------- const customComponents: Record JSX.Element> = {} Object.keys(propQueries).map((key) => { const componentName = key.charAt(0).toUpperCase() + key.slice(1) customComponents[componentName] = (props: HelperComponentProps): JSX.Element => ( <>{customMatches[key] && props.children} ) }) return { queries: { ...kaizenMatches, ...customQueryMatches }, components: { ...kaizenComponents, ...customComponents }, } }