refactor: migrate workflow onboarding modal to base dialog (#32915)

This commit is contained in:
yyh
2026-03-04 11:13:43 +08:00
committed by GitHub
parent 3398962bfa
commit b68ee600c1
10 changed files with 239 additions and 437 deletions

View File

@ -1,11 +1,13 @@
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import {
Dialog,
DialogClose,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogPortal,
DialogTitle,
DialogTrigger,
} from '../index'
@ -29,7 +31,7 @@ describe('Dialog wrapper', () => {
})
describe('Props', () => {
it('should not render close button when closable is omitted', () => {
it('should not render close button when DialogCloseButton is not provided', () => {
render(
<Dialog open>
<DialogContent>
@ -41,20 +43,47 @@ describe('Dialog wrapper', () => {
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
})
it('should render close button when closable is true', () => {
it('should render explicit close button with custom aria-label', () => {
render(
<Dialog open>
<DialogContent closable>
<DialogContent>
<DialogCloseButton aria-label="Dismiss dialog" />
<span>Dialog body</span>
</DialogContent>
</Dialog>,
)
const dialog = screen.getByRole('dialog')
const closeButton = screen.getByRole('button', { name: 'Close' })
expect(screen.getByRole('button', { name: 'Dismiss dialog' })).toBeInTheDocument()
})
expect(dialog).toContainElement(closeButton)
expect(closeButton).toHaveAttribute('aria-label', 'Close')
it('should render default close button label when aria-label is omitted', () => {
render(
<Dialog open>
<DialogContent>
<DialogCloseButton />
<span>Dialog body</span>
</DialogContent>
</Dialog>,
)
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
})
it('should forward close button props to base primitive', () => {
const onClick = vi.fn()
render(
<Dialog open>
<DialogContent>
<DialogCloseButton data-testid="close-button" disabled onClick={onClick} />
<span>Dialog body</span>
</DialogContent>
</Dialog>,
)
const closeButton = screen.getByTestId('close-button')
expect(closeButton).toBeDisabled()
fireEvent.click(closeButton)
expect(onClick).not.toHaveBeenCalled()
})
})
@ -65,6 +94,7 @@ describe('Dialog wrapper', () => {
expect(DialogTitle).toBe(BaseDialog.Title)
expect(DialogDescription).toBe(BaseDialog.Description)
expect(DialogClose).toBe(BaseDialog.Close)
expect(DialogPortal).toBe(BaseDialog.Portal)
})
})
})

View File

@ -16,22 +16,42 @@ export const DialogTrigger = BaseDialog.Trigger
export const DialogTitle = BaseDialog.Title
export const DialogDescription = BaseDialog.Description
export const DialogClose = BaseDialog.Close
export const DialogPortal = BaseDialog.Portal
type DialogCloseButtonProps = Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Close>, 'children'>
export function DialogCloseButton({
className,
'aria-label': ariaLabel = 'Close',
...props
}: DialogCloseButtonProps) {
return (
<BaseDialog.Close
aria-label={ariaLabel}
{...props}
className={cn(
'absolute right-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
>
<span aria-hidden="true" className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</BaseDialog.Close>
)
}
type DialogContentProps = {
children: React.ReactNode
className?: string
overlayClassName?: string
closable?: boolean
}
export function DialogContent({
children,
className,
overlayClassName,
closable = false,
}: DialogContentProps) {
return (
<BaseDialog.Portal>
<DialogPortal>
<BaseDialog.Backdrop
className={cn(
'fixed inset-0 z-50 bg-background-overlay',
@ -41,18 +61,13 @@ export function DialogContent({
/>
<BaseDialog.Popup
className={cn(
'fixed left-1/2 top-1/2 z-50 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'fixed left-1/2 top-1/2 z-50 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
className,
)}
>
{closable && (
<BaseDialog.Close aria-label="Close" className="absolute right-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover">
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</BaseDialog.Close>
)}
{children}
</BaseDialog.Popup>
</BaseDialog.Portal>
</DialogPortal>
)
}