mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
069e5ac320
commit
96ba8a92f4
2 changed files with 77 additions and 26 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
|
|
Loading…
Reference in a new issue