mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-31 03:47:12 +08:00
fix(combobox): keep the dropdown open while dragging its scrollbar (#6307)
* fix(combobox): keep the dropdown open while dragging its scrollbar * improvement(combobox): move pointer-press notes into TSDoc
This commit is contained in:
@@ -226,6 +226,13 @@ const Combobox = memo(
|
||||
const blurTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null)
|
||||
const internalInputRef = useRef<HTMLInputElement>(null)
|
||||
const inputRef = externalInputRef || internalInputRef
|
||||
/**
|
||||
* True while a pointer press that began inside the dropdown is still held.
|
||||
* Grabbing the list's native scrollbar blurs the editable input and parks
|
||||
* focus on `<body>` — which `handleBlur` would otherwise read as "focus
|
||||
* left the combobox" and close the dropdown mid-drag.
|
||||
*/
|
||||
const pointerDownInsideRef = useRef(false)
|
||||
|
||||
const effectiveSelectedValue = selectedValue ?? value
|
||||
|
||||
@@ -236,6 +243,34 @@ const Combobox = memo(
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Releases the pointer-press window and restores focus to the editable input,
|
||||
* which a scrollbar drag left on `<body>`. Bound to `window` so a release
|
||||
* outside the popover still clears the flag; `pointercancel` is included
|
||||
* because a touch scroll gesture ends there instead of `pointerup`.
|
||||
*
|
||||
* Focus is only restored when the press actually stole it — a press inside the
|
||||
* popover parks it on `<body>` or the `tabIndex={-1}` content, but option
|
||||
* mousedown is prevented, so it often never left the input or the search box.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!editable) return
|
||||
const endPointerPress = () => {
|
||||
if (!pointerDownInsideRef.current) return
|
||||
pointerDownInsideRef.current = false
|
||||
const active = document.activeElement
|
||||
const isTextEntry =
|
||||
active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement
|
||||
if (!isTextEntry) inputRef.current?.focus({ preventScroll: true })
|
||||
}
|
||||
window.addEventListener('pointerup', endPointerPress)
|
||||
window.addEventListener('pointercancel', endPointerPress)
|
||||
return () => {
|
||||
window.removeEventListener('pointerup', endPointerPress)
|
||||
window.removeEventListener('pointercancel', endPointerPress)
|
||||
}
|
||||
}, [editable, inputRef])
|
||||
|
||||
// Flatten groups into options if groups are provided
|
||||
const allOptions = useMemo(() => {
|
||||
if (groups) {
|
||||
@@ -326,7 +361,9 @@ const Combobox = memo(
|
||||
}, [groups, searchable, searchQuery])
|
||||
|
||||
/**
|
||||
* Handles selection of an option
|
||||
* Handles selection of an option. In editable mode the input is blurred on
|
||||
* purpose, so the pointer-press window is ended first — otherwise the `pointerup`
|
||||
* that follows would hand focus back and reopen the dropdown.
|
||||
*/
|
||||
const handleSelect = useCallback(
|
||||
(selectedValue: string, customOnSelect?: () => void, keepOpen?: boolean) => {
|
||||
@@ -355,6 +392,7 @@ const Combobox = memo(
|
||||
setHighlightedIndex(-1)
|
||||
updateSearchQuery('')
|
||||
if (editable && inputRef.current) {
|
||||
pointerDownInsideRef.current = false
|
||||
inputRef.current.blur()
|
||||
}
|
||||
}
|
||||
@@ -392,6 +430,7 @@ const Combobox = memo(
|
||||
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current)
|
||||
// Delay to allow dropdown clicks
|
||||
blurTimeoutRef.current = setTimeout(() => {
|
||||
if (pointerDownInsideRef.current) return
|
||||
const activeElement = document.activeElement
|
||||
// Check if focus is in the container, dropdown, or search input
|
||||
const isInContainer = containerRef.current?.contains(activeElement)
|
||||
@@ -681,6 +720,9 @@ const Combobox = memo(
|
||||
setTimeout(() => searchInputRef.current?.focus(), 0)
|
||||
}
|
||||
}}
|
||||
onPointerDownCapture={() => {
|
||||
if (editable) pointerDownInsideRef.current = true
|
||||
}}
|
||||
onInteractOutside={(e) => {
|
||||
// If the user clicks the anchor/trigger while the popover is open,
|
||||
// prevent Radix from auto-closing on mousedown. Our own toggle handler
|
||||
|
||||
Reference in New Issue
Block a user