mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[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.
This commit is contained in:
committed by
GitHub
parent
e26e3e2623
commit
dde9dedfc8
+57
-50
@@ -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',
|
||||
];
|
||||
const hasPointerEvents =
|
||||
typeof window !== 'undefined' && window.PointerEvent != null;
|
||||
|
||||
// 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 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 'touchmove':
|
||||
case 'touchstart':
|
||||
case 'touchend': {
|
||||
state.pointerType = 'touch';
|
||||
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
|
||||
)
|
||||
) {
|
||||
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 '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,
|
||||
|
||||
@@ -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(<Component />, 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(<Component />, 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(<Component />, container);
|
||||
|
||||
dispatchContextMenuEvents(ref, 'mouse');
|
||||
dispatchRightClickContextMenu(ref.current);
|
||||
expect(onContextMenu).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@@ -149,7 +117,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
|
||||
};
|
||||
ReactDOM.render(<Component />, 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(<Component />, 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(<Component />, container);
|
||||
|
||||
dispatchContextMenuEvents(ref, {variant: 'modified'});
|
||||
dispatchModifiedClickContextMenu(ref.current);
|
||||
expect(onContextMenu).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
describe('Focus event responder', () => {
|
||||
const forcePointerEvents = true;
|
||||
const table = [[forcePointerEvents], [!forcePointerEvents]];
|
||||
|
||||
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(<Component />, 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(<Outer />, 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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -76,9 +76,10 @@ describe.each(table)('Hover responder', hasPointerEvents => {
|
||||
ReactDOM.render(<Component />, 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 <div ref={ref} listeners={listener} />;
|
||||
};
|
||||
ReactDOM.render(<Component />, 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(<Outer />, 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(<Component />, 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',
|
||||
|
||||
+124
-35
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user