From 107521a9526a5066d7ad7bc8e2e73224b2f5de24 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Mon, 12 Aug 2019 10:10:51 -0700 Subject: [PATCH] [react-events] Focus/FocusWithin responders with fallbacks (#16343) Separate the PointerEvent and fallback implementations. Fix the unit tests to cover both PointerEvent and non-PointerEvent environments. Fix the focus-visible related callbacks to get called when keys other than "Tab" are used. --- packages/react-events/src/dom/Focus.js | 107 ++++----- .../__tests__/ContextMenu-test.internal.js | 58 ++--- .../src/dom/__tests__/Focus-test.internal.js | 212 +++++++----------- .../__tests__/FocusWithin-test.internal.js | 176 ++++++--------- .../src/dom/__tests__/Hover-test.internal.js | 85 ++++--- packages/react-events/src/dom/test-utils.js | 159 ++++++++++--- 6 files changed, 405 insertions(+), 392 deletions(-) diff --git a/packages/react-events/src/dom/Focus.js b/packages/react-events/src/dom/Focus.js index 85f9b8455ea8..1e7f5f0d1968 100644 --- a/packages/react-events/src/dom/Focus.js +++ b/packages/react-events/src/dom/Focus.js @@ -33,6 +33,7 @@ type FocusState = { isFocused: boolean, isFocusVisible: boolean, pointerType: PointerType, + isEmulatingMouseEvents: boolean, }; type FocusProps = { @@ -66,25 +67,12 @@ const isMac = const targetEventTypes = ['focus', 'blur']; -const rootEventTypes = [ - 'keydown', - 'keyup', - 'pointermove', - 'pointerdown', - 'pointerup', -]; - -// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events. -if (typeof window !== 'undefined' && window.PointerEvent === undefined) { - rootEventTypes.push( - 'mousemove', - 'mousedown', - 'mouseup', - 'touchmove', - 'touchstart', - 'touchend', - ); -} +const hasPointerEvents = + typeof window !== 'undefined' && window.PointerEvent != null; + +const rootEventTypes = hasPointerEvents + ? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup'] + : ['keydown', 'keyup', 'mousedown', 'touchmove', 'touchstart', 'touchend']; function isFunction(obj): boolean { return typeof obj === 'function'; @@ -110,13 +98,7 @@ function handleRootPointerEvent( state: FocusState, callback: boolean => void, ): void { - const {type, target} = event; - // Ignore a Safari quirks where 'mousemove' is dispatched on the 'html' - // element when the window blurs. - if (type === 'mousemove' && target.nodeName === 'HTML') { - return; - } - + const {type} = event; isGlobalFocusVisible = false; // Focus should stop being visible if a pointer is used on the element @@ -124,7 +106,7 @@ function handleRootPointerEvent( const focusTarget = state.focusTarget; if ( focusTarget !== null && - context.isTargetWithinNode(event.target, focusTarget) && + context.isTargetWithinResponderScope(focusTarget) && (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown') ) { callback(false); @@ -140,13 +122,6 @@ function handleRootEvent( const {type} = event; switch (type) { - case 'mousemove': - case 'mousedown': - case 'mouseup': { - state.pointerType = 'mouse'; - handleRootPointerEvent(event, context, state, callback); - break; - } case 'pointermove': case 'pointerdown': case 'pointerup': { @@ -156,27 +131,45 @@ function handleRootEvent( handleRootPointerEvent(event, context, state, callback); break; } + + case 'keydown': + case 'keyup': { + const nativeEvent = event.nativeEvent; + const focusTarget = state.focusTarget; + const {key, metaKey, altKey, ctrlKey} = (nativeEvent: any); + const validKey = + key === 'Enter' || + key === ' ' || + (key === 'Tab' && !(metaKey || (!isMac && altKey) || ctrlKey)); + + if (validKey) { + state.pointerType = 'keyboard'; + isGlobalFocusVisible = true; + if ( + focusTarget !== null && + context.isTargetWithinResponderScope(focusTarget) + ) { + callback(true); + } + } + break; + } + + // fallbacks for no PointerEvent support case 'touchmove': case 'touchstart': case 'touchend': { state.pointerType = 'touch'; + state.isEmulatingMouseEvents = true; handleRootPointerEvent(event, context, state, callback); break; } - - case 'keydown': - case 'keyup': { - const nativeEvent = event.nativeEvent; - if ( - nativeEvent.key === 'Tab' && - !( - nativeEvent.metaKey || - (!isMac && nativeEvent.altKey) || - nativeEvent.ctrlKey - ) - ) { - state.pointerType = 'keyboard'; - isGlobalFocusVisible = true; + case 'mousedown': { + if (!state.isEmulatingMouseEvents) { + state.pointerType = 'mouse'; + handleRootPointerEvent(event, context, state, callback); + } else { + state.isEmulatingMouseEvents = false; } break; } @@ -271,6 +264,7 @@ const focusResponderImpl = { getInitialState(): FocusState { return { focusTarget: null, + isEmulatingMouseEvents: false, isFocused: false, isFocusVisible: false, pointerType: '', @@ -303,6 +297,7 @@ const focusResponderImpl = { state.isFocusVisible = isGlobalFocusVisible; dispatchFocusEvents(context, props, state); } + state.isEmulatingMouseEvents = false; break; } case 'blur': { @@ -311,6 +306,17 @@ const focusResponderImpl = { state.isFocusVisible = isGlobalFocusVisible; state.isFocused = false; } + // This covers situations where focus is lost to another document in + // the same window (e.g., iframes). Any action that restores focus to + // the document (e.g., touch or click) first causes 'focus' to be + // dispatched, which means the 'pointerType' we provide is stale + // (it reflects the *previous* pointer). We cannot determine the + // 'pointerType' in this case, so a blur with no + // relatedTarget is used as a signal to reset the 'pointerType'. + if (event.nativeEvent.relatedTarget == null) { + state.pointerType = ''; + } + state.isEmulatingMouseEvents = false; break; } } @@ -322,7 +328,7 @@ const focusResponderImpl = { state: FocusState, ): void { handleRootEvent(event, context, state, isFocusVisible => { - if (state.isFocusVisible !== isFocusVisible) { + if (state.isFocused && state.isFocusVisible !== isFocusVisible) { state.isFocusVisible = isFocusVisible; dispatchFocusVisibleChangeEvent(context, props, isFocusVisible); } @@ -402,6 +408,7 @@ const focusWithinResponderImpl = { getInitialState(): FocusState { return { focusTarget: null, + isEmulatingMouseEvents: false, isFocused: false, isFocusVisible: false, pointerType: '', @@ -460,7 +467,7 @@ const focusWithinResponderImpl = { state: FocusState, ): void { handleRootEvent(event, context, state, isFocusVisible => { - if (state.isFocusVisible !== isFocusVisible) { + if (state.isFocused && state.isFocusVisible !== isFocusVisible) { state.isFocusVisible = isFocusVisible; dispatchFocusWithinVisibleChangeEvent( context, diff --git a/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js b/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js index 4e03d897bf82..bf2fb0f3e3a7 100644 --- a/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js +++ b/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js @@ -9,7 +9,13 @@ 'use strict'; -import {createEvent, platform, setPointerEvent} from '../test-utils'; +import { + dispatchLongPressContextMenu, + dispatchRightClickContextMenu, + dispatchModifiedClickContextMenu, + platform, + setPointerEvent, +} from '../test-utils'; let React; let ReactFeatureFlags; @@ -27,44 +33,6 @@ function initializeModules(hasPointerEvents) { .useContextMenuResponder; } -function dispatchContextMenuEvents(ref, options) { - const preventDefault = options.preventDefault || function() {}; - const variant = (options.variant: 'mouse' | 'touch' | 'modified'); - const dispatchEvent = arg => ref.current.dispatchEvent(arg); - - if (variant === 'mouse') { - // right-click - dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse', button: 2}), - ); - dispatchEvent(createEvent('mousedown', {button: 2})); - dispatchEvent(createEvent('contextmenu', {button: 2, preventDefault})); - } else if (variant === 'modified') { - // left-click + ctrl - dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse', button: 0}), - ); - dispatchEvent(createEvent('mousedown', {button: 0})); - if (platform.get() === 'mac') { - dispatchEvent( - createEvent('contextmenu', {button: 0, ctrlKey: true, preventDefault}), - ); - } - } else if (variant === 'touch') { - // long-press - dispatchEvent( - createEvent('pointerdown', {pointerType: 'touch', button: 0}), - ); - dispatchEvent( - createEvent('touchstart', { - changedTouches: [], - targetTouches: [], - }), - ); - dispatchEvent(createEvent('contextmenu', {button: 0, preventDefault})); - } -} - const forcePointerEvents = true; const table = [[forcePointerEvents], [!forcePointerEvents]]; @@ -94,7 +62,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault}); + dispatchRightClickContextMenu(ref.current, {preventDefault}); expect(preventDefault).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledWith( @@ -112,7 +80,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchContextMenuEvents(ref, {variant: 'touch', preventDefault}); + dispatchLongPressContextMenu(ref.current, {preventDefault}); expect(preventDefault).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledWith( @@ -132,7 +100,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchContextMenuEvents(ref, 'mouse'); + dispatchRightClickContextMenu(ref.current); expect(onContextMenu).toHaveBeenCalledTimes(0); }); @@ -149,7 +117,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchContextMenuEvents(ref, {variant: 'mouse', preventDefault}); + dispatchRightClickContextMenu(ref.current, {preventDefault}); expect(preventDefault).toHaveBeenCalledTimes(0); expect(onContextMenu).toHaveBeenCalledTimes(1); }); @@ -174,7 +142,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchContextMenuEvents(ref, {variant: 'modified'}); + dispatchModifiedClickContextMenu(ref.current); expect(onContextMenu).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}), @@ -201,7 +169,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchContextMenuEvents(ref, {variant: 'modified'}); + dispatchModifiedClickContextMenu(ref.current); expect(onContextMenu).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/react-events/src/dom/__tests__/Focus-test.internal.js b/packages/react-events/src/dom/__tests__/Focus-test.internal.js index 3fca82bb7052..a432f64091cf 100644 --- a/packages/react-events/src/dom/__tests__/Focus-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Focus-test.internal.js @@ -9,47 +9,41 @@ 'use strict'; +import { + blur, + focus, + keydown, + setPointerEvent, + platform, + dispatchPointerPressDown, + dispatchPointerPressRelease, +} from '../test-utils'; + let React; let ReactFeatureFlags; let ReactDOM; let FocusResponder; let useFocusResponder; -const createEvent = (type, data) => { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true); - if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; - }); - } - return event; -}; - -const createKeyboardEvent = (type, data) => { - return new KeyboardEvent(type, { - bubbles: true, - cancelable: true, - ...data, - }); -}; - -const modulesInit = () => { +function initializeModules(hasPointerEvents) { + setPointerEvent(hasPointerEvents); + jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableFlareAPI = true; React = require('react'); ReactDOM = require('react-dom'); FocusResponder = require('react-events/focus').FocusResponder; useFocusResponder = require('react-events/focus').useFocusResponder; -}; +} + +const forcePointerEvents = true; +const table = [[forcePointerEvents], [!forcePointerEvents]]; -describe('Focus event responder', () => { +describe.each(table)('Focus responder', hasPointerEvents => { let container; beforeEach(() => { - jest.resetModules(); - modulesInit(); - + initializeModules(hasPointerEvents); container = document.createElement('div'); document.body.appendChild(container); }); @@ -78,9 +72,10 @@ describe('Focus event responder', () => { ReactDOM.render(, container); }); - it('prevents custom events being dispatched', () => { - ref.current.dispatchEvent(createEvent('focus')); - ref.current.dispatchEvent(createEvent('blur')); + it('does not call callbacks', () => { + const dispatch = arg => ref.current.dispatchEvent(arg); + dispatch(focus()); + dispatch(blur()); expect(onFocus).not.toBeCalled(); expect(onBlur).not.toBeCalled(); }); @@ -102,8 +97,9 @@ describe('Focus event responder', () => { }); it('is called after "blur" event', () => { - ref.current.dispatchEvent(createEvent('focus')); - ref.current.dispatchEvent(createEvent('blur')); + const dispatch = arg => ref.current.dispatchEvent(arg); + dispatch(focus()); + dispatch(blur()); expect(onBlur).toHaveBeenCalledTimes(1); }); }); @@ -131,83 +127,52 @@ describe('Focus event responder', () => { beforeEach(componentInit); it('is called after "focus" event', () => { - ref.current.dispatchEvent(createEvent('focus')); + ref.current.dispatchEvent(focus()); expect(onFocus).toHaveBeenCalledTimes(1); }); it('is not called if descendants of target receive focus', () => { - const target = innerRef.current; - target.dispatchEvent(createEvent('focus')); + innerRef.current.dispatchEvent(focus()); expect(onFocus).not.toBeCalled(); }); - it('is called with the correct pointerType using pointer events', () => { - // Pointer mouse - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent(createEvent('focus')); + it('is called with the correct pointerType: mouse', () => { + const target = ref.current; + dispatchPointerPressDown(target, {pointerType: 'mouse'}); + dispatchPointerPressRelease(target, {pointerType: 'mouse'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'mouse'}), ); - ref.current.dispatchEvent(createEvent('blur')); - - // Pointer touch - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent(createEvent('focus')); - expect(onFocus).toHaveBeenCalledTimes(2); - expect(onFocus).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch'}), - ); - ref.current.dispatchEvent(createEvent('blur')); - - // Pointer pen - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'pen', - }), - ); - ref.current.dispatchEvent(createEvent('focus')); - expect(onFocus).toHaveBeenCalledTimes(3); - expect(onFocus).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen'}), - ); }); - it('is called with the correct pointerType without pointer events', () => { - // Mouse - ref.current.dispatchEvent(createEvent('mousedown')); - ref.current.dispatchEvent(createEvent('focus')); + it('is called with the correct pointerType: touch', () => { + const target = ref.current; + dispatchPointerPressDown(target, {pointerType: 'touch'}); + dispatchPointerPressRelease(target, {pointerType: 'touch'}); expect(onFocus).toHaveBeenCalledTimes(1); - expect(onFocus).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse'}), - ); - ref.current.dispatchEvent(createEvent('blur')); - - // Touch - ref.current.dispatchEvent(createEvent('touchstart')); - ref.current.dispatchEvent(createEvent('focus')); - expect(onFocus).toHaveBeenCalledTimes(2); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch'}), ); }); + if (hasPointerEvents) { + it('is called with the correct pointerType: pen', () => { + const target = ref.current; + dispatchPointerPressDown(target, {pointerType: 'pen'}); + dispatchPointerPressRelease(target, {pointerType: 'pen'}); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocus).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'pen'}), + ); + }); + } + it('is called with the correct pointerType using a keyboard', () => { + const target = ref.current; // Keyboard tab - ref.current.dispatchEvent( - createEvent('keydown', { - key: 'Tab', - }), - ); - ref.current.dispatchEvent(createEvent('focus')); + target.dispatchEvent(keydown({key: 'Tab'})); + target.dispatchEvent(focus()); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard'}), @@ -215,19 +180,14 @@ describe('Focus event responder', () => { }); it('is called with the correct pointerType using Tab+altKey on Mac', () => { + platform.set('mac'); jest.resetModules(); - const platformGetter = jest.spyOn(global.navigator, 'platform', 'get'); - platformGetter.mockReturnValue('MacIntel'); - modulesInit(); + initializeModules(); componentInit(); + const target = ref.current; - ref.current.dispatchEvent( - createEvent('keydown', { - key: 'Tab', - altKey: true, - }), - ); - ref.current.dispatchEvent(createEvent('focus')); + target.dispatchEvent(keydown({key: 'Tab', altKey: true})); + target.dispatchEvent(focus()); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({ @@ -235,7 +195,7 @@ describe('Focus event responder', () => { }), ); - platformGetter.mockClear(); + platform.clear(); }); }); @@ -260,18 +220,20 @@ describe('Focus event responder', () => { }); it('is called after "blur" and "focus" events', () => { - ref.current.dispatchEvent(createEvent('focus')); + const target = ref.current; + target.dispatchEvent(focus()); expect(onFocusChange).toHaveBeenCalledTimes(1); expect(onFocusChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createEvent('blur')); + target.dispatchEvent(blur()); expect(onFocusChange).toHaveBeenCalledTimes(2); expect(onFocusChange).toHaveBeenCalledWith(false); }); it('is not called after "blur" and "focus" events on descendants', () => { - innerRef.current.dispatchEvent(createEvent('focus')); + const target = innerRef.current; + target.dispatchEvent(focus()); expect(onFocusChange).toHaveBeenCalledTimes(0); - innerRef.current.dispatchEvent(createEvent('blur')); + target.dispatchEvent(blur()); expect(onFocusChange).toHaveBeenCalledTimes(0); }); }); @@ -297,52 +259,48 @@ describe('Focus event responder', () => { }); it('is called after "focus" and "blur" if keyboard navigation is active', () => { + const target = ref.current; // use keyboard first - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'})); - ref.current.dispatchEvent(createEvent('focus')); + container.dispatchEvent(keydown({key: 'Tab'})); + target.dispatchEvent(focus()); expect(onFocusVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusVisibleChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusVisibleChange).toHaveBeenCalledWith(false); }); it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => { + const target = ref.current; // use keyboard first - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'})); - ref.current.dispatchEvent(createEvent('focus')); + container.dispatchEvent(keydown({key: 'Tab'})); + target.dispatchEvent(focus()); expect(onFocusVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - ref.current.dispatchEvent(createEvent('pointerdown')); + dispatchPointerPressDown(target); expect(onFocusVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusVisibleChange).toHaveBeenCalledTimes(2); }); it('is not called after "focus" and "blur" events without keyboard', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('focus')); - container.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + const target = ref.current; + dispatchPointerPressDown(target); + dispatchPointerPressRelease(target); + dispatchPointerPressDown(container); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusVisibleChange).toHaveBeenCalledTimes(0); }); it('is not called after "blur" and "focus" events on descendants', () => { - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'})); - innerRef.current.dispatchEvent(createEvent('focus')); + const target = innerRef.current; + container.dispatchEvent(keydown({key: 'Tab'})); + target.dispatchEvent(focus()); expect(onFocusVisibleChange).toHaveBeenCalledTimes(0); - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusVisibleChange).toHaveBeenCalledTimes(0); }); }); @@ -380,10 +338,10 @@ describe('Focus event responder', () => { ReactDOM.render(, container); - outerRef.current.dispatchEvent(createEvent('focus')); - outerRef.current.dispatchEvent(createEvent('blur')); - innerRef.current.dispatchEvent(createEvent('focus')); - innerRef.current.dispatchEvent(createEvent('blur')); + outerRef.current.dispatchEvent(focus()); + outerRef.current.dispatchEvent(blur()); + innerRef.current.dispatchEvent(focus()); + innerRef.current.dispatchEvent(blur()); expect(events).toEqual([ 'outer: onFocus', 'outer: onFocusChange', diff --git a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js index 5de66686fd65..e2340a21f669 100644 --- a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js @@ -9,32 +9,24 @@ 'use strict'; +import { + blur, + focus, + keydown, + setPointerEvent, + dispatchPointerPressDown, + dispatchPointerPressRelease, +} from '../test-utils'; + let React; let ReactFeatureFlags; let ReactDOM; let FocusWithinResponder; let useFocusWithinResponder; -const createEvent = (type, data) => { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true); - if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; - }); - } - return event; -}; - -const createKeyboardEvent = (type, data) => { - return new KeyboardEvent(type, { - bubbles: true, - cancelable: true, - ...data, - }); -}; - -const modulesInit = () => { +const initializeModules = hasPointerEvents => { + setPointerEvent(hasPointerEvents); + jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableFlareAPI = true; React = require('react'); @@ -44,13 +36,14 @@ const modulesInit = () => { .useFocusWithinResponder; }; -describe('FocusWithin event responder', () => { +const forcePointerEvents = true; +const table = [[forcePointerEvents], [!forcePointerEvents]]; + +describe.each(table)('FocusWithin responder', hasPointerEvents => { let container; beforeEach(() => { - jest.resetModules(); - modulesInit(); - + initializeModules(); container = document.createElement('div'); document.body.appendChild(container); }); @@ -80,8 +73,9 @@ describe('FocusWithin event responder', () => { }); it('prevents custom events being dispatched', () => { - ref.current.dispatchEvent(createEvent('focus')); - ref.current.dispatchEvent(createEvent('blur')); + const target = ref.current; + target.dispatchEvent(focus()); + target.dispatchEvent(blur()); expect(onFocusWithinChange).not.toBeCalled(); expect(onFocusWithinVisibleChange).not.toBeCalled(); }); @@ -110,49 +104,42 @@ describe('FocusWithin event responder', () => { }); it('is called after "blur" and "focus" events on focus target', () => { - ref.current.dispatchEvent(createEvent('focus')); + const target = ref.current; + target.dispatchEvent(focus()); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); expect(onFocusWithinChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); it('is called after "blur" and "focus" events on descendants', () => { - innerRef.current.dispatchEvent(createEvent('focus')); + const target = innerRef.current; + target.dispatchEvent(focus()); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); expect(onFocusWithinChange).toHaveBeenCalledWith(true); - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); it('is only called once when focus moves within and outside the subtree', () => { + const target = ref.current; + const innerTarget1 = innerRef.current; + const innerTarget2 = innerRef2.current; // focus shifts into subtree - innerRef.current.dispatchEvent(createEvent('focus')); + innerTarget1.dispatchEvent(focus()); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); expect(onFocusWithinChange).toHaveBeenCalledWith(true); // focus moves around subtree - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef2.current}), - ); - innerRef2.current.dispatchEvent(createEvent('focus')); - innerRef2.current.dispatchEvent( - createEvent('blur', {relatedTarget: ref.current}), - ); - ref.current.dispatchEvent(createEvent('focus')); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef.current}), - ); + innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); + innerTarget2.dispatchEvent(focus()); + innerTarget2.dispatchEvent(blur({relatedTarget: target})); + target.dispatchEvent(focus()); + target.dispatchEvent(blur({relatedTarget: innerTarget1})); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); // focus shifts outside subtree - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + innerTarget1.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); @@ -181,102 +168,87 @@ describe('FocusWithin event responder', () => { }); it('is called after "focus" and "blur" on focus target if keyboard was used', () => { + const target = ref.current; // use keyboard first - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'})); - ref.current.dispatchEvent(createEvent('focus')); + container.dispatchEvent(keydown({key: 'Tab'})); + target.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); it('is called after "focus" and "blur" on descendants if keyboard was used', () => { + const innerTarget = innerRef.current; // use keyboard first - container.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'})); - innerRef.current.dispatchEvent(createEvent('focus')); + container.dispatchEvent(keydown({key: 'Tab'})); + innerTarget.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + innerTarget.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => { + const target = ref.current; + const innerTarget1 = innerRef.current; + const innerTarget2 = innerRef2.current; // use keyboard first - ref.current.dispatchEvent(createEvent('focus')); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Tab'})); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef.current}), - ); - innerRef.current.dispatchEvent(createEvent('focus')); + target.dispatchEvent(focus()); + target.dispatchEvent(keydown({key: 'Tab'})); + target.dispatchEvent(blur({relatedTarget: innerTarget1})); + innerTarget1.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the next target, focus should no longer be visible - innerRef2.current.dispatchEvent(createEvent('pointerdown')); - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef2.current}), - ); - innerRef2.current.dispatchEvent(createEvent('focus')); + dispatchPointerPressDown(innerTarget2); + innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); + innerTarget2.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); // then use keyboard again - innerRef2.current.dispatchEvent( - createKeyboardEvent('keydown', {key: 'Tab', shiftKey: true}), - ); - innerRef2.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef.current}), - ); - innerRef.current.dispatchEvent(createEvent('focus')); + innerTarget2.dispatchEvent(keydown({key: 'Tab', shiftKey: true})); + innerTarget2.dispatchEvent(blur({relatedTarget: innerTarget1})); + innerTarget1.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - innerRef.current.dispatchEvent(createEvent('pointerdown')); + dispatchPointerPressDown(innerTarget1); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + innerTarget1.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4); }); it('is not called after "focus" and "blur" events without keyboard', () => { - innerRef.current.dispatchEvent(createEvent('pointerdown')); - innerRef.current.dispatchEvent(createEvent('focus')); - container.dispatchEvent(createEvent('pointerdown')); - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + const innerTarget = innerRef.current; + dispatchPointerPressDown(innerTarget); + dispatchPointerPressRelease(innerTarget); + innerTarget.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0); }); it('is only called once when focus moves within and outside the subtree', () => { + const target = ref.current; + const innerTarget1 = innerRef.current; + const innerTarget2 = innerRef2.current; + // focus shifts into subtree - innerRef.current.dispatchEvent(createEvent('focus')); + innerTarget1.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // focus moves around subtree - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef2.current}), - ); - innerRef2.current.dispatchEvent(createEvent('focus')); - innerRef2.current.dispatchEvent( - createEvent('blur', {relatedTarget: ref.current}), - ); - ref.current.dispatchEvent(createEvent('focus')); - ref.current.dispatchEvent( - createEvent('blur', {relatedTarget: innerRef.current}), - ); + innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); + innerTarget2.dispatchEvent(focus()); + innerTarget2.dispatchEvent(blur({relatedTarget: target})); + target.dispatchEvent(focus()); + target.dispatchEvent(blur({relatedTarget: innerTarget1})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); // focus shifts outside subtree - innerRef.current.dispatchEvent( - createEvent('blur', {relatedTarget: container}), - ); + innerTarget1.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); diff --git a/packages/react-events/src/dom/__tests__/Hover-test.internal.js b/packages/react-events/src/dom/__tests__/Hover-test.internal.js index 73d5106e6fba..eb9eab90c302 100644 --- a/packages/react-events/src/dom/__tests__/Hover-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Hover-test.internal.js @@ -76,9 +76,10 @@ describe.each(table)('Hover responder', hasPointerEvents => { ReactDOM.render(, container); }); - it('prevents custom events being dispatched', () => { - dispatchPointerHoverEnter(ref); - dispatchPointerHoverExit(ref); + it('does not call callbacks', () => { + const target = ref.current; + dispatchPointerHoverEnter(target); + dispatchPointerHoverExit(target); expect(onHoverChange).not.toBeCalled(); expect(onHoverStart).not.toBeCalled(); expect(onHoverMove).not.toBeCalled(); @@ -102,18 +103,21 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('is called for mouse pointers', () => { - dispatchPointerHoverEnter(ref); + const target = ref.current; + dispatchPointerHoverEnter(target); expect(onHoverStart).toHaveBeenCalledTimes(1); }); it('is not called for touch pointers', () => { - dispatchTouchTap(ref); + const target = ref.current; + dispatchTouchTap(target); expect(onHoverStart).not.toBeCalled(); }); it('is called if a mouse pointer is used after a touch pointer', () => { - dispatchTouchTap(ref); - dispatchPointerHoverEnter(ref); + const target = ref.current; + dispatchTouchTap(target); + dispatchPointerHoverEnter(target); expect(onHoverStart).toHaveBeenCalledTimes(1); }); }); @@ -134,16 +138,18 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('is called for mouse pointers', () => { - dispatchPointerHoverEnter(ref); + const target = ref.current; + dispatchPointerHoverEnter(target); expect(onHoverChange).toHaveBeenCalledTimes(1); expect(onHoverChange).toHaveBeenCalledWith(true); - dispatchPointerHoverExit(ref); + dispatchPointerHoverExit(target); expect(onHoverChange).toHaveBeenCalledTimes(2); expect(onHoverChange).toHaveBeenCalledWith(false); }); it('is not called for touch pointers', () => { - dispatchTouchTap(ref); + const target = ref.current; + dispatchTouchTap(target); expect(onHoverChange).not.toBeCalled(); }); }); @@ -164,28 +170,31 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('is called for mouse pointers', () => { - dispatchPointerHoverEnter(ref); - dispatchPointerHoverExit(ref); + const target = ref.current; + dispatchPointerHoverEnter(target); + dispatchPointerHoverExit(target); expect(onHoverEnd).toHaveBeenCalledTimes(1); }); if (hasPointerEvents) { it('is called once for cancelled mouse pointers', () => { - dispatchPointerHoverEnter(ref); - dispatchPointerCancel(ref); + const target = ref.current; + dispatchPointerHoverEnter(target); + dispatchPointerCancel(target); expect(onHoverEnd).toHaveBeenCalledTimes(1); // only called once if cancel follows exit onHoverEnd.mockReset(); - dispatchPointerHoverEnter(ref); - dispatchPointerHoverExit(ref); - dispatchPointerCancel(ref); + dispatchPointerHoverEnter(target); + dispatchPointerHoverExit(target); + dispatchPointerCancel(target); expect(onHoverEnd).toHaveBeenCalledTimes(1); }); } it('is not called for touch pointers', () => { - dispatchTouchTap(ref); + const target = ref.current; + dispatchTouchTap(target); expect(onHoverEnd).not.toBeCalled(); }); }); @@ -201,8 +210,10 @@ describe.each(table)('Hover responder', hasPointerEvents => { return
; }; ReactDOM.render(, container); - dispatchPointerHoverEnter(ref); - dispatchPointerHoverMove(ref, {from: {x: 0, y: 0}, to: {x: 1, y: 1}}); + + const target = ref.current; + dispatchPointerHoverEnter(target); + dispatchPointerHoverMove(target, {from: {x: 0, y: 0}, to: {x: 1, y: 1}}); expect(onHoverMove).toHaveBeenCalledTimes(2); expect(onHoverMove).toHaveBeenCalledWith( expect.objectContaining({type: 'hovermove'}), @@ -242,12 +253,15 @@ describe.each(table)('Hover responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchPointerHoverEnter(outerRef, {relatedTarget: container}); - dispatchPointerHoverExit(outerRef, {relatedTarget: innerRef.current}); - dispatchPointerHoverEnter(innerRef, {relatedTarget: outerRef.current}); - dispatchPointerHoverExit(innerRef, {relatedTarget: outerRef.current}); - dispatchPointerHoverEnter(outerRef, {relatedTarget: innerRef.current}); - dispatchPointerHoverExit(outerRef, {relatedTarget: container}); + const innerTarget = innerRef.current; + const outerTarget = outerRef.current; + + dispatchPointerHoverEnter(outerTarget, {relatedTarget: container}); + dispatchPointerHoverExit(outerTarget, {relatedTarget: innerTarget}); + dispatchPointerHoverEnter(innerTarget, {relatedTarget: outerTarget}); + dispatchPointerHoverExit(innerTarget, {relatedTarget: outerTarget}); + dispatchPointerHoverEnter(outerTarget, {relatedTarget: innerTarget}); + dispatchPointerHoverExit(outerTarget, {relatedTarget: container}); expect(events).toEqual([ 'outer: onHoverStart', @@ -300,9 +314,14 @@ describe.each(table)('Hover responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchPointerHoverEnter(ref, {x: 10, y: 10}); - dispatchPointerHoverMove(ref, {from: {x: 10, y: 10}, to: {x: 20, y: 20}}); - dispatchPointerHoverExit(ref, {x: 20, y: 20}); + const target = ref.current; + + dispatchPointerHoverEnter(target, {x: 10, y: 10}); + dispatchPointerHoverMove(target, { + from: {x: 10, y: 10}, + to: {x: 20, y: 20}, + }); + dispatchPointerHoverExit(target, {x: 20, y: 20}); expect(eventLog).toEqual([ { @@ -312,7 +331,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 10, clientX: 10, clientY: 10, - target: ref.current, + target, timeStamp: timeStamps[0], type: 'hoverstart', pointerType: 'mouse', @@ -324,7 +343,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 10, clientX: 10, clientY: 10, - target: ref.current, + target, timeStamp: timeStamps[1], type: 'hovermove', pointerType: 'mouse', @@ -336,7 +355,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 20, clientX: 20, clientY: 20, - target: ref.current, + target, timeStamp: timeStamps[2], type: 'hovermove', pointerType: 'mouse', @@ -348,7 +367,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 20, clientX: 20, clientY: 20, - target: ref.current, + target, timeStamp: timeStamps[3], type: 'hoverend', pointerType: 'mouse', diff --git a/packages/react-events/src/dom/test-utils.js b/packages/react-events/src/dom/test-utils.js index ddf6bf6773a1..826ac219ce58 100644 --- a/packages/react-events/src/dom/test-utils.js +++ b/packages/react-events/src/dom/test-utils.js @@ -95,6 +95,10 @@ function click(data) { return createEvent('click', data); } +function contextmenu(data) { + return createEvent('contextmenu', data); +} + function dragstart(data) { return createEvent('dragstart', data); } @@ -107,6 +111,14 @@ function gotpointercapture(data) { return createEvent('gotpointercapture', data); } +function keydown(data) { + return createKeyboardEvent('keydown', data); +} + +function keyup(data) { + return createKeyboardEvent('keyup', data); +} + function lostpointercapture(data) { return createEvent('lostpointercapture', data); } @@ -191,8 +203,52 @@ function touchstart(data, id) { * Dispatch high-level event sequences */ -function dispatchPointerHoverEnter(ref, {relatedTarget, x, y} = {}) { - const dispatch = arg => ref.current.dispatchEvent(arg); +function emptyFunction() {} + +function dispatchLongPressContextMenu( + target, + {preventDefault = emptyFunction} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); + const button = 0; + if (hasPointerEvent()) { + dispatch(pointerdown({button, pointerType: 'touch'})); + } + dispatch(touchstart()); + dispatch(contextmenu({button, preventDefault})); +} + +function dispatchRightClickContextMenu( + target, + {preventDefault = emptyFunction} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); + const button = 2; + if (hasPointerEvent()) { + dispatch(pointerdown({button, pointerType: 'mouse'})); + } + dispatch(mousedown({button})); + dispatch(contextmenu({button, preventDefault})); +} + +function dispatchModifiedClickContextMenu( + target, + {preventDefault = emptyFunction} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); + const button = 0; + const ctrlKey = true; + if (hasPointerEvent()) { + dispatch(pointerdown({button, ctrlKey, pointerType: 'mouse'})); + } + dispatch(mousedown({button, ctrlKey})); + if (platform.get() === 'mac') { + dispatch(contextmenu({button, ctrlKey, preventDefault})); + } +} + +function dispatchPointerHoverEnter(target, {relatedTarget, x, y} = {}) { + const dispatch = arg => target.dispatchEvent(arg); const button = -1; const pointerType = 'mouse'; const event = { @@ -211,8 +267,8 @@ function dispatchPointerHoverEnter(ref, {relatedTarget, x, y} = {}) { dispatch(mouseover(event)); } -function dispatchPointerHoverMove(ref, {from, to} = {}) { - const dispatch = arg => ref.current.dispatchEvent(arg); +function dispatchPointerHoverMove(target, {from, to} = {}) { + const dispatch = arg => target.dispatchEvent(arg); const button = -1; const pointerId = 1; const pointerType = 'mouse'; @@ -233,8 +289,8 @@ function dispatchPointerHoverMove(ref, {from, to} = {}) { dispatchMove({x: to.x, y: to.y}); } -function dispatchPointerHoverExit(ref, {relatedTarget, x, y} = {}) { - const dispatch = arg => ref.current.dispatchEvent(arg); +function dispatchPointerHoverExit(target, {relatedTarget, x, y} = {}) { + const dispatch = arg => target.dispatchEvent(arg); const button = -1; const pointerType = 'mouse'; const event = { @@ -253,51 +309,82 @@ function dispatchPointerHoverExit(ref, {relatedTarget, x, y} = {}) { dispatch(mouseleave(event)); } -function dispatchPointerCancel(ref, options) { - const dispatchEvent = arg => ref.current.dispatchEvent(arg); +function dispatchPointerCancel(target, options) { + const dispatchEvent = arg => target.dispatchEvent(arg); dispatchEvent(pointercancel({pointerType: 'mouse'})); dispatchEvent(dragstart({pointerType: 'mouse'})); } -function dispatchPointerPressDown(ref, {button = 0, pointerType = 'mouse'}) { - const dispatch = arg => ref.current.dispatchEvent(arg); +function dispatchPointerPressDown( + target, + {button = 0, pointerType = 'mouse'} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); const pointerId = 1; - if (hasPointerEvent()) { - dispatch(pointerover({pointerId, pointerType, button})); - dispatch(pointerenter({pointerId, pointerType, button})); - dispatch(pointerdown({pointerId, pointerType, button})); - } - dispatch(touchstart(null, pointerId)); - if (hasPointerEvent()) { - dispatch(gotpointercapture({pointerId, pointerType, button})); + if (pointerType !== 'mouse') { + if (hasPointerEvent()) { + dispatch(pointerover({button, pointerId, pointerType})); + dispatch(pointerenter({button, pointerId, pointerType})); + dispatch(pointerdown({button, pointerId, pointerType})); + } + dispatch(touchstart(null, pointerId)); + if (hasPointerEvent()) { + dispatch(gotpointercapture({button, pointerId, pointerType})); + } + } else { + if (hasPointerEvent()) { + dispatch(pointerdown({button, pointerId, pointerType})); + } + dispatch(mousedown({button})); + if (document.activeElement !== target) { + dispatch(focus({button})); + } } } -function dispatchPointerPressRelease(ref, {button = 0, pointerType = 'mouse'}) { - const dispatch = arg => ref.current.dispatchEvent(arg); +function dispatchPointerPressRelease( + target, + {button = 0, pointerType = 'mouse'} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); const pointerId = 1; - if (hasPointerEvent()) { - dispatch(pointerup({pointerId, pointerType, button})); - dispatch(lostpointercapture({pointerId, pointerType, button})); - dispatch(pointerout({pointerId, pointerType, button})); - dispatch(pointerleave({pointerId, pointerType, button})); + if (pointerType !== 'mouse') { + if (hasPointerEvent()) { + dispatch(pointerup({button, pointerId, pointerType})); + dispatch(lostpointercapture({button, pointerId, pointerType})); + dispatch(pointerout({button, pointerId, pointerType})); + dispatch(pointerleave({button, pointerId, pointerType})); + } + dispatch(touchend(null, pointerId)); + dispatch(mouseover({button})); + dispatch(mousemove({button})); + dispatch(mousedown({button})); + if (document.activeElement !== target) { + dispatch(focus({button})); + } + dispatch(mouseup({button})); + dispatch(click({button})); + } else { + if (hasPointerEvent()) { + dispatch(pointerup({button, pointerId, pointerType})); + } + dispatch(mouseup({button})); + dispatch(click({button})); } - dispatch(touchend(null, pointerId)); - dispatch(mouseover({button})); - dispatch(mousemove({button})); - dispatch(mousedown({button})); - dispatch(focus({button})); - dispatch(mouseup({button})); - dispatch(click({button})); } -function dispatchTouchTap(ref) { - dispatchPointerPressDown(ref, {pointerType: 'touch'}); - dispatchPointerPressRelease(ref, {pointerType: 'touch'}); +function dispatchTouchTap(target) { + dispatchPointerPressDown(target, {pointerType: 'touch'}); + dispatchPointerPressRelease(target, {pointerType: 'touch'}); } module.exports = { + blur, + focus, createEvent, + dispatchLongPressContextMenu, + dispatchRightClickContextMenu, + dispatchModifiedClickContextMenu, dispatchPointerCancel, dispatchPointerHoverEnter, dispatchPointerHoverExit, @@ -305,6 +392,8 @@ module.exports = { dispatchPointerPressDown, dispatchPointerPressRelease, dispatchTouchTap, + keydown, + keyup, platform, hasPointerEvent, setPointerEvent,