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 } +}