-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[Navigation] Add aria attributes for sub nav items #3661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ import {Indicator} from '../../../Indicator'; | |
| import {UnstyledLink} from '../../../UnstyledLink'; | ||
| import {useI18n} from '../../../../utilities/i18n'; | ||
| import {useMediaQuery} from '../../../../utilities/media-query'; | ||
| import {useUniqueId} from '../../../../utilities/unique-id'; | ||
| import styles from '../../Navigation.scss'; | ||
|
|
||
| import {Secondary} from './components'; | ||
|
|
@@ -83,6 +84,7 @@ export function Item({ | |
| }: ItemProps) { | ||
| const i18n = useI18n(); | ||
| const {isNavigationCollapsed} = useMediaQuery(); | ||
| const secondaryNavigationId = useUniqueId('SecondaryNavigation'); | ||
| const {location, onNavigationDismiss} = useContext(NavigationContext); | ||
| const [expanded, setExpanded] = useState(false); | ||
| const [keyFocused, setKeyFocused] = useState(false); | ||
|
|
@@ -230,19 +232,20 @@ export function Item({ | |
|
|
||
| let secondaryNavigationMarkup: ReactNode = null; | ||
|
|
||
| if (subNavigationItems.length > 0 && showExpanded) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the scariest thing for me in this PR. This makes sub menus and their items render in the DOM, though hidden. I did this so that the aria-controls id will have a valid reference ID but not sure that's super necessary to see until the sub menu has actually rendered. This broke the Nav in web the last time I tried this, for adding animations but Alex's fixes to Collapsible has fixed that. Anyone see any potential issues in doing this?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting! I'm not sure about aria-control, but certain screen readers require elements to always be in the DOM for certain attributes (e.g aria-live)
As long as the |
||
| if (subNavigationItems.length > 0) { | ||
| const longestMatch = matchingSubNavigationItems.sort( | ||
| ({url: firstUrl}, {url: secondUrl}) => secondUrl.length - firstUrl.length, | ||
| )[0]; | ||
|
|
||
| const SecondaryNavigationClassName = classNames( | ||
| styles.SecondaryNavigation, | ||
| showExpanded && styles.isExpanded, | ||
| !icon && styles['SecondaryNavigation-noIcon'], | ||
| ); | ||
|
|
||
| secondaryNavigationMarkup = ( | ||
| <div className={SecondaryNavigationClassName}> | ||
| <Secondary expanded={showExpanded}> | ||
| <Secondary expanded={showExpanded} id={secondaryNavigationId}> | ||
| {subNavigationItems.map((item) => { | ||
| const {label, ...rest} = item; | ||
| return ( | ||
|
|
@@ -277,6 +280,11 @@ export function Item({ | |
| onClick={getClickHandler(onClick)} | ||
| onKeyUp={handleKeyUp} | ||
| onBlur={handleBlur} | ||
| {...normalizeAriaAttributes( | ||
| secondaryNavigationId, | ||
| subNavigationItems.length > 0, | ||
| showExpanded, | ||
| )} | ||
| > | ||
| {itemContentMarkup} | ||
| </UnstyledLink> | ||
|
|
@@ -386,3 +394,16 @@ function matchStateForItem( | |
| : safeStartsWith(location, url); | ||
| return matchesUrl ? MatchState.MatchUrl : MatchState.NoMatch; | ||
| } | ||
|
|
||
| function normalizeAriaAttributes( | ||
| controlId: string, | ||
| hasSubMenu: boolean, | ||
| expanded: boolean, | ||
| ) { | ||
| return hasSubMenu | ||
| ? { | ||
| 'aria-expanded': expanded, | ||
| 'aria-controls': controlId, | ||
| } | ||
| : undefined; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,12 +7,17 @@ import styles from '../../../../Navigation.scss'; | |
| interface SecondaryProps { | ||
| expanded: boolean; | ||
| children?: React.ReactNode; | ||
| id?: string; | ||
| } | ||
|
|
||
| export function Secondary({children, expanded}: SecondaryProps) { | ||
| const id = useUniqueId('SecondaryNavigation'); | ||
| export function Secondary({id, children, expanded}: SecondaryProps) { | ||
| const uid = useUniqueId('SecondaryNavigation'); | ||
| return ( | ||
| <Collapsible id={id} open={expanded}> | ||
| <Collapsible | ||
| id={id || uid} | ||
| open={expanded} | ||
| transition={{duration: '0ms', timingFunction: 'linear'}} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL we use collapsible in navigation - why do we need these props now? I don't recall there being an animation before 🤔
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Collapsible defaults to an animation so I'm cancelling that here with |
||
| > | ||
| <ul className={styles.List}>{children}</ul> | ||
| </Collapsible> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,43 @@ | ||
| import React from 'react'; | ||
| // eslint-disable-next-line no-restricted-imports | ||
| import {mountWithAppProvider} from 'test-utilities/legacy'; | ||
| import {mountWithApp} from 'test-utilities'; | ||
|
|
||
| import {Collapsible} from '../../../../../../Collapsible'; | ||
| import {Secondary} from '../Secondary'; | ||
|
|
||
| describe('Secondary()', () => { | ||
| it('mounts', () => { | ||
| const secondary = mountWithAppProvider(<Secondary expanded />); | ||
| expect(secondary.exists()).toBe(true); | ||
| it('passes a default id to Collapsible', () => { | ||
| const component = mountWithApp(<Secondary expanded />); | ||
| expect(component).toContainReactComponent(Collapsible, { | ||
| id: 'PolarisSecondaryNavigation1', | ||
| }); | ||
| }); | ||
|
|
||
| it('passes a custom id to Collapsible when provided', () => { | ||
| const component = mountWithApp( | ||
| <Secondary expanded id="CustomSecondaryId" />, | ||
| ); | ||
| expect(component).toContainReactComponent(Collapsible, { | ||
| id: 'CustomSecondaryId', | ||
| }); | ||
| }); | ||
|
|
||
| it('adds custom transition props to Collapsible', () => { | ||
| const component = mountWithApp(<Secondary expanded />); | ||
| expect(component).toContainReactComponent(Collapsible, { | ||
| transition: { | ||
| duration: expect.any(String), | ||
| timingFunction: expect.any(String), | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('passes expanded to Collapsible', () => { | ||
| const component = mountWithApp(<Secondary expanded />); | ||
| expect(component).toContainReactComponent(Collapsible, {open: true}); | ||
| }); | ||
|
|
||
| it('renders an unorders list for its children', () => { | ||
| const component = mountWithApp(<Secondary expanded />); | ||
| expect(component).toContainReactComponent('ul'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!