Skip to content

Commit bb24a54

Browse files
authored
Close hovered SubNav.SubMenu on Escape (#989)
* add test to ensure hovered submenus close when escape is pressed * create utilities for managing submenu state * close hovered submenu on escape * explicitly hide collapsed submenu children with aria-hidden * add changeset
1 parent 05aee45 commit bb24a54

File tree

3 files changed

+43
-8
lines changed

3 files changed

+43
-8
lines changed

.changeset/calm-countries-explain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react-brand': patch
3+
---
4+
5+
Allow hovered `SubNav.SubMenu` menus to be closed using <kbd>Escape</kbd> key

packages/react/src/SubNav/SubNav.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,32 @@ describe('SubNav', () => {
172172

173173
expect(toggleSubmenuButton).toHaveAttribute('aria-expanded', 'false')
174174
})
175+
176+
it('hides a hovered submenu when escape is pressed', async () => {
177+
mockUseWindowSize.mockImplementation(() => ({isLarge: true}))
178+
179+
const {getByRole, queryByRole} = render(
180+
<SubNav fullWidth>
181+
<SubNav.Link href="#" aria-current="page">
182+
Copilot
183+
<SubNav.SubMenu>
184+
<SubNav.Link href="#">Copilot feature page one</SubNav.Link>
185+
<SubNav.Link href="#">Copilot feature page two</SubNav.Link>
186+
<SubNav.Link href="#">Copilot feature page three</SubNav.Link>
187+
</SubNav.SubMenu>
188+
</SubNav.Link>
189+
<SubNav.Link href="#">Code review</SubNav.Link>
190+
<SubNav.Link href="#">Search</SubNav.Link>
191+
<SubNav.Action href="#">Call to action</SubNav.Action>
192+
</SubNav>,
193+
)
194+
195+
await userEvent.hover(getByRole('link', {name: 'Copilot'}))
196+
197+
expect(getByRole('link', {name: 'Copilot feature page one'})).toBeVisible()
198+
199+
await userEvent.keyboard('{escape}')
200+
201+
expect(queryByRole('link', {name: 'Copilot feature page one'})).not.toBeInTheDocument()
202+
})
175203
})

packages/react/src/SubNav/SubNav.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,19 +363,21 @@ const SubNavLinkWithSubmenu = forwardRef<HTMLDivElement, SubNavLinkProps>(
363363
}
364364
})
365365

366-
const [label, SubMenuChildren] = children as ReactNode[]
366+
const expand = useCallback(() => setIsExpanded(true), [])
367+
const collapse = useCallback(() => setIsExpanded(false), [])
368+
const toggleExpanded = useCallback(() => setIsExpanded(prev => !prev), [])
369+
370+
useKeyboardEscape(collapse)
367371

368-
const handleOnClick = useCallback(() => {
369-
setIsExpanded(prev => !prev)
370-
}, [])
372+
const [label, SubMenuChildren] = children as ReactNode[]
371373

372374
return (
373375
<div
374376
className={clsx(styles['SubNav__link--has-sub-menu'], isExpanded && styles['SubNav__link--expanded'])}
375377
data-testid={testId || testIds.subMenu}
376378
ref={ref}
377-
onMouseOver={() => setIsExpanded(true)}
378-
onMouseOut={() => setIsExpanded(false)}
379+
onMouseOver={expand}
380+
onMouseOut={collapse}
379381
/**
380382
* onFocus and onBlur need to be defined to keep the jsx-a11y/mouse-events-have-key-events
381383
* eslint rule happy. The focus/blur behaviour is handled by useContainsFocus
@@ -402,7 +404,7 @@ const SubNavLinkWithSubmenu = forwardRef<HTMLDivElement, SubNavLinkProps>(
402404
{isLarge && (
403405
<button
404406
className={styles['SubNav__sub-menu-toggle']}
405-
onClick={handleOnClick}
407+
onClick={toggleExpanded}
406408
aria-expanded={isExpanded ? 'true' : 'false'}
407409
aria-controls={submenuId}
408410
aria-label={`${isExpanded ? 'Close' : 'Open'} submenu`}
@@ -411,7 +413,7 @@ const SubNavLinkWithSubmenu = forwardRef<HTMLDivElement, SubNavLinkProps>(
411413
</button>
412414
)}
413415

414-
<div id={submenuId} className={styles['SubNav__sub-menu-children']}>
416+
<div id={submenuId} className={styles['SubNav__sub-menu-children']} aria-hidden={!isExpanded}>
415417
{SubMenuChildren}
416418
</div>
417419
</div>

0 commit comments

Comments
 (0)