@@ -36,6 +36,8 @@ interface MatchMediaContext {
36
36
mediaType : 'screen' | 'print' ;
37
37
viewportWidth : number ;
38
38
viewportHeight : number ;
39
+ primaryPointingDevice ?: 'touchscreen' | 'mouse' ;
40
+ secondaryPointingDevice ?: 'touchscreen' | 'mouse' ;
39
41
}
40
42
41
43
class ParsedMediaType {
@@ -128,8 +130,65 @@ class ParsedOrientation {
128
130
}
129
131
}
130
132
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
+
131
186
// 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
+ ] ;
133
192
const parsedMediaInParens = [ ...parsedMediaFeature ] ;
134
193
type ParsedMediaFeature = ParsedType < typeof parsedMediaFeature [ - 1 ] > ;
135
194
type ParsedMediaInParens = ParsedMediaFeature ;
@@ -302,8 +361,20 @@ test('screen and (min-width: 480px)', () => {
302
361
} ) ;
303
362
304
363
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
+
307
378
const printSized = ( viewportWidth : number , viewportHeight : number ) =>
308
379
( { mediaType : 'print' , viewportWidth, viewportHeight } as const ) ;
309
380
@@ -352,6 +423,89 @@ test('matchMedia()', () => {
352
423
matchMedia ( screenSized ( 100 , 100 ) , '(orientation: portrait)' ) . matches
353
424
) . toBe ( true ) ;
354
425
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
+
355
509
expect (
356
510
matchMedia ( screenSized ( 481 , 100 ) , 'screen and (min-width: 480px)' ) . matches
357
511
) . toBe ( true ) ;
@@ -365,4 +519,16 @@ test('matchMedia()', () => {
365
519
'only screen and (min-width: 480px) and (orientation: landscape)'
366
520
) . matches
367
521
) . 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 ) ;
368
534
} ) ;
0 commit comments