/ shared / components / src / utils / getMediaConditions.ts
getMediaConditions.ts
  1  import type { Breakpoints, Size } from '@amp/web-app-components/src/types';
  2  
  3  export type MediaConditions<T extends string | number | symbol = Size> = {
  4      [key in T]?: string;
  5  };
  6  
  7  type BasicBreapoints<T extends string | number | symbol> = Record<T, number>;
  8  
  9  type BreakpointOptions = { offset?: number };
 10  
 11  // eslint-disable-next-line import/prefer-default-export
 12  export function getMediaConditions<T extends string | number | symbol = Size>(
 13      breakpoints: Breakpoints<T>,
 14      options?: BreakpointOptions,
 15  ): MediaConditions<T> {
 16      const viewportOrder = {
 17          xsmall: 0,
 18          small: 1,
 19          medium: 2,
 20          large: 3,
 21          xlarge: 4,
 22      };
 23  
 24      const offset = options?.offset ?? 0;
 25      const viewportSizes = Object.keys(breakpoints).sort(
 26          (a, b) => viewportOrder[a] - viewportOrder[b],
 27      ) as T[];
 28  
 29      return viewportSizeToMediaConditions<T>(breakpoints, viewportSizes, offset);
 30  }
 31  
 32  function viewportSizeToMediaConditions<T extends string | number | symbol>(
 33      breakpoints: Breakpoints<T>,
 34      viewportSizes?: T[],
 35      offset?: number,
 36  ): MediaConditions<T> {
 37      viewportSizes ||= Object.keys(breakpoints) as T[];
 38      const queries: MediaConditions<T> = {};
 39      viewportSizes.reduce((acc, viewport) => {
 40          const { min, max } = {
 41              min: undefined,
 42              max: undefined,
 43              ...breakpoints[viewport],
 44          };
 45  
 46          if (min && !max) {
 47              acc[viewport] = `(min-width:${min + offset}px)`;
 48          } else if (!min && max) {
 49              acc[viewport] = `(max-width:${max + offset}px)`;
 50          } else if (min && max) {
 51              acc[viewport] = `(min-width:${min + offset}px) and (max-width:${
 52                  max + offset
 53              }px)`;
 54          }
 55          return acc;
 56      }, queries);
 57      return queries;
 58  }
 59  
 60  /**
 61   * Transforms a breakpoints object into media queries that match ranges between each breakpoint and the next.
 62   *
 63   * @param breakpoints - Object with breakpoint names as keys and pixel values as values
 64   * @returns Object with breakpoint names as keys and media query strings as values
 65   *
 66   * @example
 67   * const breakpoints = { XSM: 0, SM: 350, MD: 484, LG: 1000 };
 68   * const mediaQueries = breakpointsToMediaQueries(breakpoints);
 69   * // Returns:
 70   * // {
 71   * //   XSM: '(max-width: 349px)',
 72   * //   SM: '(min-width: 350px) and (max-width: 483px)',
 73   * //   MD: '(min-width: 484px) and (max-width: 999px)',
 74   * //   LG: '(min-width: 1000px)'
 75   * // }
 76   */
 77  export function breakpointsToMediaQueries<T extends string>(
 78      breakpoints: BasicBreapoints<T>,
 79  ): MediaConditions<T> {
 80      const entries = Object.entries(breakpoints) as [T, number][];
 81      entries.sort(([, a], [_, b]) => a - b);
 82      const transformedBreakpoints: Breakpoints<T> = {};
 83  
 84      entries.forEach(([breakpointName, minWidth], index) => {
 85          const isFirst = index === 0;
 86          const isLast = index === entries.length - 1;
 87          const nextBreakpointWidth = isLast ? null : entries[index + 1][1];
 88  
 89          if (isFirst && minWidth === 0) {
 90              // First breakpoint starting at 0: only max-width
 91              if (nextBreakpointWidth !== null) {
 92                  transformedBreakpoints[breakpointName] = {
 93                      max: nextBreakpointWidth - 1,
 94                  };
 95              } else {
 96                  // Edge case: only one breakpoint starting at 0
 97                  transformedBreakpoints[breakpointName] = { min: 0 };
 98              }
 99          } else if (isLast) {
100              // Last breakpoint: only min-width
101              transformedBreakpoints[breakpointName] = { min: minWidth };
102          } else {
103              // Middle breakpoints: min-width and max-width range
104              transformedBreakpoints[breakpointName] = {
105                  min: minWidth,
106                  max: nextBreakpointWidth! - 1,
107              };
108          }
109      });
110  
111      const viewportSizes = entries.map(([breakpointName]) => breakpointName);
112      return viewportSizeToMediaConditions<T>(
113          transformedBreakpoints,
114          viewportSizes,
115          0,
116      );
117  }