From 383678613d5e58977b6316dc95c0a3c63355c839 Mon Sep 17 00:00:00 2001 From: Douglas Egiemeh Date: Tue, 23 Apr 2024 12:36:41 +0200 Subject: [PATCH] feat(navbar keyboard navigation): optionally navigate to submenu items using the `Tab` and `Enter` keys. (#3482) * feat(navbar keyboard navigation): add arrow keys navigation to navbar * feat(navbar keyboard navigation): add arrow keys navigation to navbar * feat(navbar keyboard navigation): add event types * feat(navbar keyboard navigation): cleanup unused code * feat(navbar keyboard navigation): make Tab and Enter key primarily for keyboard navigation * feat(navbar keyboard navigation): make Tab and Enter key primarily for keyboard navigation * feat(navbar keyboard navigation): tabIndex to focus submenu container * feat(navbar keyboard navigation): update changeset * feat(navbar keyboard navigation): rename state variable * feat(navbar keyboard navigation): prevent default enter behaviour * feat(navbar keyboard navigation): handle submenu state after onBlur * feat(keyboard navigation): add submenu preview on navigation * feat(keyboard navigation): remove unused variables * feat(accessibility): remove unused hook * feat(main nav accessibility): update focus when navigating to a new submenu --------- Co-authored-by: Ddouglasz --- .changeset/small-clouds-exist.md | 5 +++++ .../src/components/navbar/menu-items.tsx | 7 +++++++ .../src/components/navbar/navbar.tsx | 18 ++++++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .changeset/small-clouds-exist.md diff --git a/.changeset/small-clouds-exist.md b/.changeset/small-clouds-exist.md new file mode 100644 index 0000000000..d769c3afe5 --- /dev/null +++ b/.changeset/small-clouds-exist.md @@ -0,0 +1,5 @@ +--- +'@commercetools-frontend/application-shell': minor +--- + +Improve keyboard accessibility by switching between the menu items and submenu items using mainly the Tab and Enter keys. diff --git a/packages/application-shell/src/components/navbar/menu-items.tsx b/packages/application-shell/src/components/navbar/menu-items.tsx index 1aed21b745..5751812ace 100644 --- a/packages/application-shell/src/components/navbar/menu-items.tsx +++ b/packages/application-shell/src/components/navbar/menu-items.tsx @@ -173,6 +173,7 @@ export type MenuGroupProps = { children?: ReactNode; submenuVerticalPosition?: number; isSubmenuAboveMenuItem?: boolean; + handleKeyDown?: React.KeyboardEventHandler; }; const MenuGroup = forwardRef((props, ref) => { @@ -189,6 +190,7 @@ const MenuGroup = forwardRef((props, ref) => { const isSublistActiveWhileIsMenuCollapsed = Boolean( props.level === 2 && props.isActive && !props.isExpanded ); + return ( ((props, ref) => { isSublistActiveWhileIsMenuExpanded || isSublistActiveWhileIsMenuCollapsed } + onKeyDown={props.handleKeyDown} className={classnames( { 'sublist-expanded__active': isSublistActiveWhileIsMenuExpanded, @@ -249,6 +252,7 @@ type MenuItemProps = { | FocusEventHandler; children: ReactNode; identifier?: string; + onKeyDown?: (e: React.KeyboardEvent) => void; }; const MenuItem = (props: MenuItemProps) => { return ( @@ -259,6 +263,7 @@ const MenuItem = (props: MenuItemProps) => { onMouseLeave={props.onMouseLeave as MouseEventHandler} onFocus={props.onMouseEnter as FocusEventHandler} onBlur={props.onMouseLeave as FocusEventHandler} + onKeyDown={props.onKeyDown} data-menuitem={props.identifier} className={classnames({ active: props.isActive, @@ -281,6 +286,7 @@ export type MenuItemLinkProps = { onClick?: (event: SyntheticEvent) => void; useFullRedirectsForLinks?: boolean; isSubmenuLink?: boolean; + isSubmenuFocused?: boolean; }; const menuItemLinkDefaultProps: Pick = { exactMatch: false, @@ -306,6 +312,7 @@ const MenuItemLink = (props: MenuItemLinkProps) => { activeClassName="highlighted" data-link-level={linkLevel} css={getMenuItemLinkStyles(Boolean(props.isSubmenuLink))} + tabIndex={props.isSubmenuLink && !props.isSubmenuFocused ? -1 : 0} onClick={(event) => { if (props.linkTo && props.useFullRedirectsForLinks) { event.preventDefault(); diff --git a/packages/application-shell/src/components/navbar/navbar.tsx b/packages/application-shell/src/components/navbar/navbar.tsx index b4ebee16a4..aaac2845d4 100644 --- a/packages/application-shell/src/components/navbar/navbar.tsx +++ b/packages/application-shell/src/components/navbar/navbar.tsx @@ -111,6 +111,8 @@ const getIsSubmenuRouteActive = ( export const ApplicationMenu = (props: ApplicationMenuProps) => { const [submenuVerticalPosition, setSubmenuVerticalPosition] = useState(0); const [isSubmenuAboveMenuItem, setIsSubmenuAboveMenuItem] = useState(false); + const [isSubmenuFocused, setIsSubmenuFocused] = useState(false); + const observerRef = useRef(null); const submenuRef = useRef(null); const hasSubmenu = @@ -149,8 +151,6 @@ export const ApplicationMenu = (props: ApplicationMenuProps) => { [menuItemIdentifier, props.isMenuOpen] ); - const observerRef = useRef(null); - useLayoutEffect(() => { observerRef.current = new IntersectionObserver(callbackFn, { rootMargin: '-100% 0px 0px 0px', // we want to observe if the submenu crosses the bottom line of the viewport - therefore we set the root element top margin to -100% of the viewport height @@ -166,6 +166,7 @@ export const ApplicationMenu = (props: ApplicationMenuProps) => { if (observer && currentSubmenuRef) { observer.observe(currentSubmenuRef); } + setIsSubmenuFocused(false); return () => observer?.disconnect(); }, [ menuItemIdentifier, @@ -195,6 +196,17 @@ export const ApplicationMenu = (props: ApplicationMenuProps) => { ? getMenuVisibilitiesOfSubmenus(props.menu) : getMenuVisibilityOfMainmenu(props.menu); + const handleKeyDown = (e: React.KeyboardEvent) => { + const currentlyFocusedItem = submenuRef.current?.querySelector(':focus'); + + if (e.key === 'Enter') { + setIsSubmenuFocused(true); + if (!currentlyFocusedItem) { + submenuRef.current?.querySelector('a')?.focus(); + } + } + }; + return ( { isMainMenuRouteActive={isMainMenuRouteActive} isMenuOpen={props.isMenuOpen} onClick={props.handleToggleItem} + onKeyDown={handleKeyDown} onMouseEnter={props.handleToggleItem} onMouseLeave={props.shouldCloseMenuFly} identifier={menuItemIdentifier} @@ -282,6 +295,7 @@ export const ApplicationMenu = (props: ApplicationMenuProps) => { } onClick={props.onMenuItemClick} isSubmenuLink + isSubmenuFocused={isSubmenuFocused} >