fix(a11y): my #2965 gave six dialogs two elements with the same accessible name (#2977)

**This fixes a regression I introduced in #2965, found by re-running the
full dashboard lane on `main` rather than trusting the targeted runs I
did at the time.**

`AddNodeModal` and `ConnectNodeModal` are red on main:

```
→ Found multiple elements with the text of: Add Node
→ Found multiple elements with the text of: Connect to Node
```

### Cause

#2965 dropped the redundant `" dialog"` suffix from each
`FloatingWindow`'s `ariaLabel`. That was correct — `role="dialog"`
already conveys it. What I missed is that six of those modals **also**
put an `aria-label` with the *same* text on their own inner `<div>`:

```jsx
<FloatingWindow ariaLabel={t("nodes.addNode", "Add Node")} …>
  <div className="modal modal-md add-node-modal" aria-label={t("nodes.addNode", "Add Node")}>
```

Before #2965 the two differed (`"Add Node dialog"` vs `"Add Node"`), so
`getByLabelText("Add Node")` matched exactly one element. Now both
match.

### Why the inner one goes, not the dialog's

Those inner labels sit on **role-less `<div>`s**, where assistive
technology ignores `aria-label` entirely — it was never conveying
anything to anyone. Removing it restores a single accessible name per
dialog and needs no test changes.

### Surface enumeration — four of the six were latent

Only two surfaced as failures; the other four have no test querying by
that name, so they would have shipped a duplicate accessible name
silently. Found by scanning every component for an inner `aria-label`
whose expression matches its own `ariaLabel` prop:

| modal | was it red? |
|---|---|
| `AddNodeModal` | red on main |
| `ConnectNodeModal` | red on main |
| `GroupTaskModal` | latent |
| `NodeDetailModal` | latent |
| `ScriptsModal` | latent |
| `WorkflowAddStepModal` | latent |

### Five more, deliberately untouched

`AgentDetailView`, `PlanningModeModal`, `SettingsModal`
(`role="region"`), `ScheduledTasksModal` (`role="listbox"`) and
`NewTaskModal` (`role="dialog"`) also carry their dialog's name on an
inner element — but those elements **have a role**, so the label is
meaningful rather than dead markup. A listbox named "Automations" inside
a dialog named "Automations" is redundant, not broken, and renaming it
is a UX decision rather than a cleanup. Left alone and recorded here.

**Verified:** 93/93 across `AddNodeModal`, `ConnectNodeModal`,
`NodesView`, `GroupTaskModal`, `ScriptsModal` and the #2965 aria guard;
`tsc -p tsconfig.app.json` 0 errors; lint clean; FNXC gate exit 0.

Product-code change to a11y markup, so this is user-visible but needs no
operator-facing note — say the word if you want a changeset.

**Measured dashboard-lane state on main before this PR:** `3 failed |
11173 passed`. Two are these; the third is
`MainContent.planning-project-remount`, which belongs to #2420 and is
detailed there.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-30 22:46:08 -07:00
committed by GitHub
parent eb9cd431c1
commit 7fde4bb3ad
6 changed files with 5 additions and 9 deletions

View File

@@ -258,7 +258,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, onDiscoverRemoteProjec
return (
/* FNXC:ModalTouchGeometry 2026-07-26-13:15: Shared FloatingWindow owns this modal's touch drag, resize, clamping, and persistence while phone and short viewports retain their sheet behavior. */
<FloatingWindow windowKey="add-node" title={t("nodes.addNode", "Add Node")} ariaLabel={t("nodes.addNode", "Add Node")} onClose={closeModal} hideHeader dragHandleSelector=".modal-header" className="floating-window--add-node" defaultSize={{ width: 720, height: 560 }} minSize={{ width: 360, height: 280 }} persistGeometryKey="floating-window:add-node" suspendGeometryPersistenceOnMobile suspendGeometryPersistenceOnShortViewport closeOnOutsidePointerDown>
<div className="modal modal-md add-node-modal" aria-label={t("nodes.addNode", "Add Node")}>
<div className="modal modal-md add-node-modal">
<div className="modal-header">
<h3>{t("nodes.addNode", "Add Node")}</h3>
<button className="modal-close" onClick={closeModal} disabled={isSubmitting} aria-label={t("nodes.closeNodeModal", "Close add node modal")}>

View File

@@ -169,10 +169,7 @@ export function ConnectNodeModal({ open, onClose, onConnected, addToast, onSubmi
return (
/* FNXC:ModalTouchGeometry 2026-07-26-13:15: Shared FloatingWindow owns this modal's touch drag, resize, clamping, and persistence while phone and short viewports retain their sheet behavior. */
<FloatingWindow windowKey="connect-node" title={t("nodes.modal.title", "Connect to Node")} ariaLabel={t("nodes.modal.title", "Connect to Node")} onClose={onClose} hideHeader dragHandleSelector=".modal-header" className="floating-window--connect-node" defaultSize={{ width: 720, height: 560 }} minSize={{ width: 360, height: 280 }} persistGeometryKey="floating-window:connect-node" suspendGeometryPersistenceOnMobile suspendGeometryPersistenceOnShortViewport closeOnOutsidePointerDown>
<div
className="modal modal-md connect-node-modal"
aria-label={t("nodes.modal.title", "Connect to Node")}
>
<div className="modal modal-md connect-node-modal">
<div className="modal-header">
<h3>{t("nodes.modal.title", "Connect to Node")}</h3>
<button className="modal-close" onClick={onClose} disabled={isSubmitting} aria-label={t("nodes.modal.closeButton", "Close connect node modal")}>

View File

@@ -105,7 +105,7 @@ export function GroupTaskModal({ isOpen, onClose, groupId, projectId, onOpenMemb
return (
/* FNXC:ModalTouchGeometry 2026-07-26-13:15: Shared FloatingWindow owns this modal's touch drag, resize, clamping, and persistence while phone and short viewports retain their sheet behavior. */
<FloatingWindow windowKey="group-task" title={t("groupTask.title", "Branch Group")} ariaLabel={t("groupTask.ariaLabel", "Branch group details")} onClose={onClose} hideHeader dragHandleSelector=".modal-header" className="floating-window--group-task" defaultSize={{ width: 720, height: 560 }} minSize={{ width: 360, height: 280 }} persistGeometryKey="floating-window:group-task" suspendGeometryPersistenceOnMobile suspendGeometryPersistenceOnShortViewport closeOnOutsidePointerDown>
<div className="modal modal-lg group-task-modal" aria-label={t("groupTask.ariaLabel", "Branch group details")}>
<div className="modal modal-lg group-task-modal">
<div className="modal-header">
<h2>{t("groupTask.title", "Branch Group {{id}}", { id: groupId })}</h2>
<button type="button" className="modal-close" onClick={onClose} aria-label={t("actions.closeModal", "Close modal")}>

View File

@@ -442,7 +442,6 @@ export function NodeDetailModal({
<FloatingWindow windowKey="node-detail" title={t("nodes.modalTitle", "Node Details")} ariaLabel={t("nodes.modalAriaLabel", "Node details for {{name}}", { name: node.name })} onClose={onClose} hideHeader dragHandleSelector=".modal-header" className="floating-window--node-detail" defaultSize={{ width: 720, height: 560 }} minSize={{ width: 360, height: 280 }} persistGeometryKey="floating-window:node-detail" suspendGeometryPersistenceOnMobile suspendGeometryPersistenceOnShortViewport closeOnOutsidePointerDown>
<div
className="modal modal-lg node-detail-modal"
aria-label={t("nodes.modalAriaLabel", "Node details for {{name}}", { name: node.name })}
>
<div className="modal-header">
<h3>{t("nodes.modalTitle", "Node Details")}</h3>

View File

@@ -194,7 +194,7 @@ export function ScriptsModal({ isOpen, onClose, addToast, projectId, onRunScript
/* FNXC:ModalTouchGeometry 2026-07-26-16:10: Preserve Scripts' globally default-off backdrop preference while the shared window keeps drag gestures from being mistaken for outside dismissals. */
closeOnOutsidePointerDown={dismissOnOutsidePointerDown}
>
<div className="modal scripts-modal" aria-label={t("scripts.title", "Scripts")}>
<div className="modal scripts-modal">
{/* Header */}
<div className="modal-header">
<h2>

View File

@@ -156,7 +156,7 @@ export function WorkflowAddStepModal({
suspendGeometryPersistenceOnShortViewport
closeOnOutsidePointerDown
>
<div className="wf-add-step-dialog" aria-label={t("workflowNodes.addStepTitle", "Add a step")}>
<div className="wf-add-step-dialog">
<header className="wf-add-step-header">
<h3>{t("workflowNodes.addStepTitle", "Add a step")}</h3>
<button