mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
Ensure draft comment textarea auto-focuses and preserve drafts through e
- Focus textarea on mount via requestAnimationFrame for reliable focusing - Add onDomNodeTop callback to focus textarea when zone reaches viewport - Preserve pending draft when editor model refreshes and re-anchor on reload - Add editor.getModel() checks before opening and re-anchoring drafts - Test that textarea is focused on creation and errors are surfaced
This commit is contained in:
@@ -28,7 +28,9 @@ describe('DiffCommentDraftCard', () => {
|
||||
|
||||
expect(view.getByText('Line 42')).toBeDefined()
|
||||
expect(view.queryByText('You')).toBeNull()
|
||||
expect(view.getByPlaceholderText('Add note for the AI')).toBeDefined()
|
||||
const textarea = view.getByPlaceholderText('Add note for the AI')
|
||||
expect(textarea).toBeDefined()
|
||||
expect(document.activeElement).toBe(textarea)
|
||||
expect(view.getByRole('button', { name: 'Add note' })).toBeDefined()
|
||||
expect(view.getByRole('button', { name: 'Cancel' })).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -71,6 +71,16 @@ export function DiffCommentDraftCard({
|
||||
textarea?.focus()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const focusTextarea = (): void => textareaRef.current?.focus()
|
||||
focusTextarea()
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
return
|
||||
}
|
||||
const frame = requestAnimationFrame(focusTextarea)
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const card = cardRef.current
|
||||
if (!card || !onContentResizeRef.current) {
|
||||
|
||||
@@ -75,7 +75,7 @@ export function useDiffCommentDraftZone({
|
||||
|
||||
const openDraft = useCallback(
|
||||
(draft: DiffCommentDraft, initialBody = ''): void => {
|
||||
if (!editor || !onCreateCommentRef.current || !canOpenDraft) {
|
||||
if (!editor || !editor.getModel() || !onCreateCommentRef.current || !canOpenDraft) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,12 +88,24 @@ export function useDiffCommentDraftZone({
|
||||
const disposeDomMouseDownStopper = installDiffCommentZoneMouseDownStopper(dom)
|
||||
const disposeMarginMouseDownStopper = installDiffCommentZoneMouseDownStopper(marginDom)
|
||||
const root = createRoot(dom)
|
||||
let didFocus = false
|
||||
const delegate: monacoEditor.IViewZone = {
|
||||
afterLineNumber: draft.lineNumber,
|
||||
heightInPx: DRAFT_ZONE_DEFAULT_HEIGHT,
|
||||
domNode: dom,
|
||||
marginDomNode: marginDom,
|
||||
suppressMouseDown: false
|
||||
suppressMouseDown: false,
|
||||
onDomNodeTop: () => {
|
||||
if (didFocus) {
|
||||
return
|
||||
}
|
||||
const textarea = dom.querySelector<HTMLTextAreaElement>('textarea')
|
||||
if (!textarea) {
|
||||
return
|
||||
}
|
||||
didFocus = true
|
||||
textarea.focus()
|
||||
}
|
||||
}
|
||||
const zoneId = accessor.addZone(delegate)
|
||||
const entry: DraftZoneEntry = {
|
||||
@@ -177,7 +189,7 @@ export function useDiffCommentDraftZone({
|
||||
}
|
||||
pendingDraftRef.current = { draft: current.draft, body: current.body }
|
||||
disposeDraftZone()
|
||||
if (!editor || !onCreateCommentRef.current || !canOpenDraft) {
|
||||
if (!editor || !editor.getModel() || !onCreateCommentRef.current || !canOpenDraft) {
|
||||
return
|
||||
}
|
||||
reanchorFrameRef.current = requestAnimationFrame(() => {
|
||||
@@ -190,6 +202,29 @@ export function useDiffCommentDraftZone({
|
||||
})
|
||||
}, [canOpenDraft, disposeDraftZone, editor, monacoModelIdentity, openDraft])
|
||||
|
||||
// A combined-diff model refresh can briefly clear the editor ref before the replacement mounts.
|
||||
// Re-anchor any carried draft when that replacement becomes available.
|
||||
useEffect(() => {
|
||||
if (!editor || !canOpenDraft || !pendingDraftRef.current) {
|
||||
return
|
||||
}
|
||||
const pending = pendingDraftRef.current
|
||||
pendingDraftRef.current = null
|
||||
reanchorFrameRef.current = requestAnimationFrame(() => {
|
||||
reanchorFrameRef.current = null
|
||||
openDraft(pending.draft, pending.body)
|
||||
})
|
||||
return () => {
|
||||
if (reanchorFrameRef.current !== null) {
|
||||
cancelAnimationFrame(reanchorFrameRef.current)
|
||||
reanchorFrameRef.current = null
|
||||
}
|
||||
if (!draftZoneRef.current) {
|
||||
pendingDraftRef.current = pending
|
||||
}
|
||||
}
|
||||
}, [canOpenDraft, editor, openDraft])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return
|
||||
@@ -199,7 +234,6 @@ export function useDiffCommentDraftZone({
|
||||
cancelAnimationFrame(reanchorFrameRef.current)
|
||||
reanchorFrameRef.current = null
|
||||
}
|
||||
pendingDraftRef.current = null
|
||||
disposeDraftZone(false)
|
||||
}
|
||||
}, [disposeDraftZone, editor])
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { toast } from 'sonner'
|
||||
import { submitDiffSectionComment } from './diff-section-comment-submit'
|
||||
import type { DiffSection } from './diff-section-types'
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
|
||||
|
||||
const section: DiffSection = {
|
||||
key: 'section-1',
|
||||
path: 'src/example.ts',
|
||||
status: 'modified',
|
||||
originalContent: 'before',
|
||||
modifiedContent: 'after',
|
||||
collapsed: false,
|
||||
loading: false,
|
||||
dirty: false,
|
||||
diffResult: null,
|
||||
largeDiffRenderLimit: null
|
||||
}
|
||||
|
||||
describe('submitDiffSectionComment', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(toast.error).mockClear()
|
||||
})
|
||||
|
||||
it('surfaces persistence failures while keeping the draft open', async () => {
|
||||
const addDiffComment = vi.fn().mockResolvedValue(null)
|
||||
|
||||
const result = await submitDiffSectionComment({
|
||||
addDiffComment,
|
||||
body: 'Needs revision',
|
||||
target: { lineNumber: 4 },
|
||||
section,
|
||||
worktreeId: 'worktree-1'
|
||||
})
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to save comment')
|
||||
})
|
||||
})
|
||||
@@ -73,6 +73,9 @@ test.describe('Diff note draft', () => {
|
||||
await expect(draftCard).toBeVisible({ timeout: 15_000 })
|
||||
await expect(draftCard).toContainText('Line 6')
|
||||
await expect(draftCard).not.toContainText('You')
|
||||
await expect
|
||||
.poll(() => textarea.evaluate((element) => document.activeElement === element))
|
||||
.toBe(true)
|
||||
await expect(orcaPage.locator('.orca-diff-comment-draft-margin')).toBeVisible()
|
||||
await expect
|
||||
.poll(
|
||||
|
||||
Reference in New Issue
Block a user