From 5a0795a59ba6faede7a6749749a81fabc04de4e8 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:04:38 -0400 Subject: [PATCH] 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. --- .../BrowserAddressBar.test.tsx | 36 ++++++++++++++++++ .../assemble-chrome/BrowserAddressBar.tsx | 22 +++-------- .../use-browser-address-bar-click-select.ts | 37 +++++++++++++++++++ 3 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-click-select.ts diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx index 5473a7514e7..b5c0770eb38 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx @@ -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() + }) + const input = container.querySelector('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() + }) + const input = container.querySelector('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) + }) }) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.tsx index 18a75ef85cd..f3c540a8aa7 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.tsx @@ -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(null) const closingResetTimerRef = useRef(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" diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-click-select.ts b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-click-select.ts new file mode 100644 index 00000000000..71f1d9e50cb --- /dev/null +++ b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-click-select.ts @@ -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) => void + onClick: (event: MouseEvent) => void +} { + const pressFocusedRef = useRef(false) + + const onMouseDown = useCallback((event: MouseEvent) => { + 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) => { + 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 } +}