Skip to content

Commit 2c69eb1

Browse files
Ayc0psyrendust
andauthored
Fix: trigger addEventListener properly when "complex" media changes (complex = non just lowercased) (#65)
* Fix setMedia bug with prefersColorScheme (#64) * better match between media feature & env feature * add tests for aspect-ratios & event listeners --------- Co-authored-by: Larry Gordon <[email protected]>
1 parent dd3926a commit 2c69eb1

File tree

6 files changed

+191
-18
lines changed

6 files changed

+191
-18
lines changed

src/index.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { compileQuery, matches, type Environment, type EvaluateResult, type SimplePerm } from "media-query-fns";
22

3+
// TODO: remove `deviceWidth` & `deviceHeight` from it, and only derive those from `width`, `height`, and `dppx`
34
type MediaState = { [key in keyof Environment as key extends `${infer Key}Px` ? Key : key]?: Environment[key] };
45

56
const DEFAULT_ENV: Parameters<typeof matches>[1] = {
@@ -35,6 +36,58 @@ type Feature = keyof MediaState;
3536

3637
const now = Date.now();
3738

39+
/**
40+
* Match the renaming done by media-query-fns
41+
*
42+
* {@link https://github.com/tbjgolden/media-query-fns/blob/7dae2618b9321f503cbd0a44a202a9190665e80e/lib/matches.ts#L200-L533}
43+
*/
44+
const MEDIA_FEATURE_TO_FEATURES = {
45+
// Same but different casing
46+
"any-hover": ["anyHover"],
47+
"any-pointer": ["anyPointer"],
48+
"color-gamut": ["colorGamut"],
49+
"color-index": ["colorIndex"],
50+
"display-mode": ["displayMode"],
51+
"dynamic-range": ["dynamicRange"],
52+
"environment-blending": ["environmentBlending"],
53+
"forced-colors": ["forcedColors"],
54+
grid: ["grid"],
55+
"horizontal-viewport-segments": ["horizontalViewportSegments"],
56+
hover: ["hover"],
57+
"inverted-colors": ["invertedColors"],
58+
"media-type": ["mediaType"],
59+
"nav-controls": ["navControls"],
60+
"overflow-block": ["overflowBlock"],
61+
"overflow-inline": ["overflowInline"],
62+
pointer: ["pointer"],
63+
"prefers-color-scheme": ["prefersColorScheme"],
64+
"prefers-contrast": ["prefersContrast"],
65+
"prefers-reduced-data": ["prefersReducedData"],
66+
"prefers-reduced-motion": ["prefersReducedMotion"],
67+
"prefers-reduced-transparency": ["prefersReducedTransparency"],
68+
scan: ["scan"],
69+
scripting: ["scripting"],
70+
update: ["update"],
71+
"vertical-viewport-segments": ["verticalViewportSegments"],
72+
"video-color-gamut": ["videoColorGamut"],
73+
"video-dynamic-range": ["videoDynamicRange"],
74+
75+
// Numbers
76+
monochrome: ["monochromeBits"],
77+
color: ["colorBits"],
78+
resolution: ["dppx"],
79+
80+
// Pixels
81+
width: ["width"],
82+
height: ["height"],
83+
"device-height": ["deviceHeight"],
84+
"device-width": ["deviceWidth"],
85+
86+
// Combinations
87+
"aspect-ratio": ["width", "height"],
88+
"device-aspect-ratio": ["deviceHeight", "deviceWidth"],
89+
} as const satisfies Record<string, Feature[]>;
90+
3891
// Event was added in node 15, so until we drop the support for versions before it, we need to use this
3992
class EventLegacy {
4093
type: "change";
@@ -73,7 +126,17 @@ const EventCompat: typeof Event = typeof Event === "undefined" ? EventLegacy : E
73126
const getFeaturesFromQuery = (query: EvaluateResult): Set<Feature> => {
74127
const features = new Set<Feature>();
75128
query.simplePerms.forEach((perm) => {
76-
Object.keys(perm).forEach((feature) => features.add(feature as Feature));
129+
Object.keys(perm).forEach((mediaFeature) => {
130+
if (mediaFeature in MEDIA_FEATURE_TO_FEATURES) {
131+
MEDIA_FEATURE_TO_FEATURES[mediaFeature as keyof typeof MEDIA_FEATURE_TO_FEATURES].forEach((feature) =>
132+
features.add(feature),
133+
);
134+
}
135+
// // For debut, we can comment out those:
136+
// else {
137+
// console.error("Unrecognized " + mediaFeature);
138+
// }
139+
});
77140
});
78141
return features;
79142
};

test/listeners.cjs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,13 @@
33
const { test } = require("node:test");
44
const { strict: assert } = require("node:assert");
55
const { matchMedia, setMedia, cleanupListeners, MediaQueryListEvent, cleanup } = require("mock-match-media");
6+
const { mock } = require("./utils.cjs");
67

78
test.afterEach(() => {
89
// cleanup listeners and state after each test
910
cleanup();
1011
});
1112

12-
/**
13-
* @type {() => [(event: MediaQueryListEvent) => void, MediaQueryListEvent[]]}
14-
*/
15-
const mock = () => {
16-
const calls = [];
17-
return [
18-
(event) => {
19-
calls.push(event);
20-
},
21-
calls,
22-
];
23-
};
24-
2513
test("`.addListener()`", () => {
2614
const mql = matchMedia("(min-width: 500px)");
2715

test/matchers/aspect-ratio.cjs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
const { test } = require("node:test");
44
const { strict: assert } = require("node:assert");
5-
const { matchMedia, setMedia, cleanupMedia } = require("mock-match-media");
5+
const { matchMedia, setMedia, cleanup } = require("mock-match-media");
6+
const { mock } = require("../utils.cjs");
67

78
test.beforeEach(() => {
8-
cleanupMedia();
9+
cleanup();
910
});
1011

1112
test("unset", () => {
@@ -238,3 +239,35 @@ test("other syntax", () => {
238239
});
239240
assert.equal(matchMedia("(aspect-ratio: 3)").matches, true);
240241
});
242+
243+
test("`.addEventListener()`", () => {
244+
const mqlAR3 = matchMedia("(aspect-ratio > 3)");
245+
const [cb, calls] = mock();
246+
247+
mqlAR3.addEventListener("change", cb);
248+
249+
assert.equal(matchMedia("(aspect-ratio > 3)").matches, false);
250+
251+
setMedia({
252+
width: 7,
253+
});
254+
255+
assert.equal(matchMedia("(aspect-ratio > 3)").matches, false);
256+
assert.equal(calls.length, 0);
257+
258+
setMedia({
259+
height: 2,
260+
});
261+
262+
assert.equal(matchMedia("(aspect-ratio > 3)").matches, true);
263+
assert.equal(calls.length, 1);
264+
assert.equal(calls[0].matches, true);
265+
266+
setMedia({
267+
height: 3,
268+
});
269+
270+
assert.equal(matchMedia("(aspect-ratio > 3)").matches, false);
271+
assert.equal(calls.length, 2);
272+
assert.equal(calls[1].matches, false);
273+
});

test/matchers/device-aspect-ratio.cjs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
const { test } = require("node:test");
44
const { strict: assert } = require("node:assert");
5-
const { matchMedia, setMedia, cleanupMedia } = require("mock-match-media");
5+
const { matchMedia, setMedia, cleanup } = require("mock-match-media");
6+
const { mock } = require("../utils.cjs");
67

78
test.beforeEach(() => {
8-
cleanupMedia();
9+
cleanup();
910
});
1011

1112
test("unset", () => {
@@ -208,3 +209,42 @@ test("other syntax", () => {
208209
assert.equal(matchMedia("(device-aspect-ratio: 16 / 16)").matches, true);
209210
assert.equal(matchMedia("(device-aspect-ratio: 16 / 16)").matches, true);
210211
});
212+
213+
test("`.addEventListener()`", () => {
214+
const mqlAR3 = matchMedia("(device-aspect-ratio > 3)");
215+
const [cb, calls] = mock();
216+
217+
mqlAR3.addEventListener("change", cb);
218+
219+
assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false);
220+
221+
setMedia({
222+
width: 7,
223+
});
224+
225+
assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false);
226+
assert.equal(calls.length, 0);
227+
228+
setMedia({
229+
height: 2,
230+
});
231+
232+
assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, true);
233+
assert.equal(calls.length, 1);
234+
assert.equal(calls[0].matches, true);
235+
236+
setMedia({
237+
height: 3,
238+
});
239+
240+
assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false);
241+
assert.equal(calls.length, 2);
242+
assert.equal(calls[1].matches, false);
243+
244+
setMedia({
245+
dppx: 2,
246+
});
247+
248+
assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false);
249+
assert.equal(calls.length, 2);
250+
});

test/matchers/prefers-color-scheme.cjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
const { test } = require("node:test");
44
const { strict: assert } = require("node:assert");
55
const { matchMedia, setMedia, cleanupMedia } = require("mock-match-media");
6+
const { mock } = require("../utils.cjs");
67

78
test.beforeEach(() => {
89
cleanupMedia();
910
});
1011

12+
1113
test("unset", () => {
1214
assert.equal(matchMedia("(prefers-color-scheme: light)").matches, false);
1315
assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, false);
@@ -30,3 +32,35 @@ test("dark", () => {
3032
assert.equal(matchMedia("(prefers-color-scheme: light)").matches, false);
3133
assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, true);
3234
});
35+
36+
test("`.addEventListener()`", () => {
37+
const mqlLight = matchMedia("(prefers-color-scheme: light)");
38+
const mqlDark = matchMedia("(prefers-color-scheme: dark)");
39+
const [cbLight, callsLight] = mock();
40+
const [cbDark, callsDark] = mock();
41+
42+
mqlLight.addEventListener("change", cbLight);
43+
mqlDark.addEventListener("change", cbDark);
44+
45+
setMedia({
46+
prefersColorScheme: "light",
47+
});
48+
49+
assert.equal(matchMedia("(prefers-color-scheme: light)").matches, true);
50+
assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, false);
51+
assert.equal(callsLight.length, 1);
52+
assert.equal(callsLight[0].matches, true);
53+
assert.equal(callsDark.length, 0);
54+
55+
setMedia({
56+
prefersColorScheme: "dark",
57+
});
58+
59+
assert.equal(matchMedia("(prefers-color-scheme: light)").matches, false);
60+
assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, true);
61+
assert.equal(callsLight.length, 2);
62+
assert.equal(callsLight[1].matches, false);
63+
assert.equal(callsDark.length, 1);
64+
assert.equal(callsDark[0].matches, true);
65+
})
66+

test/utils.cjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @type {() => [(event: MediaQueryListEvent) => void, MediaQueryListEvent[]]}
3+
*/
4+
exports.mock = () => {
5+
/**
6+
* @type MediaQueryListEvent[]
7+
*/
8+
const calls = [];
9+
return [
10+
(event) => {
11+
calls.push(event);
12+
},
13+
calls,
14+
];
15+
};

0 commit comments

Comments
 (0)