fix(browser): double-click in the address bar selects a word, like other browsers (#27371)

* fix(browser): double-click in the address bar selects a word, like other browsers

The click that focuses the address bar selects the whole URL. Chromium treats
a press inside a selection as a possible text drag, so the second press of a
double-click never picked a word, and re-clicking a blurred bar restored its
old selection instead of selecting all. Clear that selection before the press
(focusing first, as Firefox's urlbar does) so single, double and triple clicks
match Chrome and Firefox.

* refactor(browser): keep address-bar click selection to one flag inside its hook

The focusing press now focuses and clears the selection itself, so focus can
always select all and the bar no longer shares the press flag with its focus
and blur handlers. A second press only meets a range our click made, so
event.detail === 2 replaces the select-all flag.
This commit is contained in:
Jinwoo Hong
2026-10-11 03:04:38 -04:00
committed by GitHub
parent 29b0aa534e
commit 5a0795a59b
3 changed files with 78 additions and 17 deletions
@@ -248,4 +248,40 @@ describe('BrowserAddressBar autocomplete preview', () => {
expect(onNavigate).not.toHaveBeenCalled()
expect(onSubmit).not.toHaveBeenCalled()
})
it('lets the second press of a double-click select a word instead of the whole URL', async () => {
const url = 'https://github.com/stablyai/orca/pull/27021'
await act(async () => {
root.render(<AddressBarHarness initialValue={url} onNavigate={vi.fn()} onSubmit={vi.fn()} />)
})
const input = container.querySelector<HTMLInputElement>('input[data-orca-browser-address-bar]')!
act(() => {
input.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0, detail: 1 }))
input.setSelectionRange(url.length, url.length)
input.dispatchEvent(new MouseEvent('click', { bubbles: true, button: 0, detail: 1 }))
})
expect([input.selectionStart, input.selectionEnd]).toEqual([0, url.length])
act(() => {
input.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0, detail: 2 }))
})
// Collapsed, so Chromium's native double-click word selection is not suppressed.
expect(input.selectionStart).toBe(input.selectionEnd)
})
it('clears a selection kept from before blur when a press refocuses the bar', async () => {
const url = 'https://github.com/stablyai/orca/pull/27021'
await act(async () => {
root.render(<AddressBarHarness initialValue={url} onNavigate={vi.fn()} onSubmit={vi.fn()} />)
})
const input = container.querySelector<HTMLInputElement>('input[data-orca-browser-address-bar]')!
input.setSelectionRange(0, url.length)
act(() => {
input.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0, detail: 1 }))
})
// Collapsed, so the press places a caret and the click can expand it to the whole URL.
expect(input.selectionStart).toBe(input.selectionEnd)
})
})
@@ -9,6 +9,7 @@ import type { BrowserPageDocLocation } from '../../../../../shared/browser-works
import { buildBrowserAddressBarSuggestions } from './browser-address-bar-suggestions'
import { shouldOverlayBrowserAddressBar } from './browser-address-bar-expansion'
import { saveBrowserAddressBarEditSession } from './browser-address-bar-edit-session'
import { useBrowserAddressBarClickSelect } from './use-browser-address-bar-click-select'
import { useBrowserAddressBarDismissal } from './use-browser-address-bar-dismissal'
import type { BrowserAddressBarEditSessionBinding } from './use-browser-address-bar-edit-session'
import BrowserAddressBarSuggestionList from './BrowserAddressBarSuggestionList'
@@ -53,7 +54,7 @@ export default function BrowserAddressBar({
const browserDefaultSearchEngine = useAppStore((s) => s.browserDefaultSearchEngine)
const browserKagiSessionLink = useAppStore((s) => s.browserKagiSessionLink)
const closingRef = useRef(false)
const initialMouseDownRef = useRef(false)
const addressBarClickSelect = useBrowserAddressBarClickSelect()
const openedAtRef = useRef(0)
const blurCloseTimerRef = useRef<number | null>(null)
const closingResetTimerRef = useRef<number | null>(null)
@@ -242,15 +243,12 @@ export default function BrowserAddressBar({
window.clearTimeout(blurCloseTimerRef.current)
blurCloseTimerRef.current = null
}
if (!initialMouseDownRef.current) {
inputRef.current?.select()
}
inputRef.current?.select()
openedAtRef.current = Date.now()
setOpen(true)
}, [inputRef])
const handleBlur = useCallback(() => {
initialMouseDownRef.current = false
// Why: delay close so that clicking a suggestion item registers before
// the popover unmounts. Without this, onSelect never fires because the
// mousedown on PopoverContent triggers input blur first.
@@ -428,18 +426,8 @@ export default function BrowserAddressBar({
ref={inputRef}
value={value}
onFocus={handleFocus}
onMouseDown={(event) => {
initialMouseDownRef.current =
event.button === 0 && document.activeElement !== event.currentTarget
}}
onClick={(event) => {
const input = event.currentTarget
// Preserve native drag selection; only expand a collapsed initial click.
if (initialMouseDownRef.current && input.selectionStart === input.selectionEnd) {
input.select()
}
initialMouseDownRef.current = false
}}
onMouseDown={addressBarClickSelect.onMouseDown}
onClick={addressBarClickSelect.onClick}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
data-orca-browser-address-bar="true"
@@ -0,0 +1,37 @@
import { useCallback, useRef, type MouseEvent } from 'react'
/**
* Browser-style mouse selection for the address bar: the click that focuses it selects the whole
* URL, a double-click selects a word, and a triple-click selects everything again.
*/
export function useBrowserAddressBarClickSelect(): {
onMouseDown: (event: MouseEvent<HTMLInputElement>) => void
onClick: (event: MouseEvent<HTMLInputElement>) => void
} {
const pressFocusedRef = useRef(false)
const onMouseDown = useCallback((event: MouseEvent<HTMLInputElement>) => {
const input = event.currentTarget
pressFocusedRef.current = event.button === 0 && document.activeElement !== input
// Why: Chromium treats a press inside a selection as a text drag, so it neither places a caret
// nor picks a double-click word; clear the old or click-made selection first.
if (pressFocusedRef.current) {
// Focus first: a press that focuses the bar would restore the selection being cleared.
input.focus()
input.setSelectionRange(0, 0)
} else if (event.detail === 2) {
input.setSelectionRange(0, 0)
}
}, [])
const onClick = useCallback((event: MouseEvent<HTMLInputElement>) => {
const input = event.currentTarget
// Preserve native drag selection; only expand a collapsed focusing click.
if (pressFocusedRef.current && input.selectionStart === input.selectionEnd) {
input.select()
}
pressFocusedRef.current = false
}, [])
return { onMouseDown, onClick }
}