diff --git a/studio/frontend/src/features/settings/settings-dialog.tsx b/studio/frontend/src/features/settings/settings-dialog.tsx index e52257735e..384e359f31 100644 --- a/studio/frontend/src/features/settings/settings-dialog.tsx +++ b/studio/frontend/src/features/settings/settings-dialog.tsx @@ -74,6 +74,7 @@ export function SettingsDialog() { const activeTab = useSettingsDialogStore((s) => s.activeTab); const setActiveTab = useSettingsDialogStore((s) => s.setActiveTab); const closeDialog = useSettingsDialogStore((s) => s.closeDialog); + const opener = useSettingsDialogStore((s) => s.opener); const reduced = useReducedMotion(); const tabButtonRefs = useRef>({ general: null, @@ -98,6 +99,15 @@ export function SettingsDialog() { { + // Restore focus to the element that triggered openDialog(). + // Radix's FocusScope races our rAF-scheduled tab-button focus + // and loses the previous-focus reference, so we restore by hand. + if (opener && opener.isConnected) { + e.preventDefault(); + opener.focus({ preventScroll: true }); + } + }} className={cn( "!max-w-none h-[560px] w-[820px] p-0 overflow-hidden", "shadow-border rounded-xl border-border", diff --git a/studio/frontend/src/features/settings/stores/settings-dialog-store.ts b/studio/frontend/src/features/settings/stores/settings-dialog-store.ts index a2e32f26a3..7ac5422ff1 100644 --- a/studio/frontend/src/features/settings/stores/settings-dialog-store.ts +++ b/studio/frontend/src/features/settings/stores/settings-dialog-store.ts @@ -15,6 +15,11 @@ export type SettingsTab = interface SettingsDialogState { open: boolean; activeTab: SettingsTab; + // Element focused at the moment openDialog() ran. Radix's FocusScope + // would normally track this, but the rAF-scheduled focus() in + // settings-dialog.tsx races its previous-focus capture, leaving focus + // on after close. We restore explicitly via onCloseAutoFocus. + opener: HTMLElement | null; openDialog: (tab?: SettingsTab) => void; closeDialog: () => void; setActiveTab: (tab: SettingsTab) => void; @@ -47,11 +52,21 @@ function loadInitialTab(): SettingsTab { export const useSettingsDialogStore = create((set) => ({ open: false, activeTab: loadInitialTab(), + opener: null, openDialog: (tab) => set((state) => ({ open: true, activeTab: tab ?? state.activeTab, + opener: + typeof document !== "undefined" && + document.activeElement instanceof HTMLElement && + document.activeElement !== document.body + ? document.activeElement + : null, })), + // Do NOT clear `opener` here. onCloseAutoFocus runs on the next render + // pass after `open: false` lands, so the opener must still be readable + // from the store at that point. The next openDialog() overwrites it. closeDialog: () => set({ open: false }), setActiveTab: (tab) => { try {