mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Fix add project action selection accessibility
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -51,7 +51,17 @@ function renderServerPathStartStep(runtimeEnvironmentId: string | null): string
|
||||
)
|
||||
}
|
||||
|
||||
async function renderLocalStartStepDom(isSshLikely: boolean): Promise<{
|
||||
type LocalStartStepDomOptions = {
|
||||
isAdding?: boolean
|
||||
addProjectBusyLabel?: string | null
|
||||
nestedScanInProgress?: boolean
|
||||
nestedScanId?: string | null
|
||||
}
|
||||
|
||||
async function renderLocalStartStepDom(
|
||||
isSshLikely: boolean,
|
||||
options: LocalStartStepDomOptions = {}
|
||||
): Promise<{
|
||||
container: HTMLDivElement
|
||||
root: Root
|
||||
}> {
|
||||
@@ -61,19 +71,21 @@ async function renderLocalStartStepDom(isSshLikely: boolean): Promise<{
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<AddRepoLocalStartStep
|
||||
repoCount={1}
|
||||
isSshLikely={isSshLikely}
|
||||
isAdding={false}
|
||||
addProjectBusyLabel={null}
|
||||
nestedScanInProgress={false}
|
||||
nestedScanId={null}
|
||||
onBrowse={vi.fn()}
|
||||
onOpenCloneStep={vi.fn()}
|
||||
onOpenRemoteStep={vi.fn()}
|
||||
onOpenCreateStep={vi.fn()}
|
||||
onStopNestedScan={vi.fn()}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<AddRepoLocalStartStep
|
||||
repoCount={1}
|
||||
isSshLikely={isSshLikely}
|
||||
isAdding={options.isAdding ?? false}
|
||||
addProjectBusyLabel={options.addProjectBusyLabel ?? null}
|
||||
nestedScanInProgress={options.nestedScanInProgress ?? false}
|
||||
nestedScanId={options.nestedScanId ?? null}
|
||||
onBrowse={vi.fn()}
|
||||
onOpenCloneStep={vi.fn()}
|
||||
onOpenRemoteStep={vi.fn()}
|
||||
onOpenCreateStep={vi.fn()}
|
||||
onStopNestedScan={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -210,6 +222,62 @@ describe('AddRepoLocalStartStep', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('clears the ⏎ selection when focus leaves the action list', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
const outsideButton = document.createElement('button')
|
||||
document.body.appendChild(outsideButton)
|
||||
|
||||
await act(async () => {
|
||||
outsideButton.focus()
|
||||
})
|
||||
|
||||
expect(document.activeElement).toBe(outsideButton)
|
||||
expect(findButton(container, 'Browse folder').textContent).not.toContain('⏎')
|
||||
expect(findButton(container, 'Clone from URL').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show an ⏎ selection while add actions are busy', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false, {
|
||||
isAdding: true,
|
||||
addProjectBusyLabel: 'Scanning repositories',
|
||||
nestedScanInProgress: true,
|
||||
nestedScanId: 'scan-1'
|
||||
})
|
||||
const stopScanButton = container.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Stop scan"]'
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
stopScanButton?.focus()
|
||||
})
|
||||
|
||||
expect(document.activeElement).toBe(stopScanButton)
|
||||
expect(findButton(container, 'Browse folder').textContent).not.toContain('⏎')
|
||||
expect(findButton(container, 'Clone from URL').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('hides the visual ⏎ chip from assistive technology', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
const browseButton = findButton(container, 'Browse folder')
|
||||
const enterChip = Array.from(browseButton.querySelectorAll('[aria-hidden="true"]')).find(
|
||||
(entry) => entry.textContent?.includes('⏎')
|
||||
)
|
||||
|
||||
expect(enterChip).toBeTruthy()
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('roves selection down the action list with the ArrowDown key', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
|
||||
|
||||
@@ -99,9 +99,13 @@ export function AddRepoLocalStartStep({
|
||||
// The white fill + ⏎ chip is a roving selection indicator, not a fixed "primary" badge:
|
||||
// it follows keyboard focus so Enter always activates the highlighted action. Browse is
|
||||
// autofocused on open, so it starts selected; Tab and ↑/↓ move the highlight.
|
||||
const [selectedKind, setSelectedKind] = useState<string>(primaryAction.kind)
|
||||
const [selectedKind, setSelectedKind] = useState<string | null>(primaryAction.kind)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdding) {
|
||||
setSelectedKind(null)
|
||||
return
|
||||
}
|
||||
if (!isAdding) {
|
||||
browseActionRef.current?.focus()
|
||||
}
|
||||
@@ -125,6 +129,16 @@ export function AddRepoLocalStartStep({
|
||||
buttons[nextIndex]?.focus()
|
||||
}
|
||||
|
||||
const handleActionsBlur = (event: React.FocusEvent<HTMLDivElement>): void => {
|
||||
if (!(event.relatedTarget instanceof HTMLButtonElement)) {
|
||||
setSelectedKind(null)
|
||||
return
|
||||
}
|
||||
if (!event.relatedTarget.matches('button[data-add-repo-action]')) {
|
||||
setSelectedKind(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
@@ -141,7 +155,12 @@ export function AddRepoLocalStartStep({
|
||||
) : null}
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 pt-2" ref={actionsRef} onKeyDown={handleArrowNavigation}>
|
||||
<div
|
||||
className="space-y-3 pt-2"
|
||||
ref={actionsRef}
|
||||
onBlur={handleActionsBlur}
|
||||
onKeyDown={handleArrowNavigation}
|
||||
>
|
||||
<AddRepoPrimaryStartAction
|
||||
icon={primaryAction.icon}
|
||||
title={primaryAction.title}
|
||||
@@ -204,11 +223,12 @@ type AddRepoStartActionProps = {
|
||||
|
||||
// Shared trailing chip so the ⏎ glyph travels with the selected action across primary and secondary rows.
|
||||
const AddRepoEnterChip = (): React.JSX.Element => (
|
||||
<ShortcutKeyCombo
|
||||
keys={['⏎']}
|
||||
keyCapClassName="border-primary-foreground/20 bg-primary-foreground/10 text-primary-foreground/80"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<span aria-hidden="true" className="shrink-0">
|
||||
<ShortcutKeyCombo
|
||||
keys={['⏎']}
|
||||
keyCapClassName="border-primary-foreground/20 bg-primary-foreground/10 text-primary-foreground/80"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
const AddRepoPrimaryStartAction = ({
|
||||
|
||||
Reference in New Issue
Block a user