diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index ecd26ff7f49f..ce8b745254c6 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -476,6 +476,19 @@ 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 && + nativeEvent.clientX === 0 && + nativeEvent.clientY === 0 + ); +} + function targetIsDocument(target: null | Node): boolean { // When target is null, it is the root return target === null || target.nodeType === 9; @@ -617,6 +630,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 b1ac12e24c7e..159a039bbc69 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.virtualclick(); + 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 010b0c345ad8..876ba4bd66d4 100644 --- a/packages/react-events/src/dom/testing-library/domEvents.js +++ b/packages/react-events/src/dom/testing-library/domEvents.js @@ -170,28 +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, + clientX: virtual ? 0 : x, + clientY: virtual ? 0 : y, ctrlKey, + detail: virtual ? 0 : 1, getModifierState(keyArg) { createGetModifierState(keyArg, modifierState); }, metaKey, - movementX, - movementY, - offsetX, - offsetY, - pageX: pageX || x, - pageY: pageY || y, + 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: x, - screenY: y + defaultBrowserChromeSize, + screenX: virtual ? 0 : x, + screenY: virtual ? 0 : y + defaultBrowserChromeSize, shiftKey, }); } @@ -251,7 +253,11 @@ export function blur({relatedTarget} = {}) { } export function click(payload) { - return createMouseEvent('click', payload); + return createMouseEvent('click', payload, false); +} + +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 e1377f6d70e0..6c8f6f198894 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)); }, + virtualclick(payload) { + node.dispatchEvent(domEvents.virtualclick(payload)); + }, scroll(payload) { node.dispatchEvent(domEvents.scroll(payload)); },