**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:
@@ -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")}>
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user