fix(website): resolve accessibility violations in ported detail pages

Fixes the three axe violations surfaced by the a11y audit after the
Primer Brand redesign:

- aria-prohibited-attr: `aria-label` was set on roleless `div`s in
  InstructionDetail and PluginDetail. The "Applies to" list now uses
  list/listitem roles; plugin provenance uses a group role.
- scrollable-region-focusable: the install command `code` element
  overflows horizontally but was not keyboard reachable. Added
  tabIndex to all three render sites.
- color-contrast: the Playbook "New" label used the brand's
  success-fg on success-subtle, reaching only 4.09:1 in light mode.
  Stepped one down the same green ramp for 6.14:1. Scoped to light
  mode; dark mode already passed and its green-7 is near-black.

Also corrects the stale route list in the audit script: /hooks/,
/workflows/ and /tools/ have never existed as pages.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 80686fef-efe3-4cdd-8cd6-bfa61a5d0af6
This commit is contained in:
Aaron Powell
2026-08-14 17:46:36 +10:00
parent f29b88f27f
commit 49cdb7e010
6 changed files with 25 additions and 12 deletions
+1 -6
View File
@@ -28,21 +28,16 @@ const routes = [
'/agents/', '/agents/',
'/instructions/', '/instructions/',
'/skills/', '/skills/',
'/hooks/',
'/workflows/',
'/extensions/', '/extensions/',
'/plugins/', '/plugins/',
'/tools/',
'/contributors/', '/contributors/',
'/learning-hub/',
'/learning-hub/cookbook/', '/learning-hub/cookbook/',
'/learning-hub/github-copilot-app/',
// Representative dedicated detail pages (one per resource type) so the audit // Representative dedicated detail pages (one per resource type) so the audit
// covers the shared detail layout, sidebar, install buttons, and file browser. // covers the shared detail layout, sidebar, install buttons, and file browser.
'/agent/accessibility/', '/agent/accessibility/',
'/instruction/a11y/', '/instruction/a11y/',
'/skill/acquire-codebase-knowledge/', '/skill/acquire-codebase-knowledge/',
'/hook/dependency-license-checker/',
'/workflow/daily-issues-report/',
'/plugin/accessibility-kanban/', '/plugin/accessibility-kanban/',
'/extension/accessibility-kanban/', '/extension/accessibility-kanban/',
]; ];
@@ -74,7 +74,9 @@ function InstallCommand({ command }: { command: string }) {
return ( return (
<div className={appStyles.installBar}> <div className={appStyles.installBar}>
<code className={appStyles.installCommand}>{command}</code> <code className={appStyles.installCommand} tabIndex={0}>
{command}
</code>
<button <button
type="button" type="button"
className={appStyles.installCopy} className={appStyles.installCopy}
@@ -70,7 +70,9 @@ export function InstallCommandBar() {
))} ))}
</ActionMenu.Overlay> </ActionMenu.Overlay>
</ActionMenu> </ActionMenu>
<code className={styles.installCommand}>{selected.command}</code> <code className={styles.installCommand} tabIndex={0}>
{selected.command}
</code>
<button <button
type="button" type="button"
className={styles.installCopy} className={styles.installCopy}
@@ -125,9 +125,11 @@ export function InstructionDetail({
// are promoted into the hero rather than left in the sidebar. // are promoted into the hero rather than left in the sidebar.
const heroExtras = const heroExtras =
applyTo.length > 0 ? ( applyTo.length > 0 ? (
<div className={styles.metaValues} aria-label="Applies to"> <div className={styles.metaValues} role="list" aria-label="Applies to">
{applyTo.map((pattern) => ( {applyTo.map((pattern) => (
<Token key={pattern} text={pattern} /> <span key={pattern} role="listitem">
<Token text={pattern} />
</span>
))} ))}
</div> </div>
) : null; ) : null;
@@ -141,7 +141,9 @@ export function PluginDetail({
const install = ( const install = (
<> <>
<div className={installStyles.installBar}> <div className={installStyles.installBar}>
<code className={installStyles.installCommand}>{installCommand}</code> <code className={installStyles.installCommand} tabIndex={0}>
{installCommand}
</code>
<button <button
type="button" type="button"
className={installStyles.installCopy} className={installStyles.installCopy}
@@ -167,7 +169,7 @@ export function PluginDetail({
// Provenance sits in the hero: whether the plugin ships from this repository // Provenance sits in the hero: whether the plugin ships from this repository
// or from a third-party repo is the first thing a reader needs to know. // or from a third-party repo is the first thing a reader needs to know.
const heroExtras = ( const heroExtras = (
<div className={styles.metaValues} aria-label="Plugin provenance"> <div className={styles.metaValues} role="group" aria-label="Plugin provenance">
<Label color={item.external ? "purple" : "green"} size="medium"> <Label color={item.external ? "purple" : "green"} size="medium">
{item.external ? "External plugin" : "Built-in"} {item.external ? "External plugin" : "Built-in"}
</Label> </Label>
@@ -1049,6 +1049,16 @@
background-color: var(--brand-color-success-subtle); background-color: var(--brand-color-success-subtle);
} }
/*
* In light mode the brand's success-fg on success-subtle only reaches 4.09:1 at
* this text size. Step one down the same green ramp to clear WCAG AA (6.14:1)
* without leaving the brand palette. Dark mode already passes, and its green-7
* is near-black, so this is deliberately light-mode only.
*/
.page[data-mode="light"] .recCardLabelAccent {
color: var(--base-color-scale-green-7);
}
/* /*
* Secondary label variant: stroke only, no fill. Pairs with the neutral filled * Secondary label variant: stroke only, no fill. Pairs with the neutral filled
* .recCardLabel so the two tags on a card read as distinct (per brand cards). * .recCardLabel so the two tags on a card read as distinct (per brand cards).