Problem
Several application dialogs are rendered as ordinary fixed-position containers. They do not expose a dialog role, modal state, labelled title, focus containment, or a reliable Escape-key close path. As a result, assistive technology can continue navigating the obscured page and keyboard users can tab out of a sensitive credential dialog or relationship workflow.
Evidence (quoted code + context)
CredentialsModal renders "<div className=\"fixed inset-0 z-50 flex items-center justify-center\">" and its modal body is "<div className=\"relative w-full max-w-md ...\">" (lines 80-88), with no role, aria-modal, title id, Escape listener, or focus management. LinkPlatformDialog likewise adds onKeyDown={handleKeyDown} to a non-focusable outer <div> (lines 78-84); its Escape handler therefore does not receive keystrokes while the focused URL <input> is inside it. The relationship dialogs use the same plain overlay/body structure at LinkRelationshipDialog.tsx:110-115 and RelationshipModal.tsx:148-153.
Impact
Modal workflows for stored credentials and family relationship edits are not reliably usable with screen readers or keyboard-only input. Focus can reach and activate controls behind the overlay, and closing a modal can leave focus at an unpredictable position.
Implementation plan
Introduce one reusable accessible-dialog primitive (or consistently adopt the project's existing equivalent) that: applies role=\"dialog\", aria-modal=\"true\", and aria-labelledby; focuses an intentional initial control; traps Tab/Shift+Tab within the dialog; closes on Escape when the operation is cancellable; and restores focus to the invoking control. Convert the four dialogs to it, preserve their in-flight-operation close guards, give icon-only close and password-visibility buttons accessible names, and attach labels with htmlFor/id (or aria-label) to each form control.
Acceptance criteria
Each listed overlay has one labelled modal dialog in the accessibility tree; background controls cannot receive tab focus while it is open; Escape closes only closable dialogs; focus returns to the launch control on close; and every input/icon control has an accessible name.
Verification
Add React Testing Library tests that open each dialog, assert getByRole('dialog', { name: ... }), exercise Tab/Shift+Tab containment, dispatch Escape, and assert focus restoration. Run npm test --prefix client -- --run for those tests and npm run build --prefix client. Add/extend the relevant Playwright flow to open each dialog and keyboard-navigate it.
Dependencies and related work
No PostgreSQL migration work. Coordinate the primitive with any existing shared UI conventions before converting callers.
Scope (Complexity, files, non-goals)
Medium; the four dialog components plus focused tests and possibly one shared UI helper. Non-goals: redesigning dialog content or changing credential storage.
Problem
Several application dialogs are rendered as ordinary fixed-position containers. They do not expose a dialog role, modal state, labelled title, focus containment, or a reliable Escape-key close path. As a result, assistive technology can continue navigating the obscured page and keyboard users can tab out of a sensitive credential dialog or relationship workflow.
Evidence (quoted code + context)
CredentialsModalrenders"<div className=\"fixed inset-0 z-50 flex items-center justify-center\">"and its modal body is"<div className=\"relative w-full max-w-md ...\">"(lines 80-88), with norole,aria-modal, title id, Escape listener, or focus management.LinkPlatformDialoglikewise addsonKeyDown={handleKeyDown}to a non-focusable outer<div>(lines 78-84); its Escape handler therefore does not receive keystrokes while the focused URL<input>is inside it. The relationship dialogs use the same plain overlay/body structure atLinkRelationshipDialog.tsx:110-115andRelationshipModal.tsx:148-153.Impact
Modal workflows for stored credentials and family relationship edits are not reliably usable with screen readers or keyboard-only input. Focus can reach and activate controls behind the overlay, and closing a modal can leave focus at an unpredictable position.
Implementation plan
Introduce one reusable accessible-dialog primitive (or consistently adopt the project's existing equivalent) that: applies
role=\"dialog\",aria-modal=\"true\", andaria-labelledby; focuses an intentional initial control; traps Tab/Shift+Tab within the dialog; closes on Escape when the operation is cancellable; and restores focus to the invoking control. Convert the four dialogs to it, preserve their in-flight-operation close guards, give icon-only close and password-visibility buttons accessible names, and attach labels withhtmlFor/id(oraria-label) to each form control.Acceptance criteria
Each listed overlay has one labelled modal dialog in the accessibility tree; background controls cannot receive tab focus while it is open; Escape closes only closable dialogs; focus returns to the launch control on close; and every input/icon control has an accessible name.
Verification
Add React Testing Library tests that open each dialog, assert
getByRole('dialog', { name: ... }), exercise Tab/Shift+Tab containment, dispatch Escape, and assert focus restoration. Runnpm test --prefix client -- --runfor those tests andnpm run build --prefix client. Add/extend the relevant Playwright flow to open each dialog and keyboard-navigate it.Dependencies and related work
No PostgreSQL migration work. Coordinate the primitive with any existing shared UI conventions before converting callers.
Scope (Complexity, files, non-goals)
Medium; the four dialog components plus focused tests and possibly one shared UI helper. Non-goals: redesigning dialog content or changing credential storage.