Skip to content

Make dialogs programmatically modal and operable from the keyboard #171

Description

@atomantic

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions