Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion web/e2e/settings-connections.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,14 @@ test('copy into an unconfigured cluster saves edited headers without an intermed
binding: 'source', integration: 'metrics', context: 'staging', source: '/test/staging', inFileName: 'staging', availability: 'available', revision: 'source-revision' })
const dialog = await openSettings(page, 'Metrics')
await dialog.getByRole('button', { name: 'Copy settings from…', exact: true }).click()
await dialog.getByRole('option', { name: /staging/ }).click()
// Focus has to reach the copied form before the next frame; a later move can
// take a keystroke meant for another field.
const focusedBeforeFrame = await dialog.getByRole('option', { name: /staging/ }).evaluate(async option => {
(option as HTMLElement).click()
await Promise.resolve()
return (document.activeElement as HTMLElement | null)?.labels?.[0]?.textContent
})
expect(focusedBeforeFrame).toBe('Metrics backend URL')
await dialog.getByRole('textbox', { name: 'X-Scope-OrgID value', exact: true }).fill('new-tenant')
await dialog.getByRole('textbox', { name: 'Metrics backend URL', exact: true }).fill('https://source.example/query')
expect(state.writes).toHaveLength(0)
Expand Down
16 changes: 12 additions & 4 deletions web/src/components/settings/LocalConnectionSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import { Badge, Collapse, ConfirmDialog, SelectMenu } from '@skyhook-io/k8s-ui'
import { ArrowLeft, Info } from 'lucide-react'
import { Tooltip } from '../ui/Tooltip'
Expand Down Expand Up @@ -207,6 +207,14 @@ export function LocalConnectionSettings({
const editorRegion = useRef<HTMLFieldSetElement>(null)
const trigger = useRef<HTMLElement | null>(null)
const mounted = useRef(true)
// Focus moves in the commit that mounts a new draft form. Deferring it to a
// later frame lets keystrokes typed in between land in a different field.
const focusAfterDraft = useRef<(() => void) | null>(null)
useLayoutEffect(() => {
const focus = focusAfterDraft.current
focusAfterDraft.current = null
focus?.()
})
const copySources = catalog.filter(c => c.integration === kind && !c.error && c.url && c.binding !== profile.target.binding)
const removalContext = profile.target.context
const dirty =
Expand Down Expand Up @@ -535,7 +543,7 @@ export function LocalConnectionSettings({
setDraftGeneration((generation) => generation + 1)
setError('')
// Cost's first control is its source picker, a listbox trigger.
requestAnimationFrame(() => region.current?.querySelector<HTMLElement>('input:not(:disabled), button[aria-haspopup="listbox"]:not(:disabled)')?.focus())
focusAfterDraft.current = () => region.current?.querySelector<HTMLElement>('input:not(:disabled), button[aria-haspopup="listbox"]:not(:disabled)')?.focus()
}}
className="text-xs text-accent-text hover:underline disabled:opacity-50 shrink-0"
>
Expand All @@ -555,7 +563,7 @@ export function LocalConnectionSettings({
setDraftGeneration(generation => generation + 1)
setMessage('')
setError('')
requestAnimationFrame(() => editorRegion.current?.querySelector<HTMLInputElement>('input:not(:disabled)')?.focus())
focusAfterDraft.current = () => editorRegion.current?.querySelector<HTMLInputElement>('input:not(:disabled)')?.focus()
}
const copySource = (source: StoredConnection) => {
setSelected(source)
Expand All @@ -571,7 +579,7 @@ export function LocalConnectionSettings({
setDraftGeneration(generation => generation + 1)
setMessage('')
setError('')
requestAnimationFrame(() => editorRegion.current?.querySelector<HTMLInputElement>('input:not(:disabled)')?.focus())
focusAfterDraft.current = () => editorRegion.current?.querySelector<HTMLInputElement>('input:not(:disabled)')?.focus()
}
const reuseOptions = task === 'main' && profile.state !== 'launch' ? [
...(previous ? [{
Expand Down
Loading