diff --git a/src/renderer/src/components/sidebar/AddRepoDialog.tsx b/src/renderer/src/components/sidebar/AddRepoDialog.tsx index ade12730dee..0608a41fff7 100644 --- a/src/renderer/src/components/sidebar/AddRepoDialog.tsx +++ b/src/renderer/src/components/sidebar/AddRepoDialog.tsx @@ -283,6 +283,7 @@ export default React.memo(function AddRepoDialog({ isOpen={isOpen} step={step} isAdding={isAdding} + isCloning={isCloning} onBack={handleBack} onCloseAutoFocus={hosted?.onCloseAutoFocus} onOpenChange={handleOpenChange} diff --git a/src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx b/src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx new file mode 100644 index 00000000000..81ad68756ae --- /dev/null +++ b/src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx @@ -0,0 +1,98 @@ +// @vitest-environment happy-dom + +import { cleanup, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { DialogDescription, DialogTitle } from '@/components/ui/dialog' +import { AddRepoDialogChrome } from './AddRepoDialogChrome' + +afterEach(cleanup) + +function CloneDialog({ + isCloning, + onOpenChange +}: { + isCloning: boolean + onOpenChange: (open: boolean) => void +}) { + const [isOpen, setIsOpen] = useState(true) + return ( + {}} + onOpenChange={(open) => { + onOpenChange(open) + setIsOpen(open) + }} + > + Clone from URL + Clone progress + + ) +} + +function backdrop(): Element { + const overlay = document.querySelector('[data-slot="dialog-overlay"]') + if (!overlay) { + throw new Error('Missing dialog backdrop') + } + return overlay +} + +describe('AddRepoDialogChrome dismissal', () => { + it('keeps an in-flight clone open after a backdrop click', async () => { + const user = userEvent.setup() + const onOpenChange = vi.fn() + render() + + await user.click(backdrop()) + + expect(onOpenChange).not.toHaveBeenCalled() + expect(screen.queryByRole('dialog', { name: 'Clone from URL' })).not.toBeNull() + }) + + it('allows backdrop dismissal when no clone is running', async () => { + const user = userEvent.setup() + const onOpenChange = vi.fn() + render() + + await user.click(backdrop()) + + expect(onOpenChange).toHaveBeenCalledWith(false) + expect(screen.queryByRole('dialog', { name: 'Clone from URL' })).toBeNull() + }) + + it('allows backdrop dismissal after the clone settles', async () => { + const user = userEvent.setup() + const onOpenChange = vi.fn() + const view = render() + await user.click(backdrop()) + expect(onOpenChange).not.toHaveBeenCalled() + + view.rerender() + await user.click(backdrop()) + + expect(onOpenChange).toHaveBeenCalledWith(false) + expect(screen.queryByRole('dialog', { name: 'Clone from URL' })).toBeNull() + }) + + it.each(['Escape', 'Close'])( + 'keeps explicit %s dismissal available during a clone', + async (action) => { + const user = userEvent.setup() + const onOpenChange = vi.fn() + render() + + await (action === 'Escape' + ? user.keyboard('{Escape}') + : user.click(screen.getByRole('button', { name: 'Close' }))) + + expect(onOpenChange).toHaveBeenCalledWith(false) + expect(screen.queryByRole('dialog', { name: 'Clone from URL' })).toBeNull() + } + ) +}) diff --git a/src/renderer/src/components/sidebar/AddRepoDialogChrome.tsx b/src/renderer/src/components/sidebar/AddRepoDialogChrome.tsx index 8a4a1893b4d..4876cd7a981 100644 --- a/src/renderer/src/components/sidebar/AddRepoDialogChrome.tsx +++ b/src/renderer/src/components/sidebar/AddRepoDialogChrome.tsx @@ -1,11 +1,13 @@ import type { ReactNode } from 'react' import { Dialog, DialogContent } from '@/components/ui/dialog' +import { preventOutsideDismissWhenDirty } from '@/lib/outside-dismiss-guard' import type { AddRepoDialogStep } from './add-repo-dialog-types' import { AddRepoStepIndicator } from './AddRepoStepIndicator' export function AddRepoDialogChrome({ children, isAdding, + isCloning, isOpen, onBack, onCloseAutoFocus, @@ -14,6 +16,7 @@ export function AddRepoDialogChrome({ }: { children: ReactNode isAdding: boolean + isCloning: boolean isOpen: boolean onBack: () => void onCloseAutoFocus?: (event: Event) => void @@ -24,6 +27,8 @@ export function AddRepoDialogChrome({ isCloning)} className={`min-w-0 overflow-hidden sm:max-w-lg [&>*]:min-w-0 ${ step === 'nested' ? 'max-h-[calc(100vh-2rem)] grid-rows-[auto_auto_minmax(0,1fr)]' : '' }`}