mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01: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:
committed by
Copybot
parent
ae771e70c0
commit
caeb6ffb58
+1
-4
@@ -4,9 +4,7 @@ import EntryContainer from './entry-container'
|
|||||||
import EntryCallout from './entry-callout'
|
import EntryCallout from './entry-callout'
|
||||||
import EntryActions from './entry-actions'
|
import EntryActions from './entry-actions'
|
||||||
import Comment from './comment'
|
import Comment from './comment'
|
||||||
import AutoExpandingTextArea, {
|
import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area'
|
||||||
resetHeight,
|
|
||||||
} from '../../../../../shared/components/auto-expanding-text-area'
|
|
||||||
import Icon from '../../../../../shared/components/icon'
|
import Icon from '../../../../../shared/components/icon'
|
||||||
import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
|
import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
@@ -93,7 +91,6 @@ function CommentEntry({
|
|||||||
;(e.target as HTMLTextAreaElement).blur()
|
;(e.target as HTMLTextAreaElement).blur()
|
||||||
submitReply(threadId, replyContent)
|
submitReply(threadId, replyContent)
|
||||||
setReplyContent('')
|
setReplyContent('')
|
||||||
resetHeight(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import { callFnsInSequence } from '../../utils/functions'
|
import { callFnsInSequence } from '../../utils/functions'
|
||||||
import { MergeAndOverride } from '../../../../types/utils'
|
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<
|
type AutoExpandingTextAreaProps = MergeAndOverride<
|
||||||
React.ComponentProps<'textarea'>,
|
React.ComponentProps<'textarea'>,
|
||||||
{
|
{
|
||||||
@@ -33,10 +12,61 @@ type AutoExpandingTextAreaProps = MergeAndOverride<
|
|||||||
function AutoExpandingTextArea({
|
function AutoExpandingTextArea({
|
||||||
onChange,
|
onChange,
|
||||||
onResize,
|
onResize,
|
||||||
|
autoFocus,
|
||||||
...rest
|
...rest
|
||||||
}: AutoExpandingTextAreaProps) {
|
}: AutoExpandingTextAreaProps) {
|
||||||
const ref = useRef<HTMLTextAreaElement>(null)
|
const ref = useRef<HTMLTextAreaElement>(null)
|
||||||
const previousHeightRef = useRef<number | null>(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(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !onResize || !('ResizeObserver' in window)) {
|
if (!ref.current || !onResize || !('ResizeObserver' in window)) {
|
||||||
@@ -70,6 +100,30 @@ function AutoExpandingTextArea({
|
|||||||
}
|
}
|
||||||
}, [onResize])
|
}, [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 (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
onChange={callFnsInSequence(onChange, resetHeight)}
|
onChange={callFnsInSequence(onChange, resetHeight)}
|
||||||
|
|||||||
Reference in New Issue
Block a user