mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Event API: adds pointerType to Focus events (#15645)
This commit is contained in:
Vendored
+88
-31
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user