From 86a545d50465883e3dc696fee251fe33e3ad3ea9 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 18 Aug 2021 16:55:41 +0200 Subject: [PATCH 1/2] refactor(cdk/testing): switch to event constructor for creating fake events Switches from the legacy `init<..>Event` calls to the Event constructors given that IE11 no longer is supported. --- src/cdk/a11y/fake-event-detection.ts | 4 +- .../testbed/fake-events/dispatch-events.ts | 4 +- .../testbed/fake-events/element-focus.ts | 4 +- .../testbed/fake-events/event-objects.ts | 122 +++++------------- 4 files changed, 37 insertions(+), 97 deletions(-) diff --git a/src/cdk/a11y/fake-event-detection.ts b/src/cdk/a11y/fake-event-detection.ts index b5fe69c52003..2cb20bc02049 100644 --- a/src/cdk/a11y/fake-event-detection.ts +++ b/src/cdk/a11y/fake-event-detection.ts @@ -11,8 +11,8 @@ export function isFakeMousedownFromScreenReader(event: MouseEvent): boolean { // Some screen readers will dispatch a fake `mousedown` event when pressing enter or space on // a clickable element. We can distinguish these events when both `offsetX` and `offsetY` are // zero. Note that there's an edge case where the user could click the 0x0 spot of the screen - // themselves, but that is unlikely to contain interaction elements. Historially we used to check - // `event.buttons === 0`, however that no longer works on recent versions of NVDA. + // themselves, but that is unlikely to contain interaction elements. Historically we used to + // check `event.buttons === 0`, however that no longer works on recent versions of NVDA. return event.offsetX === 0 && event.offsetY === 0; } diff --git a/src/cdk/testing/testbed/fake-events/dispatch-events.ts b/src/cdk/testing/testbed/fake-events/dispatch-events.ts index 4063386d1215..d4c7254b211a 100644 --- a/src/cdk/testing/testbed/fake-events/dispatch-events.ts +++ b/src/cdk/testing/testbed/fake-events/dispatch-events.ts @@ -28,8 +28,8 @@ export function dispatchEvent(node: Node | Window, event: T): T * Shorthand to dispatch a fake event on a specified node. * @docs-private */ -export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?: boolean): Event { - return dispatchEvent(node, createFakeEvent(type, canBubble)); +export function dispatchFakeEvent(node: Node | Window, type: string, bubbles?: boolean): Event { + return dispatchEvent(node, createFakeEvent(type, bubbles)); } /** diff --git a/src/cdk/testing/testbed/fake-events/element-focus.ts b/src/cdk/testing/testbed/fake-events/element-focus.ts index 075ce6913ba1..a42a70a3b8bf 100644 --- a/src/cdk/testing/testbed/fake-events/element-focus.ts +++ b/src/cdk/testing/testbed/fake-events/element-focus.ts @@ -21,10 +21,12 @@ function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') { /** * Patches an elements focus and blur methods to emit events consistently and predictably. - * This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously, + * This is necessary, because some browsers can call the focus handlers asynchronously, * while others won't fire them at all if the browser window is not focused. * @docs-private */ +// TODO: Check if this element focus patching is still needed for local testing, +// where browser is not necessarily focused. export function patchElementFocus(element: HTMLElement) { element.focus = () => dispatchFakeEvent(element, 'focus'); element.blur = () => dispatchFakeEvent(element, 'blur'); diff --git a/src/cdk/testing/testbed/fake-events/event-objects.ts b/src/cdk/testing/testbed/fake-events/event-objects.ts index a4a065280b33..8de75f31c04e 100644 --- a/src/cdk/testing/testbed/fake-events/event-objects.ts +++ b/src/cdk/testing/testbed/fake-events/event-objects.ts @@ -17,9 +17,6 @@ let uniqueIds = 0; */ export function createMouseEvent( type: string, clientX = 0, clientY = 0, button = 0, modifiers: ModifierKeys = {}) { - const event = document.createEvent('MouseEvent'); - const originalPreventDefault = event.preventDefault.bind(event); - // Note: We cannot determine the position of the mouse event based on the screen // because the dimensions and position of the browser window are not available // To provide reasonable `screenX` and `screenY` coordinates, we simply use the @@ -27,34 +24,26 @@ export function createMouseEvent( const screenX = clientX; const screenY = clientY; - event.initMouseEvent(type, - /* canBubble */ true, - /* cancelable */ true, - /* view */ window, - /* detail */ 0, - /* screenX */ screenX, - /* screenY */ screenY, - /* clientX */ clientX, - /* clientY */ clientY, - /* ctrlKey */ !!modifiers.control, - /* altKey */ !!modifiers.alt, - /* shiftKey */ !!modifiers.shift, - /* metaKey */ !!modifiers.meta, - /* button */ button, - /* relatedTarget */ null); + const event = new MouseEvent(type, { + bubbles: true, + cancelable: true, + screenX, + screenY, + clientX, + clientY, + ctrlKey: modifiers.control, + altKey: modifiers.alt, + shiftKey: modifiers.shift, + metaKey: modifiers.meta, + button: button, + buttons: 1, + }); - // `initMouseEvent` doesn't allow us to pass these properties into the constructor. - // Override them to 1, because they're used for fake screen reader event detection. - defineReadonlyEventProperty(event, 'buttons', 1); + // The `MouseEvent` constructor doesn't allow us to pass these properties into the constructor. + // Override them to `1`, because they're used for fake screen reader event detection. defineReadonlyEventProperty(event, 'offsetX', 1); defineReadonlyEventProperty(event, 'offsetY', 1); - // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. - event.preventDefault = function() { - defineReadonlyEventProperty(event, 'defaultPrevented', true); - return originalPreventDefault(); - }; - return event; } @@ -85,8 +74,8 @@ export function createPointerEvent(type: string, clientX = 0, clientY = 0, * @docs-private */ export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0, clientY = 0) { - // In favor of creating events that work for most of the browsers, the event is created - // as a basic UI Event. The necessary details for the event will be set manually. + // We cannot use the `TouchEvent` or `Touch` because Firefox and Safari lack support. + // TODO: Switch to the constructor API when it is available for Firefox and Safari. const event = document.createEvent('UIEvent'); const touchDetails = {pageX, pageY, clientX, clientY, id: uniqueIds++}; @@ -108,75 +97,24 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0 */ export function createKeyboardEvent(type: string, keyCode: number = 0, key: string = '', modifiers: ModifierKeys = {}) { - const event = document.createEvent('KeyboardEvent'); - const originalPreventDefault = event.preventDefault.bind(event); - - // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyEvent. - if ((event as any).initKeyEvent !== undefined) { - (event as any).initKeyEvent(type, true, true, window, modifiers.control, modifiers.alt, - modifiers.shift, modifiers.meta, keyCode); - } else { - // `initKeyboardEvent` expects to receive modifiers as a whitespace-delimited string - // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent - let modifiersList = ''; - - if (modifiers.control) { - modifiersList += 'Control '; - } - - if (modifiers.alt) { - modifiersList += 'Alt '; - } - - if (modifiers.shift) { - modifiersList += 'Shift '; - } - - if (modifiers.meta) { - modifiersList += 'Meta '; - } - - // TS3.6 removed the `initKeyboardEvent` method and suggested porting to - // `new KeyboardEvent()` constructor. We cannot use that as we support IE11. - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent. - (event as any).initKeyboardEvent(type, - true, /* canBubble */ - true, /* cancelable */ - window, /* view */ - 0, /* char */ - key, /* key */ - 0, /* location */ - modifiersList.trim(), /* modifiersList */ - false /* repeat */); - } - - // Webkit Browsers don't set the keyCode when calling the init function. - // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735 - defineReadonlyEventProperty(event, 'keyCode', keyCode); - defineReadonlyEventProperty(event, 'key', key); - defineReadonlyEventProperty(event, 'ctrlKey', !!modifiers.control); - defineReadonlyEventProperty(event, 'altKey', !!modifiers.alt); - defineReadonlyEventProperty(event, 'shiftKey', !!modifiers.shift); - defineReadonlyEventProperty(event, 'metaKey', !!modifiers.meta); - - // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. - event.preventDefault = function() { - defineReadonlyEventProperty(event, 'defaultPrevented', true); - return originalPreventDefault(); - }; - - return event; + return new KeyboardEvent(type, { + bubbles: true, + cancelable: true, + keyCode: keyCode, + key: key, + shiftKey: modifiers.shift, + metaKey: modifiers.meta, + altKey: modifiers.alt, + ctrlKey: modifiers.control, + }); } /** * Creates a fake event object with any desired event type. * @docs-private */ -export function createFakeEvent(type: string, canBubble = false, cancelable = true) { - const event = document.createEvent('Event'); - event.initEvent(type, canBubble, cancelable); - return event; +export function createFakeEvent(type: string, bubbles = false, cancelable = true) { + return new Event(type, {bubbles, cancelable}); } /** From 366c3033b2f79f47e1f2c9a6a34a2a9368e7d7a9 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 18 Aug 2021 17:16:40 +0200 Subject: [PATCH 2/2] fix(cdk/testing): fake touch event does not set proper touch identifier The fake `TouchEvent` being created by test harnesses for the TestBed harness environment currently set an id for a `Touch` of the fake event. The `Touch` currently incorrectly sets the unique touch id to a field named `id`, while it should be `identifier`. --- .../testbed/fake-events/event-objects.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/cdk/testing/testbed/fake-events/event-objects.ts b/src/cdk/testing/testbed/fake-events/event-objects.ts index 8de75f31c04e..2c00a55a0590 100644 --- a/src/cdk/testing/testbed/fake-events/event-objects.ts +++ b/src/cdk/testing/testbed/fake-events/event-objects.ts @@ -27,6 +27,9 @@ export function createMouseEvent( const event = new MouseEvent(type, { bubbles: true, cancelable: true, + view: window, + detail: 0, + relatedTarget: null, screenX, screenY, clientX, @@ -77,7 +80,7 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0 // We cannot use the `TouchEvent` or `Touch` because Firefox and Safari lack support. // TODO: Switch to the constructor API when it is available for Firefox and Safari. const event = document.createEvent('UIEvent'); - const touchDetails = {pageX, pageY, clientX, clientY, id: uniqueIds++}; + const touchDetails = {pageX, pageY, clientX, clientY, identifier: uniqueIds++}; // TS3.6 removes the initUIEvent method and suggests porting to "new UIEvent()". (event as any).initUIEvent(type, true, true, window, 0); @@ -98,15 +101,16 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0 export function createKeyboardEvent(type: string, keyCode: number = 0, key: string = '', modifiers: ModifierKeys = {}) { return new KeyboardEvent(type, { - bubbles: true, - cancelable: true, - keyCode: keyCode, - key: key, - shiftKey: modifiers.shift, - metaKey: modifiers.meta, - altKey: modifiers.alt, - ctrlKey: modifiers.control, - }); + bubbles: true, + cancelable: true, + view: window, + keyCode: keyCode, + key: key, + shiftKey: modifiers.shift, + metaKey: modifiers.meta, + altKey: modifiers.alt, + ctrlKey: modifiers.control, + }); } /**