From e9b1ba8e6d043b76571873284e5a73e1bdc697b8 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 27 Aug 2019 13:26:51 +0100 Subject: [PATCH 1/3] [react-events] Support screen reader virtual clicks --- packages/react-events/src/dom/Press.js | 18 ++++++++ .../src/dom/__tests__/Press-test.internal.js | 12 ++++++ .../src/dom/testing-library/domEvents.js | 41 +++++++++++++++++++ .../src/dom/testing-library/index.js | 3 ++ 4 files changed, 74 insertions(+) diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index ecd26ff7f49f8..aa9b00853c563 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -476,6 +476,17 @@ function handleStopPropagation( } } +// After some investigation work, screen reader virtual +// clicks (NVDA, Jaws, VoiceOver) do not have co-ords associated with the click +// event and "detail" is always 0 (where normal clicks are > 0) +function isScreenReaderVirtualClick(nativeEvent): boolean { + return ( + nativeEvent.detail === 0 && + nativeEvent.screenX === 0 && + nativeEvent.screenY === 0 + ); +} + function targetIsDocument(target: null | Node): boolean { // When target is null, it is the root return target === null || target.nodeType === 9; @@ -617,6 +628,13 @@ const pressResponderImpl = { if (state.shouldPreventClick) { nativeEvent.preventDefault(); } + const onPress = props.onPress; + + if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) { + state.pointerType = 'keyboard'; + state.pressTarget = event.responderTarget; + dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent); + } break; } } diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index b1ac12e24c7e0..06dd82bbe21a4 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -420,6 +420,18 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { innerTarget.pointerup({pointerType: 'mouse'}); expect(onPress).toBeCalled(); }); + + it('is called once after virtual screen reader "click" event', () => { + const target = createEventTarget(ref.current); + target.screenReaderClick(); + expect(onPress).toHaveBeenCalledTimes(1); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({ + pointerType: 'keyboard', + type: 'press', + }), + ); + }); }); describe('onPressMove', () => { diff --git a/packages/react-events/src/dom/testing-library/domEvents.js b/packages/react-events/src/dom/testing-library/domEvents.js index 010b0c345ad8b..060116590a98c 100644 --- a/packages/react-events/src/dom/testing-library/domEvents.js +++ b/packages/react-events/src/dom/testing-library/domEvents.js @@ -196,6 +196,43 @@ function createMouseEvent( }); } +function createScreenReaderMouseEvent( + type, + { + altKey = false, + buttons = buttonsType.none, + ctrlKey = false, + metaKey = false, + preventDefault = emptyFunction, + shiftKey = false, + } = {}, +) { + const modifierState = {altKey, ctrlKey, metaKey, shiftKey}; + + return createEvent(type, { + altKey, + buttons, + clientX: 0, + clientY: 0, + ctrlKey, + detail: 0, + getModifierState(keyArg) { + createGetModifierState(keyArg, modifierState); + }, + metaKey, + movementX: 0, + movementY: 0, + offsetX: 0, + offsetY: 0, + pageX: 0, + pageY: 0, + preventDefault, + screenX: 0, + screenY: 0, + shiftKey, + }); +} + function createTouchEvent( type, { @@ -254,6 +291,10 @@ export function click(payload) { return createMouseEvent('click', payload); } +export function screenReaderClick(payload) { + return createScreenReaderMouseEvent('click', payload); +} + export function contextmenu(payload) { return createMouseEvent('contextmenu', payload); } diff --git a/packages/react-events/src/dom/testing-library/index.js b/packages/react-events/src/dom/testing-library/index.js index e1377f6d70e02..2981374f309ef 100644 --- a/packages/react-events/src/dom/testing-library/index.js +++ b/packages/react-events/src/dom/testing-library/index.js @@ -44,6 +44,9 @@ const createEventTarget = node => ({ keyup(payload) { node.dispatchEvent(domEvents.keyup(payload)); }, + screenReaderClick(payload) { + node.dispatchEvent(domEvents.screenReaderClick(payload)); + }, scroll(payload) { node.dispatchEvent(domEvents.scroll(payload)); }, From 42e212635cdc3a1f501611a88418606d6e5c07c0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 27 Aug 2019 17:18:22 +0100 Subject: [PATCH 2/3] Add clientX/clientY checks too --- packages/react-events/src/dom/Press.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index aa9b00853c563..ce8b745254c61 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -483,7 +483,9 @@ function isScreenReaderVirtualClick(nativeEvent): boolean { return ( nativeEvent.detail === 0 && nativeEvent.screenX === 0 && - nativeEvent.screenY === 0 + nativeEvent.screenY === 0 && + nativeEvent.clientX === 0 && + nativeEvent.clientY === 0 ); } From 7a8b04f1b3ca4dd487c896832ddc6a61c811484c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 27 Aug 2019 17:24:27 +0100 Subject: [PATCH 3/3] Address feedback --- .../src/dom/__tests__/Press-test.internal.js | 2 +- .../src/dom/testing-library/domEvents.js | 65 +++++-------------- .../src/dom/testing-library/index.js | 4 +- 3 files changed, 18 insertions(+), 53 deletions(-) diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index 06dd82bbe21a4..159a039bbc691 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -423,7 +423,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { it('is called once after virtual screen reader "click" event', () => { const target = createEventTarget(ref.current); - target.screenReaderClick(); + target.virtualclick(); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/react-events/src/dom/testing-library/domEvents.js b/packages/react-events/src/dom/testing-library/domEvents.js index 060116590a98c..876ba4bd66d4b 100644 --- a/packages/react-events/src/dom/testing-library/domEvents.js +++ b/packages/react-events/src/dom/testing-library/domEvents.js @@ -170,65 +170,30 @@ function createMouseEvent( x = 0, y = 0, } = {}, + virtual = false, ) { const modifierState = {altKey, ctrlKey, metaKey, shiftKey}; return createEvent(type, { altKey, buttons, - clientX: x, - clientY: y, - ctrlKey, - getModifierState(keyArg) { - createGetModifierState(keyArg, modifierState); - }, - metaKey, - movementX, - movementY, - offsetX, - offsetY, - pageX: pageX || x, - pageY: pageY || y, - preventDefault, - screenX: x, - screenY: y + defaultBrowserChromeSize, - shiftKey, - }); -} - -function createScreenReaderMouseEvent( - type, - { - altKey = false, - buttons = buttonsType.none, - ctrlKey = false, - metaKey = false, - preventDefault = emptyFunction, - shiftKey = false, - } = {}, -) { - const modifierState = {altKey, ctrlKey, metaKey, shiftKey}; - - return createEvent(type, { - altKey, - buttons, - clientX: 0, - clientY: 0, + clientX: virtual ? 0 : x, + clientY: virtual ? 0 : y, ctrlKey, - detail: 0, + detail: virtual ? 0 : 1, getModifierState(keyArg) { createGetModifierState(keyArg, modifierState); }, metaKey, - movementX: 0, - movementY: 0, - offsetX: 0, - offsetY: 0, - pageX: 0, - pageY: 0, + movementX: virtual ? 0 : movementX, + movementY: virtual ? 0 : movementY, + offsetX: virtual ? 0 : offsetX, + offsetY: virtual ? 0 : offsetY, + pageX: virtual ? 0 : pageX || x, + pageY: virtual ? 0 : pageY || y, preventDefault, - screenX: 0, - screenY: 0, + screenX: virtual ? 0 : x, + screenY: virtual ? 0 : y + defaultBrowserChromeSize, shiftKey, }); } @@ -288,11 +253,11 @@ export function blur({relatedTarget} = {}) { } export function click(payload) { - return createMouseEvent('click', payload); + return createMouseEvent('click', payload, false); } -export function screenReaderClick(payload) { - return createScreenReaderMouseEvent('click', payload); +export function virtualclick(payload) { + return createMouseEvent('click', payload, true); } export function contextmenu(payload) { diff --git a/packages/react-events/src/dom/testing-library/index.js b/packages/react-events/src/dom/testing-library/index.js index 2981374f309ef..6c8f6f198894b 100644 --- a/packages/react-events/src/dom/testing-library/index.js +++ b/packages/react-events/src/dom/testing-library/index.js @@ -44,8 +44,8 @@ const createEventTarget = node => ({ keyup(payload) { node.dispatchEvent(domEvents.keyup(payload)); }, - screenReaderClick(payload) { - node.dispatchEvent(domEvents.screenReaderClick(payload)); + virtualclick(payload) { + node.dispatchEvent(domEvents.virtualclick(payload)); }, scroll(payload) { node.dispatchEvent(domEvents.scroll(payload));