Skip to content

Commit 6816f3b

Browse files
sheremet-vahi-ogawaOpenCode
authored
feat(ui): navigate trace steps with keyboard and remove indirect editor focus (#11005)
Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: OpenCode <noreply@opencode.ai>
1 parent bce2d3c commit 6816f3b

5 files changed

Lines changed: 63 additions & 18 deletions

File tree

packages/ui/client/components/trace/TraceView.vue

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ function onSelectStep(index: number) {
2929
}
3030
}
3131
32+
function onStepKeydown(event: KeyboardEvent, index: number) {
33+
let nextIndex: number
34+
if (event.key === 'ArrowUp') {
35+
nextIndex = Math.max(index - 1, 0)
36+
}
37+
else if (event.key === 'ArrowDown') {
38+
nextIndex = Math.min(index + 1, entries.value.length - 1)
39+
}
40+
else if (event.key === 'Home') {
41+
nextIndex = 0
42+
}
43+
else if (event.key === 'End') {
44+
nextIndex = entries.value.length - 1
45+
}
46+
else {
47+
return
48+
}
49+
50+
event.preventDefault()
51+
if (nextIndex === index) {
52+
return
53+
}
54+
onSelectStep(nextIndex)
55+
const nextButton = (event.currentTarget as HTMLButtonElement).parentElement?.children[nextIndex]
56+
if (nextButton instanceof HTMLButtonElement) {
57+
nextButton.focus()
58+
}
59+
}
60+
3261
watch([selectedStep, iframeEl], ([step, iframe]) => {
3362
if (!step || !iframe) {
3463
return
@@ -143,18 +172,27 @@ function isTraceStepInProgress(step: NormalizedBrowserTraceEntry) {
143172
class="h-full min-h-0"
144173
>
145174
<Pane :size="30" min-size="20">
146-
<div class="h-full min-h-0 p-4" flex="~ col gap-1" overflow-auto>
175+
<div
176+
class="h-full min-h-0 p-4"
177+
flex="~ col gap-1"
178+
overflow-auto
179+
role="listbox"
180+
aria-label="Trace steps"
181+
>
147182
<button
148183
v-for="(step, index) of entries"
149184
:key="index"
150185
type="button"
186+
role="option"
151187
data-testid="trace-step"
152188
:data-test-range="step.range?.phase"
153189
class="relative w-full text-left px-2 py-1 rounded text-sm"
154190
:class="getStepButtonClass(step, index)"
155191
:style="{ paddingInlineStart: `${0.5 + step.depth}rem` }"
156-
:aria-current="selection.selectedStepIndex === index ? 'step' : undefined"
192+
:aria-selected="selection.selectedStepIndex === index"
193+
:tabindex="selection.selectedStepIndex === index ? 0 : -1"
157194
@click="onSelectStep(index)"
195+
@keydown="onStepKeydown($event, index)"
158196
>
159197
<span
160198
v-if="step.depth > 0"

packages/ui/client/components/views/ViewEditor.vue

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ watch(
6262
6363
await nextTick()
6464
65-
// fire focusing editor after loading
6665
loading.value = false
6766
},
6867
{ immediate: true },
@@ -81,17 +80,11 @@ watch(() => [loading.value, saving.value, props.file, lineNumber.value, columnNu
8180
else {
8281
codemirrorRef.value?.scrollIntoView(line, 100)
8382
nextTick(() => {
84-
codemirrorRef.value?.focus()
8583
codemirrorRef.value?.setCursor(line)
8684
})
8785
}
8886
})
8987
}
90-
else {
91-
nextTick(() => {
92-
codemirrorRef.value?.focus()
93-
})
94-
}
9588
}
9689
}, { flush: 'post' })
9790

test/ui/test/trace-stream.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ test.describe('trace stream', () => {
6666
])
6767

6868
// first step is selected by default
69-
await expect(traceSteps.nth(0)).toHaveAttribute('aria-current', 'step')
69+
await expect(traceSteps.nth(0)).toHaveAttribute('aria-selected', 'true')
7070

7171
// progresses up-to `render-b`
7272
await writeFile(resolve(gatesDir, 'b.txt'), 'open')
@@ -77,7 +77,7 @@ test.describe('trace stream', () => {
7777

7878
// select next step
7979
await traceStepNames.nth(1).click()
80-
await expect(traceSteps.nth(1)).toHaveAttribute('aria-current', 'step')
80+
await expect(traceSteps.nth(1)).toHaveAttribute('aria-selected', 'true')
8181

8282
// continue test and wait for finishes
8383
await writeFile(resolve(gatesDir, 'c.txt'), 'open')
@@ -89,7 +89,7 @@ test.describe('trace stream', () => {
8989
'test finished',
9090
])
9191
// last selected step is preserved
92-
await expect(traceSteps.nth(1)).toHaveAttribute('aria-current', 'step')
92+
await expect(traceSteps.nth(1)).toHaveAttribute('aria-selected', 'true')
9393

9494
// re-run and verify trace view is cleared
9595
await rm(gatesDir, { recursive: true, force: true })
@@ -100,7 +100,7 @@ test.describe('trace stream', () => {
100100
await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([
101101
'render-a',
102102
])
103-
await expect(traceSteps.nth(0)).toHaveAttribute('aria-current', 'step')
103+
await expect(traceSteps.nth(0)).toHaveAttribute('aria-selected', 'true')
104104
await writeFile(resolve(gatesDir, 'b.txt'), 'open')
105105
await writeFile(resolve(gatesDir, 'c.txt'), 'open')
106106
await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([

test/ui/test/trace.spec.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ async function testBasic(page: Page) {
149149

150150
// selecting steps should open source code view
151151
await expect(page.getByTestId('btn-report')).toContainClass('tab-button-active')
152-
await traceStepNames.getByText('Render simple').click()
152+
await traceSteps.nth(0).click()
153153
await expect(page.getByTestId('btn-code')).toContainClass('tab-button-active')
154+
await expect(traceSteps.nth(0)).toBeFocused()
154155

155156
// verify source location highlight
156157
const activeLine = page.getByTestId('editor').locator('.CodeMirror-activeline')
@@ -169,20 +170,32 @@ async function testBasic(page: Page) {
169170
// verify selector highlight
170171
await expect(traceFrame.getByTestId('trace-view-highlight')).toBeVisible()
171172

172-
// selecting 2nd trace step and verify again
173-
await traceStepNames.getByText('Render another').click()
173+
// selecting 2nd trace step with keyboard and verify again
174+
await traceSteps.nth(0).press('ArrowDown')
174175
await expect(traceFrame.getByRole('button', { name: 'Another' })).toBeVisible()
175176
await expect(activeLine).toContainText('Render another')
176-
await expect(traceSteps.nth(1)).toHaveAttribute('aria-current', 'step')
177+
await expect(traceSteps.nth(1)).toBeFocused()
178+
await expect(traceSteps.nth(1)).toHaveAttribute('aria-selected', 'true')
177179
await expect(traceEditorMarkers.nth(1)).not.toHaveAttribute('aria-current', 'step')
178180
await expect(traceEditorMarkers.nth(2)).toHaveAttribute('aria-current', 'step')
179181

182+
await traceSteps.nth(1).press('End')
183+
await expect(traceSteps.nth(2)).toBeFocused()
184+
await expect(traceSteps.nth(2)).toHaveAttribute('aria-selected', 'true')
185+
await traceSteps.nth(2).press('ArrowDown')
186+
await expect(traceSteps.nth(2)).toBeFocused()
187+
await traceSteps.nth(2).press('Home')
188+
await expect(traceSteps.nth(0)).toBeFocused()
189+
await expect(traceSteps.nth(0)).toHaveAttribute('aria-selected', 'true')
190+
await traceSteps.nth(0).press('ArrowUp')
191+
await expect(traceSteps.nth(0)).toBeFocused()
192+
180193
// selecting 1st trace step from editor and verify again
181194
await traceEditorMarkers.nth(1).click()
182195
await expect(traceFrame.getByRole('button', { name: 'Simple' })).toBeVisible()
183196
await expect(traceEditorMarkers.nth(1)).toHaveAttribute('aria-current', 'step')
184197
await expect(traceEditorMarkers.nth(2)).not.toHaveAttribute('aria-current', 'step')
185-
await expect(traceSteps.nth(0)).toHaveAttribute('aria-current', 'step')
198+
await expect(traceSteps.nth(0)).toHaveAttribute('aria-selected', 'true')
186199

187200
// verify selecting another test switches trace viewer
188201
await openExplorerItem(page, 'switch-target')

test/ui/test/ui.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ async function testWriteFile(page: Page, options: { enabled: boolean }) {
661661
await codeTabButton.click()
662662
const editor = page.getByTestId('editor')
663663
await expect(editor).toContainText('expect(1 + 1).toEqual(2)')
664+
await editor.click()
664665
await expect(editor.locator('.CodeMirror-cursors')).toHaveCSS('visibility', options.enabled ? 'visible' : 'hidden')
665666
await page.keyboard.type('\n// edited \n')
666667
if (options.enabled) {

0 commit comments

Comments
 (0)
Sponsor
SponsoredKunjungi sekarang
Promo