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} >