mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
Restore focus when closing worktree parent picker popover
- Find the nearest focusable ancestor of the anchor row to restore focus to instead of letting it drop on the detached input element - Simplify focus assertion in AutomationListSearchField test to verify actual focus behavior rather than autofocus attribute presence
This commit is contained in:
@@ -34,6 +34,6 @@ describe('AutomationListSearchField', () => {
|
||||
|
||||
const input = container.querySelector('input')
|
||||
expect(input).not.toBeNull()
|
||||
expect(input?.hasAttribute('autofocus') || document.activeElement === input).toBe(true)
|
||||
expect(document.activeElement).toBe(input)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Worktree } from '../../../../shared/types'
|
||||
import {
|
||||
getWorktreeParentPickerFocusRestoreTarget,
|
||||
handleWorktreeParentPickerKeyDown,
|
||||
selectWorktreeParent
|
||||
} from './WorktreeParentPickerPopover'
|
||||
@@ -173,6 +176,36 @@ describe('clampWorktreeParentPickerIndex', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorktreeParentPickerFocusRestoreTarget', () => {
|
||||
function makeSidebarRow(): { listbox: HTMLElement; row: HTMLElement } {
|
||||
const listbox = document.createElement('div')
|
||||
listbox.tabIndex = 0
|
||||
listbox.setAttribute('role', 'listbox')
|
||||
const row = document.createElement('div')
|
||||
row.setAttribute('role', 'option')
|
||||
listbox.appendChild(row)
|
||||
return { listbox, row }
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('restores focus to the focusable container of the anchored row', () => {
|
||||
const { listbox, row } = makeSidebarRow()
|
||||
document.body.appendChild(listbox)
|
||||
|
||||
expect(getWorktreeParentPickerFocusRestoreTarget(row)).toBe(listbox)
|
||||
})
|
||||
|
||||
it('skips a row whose sidebar was already unmounted or never anchored', () => {
|
||||
const { row } = makeSidebarRow()
|
||||
|
||||
expect(getWorktreeParentPickerFocusRestoreTarget(row)).toBeNull()
|
||||
expect(getWorktreeParentPickerFocusRestoreTarget(null)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parent picker keyboard input', () => {
|
||||
it.each([
|
||||
{ isComposing: true, keyCode: 13 },
|
||||
|
||||
@@ -61,6 +61,20 @@ function getAnchorRect(anchorElement: HTMLElement | null): AnchorRect | null {
|
||||
return anchorElement?.getBoundingClientRect() ?? null
|
||||
}
|
||||
|
||||
const FOCUSABLE_ANCHOR_SELECTOR =
|
||||
'a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
|
||||
|
||||
// Why: the anchor is a non-focusable `role="option"` row, so closing focus has
|
||||
// to land on its nearest focusable container (the sidebar listbox).
|
||||
export function getWorktreeParentPickerFocusRestoreTarget(
|
||||
anchorElement: HTMLElement | null
|
||||
): HTMLElement | null {
|
||||
if (!anchorElement?.isConnected) {
|
||||
return null
|
||||
}
|
||||
return anchorElement.closest<HTMLElement>(FOCUSABLE_ANCHOR_SELECTOR)
|
||||
}
|
||||
|
||||
export function selectWorktreeParent({
|
||||
childWorktreeId,
|
||||
parentWorktreeId,
|
||||
@@ -312,8 +326,13 @@ export function WorktreeParentPickerPopover({
|
||||
event.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}}
|
||||
// Why: virtual anchor has no trigger for Radix to restore focus to.
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
// Why: virtual anchor has no trigger for Radix to restore focus to, so
|
||||
// drive it back to the anchored row's listbox instead of dropping it on
|
||||
// the detached input (i.e. document.body).
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
getWorktreeParentPickerFocusRestoreTarget(anchorElement)?.focus()
|
||||
}}
|
||||
onInteractOutside={(event) => {
|
||||
if (suppressInitialOutsideCloseRef.current) {
|
||||
event.preventDefault()
|
||||
|
||||
Reference in New Issue
Block a user