Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent welcome tour keyboard navigation from hijacking left right keys on the editor #39683

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Prevent welcome tour keyboard navigation from hijacking left right keys on the editor
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const KeyboardNavigation: React.FunctionComponent< Props > = ( {
onEscape: onMinimize,
onArrowRight: onNextStepProgression,
onArrowLeft: onPreviousStepProgression,
tourContainerRef,
} );
useFocusTrap( tourContainerRef );

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ const TourKitFrame: React.FunctionComponent< Props > = ( { config } ) => {
<div
className="tour-kit-frame__container"
ref={ setPopperElement }
tabIndex={ -1 }
{ ...( stepRepositionProps as React.HTMLAttributes< HTMLDivElement > ) }
>
{ showArrowIndicator() && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,64 @@ 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;

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;
}
Expand All @@ -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;