Skip to content

Commit d037966

Browse files
authored
fix(browser): preserve omnibox focus and suggestions (#7572)
1 parent a69f416 commit d037966

2 files changed

Lines changed: 96 additions & 4 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session-ui.test.tsx‎

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
* @vitest-environment jsdom
33
*/
44
import { act } from 'react'
5-
import type { BrowserPageState } from '@sim/browser-protocol'
5+
import type {
6+
BrowserKnownSessionsState,
7+
BrowserOmniboxFocusMode,
8+
BrowserPageState,
9+
} from '@sim/browser-protocol'
610
import type { BrowserToolbarCommand } from '@sim/desktop-bridge'
711
import { createRoot, type Root } from 'react-dom/client'
812
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -25,14 +29,16 @@ const { desktop, navigateToSettings, removeResource } = vi.hoisted(() => ({
2529
setPanelFocused: vi.fn(),
2630
setPanelOccluded: vi.fn(async () => true),
2731
capturePanelSnapshot: vi.fn(async () => null),
28-
getKnownSessions: vi.fn(async () => ({ sessions: [] })),
32+
getKnownSessions: vi.fn(async (): Promise<BrowserKnownSessionsState> => ({ sessions: [] })),
2933
getDownloadsState: vi.fn(async () => ({ downloads: [] })),
3034
onAppearanceThemeChanged: vi.fn(() => () => {}),
3135
onToolbarCommand: vi.fn(
3236
(_callback: (command: BrowserToolbarCommand, scopeId: string) => void) => () => {}
3337
),
3438
onAddToChat: vi.fn(() => () => {}),
35-
onFocusOmnibox: vi.fn(() => () => {}),
39+
onFocusOmnibox: vi.fn(
40+
(_callback: (mode: BrowserOmniboxFocusMode, scopeId: string) => void) => () => {}
41+
),
3642
onOpenFind: vi.fn(() => () => {}),
3743
onCloseFind: vi.fn(() => () => {}),
3844
onDownloadsState: vi.fn(() => () => {}),
@@ -107,6 +113,86 @@ afterEach(() => {
107113
vi.restoreAllMocks()
108114
})
109115

116+
describe('browser omnibox focus', () => {
117+
beforeEach(() => {
118+
desktop.browserAgent.getKnownSessions.mockResolvedValueOnce({
119+
sessions: [{ hostname: 'example.com', evidence: 'cookies', lastObservedAt: '' }],
120+
})
121+
})
122+
123+
it('keeps focus when clicking a blank omnibox opens suggestions before focus arrives', async () => {
124+
await render({ ...PAGE, url: '' })
125+
const input = container.querySelector<HTMLInputElement>('input')!
126+
127+
await act(async () => {
128+
input.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
129+
})
130+
expect(input.getAttribute('aria-expanded')).toBe('true')
131+
await act(async () => input.focus())
132+
await act(async () => vi.advanceTimersByTime(20))
133+
134+
expect(document.activeElement === input).toBe(true)
135+
expect(input.getAttribute('aria-expanded')).toBe('true')
136+
expect(document.querySelector('[role="option"]')?.textContent).toContain('example.com')
137+
})
138+
139+
it('opens suggestions after another new tab clears an already focused omnibox', async () => {
140+
await render({ ...PAGE, url: '' })
141+
const input = container.querySelector<HTMLInputElement>('input')!
142+
const focusOmnibox = desktop.browserAgent.onFocusOmnibox.mock.calls[0][0]
143+
144+
for (let index = 0; index < 2; index++) {
145+
await act(async () => focusOmnibox('clear', PAGE.scopeId))
146+
await act(async () => vi.advanceTimersByTime(20))
147+
expect(document.activeElement === input).toBe(true)
148+
expect(input.getAttribute('aria-expanded')).toBe('false')
149+
}
150+
await act(async () => {
151+
input.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
152+
})
153+
154+
expect(document.activeElement === input).toBe(true)
155+
expect(input.getAttribute('aria-expanded')).toBe('true')
156+
})
157+
158+
it('preserves the caret on repeated clicks and dismisses on an outside click', async () => {
159+
await render({
160+
...PAGE,
161+
url: 'https://example.com',
162+
issue: {
163+
kind: 'load-error',
164+
url: 'https://example.com',
165+
code: -105,
166+
description: 'ERR_NAME_NOT_RESOLVED',
167+
},
168+
})
169+
const input = container.querySelector<HTMLInputElement>('input')!
170+
await act(async () => input.focus())
171+
await act(async () => vi.advanceTimersByTime(20))
172+
await act(async () => {
173+
input.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
174+
})
175+
await act(async () => vi.advanceTimersByTime(20))
176+
input.setSelectionRange(9, 9)
177+
await act(async () => {
178+
input.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
179+
input.click()
180+
})
181+
182+
expect(document.activeElement === input).toBe(true)
183+
expect(input.selectionStart).toBe(9)
184+
expect(input.selectionEnd).toBe(9)
185+
expect(input.getAttribute('aria-expanded')).toBe('true')
186+
187+
await act(async () => {
188+
document.body.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
189+
document.body.click()
190+
})
191+
expect(input.getAttribute('aria-expanded')).toBe('false')
192+
expect(document.activeElement === input).toBe(false)
193+
})
194+
})
195+
110196
describe('browser empty state and in-place import', () => {
111197
it('replaces the native blank page with shared guidance and no extra actions', async () => {
112198
await render()

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1339,7 +1339,10 @@ export function BrowserSession({
13391339
}
13401340
: null
13411341
setSuggestionsVisible(true)
1342-
if (document.activeElement !== event.currentTarget) {
1342+
if (
1343+
document.activeElement !== event.currentTarget ||
1344+
suggestionQuery === null
1345+
) {
13431346
setSuggestionOriginUrl(pageState?.url ?? '')
13441347
setSuggestionQuery('')
13451348
}
@@ -1433,6 +1436,9 @@ export function BrowserSession({
14331436
// Focus has to stay in the omnibox: the user is still typing, and
14341437
// the list is driven by arrow keys rather than by tabbing into it.
14351438
onOpenAutoFocus={(event) => event.preventDefault()}
1439+
onInteractOutside={(event) => {
1440+
if (event.target === urlInputRef.current) event.preventDefault()
1441+
}}
14361442
>
14371443
{suggestions.map((suggestion, index) => (
14381444
<PopoverItem

0 commit comments

Comments
 (0)