@material/web 1.1.2-nightly.c97362c.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/button/internal/button.d.ts +8 -0
  2. package/button/internal/button.js +9 -1
  3. package/button/internal/button.js.map +1 -1
  4. package/dialog/internal/_dialog.scss +50 -42
  5. package/dialog/internal/dialog-styles.css.js +1 -1
  6. package/dialog/internal/dialog-styles.css.js.map +1 -1
  7. package/divider/internal/_divider.scss +6 -6
  8. package/divider/internal/divider-styles.css.js +1 -1
  9. package/divider/internal/divider-styles.css.js.map +1 -1
  10. package/elevation/internal/_elevation.scss +10 -4
  11. package/elevation/internal/elevation-styles.css.js +1 -1
  12. package/elevation/internal/elevation-styles.css.js.map +1 -1
  13. package/iconbutton/internal/icon-button.d.ts +8 -0
  14. package/iconbutton/internal/icon-button.js +9 -1
  15. package/iconbutton/internal/icon-button.js.map +1 -1
  16. package/menu/internal/menuitem/_menu-item.scss +31 -52
  17. package/menu/internal/menuitem/menu-item-styles.css.js +1 -1
  18. package/menu/internal/menuitem/menu-item-styles.css.js.map +1 -1
  19. package/package.json +5 -3
  20. package/radio/internal/_radio.scss +34 -34
  21. package/radio/internal/radio-styles.css.js +1 -1
  22. package/radio/internal/radio-styles.css.js.map +1 -1
  23. package/ripple/internal/_ripple.scss +8 -9
  24. package/ripple/internal/ripple-styles.css.js +1 -1
  25. package/ripple/internal/ripple-styles.css.js.map +1 -1
  26. package/switch/internal/_handle.scss +42 -37
  27. package/switch/internal/_icon.scss +17 -17
  28. package/switch/internal/_switch.scss +52 -52
  29. package/switch/internal/_track.scss +18 -18
  30. package/switch/internal/switch-styles.css.js +1 -1
  31. package/switch/internal/switch-styles.css.js.map +1 -1
  32. package/testing/harness.d.ts +371 -0
  33. package/testing/harness.js +737 -0
  34. package/testing/harness.js.map +1 -0
  35. package/testing/transform-pseudo-classes.d.ts +39 -0
  36. package/testing/transform-pseudo-classes.js +172 -0
  37. package/testing/transform-pseudo-classes.js.map +1 -0
  38. package/tokens/_index.scss +0 -1
  39. package/tokens/_md-comp-list-item.scss +2 -2
  40. package/tokens/_md-comp-menu-item.scss +63 -7
  41. package/tokens/_md-comp-menu-list-item.scss +0 -162
@@ -0,0 +1,737 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2021 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { defaultTransformPseudoClasses, getTransformedPseudoClass, transformPseudoClasses, } from './transform-pseudo-classes.js';
7
+ /**
8
+ * Checks whether or not an element has a Harness attached to it on the
9
+ * `element.harness` property.
10
+ *
11
+ * @param element The element to check.
12
+ * @return True if the element has a harness property.
13
+ */
14
+ export function isElementWithHarness(element) {
15
+ return element.harness instanceof Harness;
16
+ }
17
+ /**
18
+ * A test harness class that can be used to simulate interaction with an
19
+ * element.
20
+ *
21
+ * @template E The harness's element type.
22
+ */
23
+ export class Harness {
24
+ /**
25
+ * Creates a new harness for the given element.
26
+ *
27
+ * @param element The element that this harness controls.
28
+ */
29
+ constructor(element) {
30
+ /**
31
+ * The pseudo classes that should be transformed for simulation. Component
32
+ * subclasses may override this to add additional pseudo classes.
33
+ */
34
+ this.transformPseudoClasses = defaultTransformPseudoClasses;
35
+ /**
36
+ * A set of elements that have already been patched to support transformed
37
+ * pseudo classes.
38
+ */
39
+ this.patchedElements = new WeakSet();
40
+ this.element = element;
41
+ this.element.harness = this;
42
+ }
43
+ /**
44
+ * Resets the element's simulated classes to the default state.
45
+ */
46
+ async reset() {
47
+ const element = await this.getInteractiveElement();
48
+ for (const pseudoClass of this.transformPseudoClasses) {
49
+ this.forEachNodeFrom(element, (el) => {
50
+ this.removePseudoClass(el, pseudoClass);
51
+ });
52
+ }
53
+ }
54
+ /**
55
+ * Hovers and clicks on an element. This will generate a `click` event.
56
+ *
57
+ * @param init Additional event options.
58
+ */
59
+ async clickWithMouse(init = {}) {
60
+ await this.startClickWithMouse(init);
61
+ await this.endClickWithMouse(init);
62
+ }
63
+ /**
64
+ * Begins a click with a mouse. Use this along with `endClickWithMouse()` to
65
+ * customize the length of the click.
66
+ *
67
+ * @param init Additional event options.
68
+ */
69
+ async startClickWithMouse(init = {}) {
70
+ const element = await this.getInteractiveElement();
71
+ await this.startHover();
72
+ this.simulateMousePress(element, init);
73
+ }
74
+ /**
75
+ * Finishes a click with a mouse. Use this along with `startClickWithMouse()`
76
+ * to customize the length of the click. This will generate a `click` event.
77
+ *
78
+ * @param init Additional event options.
79
+ */
80
+ async endClickWithMouse(init = {}) {
81
+ const element = await this.getInteractiveElement();
82
+ this.simulateMouseRelease(element, init);
83
+ if ((init?.button ?? 0) === 0) {
84
+ // Dispatch a click for left-click only (default).
85
+ this.simulateClick(element, init);
86
+ }
87
+ }
88
+ /**
89
+ * Clicks an element with the keyboard (defaults to spacebar). This will
90
+ * generate a `click` event.
91
+ *
92
+ * @param init Additional event options.
93
+ */
94
+ async clickWithKeyboard(init = {}) {
95
+ const element = await this.getInteractiveElement();
96
+ await this.startClickWithKeyboard(init);
97
+ await this.endClickWithKeyboard(init);
98
+ this.simulateClick(element, init);
99
+ }
100
+ /**
101
+ * Begins a click with the keyboard (defaults to spacebar). Use this along
102
+ * with `endClickWithKeyboard()` to customize the length of the click.
103
+ *
104
+ * @param init Additional event options.
105
+ */
106
+ async startClickWithKeyboard(init = {}) {
107
+ const element = await this.getInteractiveElement();
108
+ await this.focusWithKeyboard(init);
109
+ this.simulateKeydown(element, init.key ?? ' ', init);
110
+ this.simulateClick(element, init);
111
+ }
112
+ /**
113
+ * Finishes a click with the keyboard (defaults to spacebar). Use this along
114
+ * with `startClickWithKeyboard()` to customize the length of the click.
115
+ *
116
+ * @param init Additional event options.
117
+ */
118
+ async endClickWithKeyboard(init = {}) {
119
+ const element = await this.getInteractiveElement();
120
+ this.simulateKeyup(element, init.key ?? ' ', init);
121
+ this.simulateClick(element, init);
122
+ }
123
+ /**
124
+ * Right-clicks and opens a context menu. This will generate a `contextmenu`
125
+ * event.
126
+ */
127
+ async rightClickWithMouse() {
128
+ const element = await this.getInteractiveElement();
129
+ const rightMouseButton = { button: 2, buttons: 2 };
130
+ await this.startClickWithMouse(rightMouseButton);
131
+ // Note: contextmenu right clicks do not generate the up events
132
+ this.simulateContextmenu(element, rightMouseButton);
133
+ }
134
+ /**
135
+ * Taps once on the element with a simulated touch. This will generate a
136
+ * `click` event.
137
+ *
138
+ * @param init Additional event options.
139
+ * @param touchInit Additional touch event options.
140
+ */
141
+ async tap(init = {}, touchInit = {}) {
142
+ const element = await this.getInteractiveElement();
143
+ this.simulateTouchPress(element, init, touchInit);
144
+ this.simulateTouchRelease(element, init, touchInit);
145
+ if ((init?.isPrimary ?? true) === true) {
146
+ // Dispatch a click for primary touches only (default).
147
+ await this.endTapClick(init);
148
+ }
149
+ }
150
+ /**
151
+ * Begins a touch tap. Use this along with `endTap()` to customize the length
152
+ * or number of taps.
153
+ *
154
+ * @param init Additional event options.
155
+ * @param touchInit Additional touch event options.
156
+ */
157
+ async startTap(init = {}, touchInit = {}) {
158
+ const element = await this.getInteractiveElement();
159
+ this.simulateTouchPress(element, init, touchInit);
160
+ }
161
+ /**
162
+ * Simulates a `contextmenu` event for touch. Use this along with `startTap()`
163
+ * to generate a tap-and-hold context menu interaction.
164
+ *
165
+ * @param init Additional event options.
166
+ */
167
+ async startTapContextMenu(init = {}) {
168
+ const element = await this.getInteractiveElement();
169
+ this.simulateContextmenu(element, init);
170
+ }
171
+ /**
172
+ * Finished a touch tap. Use this along with `startTap()` to customize the
173
+ * length or number of taps.
174
+ *
175
+ * This will NOT generate a `click` event.
176
+ *
177
+ * @param init Additional event options.
178
+ * @param touchInit Additional touch event options.
179
+ */
180
+ async endTap(init = {}, touchInit = {}) {
181
+ const element = await this.getInteractiveElement();
182
+ this.simulateTouchRelease(element, init, touchInit);
183
+ }
184
+ /**
185
+ * Simulates a `click` event for touch. Use this along with `endTap()` to
186
+ * control the timing of tap and click events.
187
+ *
188
+ * @param init Additional event options.
189
+ */
190
+ async endTapClick(init = {}) {
191
+ const element = await this.getInteractiveElement();
192
+ this.simulateClick(element, {
193
+ pointerType: 'touch',
194
+ ...init,
195
+ });
196
+ }
197
+ /**
198
+ * Cancels a touch tap.
199
+ *
200
+ * @param init Additional event options.
201
+ * @param touchInit Additional touch event options.
202
+ */
203
+ async cancelTap(init = {}, touchInit = {}) {
204
+ const element = await this.getInteractiveElement();
205
+ this.simulateTouchCancel(element, init, touchInit);
206
+ }
207
+ /**
208
+ * Hovers over the element with a simulated mouse.
209
+ */
210
+ async startHover() {
211
+ const element = await this.getInteractiveElement();
212
+ this.simulateStartHover(element);
213
+ }
214
+ /**
215
+ * Moves the simulated mouse cursor off of the element.
216
+ */
217
+ async endHover() {
218
+ const element = await this.getInteractiveElement();
219
+ this.simulateEndHover(element);
220
+ }
221
+ /**
222
+ * Simulates focusing an element with the keyboard.
223
+ *
224
+ * @param init Additional event options.
225
+ */
226
+ async focusWithKeyboard(init = {}) {
227
+ const element = await this.getInteractiveElement();
228
+ this.simulateKeyboardFocus(element);
229
+ }
230
+ /**
231
+ * Simulates focusing an element with a pointer.
232
+ */
233
+ async focusWithPointer() {
234
+ const element = await this.getInteractiveElement();
235
+ await this.startHover();
236
+ this.simulatePointerFocus(element);
237
+ }
238
+ /**
239
+ * Simulates unfocusing an element.
240
+ */
241
+ async blur() {
242
+ const element = await this.getInteractiveElement();
243
+ await this.endHover();
244
+ this.simulateBlur(element);
245
+ }
246
+ /**
247
+ * Simulates a keypress on an element.
248
+ *
249
+ * @param key The key to press.
250
+ * @param init Additional event options.
251
+ */
252
+ async keypress(key, init = {}) {
253
+ const element = await this.getInteractiveElement();
254
+ this.simulateKeypress(element, key, init);
255
+ }
256
+ /**
257
+ * Simulates submitting the element's associated form element.
258
+ *
259
+ * @param form (Optional) form to submit, defaults to the elemnt's form.
260
+ * @return The submitted form data or null if the element has no associated
261
+ * form.
262
+ */
263
+ submitForm(form = this.element.form) {
264
+ if (!form) {
265
+ return new FormData();
266
+ }
267
+ return new Promise((resolve) => {
268
+ const submitListener = (event) => {
269
+ event.preventDefault();
270
+ const data = new FormData(form);
271
+ resolve(data);
272
+ return false;
273
+ };
274
+ form.addEventListener('submit', submitListener, { once: true });
275
+ form.requestSubmit();
276
+ });
277
+ }
278
+ /**
279
+ * Returns the element that should be used for interaction simulation.
280
+ * Defaults to the host element itself.
281
+ *
282
+ * Subclasses should override this if the interactive element is not the host.
283
+ *
284
+ * @return The element to use in simulation.
285
+ */
286
+ async getInteractiveElement() {
287
+ return this.element;
288
+ }
289
+ /**
290
+ * Adds a pseudo class to an element. The element's shadow root styles (or
291
+ * document if not in a shadow root) will be transformed to support
292
+ * simulated pseudo classes.
293
+ *
294
+ * @param element The element to add a pseudo class to.
295
+ * @param pseudoClass The pseudo class to add.
296
+ */
297
+ addPseudoClass(element, pseudoClass) {
298
+ if (!this.transformPseudoClasses.includes(pseudoClass)) {
299
+ return;
300
+ }
301
+ const root = element.getRootNode();
302
+ if (element.shadowRoot) {
303
+ transformPseudoClasses(element.shadowRoot.adoptedStyleSheets || [], this.transformPseudoClasses);
304
+ }
305
+ transformPseudoClasses(root.styleSheets, this.transformPseudoClasses);
306
+ transformPseudoClasses(root.adoptedStyleSheets || [], this.transformPseudoClasses);
307
+ element.classList.add(getTransformedPseudoClass(pseudoClass));
308
+ this.patchForTransformedPseudoClasses(element);
309
+ }
310
+ /**
311
+ * Removes a pseudo class from an element.
312
+ *
313
+ * @param element The element to remove a pseudo class from.
314
+ * @param pseudoClass The pseudo class to remove.
315
+ */
316
+ removePseudoClass(element, pseudoClass) {
317
+ element.classList.remove(getTransformedPseudoClass(pseudoClass));
318
+ }
319
+ /**
320
+ * Simulates a click event.
321
+ *
322
+ * @param element The element to click.
323
+ * @param init Additional event options.
324
+ */
325
+ simulateClick(element, init = {}) {
326
+ // Firefox does not support some simulations with PointerEvents, such as
327
+ // selecting an <input type="checkbox">. Use MouseEvent for browser support.
328
+ element.dispatchEvent(new MouseEvent('click', {
329
+ ...this.createMouseEventInit(element),
330
+ ...init,
331
+ }));
332
+ }
333
+ /**
334
+ * Simulates a contextmenu event.
335
+ *
336
+ * @param element The element to generate an event for.
337
+ * @param init Additional event options.
338
+ */
339
+ simulateContextmenu(element, init = {}) {
340
+ element.dispatchEvent(new MouseEvent('contextmenu', {
341
+ ...this.createMouseEventInit(element),
342
+ button: 2,
343
+ buttons: 2,
344
+ ...init,
345
+ }));
346
+ }
347
+ /**
348
+ * Simulates focusing with a keyboard. The difference between this and
349
+ * `simulatePointerFocus` is that keyboard focus will include the
350
+ * `:focus-visible` pseudo class.
351
+ *
352
+ * @param element The element to focus with a keyboard.
353
+ */
354
+ simulateKeyboardFocus(element) {
355
+ this.simulateKeydown(element.ownerDocument, 'Tab');
356
+ this.addPseudoClass(element, ':focus-visible');
357
+ this.simulatePointerFocus(element);
358
+ this.simulateKeyup(element, 'Tab');
359
+ }
360
+ /**
361
+ * Simulates focusing with a pointer.
362
+ *
363
+ * @param element The element to focus with a pointer.
364
+ */
365
+ simulatePointerFocus(element) {
366
+ this.addPseudoClass(element, ':focus');
367
+ this.forEachNodeFrom(element, (el) => {
368
+ this.addPseudoClass(el, ':focus-within');
369
+ });
370
+ element.dispatchEvent(new FocusEvent('focus', { composed: true }));
371
+ element.dispatchEvent(new FocusEvent('focusin', { bubbles: true, composed: true }));
372
+ }
373
+ /**
374
+ * Simulates unfocusing an element.
375
+ *
376
+ * @param element The element to blur.
377
+ */
378
+ simulateBlur(element) {
379
+ this.removePseudoClass(element, ':focus');
380
+ this.removePseudoClass(element, ':focus-visible');
381
+ this.forEachNodeFrom(element, (el) => {
382
+ this.removePseudoClass(el, ':focus-within');
383
+ });
384
+ element.dispatchEvent(new FocusEvent('blur', { composed: true }));
385
+ element.dispatchEvent(new FocusEvent('focusout', { bubbles: true, composed: true }));
386
+ }
387
+ /**
388
+ * Simulates a mouse pointer hovering over an element.
389
+ *
390
+ * @param element The element to hover over.
391
+ * @param init Additional event options.
392
+ */
393
+ simulateStartHover(element, init = {}) {
394
+ this.forEachNodeFrom(element, (el) => {
395
+ this.addPseudoClass(el, ':hover');
396
+ });
397
+ const rect = element.getBoundingClientRect();
398
+ const mouseInit = this.createMouseEventInit(element);
399
+ const mouseEnterInit = {
400
+ ...mouseInit,
401
+ bubbles: false,
402
+ clientX: rect.left,
403
+ clientY: rect.top,
404
+ screenX: rect.left,
405
+ screenY: rect.top,
406
+ };
407
+ const pointerInit = {
408
+ ...mouseInit,
409
+ isPrimary: true,
410
+ pointerType: 'mouse',
411
+ };
412
+ const pointerEnterInit = {
413
+ ...pointerInit,
414
+ ...mouseEnterInit,
415
+ ...init,
416
+ };
417
+ element.dispatchEvent(new PointerEvent('pointerover', pointerInit));
418
+ element.dispatchEvent(new PointerEvent('pointerenter', pointerEnterInit));
419
+ element.dispatchEvent(new MouseEvent('mouseover', mouseInit));
420
+ element.dispatchEvent(new MouseEvent('mouseenter', mouseEnterInit));
421
+ }
422
+ /**
423
+ * Simulates a mouse pointer leaving the element.
424
+ *
425
+ * @param element The element to stop hovering over.
426
+ * @param init Additional event options.
427
+ */
428
+ simulateEndHover(element, init = {}) {
429
+ this.forEachNodeFrom(element, (el) => {
430
+ this.removePseudoClass(el, ':hover');
431
+ });
432
+ const rect = element.getBoundingClientRect();
433
+ const mouseInit = this.createMouseEventInit(element);
434
+ const mouseLeaveInit = {
435
+ ...mouseInit,
436
+ bubbles: false,
437
+ clientX: rect.left - 1,
438
+ clientY: rect.top - 1,
439
+ screenX: rect.left - 1,
440
+ screenY: rect.top - 1,
441
+ };
442
+ const pointerInit = {
443
+ ...mouseInit,
444
+ isPrimary: true,
445
+ pointerType: 'mouse',
446
+ ...init,
447
+ };
448
+ const pointerLeaveInit = {
449
+ ...pointerInit,
450
+ ...mouseLeaveInit,
451
+ };
452
+ element.dispatchEvent(new PointerEvent('pointerout', pointerInit));
453
+ element.dispatchEvent(new PointerEvent('pointerleave', pointerLeaveInit));
454
+ element.dispatchEvent(new MouseEvent('pointerout', mouseInit));
455
+ element.dispatchEvent(new MouseEvent('mouseleave', mouseLeaveInit));
456
+ }
457
+ /**
458
+ * Simulates a mouse press and hold on an element.
459
+ *
460
+ * @param element The element to press with a mouse.
461
+ * @param init Additional event options.
462
+ */
463
+ simulateMousePress(element, init = {}) {
464
+ this.addPseudoClass(element, ':active');
465
+ this.forEachNodeFrom(element, (el) => {
466
+ this.addPseudoClass(el, ':active');
467
+ });
468
+ const mouseInit = this.createMouseEventInit(element);
469
+ const pointerInit = {
470
+ ...mouseInit,
471
+ isPrimary: true,
472
+ pointerType: 'mouse',
473
+ ...init,
474
+ };
475
+ element.dispatchEvent(new PointerEvent('pointerdown', pointerInit));
476
+ element.dispatchEvent(new MouseEvent('mousedown', mouseInit));
477
+ this.simulatePointerFocus(element);
478
+ }
479
+ /**
480
+ * Simulates a mouse press release from an element.
481
+ *
482
+ * @param element The element to release pressing from.
483
+ * @param init Additional event options.
484
+ */
485
+ simulateMouseRelease(element, init = {}) {
486
+ this.removePseudoClass(element, ':active');
487
+ this.forEachNodeFrom(element, (el) => {
488
+ this.removePseudoClass(el, ':active');
489
+ });
490
+ const mouseInit = this.createMouseEventInit(element);
491
+ const pointerInit = {
492
+ ...mouseInit,
493
+ isPrimary: true,
494
+ pointerType: 'mouse',
495
+ ...init,
496
+ };
497
+ element.dispatchEvent(new PointerEvent('pointerup', pointerInit));
498
+ element.dispatchEvent(new MouseEvent('mouseup', mouseInit));
499
+ }
500
+ /**
501
+ * Simulates a touch press and hold on an element.
502
+ *
503
+ * @param element The element to press with a touch pointer.
504
+ * @param init Additional event options.
505
+ */
506
+ simulateTouchPress(element, init = {}, touchInit = {}) {
507
+ this.addPseudoClass(element, ':active');
508
+ this.forEachNodeFrom(element, (el) => {
509
+ this.addPseudoClass(el, ':active');
510
+ });
511
+ const mouseInit = this.createMouseEventInit(element);
512
+ const pointerInit = {
513
+ ...mouseInit,
514
+ isPrimary: true,
515
+ pointerType: 'touch',
516
+ ...init,
517
+ };
518
+ element.dispatchEvent(new PointerEvent('pointerdown', pointerInit));
519
+ // Firefox does not support TouchEvent constructor
520
+ if (window.TouchEvent) {
521
+ const touch = this.createTouch(element);
522
+ element.dispatchEvent(new TouchEvent('touchstart', {
523
+ touches: [touch],
524
+ targetTouches: [touch],
525
+ changedTouches: [touch],
526
+ ...touchInit,
527
+ }));
528
+ }
529
+ this.simulatePointerFocus(element);
530
+ }
531
+ /**
532
+ * Simulates a touch press release from an element.
533
+ *
534
+ * @param element The element to release pressing from.
535
+ * @param init Additional event options.
536
+ */
537
+ simulateTouchRelease(element, init = {}, touchInit = {}) {
538
+ this.removePseudoClass(element, ':active');
539
+ this.forEachNodeFrom(element, (el) => {
540
+ this.removePseudoClass(el, ':active');
541
+ });
542
+ const mouseInit = this.createMouseEventInit(element);
543
+ const pointerInit = {
544
+ ...mouseInit,
545
+ isPrimary: true,
546
+ pointerType: 'touch',
547
+ ...init,
548
+ };
549
+ element.dispatchEvent(new PointerEvent('pointerup', pointerInit));
550
+ // Firefox does not support TouchEvent constructor
551
+ if (window.TouchEvent) {
552
+ const touch = this.createTouch(element);
553
+ element.dispatchEvent(new TouchEvent('touchend', { changedTouches: [touch], ...touchInit }));
554
+ }
555
+ }
556
+ /**
557
+ * Simulates a touch cancel from an element.
558
+ *
559
+ * @param element The element to cancel a touch for.
560
+ * @param init Additional event options.
561
+ */
562
+ simulateTouchCancel(element, init = {}, touchInit = {}) {
563
+ this.removePseudoClass(element, ':active');
564
+ this.forEachNodeFrom(element, (el) => {
565
+ this.removePseudoClass(el, ':active');
566
+ });
567
+ const mouseInit = this.createMouseEventInit(element);
568
+ const pointerInit = {
569
+ ...mouseInit,
570
+ isPrimary: true,
571
+ pointerType: 'touch',
572
+ ...init,
573
+ };
574
+ element.dispatchEvent(new PointerEvent('pointercancel', pointerInit));
575
+ // Firefox does not support TouchEvent constructor
576
+ if (window.TouchEvent) {
577
+ const touch = this.createTouch(element);
578
+ element.dispatchEvent(new TouchEvent('touchcancel', { changedTouches: [touch], ...touchInit }));
579
+ }
580
+ }
581
+ /**
582
+ * Simulates a keypress on an element.
583
+ *
584
+ * @param element The element to press a key on.
585
+ * @param key The key to press.
586
+ * @param init Additional event options.
587
+ */
588
+ simulateKeypress(element, key, init = {}) {
589
+ this.simulateKeydown(element, key, init);
590
+ this.simulateKeyup(element, key, init);
591
+ }
592
+ /**
593
+ * Simulates a keydown press on an element.
594
+ *
595
+ * @param element The element to press a key on.
596
+ * @param key The key to press.
597
+ * @param init Additional event options.
598
+ */
599
+ simulateKeydown(element, key, init = {}) {
600
+ element.dispatchEvent(new KeyboardEvent('keydown', {
601
+ ...init,
602
+ key,
603
+ bubbles: true,
604
+ composed: true,
605
+ cancelable: true,
606
+ }));
607
+ }
608
+ /**
609
+ * Simulates a keyup release from an element.
610
+ *
611
+ * @param element The element to release a key from.
612
+ * @param key The key to release.
613
+ * @param init Additional keyboard options.
614
+ */
615
+ simulateKeyup(element, key, init = {}) {
616
+ element.dispatchEvent(new KeyboardEvent('keyup', {
617
+ ...init,
618
+ key,
619
+ bubbles: true,
620
+ composed: true,
621
+ cancelable: true,
622
+ }));
623
+ }
624
+ /**
625
+ * Creates a MouseEventInit for an element. The default x/y coordinates of the
626
+ * event init will be in the center of the element.
627
+ *
628
+ * @param element The element to create a `MouseEventInit` for.
629
+ * @return The init object for a `MouseEvent`.
630
+ */
631
+ createMouseEventInit(element) {
632
+ const rect = element.getBoundingClientRect();
633
+ return {
634
+ bubbles: true,
635
+ cancelable: true,
636
+ composed: true,
637
+ clientX: (rect.left + rect.right) / 2,
638
+ clientY: (rect.top + rect.bottom) / 2,
639
+ screenX: (rect.left + rect.right) / 2,
640
+ screenY: (rect.top + rect.bottom) / 2,
641
+ // Primary button (usually the left button)
642
+ button: 0,
643
+ buttons: 1,
644
+ };
645
+ }
646
+ /**
647
+ * Creates a Touch instance for an element. The default x/y coordinates of the
648
+ * touch will be in the center of the element. This can be used in the
649
+ * `TouchEvent` constructor.
650
+ *
651
+ * @param element The element to create a touch for.
652
+ * @param identifier Optional identifier for the touch. Defaults to 0 for
653
+ * every touch instance.
654
+ * @return The `Touch` instance.
655
+ */
656
+ createTouch(element, identifier = 0) {
657
+ const rect = element.getBoundingClientRect();
658
+ return new Touch({
659
+ identifier,
660
+ target: element,
661
+ clientX: (rect.left + rect.right) / 2,
662
+ clientY: (rect.top + rect.bottom) / 2,
663
+ screenX: (rect.left + rect.right) / 2,
664
+ screenY: (rect.top + rect.bottom) / 2,
665
+ pageX: (rect.left + rect.right) / 2,
666
+ pageY: (rect.top + rect.bottom) / 2,
667
+ touchType: 'direct',
668
+ });
669
+ }
670
+ /**
671
+ * Visit each node up the parent tree from the given child until reaching the
672
+ * given parent.
673
+ *
674
+ * This is used to perform logic such as adding/removing recursive pseudo
675
+ * classes like `:hover`.
676
+ *
677
+ * @param child The first child element to start from.
678
+ * @param callback A callback that is invoked with each `HTMLElement` node
679
+ * from the child to the parent.
680
+ * @param parent The last parent element to visit.
681
+ */
682
+ forEachNodeFrom(child, callback, parent = this.element) {
683
+ let nextNode = child;
684
+ while (nextNode && nextNode !== this.element) {
685
+ const currentNode = nextNode;
686
+ nextNode = currentNode.parentNode || currentNode.host;
687
+ if (!(currentNode instanceof HTMLElement)) {
688
+ continue;
689
+ }
690
+ callback(currentNode);
691
+ if (nextNode instanceof HTMLElement && nextNode.shadowRoot) {
692
+ const slot = currentNode.getAttribute('slot');
693
+ const slotSelector = slot ? `slot[name=${slot}]` : 'slot:not([name])';
694
+ const slotElement = nextNode.shadowRoot.querySelector(slotSelector);
695
+ if (slotElement) {
696
+ this.forEachNodeFrom(slotElement, callback, nextNode);
697
+ }
698
+ }
699
+ }
700
+ callback(parent);
701
+ }
702
+ /**
703
+ * Patch an element's methods, such as `querySelector` and `matches` to
704
+ * handle transformed pseudo classes.
705
+ *
706
+ * For example, `element.matches(':focus')` will return true when the
707
+ * `._focus` class is applied.
708
+ *
709
+ * @param element The element to patch.
710
+ */
711
+ patchForTransformedPseudoClasses(element) {
712
+ if (this.patchedElements.has(element)) {
713
+ return;
714
+ }
715
+ // Patch functions to handle pseudo selectors.
716
+ const getSelector = (selector) => {
717
+ if (this.transformPseudoClasses.includes(selector)) {
718
+ return `.${getTransformedPseudoClass(selector)}`;
719
+ }
720
+ return selector;
721
+ };
722
+ const superMatches = this.element.matches;
723
+ element.matches = (selector) => {
724
+ return superMatches.call(element, getSelector(selector));
725
+ };
726
+ const superQuerySelector = this.element.querySelector;
727
+ element.querySelector = (selector) => {
728
+ return superQuerySelector.call(element, getSelector(selector));
729
+ };
730
+ const superQuerySelectorAll = this.element.querySelectorAll;
731
+ element.querySelectorAll = (selector) => {
732
+ return superQuerySelectorAll.call(element, getSelector(selector));
733
+ };
734
+ this.patchedElements.add(element);
735
+ }
736
+ }
737
+ //# sourceMappingURL=harness.js.map