autosize-textarea.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import * as React from "react"
  2. import { cn } from "@/lib/utils"
  3. interface UseAutosizeTextAreaProps {
  4. textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>
  5. minHeight?: number
  6. maxHeight?: number
  7. triggerAutoSize: string
  8. }
  9. export const useAutosizeTextArea = ({
  10. textAreaRef,
  11. triggerAutoSize,
  12. maxHeight = Number.MAX_SAFE_INTEGER,
  13. minHeight = 0,
  14. }: UseAutosizeTextAreaProps) => {
  15. const [init, setInit] = React.useState(true)
  16. React.useEffect(() => {
  17. // We need to reset the height momentarily to get the correct scrollHeight
  18. // for the textarea.
  19. const offsetBorder = 6
  20. const textAreaElement = textAreaRef.current
  21. if (textAreaElement) {
  22. if (init) {
  23. textAreaElement.style.minHeight = `${minHeight + offsetBorder}px`
  24. if (maxHeight > minHeight) {
  25. textAreaElement.style.maxHeight = `${maxHeight}px`
  26. }
  27. setInit(false)
  28. }
  29. textAreaElement.style.height = `${minHeight + offsetBorder}px`
  30. const scrollHeight = textAreaElement.scrollHeight
  31. // We then set the height directly, outside of the render loop
  32. // Trying to set this with state or a ref will product an incorrect value.
  33. if (scrollHeight > maxHeight) {
  34. textAreaElement.style.height = `${maxHeight}px`
  35. } else {
  36. textAreaElement.style.height = `${scrollHeight + offsetBorder}px`
  37. }
  38. }
  39. }, [init, minHeight, maxHeight, textAreaRef, triggerAutoSize])
  40. }
  41. export type AutosizeTextAreaRef = {
  42. textArea: HTMLTextAreaElement
  43. minHeight: number
  44. maxHeight: number
  45. }
  46. type AutosizeTextAreaProps = {
  47. minHeight: number
  48. maxHeight: number
  49. } & React.TextareaHTMLAttributes<HTMLTextAreaElement>
  50. export const AutosizeTextarea = React.forwardRef<AutosizeTextAreaRef, AutosizeTextAreaProps>(
  51. (
  52. { minHeight, maxHeight, className, onChange, value, ...props }: AutosizeTextAreaProps,
  53. ref: React.Ref<AutosizeTextAreaRef>,
  54. ) => {
  55. const textAreaRef = React.useRef<HTMLTextAreaElement | null>(null)
  56. const [triggerAutoSize, setTriggerAutoSize] = React.useState("")
  57. useAutosizeTextArea({
  58. textAreaRef,
  59. triggerAutoSize: triggerAutoSize,
  60. maxHeight,
  61. minHeight,
  62. })
  63. React.useImperativeHandle(ref, () => ({
  64. textArea: textAreaRef.current as HTMLTextAreaElement,
  65. focus: () => textAreaRef?.current?.focus(),
  66. maxHeight,
  67. minHeight,
  68. }))
  69. React.useEffect(() => {
  70. setTriggerAutoSize(value as string)
  71. }, [props?.defaultValue, value])
  72. return (
  73. <textarea
  74. {...props}
  75. value={value}
  76. ref={textAreaRef}
  77. className={cn(
  78. "flex w-full rounded-xs ring-offset-background placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50 scrollbar-hide",
  79. "border-[var(--vscode-input-border,var(--vscode-input-background))] focus-visible:border-vscode-focusBorder",
  80. "bg-vscode-input-background",
  81. "text-vscode-input-foreground",
  82. className,
  83. )}
  84. onChange={(e) => {
  85. setTriggerAutoSize(e.target.value)
  86. onChange?.(e)
  87. }}
  88. />
  89. )
  90. },
  91. )
  92. AutosizeTextarea.displayName = "AutosizeTextarea"