Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 57 additions & 50 deletions packages/react-events/src/dom/Focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type FocusState = {
isFocused: boolean,
isFocusVisible: boolean,
pointerType: PointerType,
isEmulatingMouseEvents: boolean,
};

type FocusProps = {
Expand Down Expand Up @@ -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'];
Copy link
Contributor

@TrySound TrySound Aug 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mouseup and mousemove are missing. Is it intentional?


function isFunction(obj): boolean {
return typeof obj === 'function';
Expand All @@ -110,21 +98,15 @@ 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
// after it was focused using a keyboard.
const focusTarget = state.focusTarget;
if (
focusTarget !== null &&
context.isTargetWithinNode(event.target, focusTarget) &&
context.isTargetWithinResponderScope(focusTarget) &&
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
) {
callback(false);
Expand All @@ -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': {
Expand All @@ -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;
}
Expand Down Expand Up @@ -271,6 +264,7 @@ const focusResponderImpl = {
getInitialState(): FocusState {
return {
focusTarget: null,
isEmulatingMouseEvents: false,
isFocused: false,
isFocusVisible: false,
pointerType: '',
Expand Down Expand Up @@ -303,6 +297,7 @@ const focusResponderImpl = {
state.isFocusVisible = isGlobalFocusVisible;
dispatchFocusEvents(context, props, state);
}
state.isEmulatingMouseEvents = false;
break;
}
case 'blur': {
Expand All @@ -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;
}
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -402,6 +408,7 @@ const focusWithinResponderImpl = {
getInitialState(): FocusState {
return {
focusTarget: null,
isEmulatingMouseEvents: false,
isFocused: false,
isFocusVisible: false,
pointerType: '',
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]];

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -132,7 +100,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchContextMenuEvents(ref, 'mouse');
dispatchRightClickContextMenu(ref.current);
expect(onContextMenu).toHaveBeenCalledTimes(0);
});

Expand All @@ -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);
});
Expand All @@ -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'}),
Expand All @@ -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);
});
});
Expand Down
Loading