Skip to content

Commit d0757df

Browse files
authored
Improve keyboard navigation in SubMenu anchor variant (#1084)
* remove visibility:hidden from SubNav anchor variant * ensure SubNav anchor menu is visible when it contains focus * convert AnchorNavVariant story to reusable template * add a storybook interaction test to assert focus order in SubNav anchor variant * add changeset
1 parent 6531558 commit d0757df

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

.changeset/breezy-shrimps-sell.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@primer/react-brand': patch
3+
---
4+
5+
Improved keyboard navigation in `SubNav` anchor variant.
6+
7+
The anchor-based submenu now appears after the main `SubNav` items in the focus order, and becomes visible when focussed.

packages/react/src/SubNav/SubNav.features.stories.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {useEffect} from 'react'
2-
import {Meta} from '@storybook/react'
2+
import type {Meta, StoryFn} from '@storybook/react'
33
import {INITIAL_VIEWPORTS} from '@storybook/addon-viewport'
44
import {linkTo} from '@storybook/addon-links'
55

@@ -250,7 +250,7 @@ const AnchorNavVariantData = {
250250
['Reliability']: 'reliability',
251251
}
252252

253-
export const AnchorNavVariant = args => (
253+
export const AnchorNavVariantTemplate: StoryFn<typeof SubNav> = args => (
254254
<main>
255255
<Box paddingBlockStart={64} backgroundColor="subtle" style={{position: 'relative', zIndex: 32}}></Box>
256256
<SubNav {...args}>
@@ -290,6 +290,9 @@ export const AnchorNavVariant = args => (
290290
</Box>
291291
</main>
292292
)
293+
294+
const AnchorNavVariant = AnchorNavVariantTemplate.bind({})
295+
293296
AnchorNavVariant.parameters = {
294297
layout: 'fullscreen',
295298
}
@@ -454,3 +457,54 @@ KeyboardNavigation.play = async ({canvasElement}) => {
454457
await userEvent.tab({shift: true})
455458
expect(getByRole('link', {name: 'Discussions'})).toHaveFocus()
456459
}
460+
461+
export const AnchorNavVariantKeyboardNavigation = AnchorNavVariant.bind({})
462+
463+
AnchorNavVariantKeyboardNavigation.play = async ({canvasElement}) => {
464+
const {getByRole, getAllByRole} = within(canvasElement)
465+
466+
await userEvent.tab()
467+
expect(getByRole('link', {name: 'Enterprise'})).toHaveFocus()
468+
469+
await userEvent.tab()
470+
expect(getByRole('link', {name: 'Overview'})).toHaveFocus()
471+
472+
await userEvent.tab()
473+
expect(getByRole('link', {name: 'Advanced Security'})).toHaveFocus()
474+
475+
await userEvent.tab()
476+
expect(getByRole('link', {name: 'Copilot Enterprise'})).toHaveFocus()
477+
478+
await userEvent.tab()
479+
expect(getByRole('link', {name: 'Premium Support'})).toHaveFocus()
480+
481+
await userEvent.tab()
482+
expect(getByRole('link', {name: 'Scale'})).toHaveFocus()
483+
484+
await userEvent.tab()
485+
expect(getByRole('link', {name: 'AI'})).toHaveFocus()
486+
487+
await userEvent.tab()
488+
expect(getByRole('link', {name: 'Security'})).toHaveFocus()
489+
490+
await userEvent.tab()
491+
expect(getByRole('link', {name: 'Reliability'})).toHaveFocus()
492+
493+
await userEvent.tab()
494+
expect(getAllByRole('button', {name: 'Learn more'})[0]).toHaveFocus()
495+
496+
await userEvent.tab({shift: true})
497+
expect(getByRole('link', {name: 'Reliability'})).toHaveFocus()
498+
499+
await userEvent.tab({shift: true})
500+
expect(getByRole('link', {name: 'Security'})).toHaveFocus()
501+
502+
await userEvent.tab({shift: true})
503+
expect(getByRole('link', {name: 'AI'})).toHaveFocus()
504+
505+
await userEvent.tab({shift: true})
506+
expect(getByRole('link', {name: 'Scale'})).toHaveFocus()
507+
508+
await userEvent.tab({shift: true})
509+
expect(getByRole('link', {name: 'Premium Support'})).toHaveFocus()
510+
}

packages/react/src/SubNav/SubNav.module.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
background-color var(--brand-animation-duration-default) var(--brand-animation-easing-default);
105105
}
106106

107+
.SubNav__anchor-menu-outer-container:focus-within .SubNav__anchor-menu-container,
107108
.SubNav__anchor-menu-outer-container--stuck .SubNav__anchor-menu-container {
108109
transform: translateY(0);
109110
opacity: 1;
@@ -115,19 +116,20 @@
115116
transition: height var(--brand-animation-duration-default) var(--brand-animation-easing-default);
116117
}
117118

119+
.SubNav__anchor-menu-outer-container:focus-within,
118120
.SubNav__anchor-menu-outer-container--stuck {
119121
height: auto;
120122
}
121123

122124
.SubNav__sub-menu--anchor {
123125
opacity: 0;
124-
visibility: hidden;
125126
transform: translateY(var(--brand-animation-variant-slideInDown-distance));
126127
transition: opacity var(--brand-animation-duration-default) var(--brand-animation-easing-default),
127128
height var(--brand-animation-duration-default) var(--brand-animation-easing-default),
128129
transform var(--brand-animation-duration-default) var(--brand-animation-easing-default);
129130
}
130131

132+
.SubNav__anchor-menu-outer-container:focus-within .SubNav__sub-menu--anchor,
131133
.SubNav__anchor-menu-outer-container--stuck .SubNav__sub-menu--anchor {
132134
visibility: visible;
133135
opacity: 1;

0 commit comments

Comments
 (0)