Merge pull request #14757 from overleaf/td-review-panel-auto-expand-textarea-cursor

React review panel: place cursor at the end when autofocussing textarea

GitOrigin-RevId: c277e949dadc16bef2ed1a9ace69285e99ba29ad
This commit is contained in:
Mathias Jakobsen 2023-09-13 10:06:30 +01:00 committed by Copybot
parent 069e5ac320
commit 96ba8a92f4
2 changed files with 77 additions and 26 deletions

View file

@ -4,9 +4,7 @@ import EntryContainer from './entry-container'
import EntryCallout from './entry-callout'
import EntryActions from './entry-actions'
import Comment from './comment'
import AutoExpandingTextArea, {
resetHeight,
} from '../../../../../shared/components/auto-expanding-text-area'
import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area'
import Icon from '../../../../../shared/components/icon'
import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
import classnames from 'classnames'
@ -93,7 +91,6 @@ function CommentEntry({
;(e.target as HTMLTextAreaElement).blur()
submitReply(threadId, replyContent)
setReplyContent('')
resetHeight(e)
}
}
}

View file

@ -1,28 +1,7 @@
import { useEffect, useRef } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { callFnsInSequence } from '../../utils/functions'
import { MergeAndOverride } from '../../../../types/utils'
export const resetHeight = (
e:
| React.ChangeEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLTextAreaElement>
) => {
const el = e.target as HTMLTextAreaElement
window.requestAnimationFrame(() => {
const curHeight = el.offsetHeight
const fitHeight = el.scrollHeight
// clear height if text area is empty
if (!el.value.length) {
el.style.removeProperty('height')
}
// otherwise expand to fit text
else if (fitHeight > curHeight) {
el.style.height = `${fitHeight}px`
}
})
}
type AutoExpandingTextAreaProps = MergeAndOverride<
React.ComponentProps<'textarea'>,
{
@ -33,10 +12,61 @@ type AutoExpandingTextAreaProps = MergeAndOverride<
function AutoExpandingTextArea({
onChange,
onResize,
autoFocus,
...rest
}: AutoExpandingTextAreaProps) {
const ref = useRef<HTMLTextAreaElement>(null)
const previousHeightRef = useRef<number | null>(null)
const previousMeasurementRef = useRef<{
heightAdjustment: number
value: string
} | null>(null)
const resetHeight = useCallback(() => {
const el = ref.current
if (!el) {
return
}
const { value } = el
const previousMeasurement = previousMeasurementRef.current
// Do nothing if the textarea value hasn't changed since the last reset
if (previousMeasurement !== null && value === previousMeasurement.value) {
return
}
let heightAdjustment
if (previousMeasurement === null) {
const computedStyle = window.getComputedStyle(el)
heightAdjustment =
computedStyle.boxSizing === 'border-box'
? Math.ceil(
parseFloat(computedStyle.borderTopWidth) +
parseFloat(computedStyle.borderBottomWidth)
)
: -Math.floor(
parseFloat(computedStyle.paddingTop) +
parseFloat(computedStyle.paddingBottom)
)
} else {
heightAdjustment = previousMeasurement.heightAdjustment
}
const curHeight = el.clientHeight
const fitHeight = el.scrollHeight
// Clear height if text area is empty
if (value === '') {
el.style.removeProperty('height')
}
// Otherwise, expand to fit text
else if (fitHeight > curHeight) {
el.style.height = fitHeight + heightAdjustment + 'px'
}
previousMeasurementRef.current = { heightAdjustment, value }
}, [])
useEffect(() => {
if (!ref.current || !onResize || !('ResizeObserver' in window)) {
@ -70,6 +100,30 @@ function AutoExpandingTextArea({
}
}, [onResize])
// Implement autofocus manually so that the cursor is placed at the end of
// the textarea content
useEffect(() => {
const el = ref.current
if (!el) {
return
}
resetHeight()
if (autoFocus) {
const cursorPos = el.value.length
el.focus()
el.setSelectionRange(cursorPos, cursorPos)
}
}, [autoFocus, resetHeight])
// Reset height when the value changes via the `value` prop. If the textarea
// is controlled, this means resetHeight is called twice per keypress, but
// this is mitigated by a check on whether the value has actually changed in
// resetHeight()
useEffect(() => {
resetHeight()
}, [rest.value, resetHeight])
return (
<textarea
onChange={callFnsInSequence(onChange, resetHeight)}