mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
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:
@@ -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"
|
||||
|
||||
+37
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user