Skip to content

Commit c38aff8

Browse files
committed
@atomic-layout/core: Adds "staticMatchMedia" utility
1 parent abaf32f commit c38aff8

File tree

4 files changed

+250
-0
lines changed

4 files changed

+250
-0
lines changed

packages/atomic-layout-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
joinQueryList,
1919
} from './utils/styles/createMediaQuery'
2020
export { default as normalizeQuery } from './utils/styles/normalizeQuery'
21+
export { staticMatchMedia } from './utils/styles/staticMatchMedia'
2122

2223
/* Breakpoints */
2324
export { default as withBreakpoints } from './utils/breakpoints/withBreakpoints'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './staticMatchMedia'
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { staticMatchMedia } from './staticMatchMedia'
5+
6+
describe('staticMatchMedia', () => {
7+
describe('given "up" behavior', () => {
8+
describe('and actual breakpoint matches', () => {
9+
it('should return true', () => {
10+
const result = staticMatchMedia(
11+
{
12+
behavior: 'up',
13+
breakpoint: {
14+
minWidth: 500,
15+
maxWidth: 750,
16+
},
17+
},
18+
{
19+
width: 600,
20+
},
21+
)
22+
23+
expect(result).toHaveProperty('matches', true)
24+
})
25+
})
26+
27+
describe('and actual breakpoint does not match', () => {
28+
it('should return false', () => {
29+
const result = staticMatchMedia(
30+
{
31+
behavior: 'up',
32+
breakpoint: {
33+
minWidth: 500,
34+
maxWidth: 750,
35+
},
36+
},
37+
{
38+
width: 499,
39+
},
40+
)
41+
42+
expect(result).toHaveProperty('matches', false)
43+
})
44+
})
45+
})
46+
47+
describe('given "down" behavior', () => {
48+
describe('and actual breakpoint matches', () => {
49+
it('should return true', () => {
50+
const result = staticMatchMedia(
51+
{
52+
behavior: 'down',
53+
breakpoint: {
54+
minWidth: 500,
55+
maxWidth: 750,
56+
},
57+
},
58+
{
59+
width: 499,
60+
},
61+
)
62+
63+
expect(result).toHaveProperty('matches', true)
64+
})
65+
})
66+
67+
describe('and actual breakpoint does not match', () => {
68+
it('should return false', () => {
69+
const result = staticMatchMedia(
70+
{
71+
behavior: 'down',
72+
breakpoint: {
73+
minWidth: 500,
74+
maxWidth: 750,
75+
},
76+
},
77+
{
78+
width: 751,
79+
},
80+
)
81+
82+
expect(result).toHaveProperty('matches', false)
83+
})
84+
})
85+
})
86+
87+
describe('given "only" behavior', () => {
88+
describe('and actual breakpoint matches', () => {
89+
it('should return true', () => {
90+
const result = staticMatchMedia(
91+
{
92+
behavior: 'only',
93+
breakpoint: {
94+
minWidth: 500,
95+
maxWidth: 750,
96+
},
97+
},
98+
{
99+
width: 625,
100+
},
101+
)
102+
103+
expect(result).toHaveProperty('matches', true)
104+
})
105+
})
106+
107+
describe('and actual breakpoint is below expected', () => {
108+
it('should return false', () => {
109+
const result = staticMatchMedia(
110+
{
111+
behavior: 'only',
112+
breakpoint: {
113+
minWidth: 500,
114+
maxWidth: 750,
115+
},
116+
},
117+
{
118+
width: 499,
119+
},
120+
)
121+
122+
expect(result).toHaveProperty('matches', false)
123+
})
124+
})
125+
126+
describe('and actual breakpoint is above expected', () => {
127+
it('should return false', () => {
128+
const result = staticMatchMedia(
129+
{
130+
behavior: 'only',
131+
breakpoint: {
132+
minWidth: 500,
133+
maxWidth: 750,
134+
},
135+
},
136+
{
137+
width: 751,
138+
},
139+
)
140+
141+
expect(result).toHaveProperty('matches', false)
142+
})
143+
})
144+
})
145+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { AreaRecord } from '../../breakpoints/getAreaRecords'
2+
import { Breakpoint, BreakpointBehavior } from '../../../const/defaultOptions'
3+
import createMediaQuery, { createQueryList } from '../createMediaQuery'
4+
import openBreakpoint from '../../breakpoints/openBreakpoint'
5+
import closeBreakpoint from '../../breakpoints/closeBreakpoint'
6+
7+
type MatchMediaReturnType = ReturnType<typeof matchMedia>
8+
type DimensionalComparator = (expected: number, actual: number) => boolean
9+
10+
/**
11+
* Returns a comparator function by given prefix (min/max).
12+
*/
13+
const getComparator = (propertyPrefix: string): DimensionalComparator => {
14+
if (propertyPrefix === 'min') {
15+
return (expected, actual) => actual >= expected
16+
}
17+
18+
return (expected, actual) => actual <= expected
19+
}
20+
21+
const transformBreakpoint = (
22+
breakpoint: Breakpoint,
23+
behavior: BreakpointBehavior,
24+
): Breakpoint => {
25+
if (behavior === 'up') {
26+
return openBreakpoint(breakpoint)
27+
}
28+
29+
if (behavior === 'down') {
30+
return closeBreakpoint(breakpoint)
31+
}
32+
33+
return breakpoint
34+
}
35+
36+
export const staticMatchMedia = (
37+
record: AreaRecord,
38+
clientOverride?: Breakpoint,
39+
): MatchMediaReturnType => {
40+
const { breakpoint, behavior } = record
41+
42+
if (typeof window !== 'undefined') {
43+
// Convert object-like breakpoint to an actual media query string.
44+
const mediaQueryString = createMediaQuery(breakpoint, behavior)
45+
46+
// Use native "matchMedia" on the client-side.
47+
// This resolves all types of media queries: dimensional and non-dimensional.
48+
return matchMedia(mediaQueryString)
49+
}
50+
51+
// Compose a client state by merging sensible defaults
52+
// with the given explicit overrides.
53+
const clientState: Breakpoint = {
54+
orientation: 'landscape',
55+
...clientOverride,
56+
}
57+
58+
const resolvedBreakpoint = transformBreakpoint(breakpoint, behavior)
59+
const queryParams = createQueryList(resolvedBreakpoint, behavior)
60+
61+
// Each media query parameter must match
62+
const matches = queryParams.every(({ prefix, name, value }) => {
63+
// Skip breakpoint property values with "undefined" value.
64+
// This accounts for opened/closed breakpoints, where
65+
// certain properties are set to explicit "undefined".
66+
if (value === undefined) {
67+
return true
68+
}
69+
70+
/**
71+
* @todo Skip string values for now.
72+
* Ideally, use custom comparator for different query params
73+
* (aspect ratio, resolution, etc.).
74+
*/
75+
if (typeof value === 'string') {
76+
return true
77+
}
78+
79+
const actualValue = clientState[name]
80+
const compare = getComparator(prefix)
81+
82+
/**
83+
* @todo Support comparison of Numeriv values with measurement unit.
84+
* For example, "576px" and "768px". Currently considered a string and
85+
* always results to true in the upper closure.
86+
*/
87+
return compare(value, actualValue)
88+
})
89+
90+
return {
91+
matches,
92+
media: null /** @todo Stub the value of "media" */,
93+
94+
// Stub MatchMedia methods to be callable, but do nothing
95+
// on the server-side.
96+
addEventListener: (): void => null,
97+
addListener: () => null,
98+
removeListener: () => null,
99+
removeEventListener: (): void => null,
100+
dispatchEvent: () => null,
101+
onchange: () => null,
102+
}
103+
}

0 commit comments

Comments
 (0)