Skip to content

Commit 9034c8e

Browse files
alicodingclaude
andcommitted
fix: each extensions section is its own labeled list -- valid ARIA both ways
Primer's ActionList.Group is structurally unusable here: with list semantics its <li role="presentation"> wrapper hoists the heading and inner <ul role="group"> into the outer list in the accessibility tree (aria-required-children); without list semantics every Item renders as a <button>, nesting the row's own switch inside an interactive (nested-interactive). A section as its own h3-labeled ActionList keeps the same rendered chrome and passes the WCAG gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
1 parent 4488806 commit 9034c8e

1 file changed

Lines changed: 30 additions & 22 deletions

File tree

frontend/src/views/ExtensionsSection.tsx

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,34 +97,42 @@ export default function ExtensionsSection() {
9797
{t(allOff ? 'settings.extensions.turnAllOn' : 'settings.extensions.turnAllOff')}
9898
</Button>
9999
</Stack>
100-
<ActionList role="list" showDividers data-testid="extensions-list">
100+
{/* One ActionList PER SECTION rather than ActionList.Group:
101+
with list semantics Primer's Group renders its own <li
102+
role="presentation"> wrapper, hoisting the heading and inner
103+
<ul role="group"> into the outer list in the accessibility
104+
tree (aria-required-children, WCAG gate); WITHOUT list
105+
semantics every Item renders as a <button>, nesting the
106+
row's own switch inside an interactive (nested-interactive).
107+
A section as its own h3-labeled list is valid both ways and
108+
keeps the same rendered chrome. h3 nests under the page's
109+
own h2 section headings (SettingsView.tsx). */}
110+
<Stack direction="vertical" gap="condensed" data-testid="extensions-list">
101111
{SECTION_ORDER.map((group) => {
102112
const rows = EXTENSION_ROWS.filter((row) => row.group === group)
103113
if (rows.length === 0) return null
104114
return (
105-
<ActionList.Group key={group} data-testid={`extensions-group-${group}`}>
106-
{/* as="h3" is REQUIRED here -- Primer's own GroupHeading
107-
throws ("requires a heading level") without it once the
108-
parent ActionList carries role="list" (this one does,
109-
for the assistive-tech list semantics every other row
110-
in Settings already gets). h3 nests correctly under the
111-
page's own h2 section headings (SettingsView.tsx). */}
112-
<ActionList.GroupHeading as="h3" variant="subtle">{groupSectionLabel(group)}</ActionList.GroupHeading>
113-
{rows.map((row) => (
114-
<ActionList.Item key={row.id}>
115-
<ExtensionRow
116-
row={row}
117-
builtIn={row.id === CARD_TOOL_ID}
118-
enabled={!disabledIds.includes(row.id)}
119-
appVersion={appVersion}
120-
onToggle={(enabled) => toggle(row.id, enabled)}
121-
/>
122-
</ActionList.Item>
123-
))}
124-
</ActionList.Group>
115+
<Stack direction="vertical" gap="none" key={group} data-testid={`extensions-group-${group}`}>
116+
<Text as="h3" size="small" weight="semibold" className={styles.muted}>
117+
{groupSectionLabel(group)}
118+
</Text>
119+
<ActionList role="list" showDividers aria-label={groupSectionLabel(group)}>
120+
{rows.map((row) => (
121+
<ActionList.Item key={row.id}>
122+
<ExtensionRow
123+
row={row}
124+
builtIn={row.id === CARD_TOOL_ID}
125+
enabled={!disabledIds.includes(row.id)}
126+
appVersion={appVersion}
127+
onToggle={(enabled) => toggle(row.id, enabled)}
128+
/>
129+
</ActionList.Item>
130+
))}
131+
</ActionList>
132+
</Stack>
125133
)
126134
})}
127-
</ActionList>
135+
</Stack>
128136
</Stack>
129137
)
130138
}

0 commit comments

Comments
 (0)