فهرست منبع

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
Mathias Jakobsen 2 سال پیش
والد
کامیت
96ba8a92f4

+ 1 - 4
services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx

@@ -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)
       }
     }
   }

+ 76 - 22
services/web/frontend/js/shared/components/auto-expanding-text-area.tsx

@@ -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)}