From af19e2eb2f8a849a11d2f5fa98d65d46087058e2 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 May 2019 15:59:36 +0100 Subject: [PATCH] Event API: adds pointerType to Focus events (#15645) --- packages/react-events/src/Focus.js | 119 +++++++++++++----- .../src/__tests__/Focus-test.internal.js | 73 +++++++++++ 2 files changed, 161 insertions(+), 31 deletions(-) diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js index 7f4664b87b..9d276cd41d 100644 --- a/packages/react-events/src/Focus.js +++ b/packages/react-events/src/Focus.js @@ -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; diff --git a/packages/react-events/src/__tests__/Focus-test.internal.js b/packages/react-events/src/__tests__/Focus-test.internal.js index e91c29a20d..21300f4af8 100644 --- a/packages/react-events/src/__tests__/Focus-test.internal.js +++ b/packages/react-events/src/__tests__/Focus-test.internal.js @@ -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', () => {