[react-events] Support screen reader virtual clicks (#16584)
Dominic Gannaway committed
Aug 27, 2019 at 17:32 UTC
4ef269606c691f9b8c833f18d75342b34ef021db
4 files changed
+52
-11
packages/react-events/src/dom/Press.js
+20
@@ -476,6 +476,19 @@ function handleStopPropagation(
476
}
477
}
478
479
+// After some investigation work, screen reader virtual
480
+// clicks (NVDA, Jaws, VoiceOver) do not have co-ords associated with the click
481
+// event and "detail" is always 0 (where normal clicks are > 0)
482
+function isScreenReaderVirtualClick(nativeEvent): boolean {
483
+ return (
484
+ nativeEvent.detail === 0 &&
485
+ nativeEvent.screenX === 0 &&
486
+ nativeEvent.screenY === 0 &&
487
+ nativeEvent.clientX === 0 &&
488
+ nativeEvent.clientY === 0
489
+ );
490
+}
491
+
492
function targetIsDocument(target: null | Node): boolean {
493
// When target is null, it is the root
494
return target === null || target.nodeType === 9;
@@ -617,6 +630,13 @@ const pressResponderImpl = {
630
if (state.shouldPreventClick) {
631
nativeEvent.preventDefault();
632
}
633
+ const onPress = props.onPress;
634
+
635
+ if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
636
+ state.pointerType = 'keyboard';
637
+ state.pressTarget = event.responderTarget;
638
+ dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
639
+ }
640
break;
641
}
642
}
packages/react-events/src/dom/__tests__/Press-test.internal.js
+12
@@ -420,6 +420,18 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
420
innerTarget.pointerup({pointerType: 'mouse'});
421
expect(onPress).toBeCalled();
422
});
423
+
424
+ it('is called once after virtual screen reader "click" event', () => {
425
+ const target = createEventTarget(ref.current);
426
+ target.virtualclick();
427
+ expect(onPress).toHaveBeenCalledTimes(1);
428
+ expect(onPress).toHaveBeenCalledWith(
429
+ expect.objectContaining({
430
+ pointerType: 'keyboard',
431
+ type: 'press',
432
+ }),
433
+ );
434
+ });
435
});
436
437
describe('onPressMove', () => {
packages/react-events/src/dom/testing-library/domEvents.js
+17
-11
@@ -170,28 +170,30 @@ function createMouseEvent(
170
x = 0,
171
y = 0,
172
} = {},
173
+ virtual = false,
174
) {
175
const modifierState = {altKey, ctrlKey, metaKey, shiftKey};
176
177
return createEvent(type, {
178
altKey,
179
buttons,
179
- clientX: x,
180
- clientY: y,
180
+ clientX: virtual ? 0 : x,
181
+ clientY: virtual ? 0 : y,
182
ctrlKey,
183
+ detail: virtual ? 0 : 1,
184
getModifierState(keyArg) {
185
createGetModifierState(keyArg, modifierState);
186
},
187
metaKey,
186
- movementX,
187
- movementY,
188
- offsetX,
189
- offsetY,
190
- pageX: pageX || x,
191
- pageY: pageY || y,
188
+ movementX: virtual ? 0 : movementX,
189
+ movementY: virtual ? 0 : movementY,
190
+ offsetX: virtual ? 0 : offsetX,
191
+ offsetY: virtual ? 0 : offsetY,
192
+ pageX: virtual ? 0 : pageX || x,
193
+ pageY: virtual ? 0 : pageY || y,
194
preventDefault,
193
- screenX: x,
194
- screenY: y + defaultBrowserChromeSize,
195
+ screenX: virtual ? 0 : x,
196
+ screenY: virtual ? 0 : y + defaultBrowserChromeSize,
197
shiftKey,
198
});
199
}
@@ -251,7 +253,11 @@ export function blur({relatedTarget} = {}) {
253
}
254
255
export function click(payload) {
254
- return createMouseEvent('click', payload);
256
+ return createMouseEvent('click', payload, false);
257
+}
258
+
259
+export function virtualclick(payload) {
260
+ return createMouseEvent('click', payload, true);
261
}
262
263
export function contextmenu(payload) {
packages/react-events/src/dom/testing-library/index.js
+3
@@ -44,6 +44,9 @@ const createEventTarget = node => ({
44
keyup(payload) {
45
node.dispatchEvent(domEvents.keyup(payload));
46
},
47
+ virtualclick(payload) {
48
+ node.dispatchEvent(domEvents.virtualclick(payload));
49
+ },
50
scroll(payload) {
51
node.dispatchEvent(domEvents.scroll(payload));
52
},