Skip to content

Commit a6ac33c

Browse files
committed
feat: Add breakpoints helper tools
1 parent a228078 commit a6ac33c

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed

packages/react/src/hooks/useMediaQuery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export function useMediaQuery(
3636
initializeWithValue = true,
3737
}: UseMediaQueryOptions = {},
3838
): boolean {
39+
query = query?.replace(/^@media( ?)/m, '');
40+
3941
const getMatches = (query: string): boolean => {
4042
if (IS_SERVER) {
4143
return defaultValue;

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './hooks';
2+
export * from './utils';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Remove properties `K` from `T`.
3+
* Distributive for union types.
4+
*/
5+
declare type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
6+
7+
/**
8+
* Like `T & U`, but using the value types from `U` where their properties overlap.
9+
*/
10+
declare type Overwrite<T, U> = DistributiveOmit<T, keyof U> & U;
11+
12+
/**
13+
* Generate a set of string literal types with the given default record `T` and
14+
* override record `U`.
15+
*
16+
* If the property value was `true`, the property key will be added to the
17+
* string union.
18+
*/
19+
declare type OverridableStringUnion<T extends string | number, U = any> = GenerateStringUnion<
20+
Overwrite<Record<T, true>, U>
21+
>;
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
export interface BreakpointOverrides {}
2+
3+
export type Breakpoint = OverridableStringUnion<
4+
'xs' | 'sm' | 'md' | 'lg' | 'xl',
5+
BreakpointOverrides
6+
>;
7+
8+
export interface Breakpoints {
9+
keys: Breakpoint[];
10+
11+
/**
12+
* Each breakpoint (a key) matches with a fixed screen width (a value).
13+
* @default {
14+
* // extra-small
15+
* xs: 0,
16+
* // small
17+
* sm: 576,
18+
* // medium
19+
* md: 768,
20+
* // large
21+
* lg: 992,
22+
* // extra-large
23+
* xl: 1200,
24+
* }
25+
*/
26+
values: { [key in Breakpoint]: number };
27+
28+
/**
29+
* @param key - A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
30+
* @returns A media query string ready to be used with most styling solutions,
31+
* which matches screen widths greater than the screen size given by the breakpoint
32+
* key (inclusive).
33+
*/
34+
up: (key: Breakpoint | number) => string;
35+
36+
/**
37+
* @param key - A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
38+
* @returns A media query string ready to be used with most styling solutions, which matches screen widths less than the screen size given by the breakpoint key (exclusive).
39+
*/
40+
down: (key: Breakpoint | number) => string;
41+
42+
/**
43+
* @param start - A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
44+
* @param end - A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
45+
* @returns A media query string ready to be used with most styling solutions,
46+
* which matches screen widths greater than the screen size given by the breakpoint
47+
* key in the first argument (inclusive) and less than the screen size given by the
48+
* breakpoint key in the second argument (exclusive).
49+
*/
50+
between: (start: Breakpoint | number, end: Breakpoint | number) => string;
51+
52+
/**
53+
* @param key - A breakpoint key (`xs`, `sm`, etc.) or a screen width number in px.
54+
* @returns A media query string ready to be used with most styling solutions,
55+
* which matches screen widths starting from the screen size given by the breakpoint
56+
* key (inclusive) and stopping at the screen size given by the next breakpoint
57+
* key (exclusive).
58+
*/
59+
only: (key: Breakpoint) => string;
60+
61+
/**
62+
* @param key - A breakpoint key (`xs`, `sm`, etc.).
63+
* @returns A media query string ready to be used with most styling solutions,
64+
* which matches screen widths stopping at the screen size given by the breakpoint
65+
* key (exclusive) and starting at the screen size given by the next breakpoint
66+
* key (inclusive).
67+
*/
68+
not: (key: Breakpoint) => string;
69+
70+
/**
71+
* The unit used for the breakpoint's values.
72+
* @default 'px'
73+
*/
74+
unit?: string | undefined;
75+
}
76+
77+
export interface BreakpointsOptions extends Partial<Breakpoints> {
78+
/**
79+
* The increment divided by 100 used to implement exclusive breakpoints.
80+
* For example, `step: 5` means that `down(500)` will result in `'(max-width: 499.95px)'`.
81+
* @default 5
82+
*/
83+
step?: number | undefined;
84+
85+
/**
86+
* The unit used for the breakpoint's values.
87+
* @default 'px'
88+
*/
89+
unit?: string | undefined;
90+
}
91+
92+
// Sorted ASC by size. That's important.
93+
// It can't be configured as it's used statically for propTypes.
94+
export const breakpointKeys = ['xs', 'sm', 'md', 'lg', 'xl'];
95+
96+
const sortBreakpointsValues = (values: Breakpoints['values']) => {
97+
const breakpointsAsArray = Object.keys(values).map(key => ({
98+
key,
99+
val: values[key],
100+
})) || [];
101+
102+
// Sort in ascending order
103+
breakpointsAsArray.sort(
104+
(breakpoint1, breakpoint2) => breakpoint1.val - breakpoint2.val,
105+
);
106+
107+
return breakpointsAsArray.reduce((acc, obj) => {
108+
return {
109+
...acc,
110+
[obj.key]: obj.val,
111+
};
112+
}, {});
113+
};
114+
115+
// Keep in mind that @media is inclusive by the CSS specification.
116+
export default function createBreakpoints(breakpoints: BreakpointsOptions): Breakpoints {
117+
const {
118+
// The breakpoint **start** at this value.
119+
// For instance with the first breakpoint xs: [xs, sm).
120+
values = {
121+
xs: 0,
122+
// phone
123+
sm: 576,
124+
// tablet
125+
md: 768,
126+
// small laptop
127+
lg: 992,
128+
// desktop / large screen
129+
xl: 1200,
130+
},
131+
unit = 'px',
132+
step = 5,
133+
...other
134+
} = breakpoints;
135+
const sortedValues = sortBreakpointsValues(values);
136+
const keys = Object.keys(sortedValues);
137+
138+
function up(key: Breakpoint | number) {
139+
const value = typeof values[key] === 'number' ? values[key] : key;
140+
return `@media (min-width:${value}${unit})`;
141+
}
142+
143+
function down(key: Breakpoint | number) {
144+
const value = typeof values[key] === 'number' ? values[key] : key;
145+
return `@media (max-width:${value - step / 100}${unit})`;
146+
}
147+
148+
function between(start: Breakpoint | number, end: Breakpoint | number) {
149+
const endIndex = keys.indexOf(end);
150+
return (
151+
`@media (min-width:${typeof values[start] === 'number' ? values[start] : start}${unit}) and `
152+
+ `(max-width:${(endIndex !== -1 && typeof values[keys[endIndex]] === 'number' ? values[keys[endIndex]] : end) - step / 100}${unit})`
153+
);
154+
}
155+
156+
function only(key: Breakpoint) {
157+
if (keys.indexOf(key) + 1 < keys.length) {
158+
return between(key, keys[keys.indexOf(key) + 1]);
159+
}
160+
return up(key);
161+
}
162+
163+
function not(key: Breakpoint) {
164+
// handle first and last key separately, for better readability
165+
const keyIndex = keys.indexOf(key);
166+
if (keyIndex === 0) {
167+
return up(keys[1]);
168+
}
169+
if (keyIndex === keys.length - 1) {
170+
return down(keys[keyIndex]);
171+
}
172+
return between(key, keys[keys.indexOf(key) + 1]).replace(
173+
'@media',
174+
'@media not all and',
175+
);
176+
}
177+
178+
return {
179+
keys,
180+
values: sortedValues,
181+
up,
182+
down,
183+
between,
184+
only,
185+
not,
186+
unit,
187+
...other,
188+
};
189+
}

packages/react/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as createBreakpoints } from './create-breakpoints';

0 commit comments

Comments
 (0)