fix(web): add close button to dialog so modals are dismissable on mobile (#792)

* fix(web): add close button to dialog so modals are dismissable on mobile

The shared DialogContent had no close affordance — desktop users could
press Escape or click the overlay, but on mobile (no Escape key, dialog
spans calc(100vw-24px) leaving almost no tappable overlay) there was no
way to dismiss it. Add a DialogPrimitive.Close X button in the top-right,
fixing every dialog that uses this component at once.

* fix(web): reserve header space for dialog close button

Address review feedback: the absolutely-positioned close button overlaps
the top-right of every dialog. Long/breaking titles (e.g. DiffView's
break-all filename) could wrap underneath the 32px tap target. Add pr-12
to DialogHeader rather than padding DialogContent globally, so the title
row clears the button while body content (code blocks, diffs) keeps full
width.

* fix(web): localize dialog close button aria-label

Use the existing button.close locale string instead of a hardcoded
"Close" so screen-reader users get the label in their language (zh-CN: 关闭).
This commit is contained in:
hanger
2026-06-04 17:53:40 +08:00
committed by GitHub
parent f9ef3a4489
commit bffbe1b861
+29 -14
View File
@@ -1,6 +1,8 @@
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { cn } from '@/lib/utils'
import { CloseIcon } from '@/components/icons'
import { useTranslation } from '@/lib/use-translation'
export const Dialog = DialogPrimitive.Root
export const DialogTrigger = DialogPrimitive.Trigger
@@ -8,23 +10,36 @@ export const DialogTrigger = DialogPrimitive.Trigger
export const DialogContent = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50" />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-1/2 top-1/2 z-50 w-[calc(100vw-24px)] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-xl bg-[var(--app-dialog-bg)] p-4 shadow-2xl',
className
)}
{...props}
/>
</DialogPrimitive.Portal>
))
>(({ className, children, ...props }, ref) => {
const { t } = useTranslation()
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50" />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-1/2 top-1/2 z-50 w-[calc(100vw-24px)] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-xl bg-[var(--app-dialog-bg)] p-4 shadow-2xl',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
aria-label={t('button.close')}
>
<CloseIcon className="h-4 w-4" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
})
DialogContent.displayName = 'DialogContent'
export const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
// pr-12 reserves space for the absolutely-positioned close button (top-right)
// so long/breaking titles don't wrap underneath the tap target.
<div className={cn('flex flex-col space-y-1.5 pr-12 text-center sm:text-left', className)} {...props} />
)
export const DialogTitle = React.forwardRef<