diff --git a/projects/packages/jetpack-mu-wpcom/changelog/prevent-welcome-tour-keyboard-navigation-from-hijacking-left-right-keys-on-the-editor b/projects/packages/jetpack-mu-wpcom/changelog/prevent-welcome-tour-keyboard-navigation-from-hijacking-left-right-keys-on-the-editor new file mode 100644 index 0000000000000..b9ff1ac7fed20 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/prevent-welcome-tour-keyboard-navigation-from-hijacking-left-right-keys-on-the-editor @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Prevent welcome tour keyboard navigation from hijacking left right keys on the editor diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx index 7d913d40d3a7f..002a3a3ad050c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/keyboard-navigation.tsx @@ -34,6 +34,7 @@ const KeyboardNavigation: React.FunctionComponent< Props > = ( { onEscape: onMinimize, onArrowRight: onNextStepProgression, onArrowLeft: onPreviousStepProgression, + tourContainerRef, } ); useFocusTrap( tourContainerRef ); @@ -44,7 +45,7 @@ const KeyboardNavigation: React.FunctionComponent< Props > = ( { * Minimize Tour Nav */ function MinimizedTourNav() { - useKeydownHandler( { onEscape: onDismiss( 'esc-key-minimized' ) } ); + useKeydownHandler( { onEscape: onDismiss( 'esc-key-minimized' ), tourContainerRef } ); return null; } diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx index e030a37cdda26..0d5aeb3d5b091 100644 --- a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/components/tour-kit-frame.tsx @@ -247,6 +247,7 @@ const TourKitFrame: React.FunctionComponent< Props > = ( { config } ) => {
) } > { showArrowIndicator() && ( diff --git a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts index 1cdf93219b40e..8d88f2435d4e0 100644 --- a/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts +++ b/projects/packages/jetpack-mu-wpcom/src/common/tour-kit/hooks/use-keydown-handler.ts @@ -8,12 +8,31 @@ interface Props { onEscape?: () => void; onArrowRight?: () => void; onArrowLeft?: () => void; + tourContainerRef: React.MutableRefObject< null | HTMLElement >; } /** * A hook the applies the respective callbacks in response to keydown events. */ -const useKeydownHandler = ( { onEscape, onArrowRight, onArrowLeft }: Props ): void => { +const useKeydownHandler = ( { + onEscape, + onArrowRight, + onArrowLeft, + tourContainerRef, +}: Props ): void => { + const isActiveElementOutsideTourContainer = useCallback( (): boolean => { + return !! ( + tourContainerRef.current && + ! tourContainerRef.current.contains( tourContainerRef.current.ownerDocument.activeElement ) + ); + }, [ tourContainerRef ] ); + + const focusTourContainer = useCallback( () => { + ( + tourContainerRef.current?.querySelector( '.tour-kit-frame__container' ) as HTMLElement + )?.focus(); + }, [ tourContainerRef ] ); + const handleKeydown = useCallback( ( event: KeyboardEvent ) => { let handled = false; @@ -21,18 +40,32 @@ const useKeydownHandler = ( { onEscape, onArrowRight, onArrowLeft }: Props ): vo switch ( event.key ) { case 'Escape': if ( onEscape ) { + if ( isActiveElementOutsideTourContainer() ) { + return; + } + onEscape(); + // focus the container after minimizing so the user can dismiss it + focusTourContainer(); handled = true; } break; case 'ArrowRight': if ( onArrowRight ) { + if ( isActiveElementOutsideTourContainer() ) { + return; + } + onArrowRight(); handled = true; } break; case 'ArrowLeft': if ( onArrowLeft ) { + if ( isActiveElementOutsideTourContainer() ) { + return; + } + onArrowLeft(); handled = true; } @@ -46,16 +79,40 @@ const useKeydownHandler = ( { onEscape, onArrowRight, onArrowLeft }: Props ): vo event.stopPropagation(); } }, - [ onEscape, onArrowRight, onArrowLeft ] + [ onEscape, onArrowRight, onArrowLeft, isActiveElementOutsideTourContainer, focusTourContainer ] + ); + + // when clicking on the container, if the target is not a focusable element, + // force focus on the first children so keyboard navigation works + const handleTourContainerClick = useCallback( + ( event: MouseEvent ) => { + const isFocusable = ( element: HTMLElement ) => { + const focusableElements = [ 'A', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT' ]; + + // Check if the element is focusable by its tag or has a tabindex >= 0 + return focusableElements.includes( element?.tagName ) || element?.tabIndex >= 0; + }; + + if ( isFocusable( event.target as HTMLElement ) ) { + return; + } + + focusTourContainer(); + }, + [ focusTourContainer ] ); useEffect( () => { + const tourContainer = tourContainerRef.current; + document.addEventListener( 'keydown', handleKeydown ); + tourContainer?.addEventListener( 'click', handleTourContainerClick ); return () => { document.removeEventListener( 'keydown', handleKeydown ); + tourContainer?.removeEventListener( 'click', handleTourContainerClick ); }; - }, [ handleKeydown ] ); + }, [ handleKeydown, handleTourContainerClick, tourContainerRef ] ); }; export default useKeydownHandler;