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