4ed69f640c
* bail the refocus if focus is already on the correct element * use `mousedown` instead of `click` event The `mousedown` event happens before the `focus` event. When we `e.preventDefault()` in this listener, the `focus` event will not fire. This also means that the focus is not lost on the actual `input` component which in turn means that we can maintain the selection / cursor position inside the `input`. We still use the `refocusInput()` as a fallback in case something else goes wrong. * add comments to describe _why_ we use `mousedown` * ensure we handle mouse buttons correctly * ensure we handle `Enter` and `Space` explicitly Now that we use the `mousedown` event instead of the `click` event, we have to make sure that we handle the `enter` and `space` keys explicitly. This used to be covered by the `click` event, but not for the `mousedown` event. * ensure we focus the first element when using `ArrowDown` on the `ComboboxButton` We go to the last one on `ArrownUp`, but we forgot to do this on `ArrowDown`. * fix tiny typo Not related to this PR, but noticed it and fixed it anyway. * update changelog * ensure we reset the `isTyping` flag While we are typing, the flag can remain true. But once we stop typing, the `nextFrame` handler will kick in and set it to `false` again. It currently behaves as a debounce-like function such that the `nextFrame` callbacks are cancelled once a new event is fired. * ensure unique callbacks in the `_disposables` array This allows us to keep re-adding dispose functions and only register the callbacks once. Ideally we can use a `Set`, but we also want to remove a single callback if the callback is disposed on its own instead of disposing the whole group. For this we do require an `idx` which is not available in a `Set` unless you are looping over all disposable functions. * Update packages/@headlessui-react/src/components/combobox/combobox.tsx Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/components/combobox/combobox.tsx Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * update comments * abstract confusing logic to a `useFrameDebounce()` hook * use correct path import * add some breathing room --------- Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
import { useRef, type MutableRefObject } from 'react'
|
|
import { useEvent } from './use-event'
|
|
import { useEventListener } from './use-event-listener'
|
|
|
|
/**
|
|
* The `useRefocusableInput` hook exposes a function to re-focus the input element.
|
|
*
|
|
* This hook will also keep the cursor position into account to make sure the
|
|
* cursor is placed at the correct position as-if we didn't loose focus at all.
|
|
*/
|
|
export function useRefocusableInput(ref: MutableRefObject<HTMLInputElement | null>) {
|
|
// Track the cursor position and the value of the input
|
|
let info = useRef({
|
|
value: '',
|
|
selectionStart: null as number | null,
|
|
selectionEnd: null as number | null,
|
|
})
|
|
|
|
useEventListener(ref.current, 'blur', (event) => {
|
|
let target = event.target
|
|
if (!(target instanceof HTMLInputElement)) return
|
|
|
|
info.current = {
|
|
value: target.value,
|
|
selectionStart: target.selectionStart,
|
|
selectionEnd: target.selectionEnd,
|
|
}
|
|
})
|
|
|
|
return useEvent(() => {
|
|
let input = ref.current
|
|
|
|
// If the input is already focused, we don't need to do anything
|
|
if (document.activeElement === input) return
|
|
|
|
if (!(input instanceof HTMLInputElement)) return
|
|
if (!input.isConnected) return
|
|
|
|
// Focus the input
|
|
input.focus({ preventScroll: true })
|
|
|
|
// Try to restore the cursor position
|
|
//
|
|
// If the value changed since we recorded the cursor position, then we can't
|
|
// restore the cursor position and we'll just leave it at the end.
|
|
if (input.value !== info.current.value) {
|
|
input.setSelectionRange(input.value.length, input.value.length)
|
|
}
|
|
|
|
// If the value is the same, we can restore to the previous cursor position.
|
|
else {
|
|
let { selectionStart, selectionEnd } = info.current
|
|
if (selectionStart !== null && selectionEnd !== null) {
|
|
input.setSelectionRange(selectionStart, selectionEnd)
|
|
}
|
|
}
|
|
|
|
// Reset the cursor position
|
|
info.current = { value: '', selectionStart: null, selectionEnd: null }
|
|
})
|
|
}
|