From 600b7b7ea20f9b4621e5b30ec74e4cf106cd6c5a Mon Sep 17 00:00:00 2001 From: huyplb Date: Sun, 1 Nov 2026 14:51:15 -0600 Subject: [PATCH 1/2] fix(web): announce the Safe-mode confirm and Saved Credentials as dialogs; Escape closes Saved Credentials From the 2026-11-01 manual walk: - The Safe-mode confirm (sql-write-confirm) and Saved Credentials were plain divs: a screen reader was not told a dialog had opened. Both now have role="dialog", aria-modal and aria-labelledby on their title. - Saved Credentials did not close on Escape, unlike the other dialogs, and its close button was an unnamed icon. It closes on Escape now, but not while the credential form is open on top, and the button is named. useEscapeKey (shared/lib) is the Escape listener WorkspaceSettingsDialog and others each write by hand, with an enabled flag for layered dialogs. Co-Authored-By: Claude Opus 5.5 --- .../components/CredentialManager.test.tsx | 48 +++++++++++++++++++ .../components/CredentialManager.tsx | 11 ++++- .../components/WriteConfirmDialog.test.tsx | 19 ++++++++ .../components/WriteConfirmDialog.tsx | 8 +++- .../src/frontend/shared/lib/useEscapeKey.ts | 24 ++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx create mode 100644 apps/web/src/frontend/features/sql-editor/components/WriteConfirmDialog.test.tsx create mode 100644 apps/web/src/frontend/shared/lib/useEscapeKey.ts diff --git a/apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx b/apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx new file mode 100644 index 000000000..c2a952b90 --- /dev/null +++ b/apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx @@ -0,0 +1,48 @@ +// @vitest-environment jsdom +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +// The editor store loads the theme store, which reads the colour scheme. +window.matchMedia ??= ((media: string) => ({ + matches: false, + media, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false, +})) as unknown as typeof window.matchMedia; + +// The credential form is its own layer; a stand-in is enough to open it. +vi.mock('./ConnectionModal', () => ({ + ConnectionModal: ({ open }: { open: boolean }) => (open ?
: null), +})); + +import { CredentialManager } from './CredentialManager'; + +describe('CredentialManager', () => { + it('is a modal dialog named Saved Credentials, with a named close button', () => { + render( undefined} />); + const dialog = screen.getByRole('dialog', { name: 'Saved Credentials' }); + expect(dialog.getAttribute('aria-modal')).toBe('true'); + expect(screen.getByRole('button', { name: 'Close saved credentials' })).toBeTruthy(); + }); + + it('closes on Escape', () => { + const onClose = vi.fn(); + render(); + fireEvent.keyDown(window, { key: 'Escape' }); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('leaves Escape to the credential form while it is open on top', () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: /Add Credential/ })); + expect(screen.getByTestId('connection-form')).toBeTruthy(); + fireEvent.keyDown(window, { key: 'Escape' }); + expect(onClose).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/frontend/features/connections/components/CredentialManager.tsx b/apps/web/src/frontend/features/connections/components/CredentialManager.tsx index a32f23176..7a300b5da 100644 --- a/apps/web/src/frontend/features/connections/components/CredentialManager.tsx +++ b/apps/web/src/frontend/features/connections/components/CredentialManager.tsx @@ -21,6 +21,7 @@ import { CloudProviderCredentialsSection } from '@/features/connections/componen import { PROVIDER_SETTINGS, type ConnectionOptions, type Dialect } from '@/shared/lib/provider-settings'; import { dialectLabel } from '@/shared/lib/dialectLabel'; import { sessionPasswordMap, setSessionPassword } from '@/shared/lib/sessionPasswords'; +import { useEscapeKey } from '@/shared/lib/useEscapeKey'; import type { SavedConnectionSummary } from '@/shared/api/authApi'; interface Props { @@ -157,6 +158,9 @@ export const CredentialManager: React.FC = ({ open, onClose }) => { return groups; }, [visible]); + // Escape closes the manager, but not while the credential form is open on top. + useEscapeKey(onClose, open && !adding && !editing); + if (!open) return null; return createPortal( @@ -165,6 +169,9 @@ export const CredentialManager: React.FC = ({ open, onClose }) => { className="fixed inset-0 z-[90] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4" >
e.stopPropagation()} > @@ -174,7 +181,7 @@ export const CredentialManager: React.FC = ({ open, onClose }) => {
-

Saved Credentials

+

Saved Credentials

{tab === 'database' ? 'Encrypted DB connections · reusable from the connection dropdowns' @@ -192,6 +199,8 @@ export const CredentialManager: React.FC = ({ open, onClose }) => { )}