Fix M15: replace role=button divs with native button elements in LibraryView

Both interactive headers in LibraryView used role="button" on divs, which is
invalid when they contain child interactive elements (group header had nested
All/None and Download buttons).

- Group header (groupHead): split into an outer flex div + a native <button>
  (groupToggle style) for the toggle/expand action + sibling Fluent Buttons
  for All/None and Download. The toggle button gets aria-expanded and a
  composite aria-label (title + item count). e.stopPropagation() removed
  from the sibling buttons since they are no longer inside the toggle.

- Source card header (cardHead): converted from a role="button" div to a
  native <button> with width:100%, border:none, and background:transparent.
  aria-label set to source.title. tabIndex/onKeyDown removed (native button
  handles keyboard by default).

typecheck + 242 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:37:33 -04:00
parent 2f59a6ce63
commit 53aaed5fef
2 changed files with 39 additions and 27 deletions
+1 -1
View File
@@ -263,7 +263,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [ ] **M14 — Settings search mutates React-owned DOM.** The search toggles each card's
`el.style.display` directly; fragile (breaks if a card ever gets a conditional `style`) and
matches on `textContent` incl. hidden text. Prefer state-driven filtering.
- [ ] **M15 — Nested interactive controls in `role="button"` (a11y).** LibraryView's group header
- [x] **M15 — Nested interactive controls in `role="button"` (a11y).** LibraryView's group header
is a `role="button"` div containing `<Button>`s (All / Download) — invalid ARIA / keyboard
semantics. Make the header a real element with sibling buttons.
- [x] **M16 — No renderer error boundary.** An exception in any view unmounts the whole shell to
+38 -26
View File
@@ -124,6 +124,11 @@ const useStyles = makeStyles({
alignItems: 'center',
gap: '12px',
padding: '12px 14px',
width: '100%',
border: 'none',
backgroundColor: 'transparent',
fontFamily: tokens.fontFamilyBase,
textAlign: 'left',
cursor: 'pointer',
':hover': { backgroundColor: tokens.colorNeutralBackground1Hover }
},
@@ -164,9 +169,22 @@ const useStyles = makeStyles({
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 4px',
padding: '2px 4px'
},
groupToggle: {
display: 'flex',
alignItems: 'center',
gap: '8px',
flexGrow: 1,
minWidth: 0,
padding: '4px 4px',
border: 'none',
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground2,
cursor: 'pointer',
fontFamily: tokens.fontFamilyBase,
fontSize: tokens.fontSizeBase300,
textAlign: 'left',
...shorthands.borderRadius(tokens.borderRadiusMedium),
':hover': { backgroundColor: tokens.colorNeutralBackground1Hover }
},
@@ -413,25 +431,23 @@ export function LibraryView(): React.JSX.Element {
const groupActionable = groupActionableItems.length
const allOn = groupActionable > 0 && groupActionableItems.every((it) => selected.has(it.id))
return (
<div
className={mergeClasses(styles.groupHead, focus.focusRing)}
onClick={() => toggleGroupExpand(row.title)}
role="button"
tabIndex={0}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && toggleGroupExpand(row.title)}
aria-expanded={open}
>
{open ? <ChevronDownRegular /> : <ChevronRightRegular />}
<AppsListRegular />
<span className={styles.groupTitle}>{row.title}</span>
<Caption1 className={styles.srcSub}>{row.items.length}</Caption1>
<div className={styles.groupHead}>
<button
type="button"
className={mergeClasses(styles.groupToggle, focus.focusRing)}
onClick={() => toggleGroupExpand(row.title)}
aria-expanded={open}
aria-label={`${row.title}, ${row.items.length} items`}
>
{open ? <ChevronDownRegular /> : <ChevronRightRegular />}
<AppsListRegular />
<span className={styles.groupTitle}>{row.title}</span>
<Caption1 className={styles.srcSub}>{row.items.length}</Caption1>
</button>
<Button
size="small"
appearance="subtle"
onClick={(e) => {
e.stopPropagation()
toggleGroup(row.items, !allOn)
}}
onClick={() => toggleGroup(row.items, !allOn)}
>
{allOn ? 'None' : 'All'}
</Button>
@@ -440,10 +456,7 @@ export function LibraryView(): React.JSX.Element {
appearance="subtle"
icon={<ArrowDownloadRegular />}
disabled={groupActionable === 0}
onClick={(e) => {
e.stopPropagation()
downloadGroup(row.items)
}}
onClick={() => downloadGroup(row.items)}
>
Download{groupActionable > 0 ? ` ${groupActionable}` : ''}
</Button>
@@ -729,13 +742,12 @@ function SourceCard({
const focus = useFocusStyles()
return (
<div className={styles.card}>
<div
<button
type="button"
className={mergeClasses(styles.cardHead, focus.focusRing)}
onClick={onToggleExpand}
role="button"
tabIndex={0}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && onToggleExpand()}
aria-expanded={expanded}
aria-label={source.title}
>
{expanded ? <ChevronDownRegular /> : <ChevronRightRegular />}
<div className={styles.srcIcon}>
@@ -755,7 +767,7 @@ function SourceCard({
{source.channel && source.channel !== source.title ? ` · ${source.channel}` : ''}
</Caption1>
</div>
</div>
</button>
{expanded && children}
</div>
)