Skip to content

Commit 4b0a834

Browse files
docs-botCopilotheiskr
authored
fix: address HCL a11y audit Sev-2 issues — focus order, label, shortcuts, live region (June 2026) (#61834)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com>
1 parent 6ad0f8b commit 4b0a834

6 files changed

Lines changed: 34 additions & 1 deletion

File tree

data/ui.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ product_landing:
324324
article_grid:
325325
heading: Articles
326326
all_categories: All categories
327+
filter_by_category: Category
327328
search_articles: Search articles
328329
no_articles_found: No articles found matching your criteria.
329330
showing_results: Showing {start}-{end} of {total}

src/fixtures/fixtures/data/ui.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ product_landing:
324324
article_grid:
325325
heading: Articles
326326
all_categories: All categories
327+
filter_by_category: Category
327328
search_articles: Search articles
328329
no_articles_found: No articles found matching your criteria.
329330
showing_results: Showing {start}-{end} of {total}

src/landings/components/shared/LandingArticleGridWithFilter.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export const ArticleGrid = ({
266266
<div className={styles.categoryDropdown}>
267267
<ActionMenu>
268268
<ActionMenu.Button>
269+
{t('article_grid.filter_by_category')}:{' '}
269270
{categories[selectedCategoryIndex] === ALL_CATEGORIES
270271
? t('article_grid.all_categories')
271272
: categories[selectedCategoryIndex]}

src/links/components/LinkPreviewPopover.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,9 @@ export function LinkPreviewPopover() {
555555
link.addEventListener('mouseover', showPopover)
556556
link.addEventListener('mouseout', hidePopover)
557557
link.addEventListener('keydown', keyboardHandler)
558+
// Expose the keyboard shortcut to assistive technologies so screen
559+
// reader users can discover how to open the link preview hovercard.
560+
link.setAttribute('aria-keyshortcuts', 'Alt+ArrowUp')
558561
}
559562

560563
document.addEventListener('keydown', escapeHandler)
@@ -564,6 +567,7 @@ export function LinkPreviewPopover() {
564567
link.removeEventListener('mouseover', showPopover)
565568
link.removeEventListener('mouseout', hidePopover)
566569
link.removeEventListener('keydown', keyboardHandler)
570+
link.removeAttribute('aria-keyshortcuts')
567571
}
568572
document.removeEventListener('keydown', escapeHandler)
569573
}

src/tools/components/InArticlePicker.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useLayoutEffect, useState } from 'react'
1+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
22
import Cookies from '@/frame/components/lib/cookies'
33
import { UnderlineNav } from '@primer/react'
44
import { sendEvent } from '@/events/components/events'
@@ -39,6 +39,11 @@ export const InArticlePicker = ({
3939
const { query, locale } = router
4040
const [currentValue, setCurrentValue] = useState('')
4141

42+
// Tracks whether the last currentValue change was triggered by a user click
43+
// (as opposed to initial mount or external navigation). When true, we move
44+
// focus to the newly-selected tab so keyboard users don't lose their place.
45+
const focusAfterNavRef = useRef(false)
46+
4247
// Run on mount for client-side only features
4348
useEffect(() => {
4449
const raw = query[queryStringKey]
@@ -128,6 +133,7 @@ export const InArticlePicker = ({
128133
}, [currentValue])
129134

130135
function onClickChoice(value: string) {
136+
focusAfterNavRef.current = true
131137
const params = new URLSearchParams(asPathQuery)
132138
params.set(queryStringKey, value)
133139
const newPath = `/${locale}${asPathRoot}?${params}`
@@ -142,6 +148,21 @@ export const InArticlePicker = ({
142148
Cookies.set(cookieKey, value)
143149
}
144150

151+
// After a user clicks a tab, the shallow route change updates `currentValue`.
152+
// Once the DOM reflects the new selection (aria-current="page" is on the new
153+
// tab), move keyboard focus there so the user's context is preserved.
154+
// WCAG 2.4.3 Focus Order — focus must land on the triggered control.
155+
useEffect(() => {
156+
if (!focusAfterNavRef.current || !currentValue) return
157+
focusAfterNavRef.current = false
158+
159+
const container = document.querySelector<HTMLElement>(
160+
`[data-testid="${queryStringKey}-picker"]`,
161+
)
162+
const selectedTab = container?.querySelector<HTMLElement>('[aria-current="page"]')
163+
selectedTab?.focus()
164+
}, [currentValue, queryStringKey])
165+
145166
const sharedContainerProps = {
146167
'aria-label': ariaLabel,
147168
}

src/webhooks/components/Webhook.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import useSWR from 'swr'
44
import { useRouter } from 'next/router'
55
import { slug } from 'github-slugger'
66
import cx from 'classnames'
7+
import { announce } from '@primer/live-region-element'
78

89
import { useVersion } from '@/versions/components/useVersion'
910
import { HeadingLink } from '@/frame/components/article/HeadingLink'
@@ -85,6 +86,10 @@ export function Webhook({ webhook }: Props) {
8586
setSelectedWebhookActionType(type)
8687
setSelectedActionTypeIndex(index)
8788

89+
// Announce the newly selected action type to screen readers so users
90+
// relying on AT know the page content has changed.
91+
announce(`${t('action_type')}: ${type}`, { politeness: 'polite' })
92+
8893
const { asPath, locale } = router
8994
let [pathRoot, pathQuery = ''] = asPath.split('?')
9095
const params = new URLSearchParams(pathQuery)

0 commit comments

Comments
 (0)