Merge pull request #35646 from overleaf/jpa-fix-input-selection

[web] fix race condition of input selection and user edit

GitOrigin-RevId: 2180fba1f77da2d13f216d33d16f49ee2dd58442
This commit is contained in:
Brian Gough
2026-09-17 11:34:59 +00:00
committed by Copybot
parent 3fb3baf03a
commit 158d8a5839
2 changed files with 294 additions and 27 deletions
@@ -1,4 +1,4 @@
import { useRef, useEffect, useCallback } from 'react'
import { useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useFileTreeCreateName } from '../../contexts/file-tree-create-name'
import {
@@ -36,34 +36,38 @@ export default function FileTreeCreateNameInput({
// the value is stored in a context provider, so it's available elsewhere in the form
const { name, setName, touchedName, validName } = useFileTreeCreateName()
const nameFocusedOnce = useRef(false)
const touchedNameRef = useRef(touchedName)
touchedNameRef.current = touchedName
// focus the first part of the filename if needed
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (inputRef.current && focusName) {
const input = inputRef.current
if (input && focusName) {
const { selectionStart, selectionEnd } = input
// Defer the focus so it is not overridden while the modal is opening.
window.requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const input = inputRef.current
if (!input) {
return // The input has been unmounted before the focus could be applied.
}
if (touchedNameRef.current) {
return // The name has been edited already; focusing and selecting the initial name now would clobber that edit.
}
if (
input.selectionStart !== selectionStart ||
input.selectionEnd !== selectionEnd
) {
return // The selection has been changed already (e.g. a select-all ahead of deleting the name); selecting the initial name now would clobber that change.
}
input.focus()
// Select the name part so that typing keeps the extension.
input.setSelectionRange(0, input.value.lastIndexOf('.'))
})
}
}, [focusName])
const onFocus = useCallback(() => {
if (nameFocusedOnce.current) return
if (!focusName) return
window.requestAnimationFrame(() => {
if (!inputRef.current) return
nameFocusedOnce.current = true
inputRef.current.setSelectionRange(
0,
inputRef.current.value.lastIndexOf('.')
)
})
}, [focusName])
return (
<OLFormGroup controlId="new-doc-name" className={classes.formGroup}>
<OLFormLabel>{label || t('file_name')}</OLFormLabel>
@@ -75,7 +79,6 @@ export default function FileTreeCreateNameInput({
onChange={event => setName(event.target.value)}
ref={inputRef}
disabled={inFlight}
onFocus={onFocus}
/>
{touchedName && !validName && (
<div className="notification-list">