summaryrefslogtreecommitdiff
path: root/shared/components/src/utils/getMediaConditions.ts
blob: 2d5028b3d9edc48ce8151ad9eecc8ea2e8d4b8a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import type { Breakpoints, Size } from '@amp/web-app-components/src/types';

export type MediaConditions<T extends string | number | symbol = Size> = {
    [key in T]?: string;
};

type BasicBreapoints<T extends string | number | symbol> = Record<T, number>;

type BreakpointOptions = { offset?: number };

// eslint-disable-next-line import/prefer-default-export
export function getMediaConditions<T extends string | number | symbol = Size>(
    breakpoints: Breakpoints<T>,
    options?: BreakpointOptions,
): MediaConditions<T> {
    const viewportOrder = {
        xsmall: 0,
        small: 1,
        medium: 2,
        large: 3,
        xlarge: 4,
    };

    const offset = options?.offset ?? 0;
    const viewportSizes = Object.keys(breakpoints).sort(
        (a, b) => viewportOrder[a] - viewportOrder[b],
    ) as T[];

    return viewportSizeToMediaConditions<T>(breakpoints, viewportSizes, offset);
}

function viewportSizeToMediaConditions<T extends string | number | symbol>(
    breakpoints: Breakpoints<T>,
    viewportSizes?: T[],
    offset?: number,
): MediaConditions<T> {
    viewportSizes ||= Object.keys(breakpoints) as T[];
    const queries: MediaConditions<T> = {};
    viewportSizes.reduce((acc, viewport) => {
        const { min, max } = {
            min: undefined,
            max: undefined,
            ...breakpoints[viewport],
        };

        if (min && !max) {
            acc[viewport] = `(min-width:${min + offset}px)`;
        } else if (!min && max) {
            acc[viewport] = `(max-width:${max + offset}px)`;
        } else if (min && max) {
            acc[viewport] = `(min-width:${min + offset}px) and (max-width:${
                max + offset
            }px)`;
        }
        return acc;
    }, queries);
    return queries;
}

/**
 * Transforms a breakpoints object into media queries that match ranges between each breakpoint and the next.
 *
 * @param breakpoints - Object with breakpoint names as keys and pixel values as values
 * @returns Object with breakpoint names as keys and media query strings as values
 *
 * @example
 * const breakpoints = { XSM: 0, SM: 350, MD: 484, LG: 1000 };
 * const mediaQueries = breakpointsToMediaQueries(breakpoints);
 * // Returns:
 * // {
 * //   XSM: '(max-width: 349px)',
 * //   SM: '(min-width: 350px) and (max-width: 483px)',
 * //   MD: '(min-width: 484px) and (max-width: 999px)',
 * //   LG: '(min-width: 1000px)'
 * // }
 */
export function breakpointsToMediaQueries<T extends string>(
    breakpoints: BasicBreapoints<T>,
): MediaConditions<T> {
    const entries = Object.entries(breakpoints) as [T, number][];
    entries.sort(([, a], [_, b]) => a - b);
    const transformedBreakpoints: Breakpoints<T> = {};

    entries.forEach(([breakpointName, minWidth], index) => {
        const isFirst = index === 0;
        const isLast = index === entries.length - 1;
        const nextBreakpointWidth = isLast ? null : entries[index + 1][1];

        if (isFirst && minWidth === 0) {
            // First breakpoint starting at 0: only max-width
            if (nextBreakpointWidth !== null) {
                transformedBreakpoints[breakpointName] = {
                    max: nextBreakpointWidth - 1,
                };
            } else {
                // Edge case: only one breakpoint starting at 0
                transformedBreakpoints[breakpointName] = { min: 0 };
            }
        } else if (isLast) {
            // Last breakpoint: only min-width
            transformedBreakpoints[breakpointName] = { min: minWidth };
        } else {
            // Middle breakpoints: min-width and max-width range
            transformedBreakpoints[breakpointName] = {
                min: minWidth,
                max: nextBreakpointWidth! - 1,
            };
        }
    });

    const viewportSizes = entries.map(([breakpointName]) => breakpointName);
    return viewportSizeToMediaConditions<T>(
        transformedBreakpoints,
        viewportSizes,
        0,
    );
}