diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/CloseProjectDialog.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/CloseProjectDialog.tsx index 4d5cd2303bf..34046ff8587 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/CloseProjectDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/CloseProjectDialog.tsx @@ -1,10 +1,11 @@ import { AlertDialog, - AlertDialogContent, + AlertDialogAction, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + EnterEnabledAlertDialogContent, } from "@superset/ui/alert-dialog"; import { Button } from "@superset/ui/button"; @@ -23,14 +24,9 @@ export function CloseProjectDialog({ onOpenChange, onConfirm, }: CloseProjectDialogProps) { - const handleConfirm = () => { - onOpenChange(false); - onConfirm(); - }; - return ( - + Close project "{projectName}"? @@ -58,16 +54,16 @@ export function CloseProjectDialog({ > Cancel - + - + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/UnsavedChangesDialog.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/UnsavedChangesDialog.tsx index 0203efdd70f..38e9d03ef72 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/UnsavedChangesDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/UnsavedChangesDialog.tsx @@ -2,11 +2,11 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, - AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + EnterEnabledAlertDialogContent, } from "@superset/ui/alert-dialog"; import { Button } from "@superset/ui/button"; import { LuLoader } from "react-icons/lu"; @@ -34,36 +34,32 @@ export function UnsavedChangesDialog({ discardLabel = "Discard & Continue", saveLabel = "Save & Continue", }: UnsavedChangesDialogProps) { - const handleSaveAndSwitch = (e: React.MouseEvent) => { - e.preventDefault(); + const handleSaveAndSwitch = () => { onSave(); - // Don't close dialog - parent will close on success }; - const handleDiscardAndSwitch = (e: React.MouseEvent) => { - e.preventDefault(); + const handleDiscardAndSwitch = () => { onDiscard(); - onOpenChange(false); }; return ( - + {title} {description} Cancel - - + + - + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/DiscardConfirmDialog/DiscardConfirmDialog.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/DiscardConfirmDialog/DiscardConfirmDialog.tsx index db3841ad710..2055c5aaf83 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/DiscardConfirmDialog/DiscardConfirmDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/DiscardConfirmDialog/DiscardConfirmDialog.tsx @@ -1,10 +1,11 @@ import { AlertDialog, - AlertDialogContent, + AlertDialogAction, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + EnterEnabledAlertDialogContent, } from "@superset/ui/alert-dialog"; import { Button } from "@superset/ui/button"; @@ -29,7 +30,7 @@ export function DiscardConfirmDialog({ }: DiscardConfirmDialogProps) { return ( - + {title} {description} @@ -43,20 +44,17 @@ export function DiscardConfirmDialog({ > Cancel - + - + ); } diff --git a/packages/ui/src/components/ui/alert-dialog.tsx b/packages/ui/src/components/ui/alert-dialog.tsx index ced90f31e70..28bcedd6728 100644 --- a/packages/ui/src/components/ui/alert-dialog.tsx +++ b/packages/ui/src/components/ui/alert-dialog.tsx @@ -1,11 +1,16 @@ "use client"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; +import type { VariantProps } from "class-variance-authority"; import type * as React from "react"; +import { focusEnterEnabledAlertDialogPrimaryAction } from "../../lib/focus-enter-enabled-alert-dialog-primary-action"; import { cn } from "../../lib/utils"; import { buttonVariants } from "./button"; +const alertDialogContentClassName = + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg"; + function AlertDialog({ ...props }: React.ComponentProps) { @@ -53,10 +58,28 @@ function AlertDialogContent({ + + ); +} + +function EnterEnabledAlertDialogContent({ + className, + onOpenAutoFocus, + ...props +}: React.ComponentProps) { + return ( + + + { + onOpenAutoFocus?.(event); + focusEnterEnabledAlertDialogPrimaryAction(event); + }} {...props} /> @@ -120,11 +143,15 @@ function AlertDialogDescription({ function AlertDialogAction({ className, + size, + variant, ...props -}: React.ComponentProps) { +}: React.ComponentProps & + VariantProps) { return ( ); @@ -148,6 +175,7 @@ export { AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, + EnterEnabledAlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, diff --git a/packages/ui/src/lib/focus-enter-enabled-alert-dialog-primary-action.test.ts b/packages/ui/src/lib/focus-enter-enabled-alert-dialog-primary-action.test.ts new file mode 100644 index 00000000000..efb22a67770 --- /dev/null +++ b/packages/ui/src/lib/focus-enter-enabled-alert-dialog-primary-action.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from "bun:test"; + +import { + alertDialogPrimaryActionSelector, + focusEnterEnabledAlertDialogPrimaryAction, +} from "./focus-enter-enabled-alert-dialog-primary-action"; + +describe("focusEnterEnabledAlertDialogPrimaryAction", () => { + test("focuses the alert dialog action and prevents default autofocus", () => { + let prevented = false; + let focused = false; + let queriedSelector: string | null = null; + + focusEnterEnabledAlertDialogPrimaryAction({ + currentTarget: { + querySelector: (selector: string) => { + queriedSelector = selector; + return { + focus: () => { + focused = true; + }, + }; + }, + }, + defaultPrevented: false, + preventDefault: () => { + prevented = true; + }, + }); + + expect(String(queriedSelector)).toBe(alertDialogPrimaryActionSelector); + expect(prevented).toBe(true); + expect(focused).toBe(true); + }); + + test("does nothing when no primary action is marked", () => { + let prevented = false; + + focusEnterEnabledAlertDialogPrimaryAction({ + currentTarget: { + querySelector: () => null, + }, + defaultPrevented: false, + preventDefault: () => { + prevented = true; + }, + }); + + expect(prevented).toBe(false); + }); + + test("respects an already prevented autofocus event", () => { + let queried = false; + let prevented = false; + + focusEnterEnabledAlertDialogPrimaryAction({ + currentTarget: { + querySelector: () => { + queried = true; + return null; + }, + }, + defaultPrevented: true, + preventDefault: () => { + prevented = true; + }, + }); + + expect(queried).toBe(false); + expect(prevented).toBe(false); + }); +}); diff --git a/packages/ui/src/lib/focus-enter-enabled-alert-dialog-primary-action.ts b/packages/ui/src/lib/focus-enter-enabled-alert-dialog-primary-action.ts new file mode 100644 index 00000000000..f5ffb8cf87b --- /dev/null +++ b/packages/ui/src/lib/focus-enter-enabled-alert-dialog-primary-action.ts @@ -0,0 +1,52 @@ +export const alertDialogPrimaryActionSelector = + "[data-slot='alert-dialog-action']:not([disabled])"; + +interface FocusableLike { + focus: () => void; +} + +interface EnterEnabledAlertDialogCurrentTargetLike { + querySelector: (selector: string) => FocusableLike | null; +} + +type EnterEnabledAlertDialogCurrentTarget = + | EnterEnabledAlertDialogCurrentTargetLike + | EventTarget + | null; + +interface EnterEnabledAlertDialogOpenAutoFocusEventLike { + currentTarget: EnterEnabledAlertDialogCurrentTarget; + defaultPrevented: boolean; + preventDefault: () => void; +} + +function isEnterEnabledAlertDialogCurrentTargetLike( + target: EnterEnabledAlertDialogCurrentTarget, +): target is EnterEnabledAlertDialogCurrentTargetLike { + return ( + !!target && + typeof (target as { querySelector?: unknown }).querySelector === "function" + ); +} + +export function focusEnterEnabledAlertDialogPrimaryAction( + event: EnterEnabledAlertDialogOpenAutoFocusEventLike, +) { + if ( + event.defaultPrevented || + !isEnterEnabledAlertDialogCurrentTargetLike(event.currentTarget) + ) { + return; + } + + const primaryAction = event.currentTarget.querySelector( + alertDialogPrimaryActionSelector, + ); + + if (!primaryAction) { + return; + } + + event.preventDefault(); + primaryAction.focus(); +}