Skip to content

Commit

Permalink
Prevent welcome tour keyboard navigation from hijacking left right ke…
Browse files Browse the repository at this point in the history
…ys on the editor (#39683)
  • Loading branch information
xavier-lc authored and gogdzl committed Oct 25, 2024
1 parent 5200252 commit 333a224
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 4 deletions.
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;

0 comments on commit 333a224

Please sign in to comment.