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 }