diff --git a/src/index.ts b/src/index.ts index e92815e..a2bfcb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { compileQuery, matches, type Environment, type EvaluateResult, type SimplePerm } from "media-query-fns"; +// TODO: remove `deviceWidth` & `deviceHeight` from it, and only derive those from `width`, `height`, and `dppx` type MediaState = { [key in keyof Environment as key extends `${infer Key}Px` ? Key : key]?: Environment[key] }; const DEFAULT_ENV: Parameters[1] = { @@ -35,6 +36,58 @@ type Feature = keyof MediaState; const now = Date.now(); +/** + * Match the renaming done by media-query-fns + * + * {@link https://github.com/tbjgolden/media-query-fns/blob/7dae2618b9321f503cbd0a44a202a9190665e80e/lib/matches.ts#L200-L533} + */ +const MEDIA_FEATURE_TO_FEATURES = { + // Same but different casing + "any-hover": ["anyHover"], + "any-pointer": ["anyPointer"], + "color-gamut": ["colorGamut"], + "color-index": ["colorIndex"], + "display-mode": ["displayMode"], + "dynamic-range": ["dynamicRange"], + "environment-blending": ["environmentBlending"], + "forced-colors": ["forcedColors"], + grid: ["grid"], + "horizontal-viewport-segments": ["horizontalViewportSegments"], + hover: ["hover"], + "inverted-colors": ["invertedColors"], + "media-type": ["mediaType"], + "nav-controls": ["navControls"], + "overflow-block": ["overflowBlock"], + "overflow-inline": ["overflowInline"], + pointer: ["pointer"], + "prefers-color-scheme": ["prefersColorScheme"], + "prefers-contrast": ["prefersContrast"], + "prefers-reduced-data": ["prefersReducedData"], + "prefers-reduced-motion": ["prefersReducedMotion"], + "prefers-reduced-transparency": ["prefersReducedTransparency"], + scan: ["scan"], + scripting: ["scripting"], + update: ["update"], + "vertical-viewport-segments": ["verticalViewportSegments"], + "video-color-gamut": ["videoColorGamut"], + "video-dynamic-range": ["videoDynamicRange"], + + // Numbers + monochrome: ["monochromeBits"], + color: ["colorBits"], + resolution: ["dppx"], + + // Pixels + width: ["width"], + height: ["height"], + "device-height": ["deviceHeight"], + "device-width": ["deviceWidth"], + + // Combinations + "aspect-ratio": ["width", "height"], + "device-aspect-ratio": ["deviceHeight", "deviceWidth"], +} as const satisfies Record; + // Event was added in node 15, so until we drop the support for versions before it, we need to use this class EventLegacy { type: "change"; @@ -73,7 +126,17 @@ const EventCompat: typeof Event = typeof Event === "undefined" ? EventLegacy : E const getFeaturesFromQuery = (query: EvaluateResult): Set => { const features = new Set(); query.simplePerms.forEach((perm) => { - Object.keys(perm).forEach((feature) => features.add(feature as Feature)); + Object.keys(perm).forEach((mediaFeature) => { + if (mediaFeature in MEDIA_FEATURE_TO_FEATURES) { + MEDIA_FEATURE_TO_FEATURES[mediaFeature as keyof typeof MEDIA_FEATURE_TO_FEATURES].forEach((feature) => + features.add(feature), + ); + } + // // For debut, we can comment out those: + // else { + // console.error("Unrecognized " + mediaFeature); + // } + }); }); return features; }; diff --git a/test/listeners.cjs b/test/listeners.cjs index 08f31a9..d1077cb 100644 --- a/test/listeners.cjs +++ b/test/listeners.cjs @@ -3,25 +3,13 @@ const { test } = require("node:test"); const { strict: assert } = require("node:assert"); const { matchMedia, setMedia, cleanupListeners, MediaQueryListEvent, cleanup } = require("mock-match-media"); +const { mock } = require("./utils.cjs"); test.afterEach(() => { // cleanup listeners and state after each test cleanup(); }); -/** - * @type {() => [(event: MediaQueryListEvent) => void, MediaQueryListEvent[]]} - */ -const mock = () => { - const calls = []; - return [ - (event) => { - calls.push(event); - }, - calls, - ]; -}; - test("`.addListener()`", () => { const mql = matchMedia("(min-width: 500px)"); diff --git a/test/matchers/aspect-ratio.cjs b/test/matchers/aspect-ratio.cjs index 3cf292a..244c70b 100644 --- a/test/matchers/aspect-ratio.cjs +++ b/test/matchers/aspect-ratio.cjs @@ -2,10 +2,11 @@ const { test } = require("node:test"); const { strict: assert } = require("node:assert"); -const { matchMedia, setMedia, cleanupMedia } = require("mock-match-media"); +const { matchMedia, setMedia, cleanup } = require("mock-match-media"); +const { mock } = require("../utils.cjs"); test.beforeEach(() => { - cleanupMedia(); + cleanup(); }); test("unset", () => { @@ -238,3 +239,35 @@ test("other syntax", () => { }); assert.equal(matchMedia("(aspect-ratio: 3)").matches, true); }); + +test("`.addEventListener()`", () => { + const mqlAR3 = matchMedia("(aspect-ratio > 3)"); + const [cb, calls] = mock(); + + mqlAR3.addEventListener("change", cb); + + assert.equal(matchMedia("(aspect-ratio > 3)").matches, false); + + setMedia({ + width: 7, + }); + + assert.equal(matchMedia("(aspect-ratio > 3)").matches, false); + assert.equal(calls.length, 0); + + setMedia({ + height: 2, + }); + + assert.equal(matchMedia("(aspect-ratio > 3)").matches, true); + assert.equal(calls.length, 1); + assert.equal(calls[0].matches, true); + + setMedia({ + height: 3, + }); + + assert.equal(matchMedia("(aspect-ratio > 3)").matches, false); + assert.equal(calls.length, 2); + assert.equal(calls[1].matches, false); +}); diff --git a/test/matchers/device-aspect-ratio.cjs b/test/matchers/device-aspect-ratio.cjs index 04a7764..e606149 100644 --- a/test/matchers/device-aspect-ratio.cjs +++ b/test/matchers/device-aspect-ratio.cjs @@ -2,10 +2,11 @@ const { test } = require("node:test"); const { strict: assert } = require("node:assert"); -const { matchMedia, setMedia, cleanupMedia } = require("mock-match-media"); +const { matchMedia, setMedia, cleanup } = require("mock-match-media"); +const { mock } = require("../utils.cjs"); test.beforeEach(() => { - cleanupMedia(); + cleanup(); }); test("unset", () => { @@ -208,3 +209,42 @@ test("other syntax", () => { assert.equal(matchMedia("(device-aspect-ratio: 16 / 16)").matches, true); assert.equal(matchMedia("(device-aspect-ratio: 16 / 16)").matches, true); }); + +test("`.addEventListener()`", () => { + const mqlAR3 = matchMedia("(device-aspect-ratio > 3)"); + const [cb, calls] = mock(); + + mqlAR3.addEventListener("change", cb); + + assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false); + + setMedia({ + width: 7, + }); + + assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false); + assert.equal(calls.length, 0); + + setMedia({ + height: 2, + }); + + assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, true); + assert.equal(calls.length, 1); + assert.equal(calls[0].matches, true); + + setMedia({ + height: 3, + }); + + assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false); + assert.equal(calls.length, 2); + assert.equal(calls[1].matches, false); + + setMedia({ + dppx: 2, + }); + + assert.equal(matchMedia("(device-aspect-ratio > 3)").matches, false); + assert.equal(calls.length, 2); +}); diff --git a/test/matchers/prefers-color-scheme.cjs b/test/matchers/prefers-color-scheme.cjs index 08164b6..9f74430 100644 --- a/test/matchers/prefers-color-scheme.cjs +++ b/test/matchers/prefers-color-scheme.cjs @@ -3,11 +3,13 @@ const { test } = require("node:test"); const { strict: assert } = require("node:assert"); const { matchMedia, setMedia, cleanupMedia } = require("mock-match-media"); +const { mock } = require("../utils.cjs"); test.beforeEach(() => { cleanupMedia(); }); + test("unset", () => { assert.equal(matchMedia("(prefers-color-scheme: light)").matches, false); assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, false); @@ -30,3 +32,35 @@ test("dark", () => { assert.equal(matchMedia("(prefers-color-scheme: light)").matches, false); assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, true); }); + +test("`.addEventListener()`", () => { + const mqlLight = matchMedia("(prefers-color-scheme: light)"); + const mqlDark = matchMedia("(prefers-color-scheme: dark)"); + const [cbLight, callsLight] = mock(); + const [cbDark, callsDark] = mock(); + + mqlLight.addEventListener("change", cbLight); + mqlDark.addEventListener("change", cbDark); + + setMedia({ + prefersColorScheme: "light", + }); + + assert.equal(matchMedia("(prefers-color-scheme: light)").matches, true); + assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, false); + assert.equal(callsLight.length, 1); + assert.equal(callsLight[0].matches, true); + assert.equal(callsDark.length, 0); + + setMedia({ + prefersColorScheme: "dark", + }); + + assert.equal(matchMedia("(prefers-color-scheme: light)").matches, false); + assert.equal(matchMedia("(prefers-color-scheme: dark)").matches, true); + assert.equal(callsLight.length, 2); + assert.equal(callsLight[1].matches, false); + assert.equal(callsDark.length, 1); + assert.equal(callsDark[0].matches, true); +}) + diff --git a/test/utils.cjs b/test/utils.cjs new file mode 100644 index 0000000..c7249fd --- /dev/null +++ b/test/utils.cjs @@ -0,0 +1,15 @@ +/** + * @type {() => [(event: MediaQueryListEvent) => void, MediaQueryListEvent[]]} + */ +exports.mock = () => { + /** + * @type MediaQueryListEvent[] + */ + const calls = []; + return [ + (event) => { + calls.push(event); + }, + calls, + ]; +};