Event API: adds pointerType to Focus events (#15645)

This commit is contained in:
Dominic Gannaway
2019-05-14 15:59:36 +01:00
committed by GitHub
parent cc24d0ea56
commit af19e2eb2f
2 changed files with 161 additions and 31 deletions
+88 -31
View File
@@ -26,13 +26,16 @@ type FocusState = {
focusTarget: null | Element | Document,
isFocused: boolean,
isLocalFocusVisible: boolean,
pointerType: PointerType,
};
type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
type FocusEvent = {|
target: Element | Document,
type: FocusEventType,
pointerType: PointerType,
timeStamp: number,
|};
@@ -45,25 +48,33 @@ const rootEventTypes = [
'keydown',
'keypress',
'keyup',
'mousemove',
'mousedown',
'mouseup',
'pointermove',
'pointerdown',
'pointerup',
'touchmove',
'touchstart',
'touchend',
];
// 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',
);
}
function createFocusEvent(
context: ReactResponderContext,
type: FocusEventType,
target: Element | Document,
pointerType: PointerType,
): FocusEvent {
return {
target,
type,
pointerType,
timeStamp: context.getTimeStamp(),
};
}
@@ -73,16 +84,27 @@ function dispatchFocusInEvents(
props: FocusProps,
state: FocusState,
) {
const pointerType = state.pointerType;
const target = ((state.focusTarget: any): Element | Document);
if (props.onFocus) {
const syntheticEvent = createFocusEvent(context, 'focus', target);
const syntheticEvent = createFocusEvent(
context,
'focus',
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true});
}
if (props.onFocusChange) {
const listener = () => {
props.onFocusChange(true);
};
const syntheticEvent = createFocusEvent(context, 'focuschange', target);
const syntheticEvent = createFocusEvent(
context,
'focuschange',
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
}
if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
@@ -93,6 +115,7 @@ function dispatchFocusInEvents(
context,
'focusvisiblechange',
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
}
@@ -103,16 +126,27 @@ function dispatchFocusOutEvents(
props: FocusProps,
state: FocusState,
) {
const pointerType = state.pointerType;
const target = ((state.focusTarget: any): Element | Document);
if (props.onBlur) {
const syntheticEvent = createFocusEvent(context, 'blur', target);
const syntheticEvent = createFocusEvent(
context,
'blur',
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true});
}
if (props.onFocusChange) {
const listener = () => {
props.onFocusChange(false);
};
const syntheticEvent = createFocusEvent(context, 'focuschange', target);
const syntheticEvent = createFocusEvent(
context,
'focuschange',
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
}
dispatchFocusVisibleOutEvent(context, props, state);
@@ -123,6 +157,7 @@ function dispatchFocusVisibleOutEvent(
props: FocusProps,
state: FocusState,
) {
const pointerType = state.pointerType;
const target = ((state.focusTarget: any): Element | Document);
if (props.onFocusVisibleChange && state.isLocalFocusVisible) {
const listener = () => {
@@ -132,6 +167,7 @@ function dispatchFocusVisibleOutEvent(
context,
'focusvisiblechange',
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, listener, {discrete: true});
state.isLocalFocusVisible = false;
@@ -148,6 +184,31 @@ function unmountResponder(
}
}
function handleRootPointerEvent(
event: ReactResponderEvent,
context: ReactResponderContext,
props: FocusProps,
state: FocusState,
): 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;
}
isGlobalFocusVisible = false;
// Focus should stop being visible if a pointer is used on the element
// after it was focused using a keyboard.
if (
state.focusTarget === context.getEventCurrentTarget(event) &&
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
) {
dispatchFocusVisibleOutEvent(context, props, state);
}
}
let isGlobalFocusVisible = true;
const FocusResponder = {
@@ -158,6 +219,7 @@ const FocusResponder = {
focusTarget: null,
isFocused: false,
isLocalFocusVisible: false,
pointerType: '',
};
},
stopLocalPropagation: true,
@@ -208,36 +270,30 @@ const FocusResponder = {
props: FocusProps,
state: FocusState,
): void {
const {type, target} = event;
const {type} = event;
switch (type) {
case 'mousemove':
case 'mousedown':
case 'mouseup':
case 'mouseup': {
state.pointerType = 'mouse';
handleRootPointerEvent(event, context, props, state);
break;
}
case 'pointermove':
case 'pointerdown':
case 'pointerup':
case 'pointerup': {
// $FlowFixMe: Flow doesn't know about PointerEvents
const nativeEvent = ((event.nativeEvent: any): PointerEvent);
state.pointerType = nativeEvent.pointerType;
handleRootPointerEvent(event, context, props, state);
break;
}
case 'touchmove':
case 'touchstart':
case 'touchend': {
// Ignore a Safari quirks where 'mousemove' is dispatched on the 'html'
// element when the window blurs.
if (type === 'mousemove' && target.nodeName === 'HTML') {
return;
}
isGlobalFocusVisible = false;
// Focus should stop being visible if a pointer is used on the element
// after it was focused using a keyboard.
if (
state.focusTarget === context.getEventCurrentTarget(event) &&
(type === 'mousedown' ||
type === 'touchstart' ||
type === 'pointerdown')
) {
dispatchFocusVisibleOutEvent(context, props, state);
}
state.pointerType = 'touch';
handleRootPointerEvent(event, context, props, state);
break;
}
@@ -249,6 +305,7 @@ const FocusResponder = {
nativeEvent.key === 'Tab' &&
!(nativeEvent.metaKey || nativeEvent.altKey || nativeEvent.ctrlKey)
) {
state.pointerType = 'keyboard';
isGlobalFocusVisible = true;
}
break;
@@ -131,6 +131,79 @@ describe('Focus event responder', () => {
target.dispatchEvent(createFocusEvent('focus'));
expect(onFocus).not.toBeCalled();
});
it('is called with the correct pointerType using pointer events', () => {
// Pointer mouse
ref.current.dispatchEvent(
createPointerEvent('pointerdown', {
pointerType: 'mouse',
}),
);
ref.current.dispatchEvent(createFocusEvent('focus'));
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse'}),
);
ref.current.dispatchEvent(createFocusEvent('blur'));
// Pointer touch
ref.current.dispatchEvent(
createPointerEvent('pointerdown', {
pointerType: 'touch',
}),
);
ref.current.dispatchEvent(createFocusEvent('focus'));
expect(onFocus).toHaveBeenCalledTimes(2);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'touch'}),
);
ref.current.dispatchEvent(createFocusEvent('blur'));
// Pointer pen
ref.current.dispatchEvent(
createPointerEvent('pointerdown', {
pointerType: 'pen',
}),
);
ref.current.dispatchEvent(createFocusEvent('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(createPointerEvent('mousedown'));
ref.current.dispatchEvent(createFocusEvent('focus'));
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse'}),
);
ref.current.dispatchEvent(createFocusEvent('blur'));
// Touch
ref.current.dispatchEvent(createPointerEvent('touchstart'));
ref.current.dispatchEvent(createFocusEvent('focus'));
expect(onFocus).toHaveBeenCalledTimes(2);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'touch'}),
);
});
it('is called with the correct pointerType using a keyboard', () => {
// Keyboard tab
ref.current.dispatchEvent(
createPointerEvent('keypress', {
key: 'Tab',
}),
);
ref.current.dispatchEvent(createFocusEvent('focus'));
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'keyboard'}),
);
});
});
describe('onFocusChange', () => {