Skip to content

Commit 18c1867

Browse files
committed
Support hover and any-hover
1 parent e63464f commit 18c1867

File tree

1 file changed

+169
-3
lines changed

1 file changed

+169
-3
lines changed

src/media-query.test.ts

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ interface MatchMediaContext {
3636
mediaType: 'screen' | 'print';
3737
viewportWidth: number;
3838
viewportHeight: number;
39+
primaryPointingDevice?: 'touchscreen' | 'mouse';
40+
secondaryPointingDevice?: 'touchscreen' | 'mouse';
3941
}
4042

4143
class ParsedMediaType {
@@ -128,8 +130,65 @@ class ParsedOrientation {
128130
}
129131
}
130132

133+
/**
134+
https://www.w3.org/TR/mediaqueries-5/#hover
135+
*/
136+
class ParsedHover {
137+
constructor(
138+
public readonly hover: 'none' | 'hover',
139+
public readonly any?: 'any'
140+
) {}
141+
142+
private canPrimaryHover(context: MatchMediaContext) {
143+
switch (context.primaryPointingDevice) {
144+
case 'mouse':
145+
return true;
146+
default:
147+
return false;
148+
}
149+
}
150+
151+
private canAnyHover(context: MatchMediaContext) {
152+
switch (context.secondaryPointingDevice) {
153+
case 'mouse':
154+
return true;
155+
default:
156+
return this.canPrimaryHover(context);
157+
}
158+
}
159+
160+
matches(context: MatchMediaContext) {
161+
const canHover =
162+
this.any === 'any'
163+
? this.canAnyHover(context)
164+
: this.canPrimaryHover(context);
165+
166+
if (canHover) {
167+
return this.hover === 'hover';
168+
} else {
169+
return this.hover === 'none';
170+
}
171+
}
172+
173+
static *Parser() {
174+
yield optionalWhitespace;
175+
yield '(';
176+
const any: boolean = yield has('any-');
177+
yield 'hover:';
178+
yield optionalWhitespace;
179+
const hover: 'none' | 'hover' = yield ['none', 'hover'];
180+
yield optionalWhitespace;
181+
yield ')';
182+
return new ParsedHover(hover, any ? 'any' : undefined);
183+
}
184+
}
185+
131186
// See https://www.w3.org/TR/mediaqueries-5/#mq-syntax
132-
const parsedMediaFeature = [ParsedMinWidth.Parser, ParsedOrientation.Parser];
187+
const parsedMediaFeature = [
188+
ParsedMinWidth.Parser,
189+
ParsedOrientation.Parser,
190+
ParsedHover.Parser,
191+
];
133192
const parsedMediaInParens = [...parsedMediaFeature];
134193
type ParsedMediaFeature = ParsedType<typeof parsedMediaFeature[-1]>;
135194
type ParsedMediaInParens = ParsedMediaFeature;
@@ -302,8 +361,20 @@ test('screen and (min-width: 480px)', () => {
302361
});
303362

304363
test('matchMedia()', () => {
305-
const screenSized = (viewportWidth: number, viewportHeight: number) =>
306-
({ mediaType: 'screen', viewportWidth, viewportHeight } as const);
364+
const screenSized = (
365+
viewportWidth: number,
366+
viewportHeight: number,
367+
primaryPointingDevice: 'touchscreen' | 'mouse' | undefined = 'touchscreen',
368+
secondaryPointingDevice?: 'touchscreen' | 'mouse'
369+
) =>
370+
({
371+
mediaType: 'screen',
372+
viewportWidth,
373+
viewportHeight,
374+
primaryPointingDevice,
375+
secondaryPointingDevice,
376+
} as const);
377+
307378
const printSized = (viewportWidth: number, viewportHeight: number) =>
308379
({ mediaType: 'print', viewportWidth, viewportHeight } as const);
309380

@@ -352,6 +423,89 @@ test('matchMedia()', () => {
352423
matchMedia(screenSized(100, 100), '(orientation: portrait)').matches
353424
).toBe(true);
354425

426+
expect(
427+
matchMedia(screenSized(100, 100, 'touchscreen'), '(hover: none)').matches
428+
).toBe(true);
429+
expect(
430+
matchMedia(screenSized(100, 100, 'touchscreen'), '(hover: hover)').matches
431+
).toBe(false);
432+
expect(
433+
matchMedia(screenSized(100, 100, 'touchscreen'), '(any-hover: none)')
434+
.matches
435+
).toBe(true);
436+
expect(
437+
matchMedia(screenSized(100, 100, 'touchscreen'), '(any-hover: hover)')
438+
.matches
439+
).toBe(false);
440+
441+
expect(
442+
matchMedia(screenSized(100, 100, 'touchscreen', 'mouse'), '(hover: none)')
443+
.matches
444+
).toBe(true);
445+
expect(
446+
matchMedia(screenSized(100, 100, 'touchscreen', 'mouse'), '(hover: hover)')
447+
.matches
448+
).toBe(false);
449+
expect(
450+
matchMedia(
451+
screenSized(100, 100, 'touchscreen', 'mouse'),
452+
'(any-hover: none)'
453+
).matches
454+
).toBe(false);
455+
expect(
456+
matchMedia(
457+
screenSized(100, 100, 'touchscreen', 'mouse'),
458+
'(any-hover: hover)'
459+
).matches
460+
).toBe(true);
461+
462+
expect(
463+
matchMedia(screenSized(100, 100, 'mouse'), '(hover: none)').matches
464+
).toBe(false);
465+
expect(
466+
matchMedia(screenSized(100, 100, 'mouse'), '(hover: hover)').matches
467+
).toBe(true);
468+
expect(
469+
matchMedia(screenSized(100, 100, 'mouse'), '(any-hover: none)').matches
470+
).toBe(false);
471+
expect(
472+
matchMedia(screenSized(100, 100, 'mouse'), '(any-hover: hover)').matches
473+
).toBe(true);
474+
475+
expect(
476+
matchMedia(screenSized(100, 100, 'mouse', 'touchscreen'), '(hover: none)')
477+
.matches
478+
).toBe(false);
479+
expect(
480+
matchMedia(screenSized(100, 100, 'mouse', 'touchscreen'), '(hover: hover)')
481+
.matches
482+
).toBe(true);
483+
expect(
484+
matchMedia(
485+
screenSized(100, 100, 'mouse', 'touchscreen'),
486+
'(any-hover: none)'
487+
).matches
488+
).toBe(false);
489+
expect(
490+
matchMedia(
491+
screenSized(100, 100, 'mouse', 'touchscreen'),
492+
'(any-hover: hover)'
493+
).matches
494+
).toBe(true);
495+
496+
expect(
497+
matchMedia(screenSized(100, 100, undefined), '(hover: none)').matches
498+
).toBe(true);
499+
expect(
500+
matchMedia(screenSized(100, 100, undefined), '(hover: hover)').matches
501+
).toBe(false);
502+
expect(
503+
matchMedia(screenSized(100, 100, undefined), '(any-hover: none)').matches
504+
).toBe(true);
505+
expect(
506+
matchMedia(screenSized(100, 100, undefined), '(any-hover: hover)').matches
507+
).toBe(false);
508+
355509
expect(
356510
matchMedia(screenSized(481, 100), 'screen and (min-width: 480px)').matches
357511
).toBe(true);
@@ -365,4 +519,16 @@ test('matchMedia()', () => {
365519
'only screen and (min-width: 480px) and (orientation: landscape)'
366520
).matches
367521
).toBe(true);
522+
expect(
523+
matchMedia(
524+
screenSized(481, 100, "touchscreen"),
525+
'only screen and (min-width: 480px) and (orientation: landscape) and (any-hover: hover)'
526+
).matches
527+
).toBe(false);
528+
expect(
529+
matchMedia(
530+
screenSized(481, 100, "touchscreen", "mouse"),
531+
'only screen and (min-width: 480px) and (orientation: landscape) and (any-hover: hover)'
532+
).matches
533+
).toBe(true);
368534
});

0 commit comments

Comments
 (0)