fix(repos): keep active clone dialog open on outside clicks

Ignore accidental outside dismissal only while cloning; Escape, Close and Back still cancel.

Related: https://github.com/stablyai/orca/pull/24581, https://github.com/stablyai/orca/pull/23430

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Nawapat Buakoet <nawapat.b@covest.finance>
This commit is contained in:
Neil
2026-10-03 00:52:06 -07:00
committed by GitHub
co-authored by Neil Nawapat Buakoet
parent df9acd83d4
commit 752dbc9ef8
3 changed files with 104 additions and 0 deletions
@@ -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}
@@ -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 (
<AddRepoDialogChrome
isCloning={isCloning}
isAdding={false}
isOpen={isOpen}
step="clone"
onBack={() => {}}
onOpenChange={(open) => {
onOpenChange(open)
setIsOpen(open)
}}
>
<DialogTitle>Clone from URL</DialogTitle>
<DialogDescription>Clone progress</DialogDescription>
</AddRepoDialogChrome>
)
}
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(<CloneDialog isCloning onOpenChange={onOpenChange} />)
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(<CloneDialog isCloning={false} onOpenChange={onOpenChange} />)
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(<CloneDialog isCloning onOpenChange={onOpenChange} />)
await user.click(backdrop())
expect(onOpenChange).not.toHaveBeenCalled()
view.rerender(<CloneDialog isCloning={false} onOpenChange={onOpenChange} />)
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(<CloneDialog isCloning onOpenChange={onOpenChange} />)
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()
}
)
})
@@ -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({
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent
onCloseAutoFocus={onCloseAutoFocus}
// Closing the dialog aborts the clone; require an explicit close while it runs.
onInteractOutside={preventOutsideDismissWhenDirty(() => 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)]' : ''
}`}