import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import mergeRefs from 'react-merge-refs'; import TextareaAutosize from 'react-textarea-autosize'; import cn from 'classnames'; import { FormControl } from '@alfalab/core-components-form-control'; import { useIsDesktop } from '@alfalab/core-components-mq'; import { getDataTestId } from '@alfalab/core-components-shared'; import { useFocus } from '@alfalab/hooks'; import { PseudoTextArea } from './components'; import { type TextareaProps } from './typings'; import defaultColors from './default.module.css'; import styles from './index.module.css'; import invertedColors from './inverted.module.css'; const colorStyles = { default: defaultColors, inverted: invertedColors, }; export const getDefaultCounterText = (textLength: number, maxLength = 0): string => `${textLength}/${maxLength} символов`; export const Textarea = forwardRef( ( { autoComplete = 'on', autosize = true, size = 48, colors = 'default', block = false, bottomAddons, fieldClassName, className, dataTestId, disabled, error, hint, textareaClassName, label, labelView = 'inner', leftAddons, onFocus, onBlur, onChange, onHeightChange, rightAddons, maxRows, minRows, maxHeight, resize = 'none', value, defaultValue, rows = autosize ? 1 : 3, showCounter = false, getCounterText = getDefaultCounterText, maxLength, allowOverflow = true, nativeScrollbar: nativeScrollbarProp, wrapperRef, breakpoint, client, ...restProps }, ref, ) => { const uncontrolled = value === undefined; const isDesktop = useIsDesktop(breakpoint, client === 'desktop'); let nativeScrollbar = !isDesktop; nativeScrollbar = Boolean(nativeScrollbarProp ?? nativeScrollbar); const [textareaNode, setTextareaNode] = useState(null); const pseudoTextareaRef = useRef(null); const [focused, setFocused] = useState(false); const [stateValue, setStateValue] = useState(defaultValue || ''); const [scrollPosition, setScrollPosition] = useState(0); const [focusVisible] = useFocus( useMemo(() => ({ current: textareaNode }), [textareaNode]), 'keyboard', ); const filled = Boolean(uncontrolled ? stateValue : value); const hasInnerLabel = label && labelView === 'inner'; const hasOverflow = Boolean( maxLength && allowOverflow && (value?.slice(maxLength) || stateValue.slice(maxLength)), ); useEffect(() => { const pseudoNode = pseudoTextareaRef.current; if (pseudoNode) { pseudoNode.scrollTop = scrollPosition; } }, [scrollPosition, stateValue, hasOverflow]); // Хак, так как react-textarea-autosize перестал поддерживать maxHeight useEffect(() => { if (autosize && maxHeight && textareaNode?.style) { textareaNode.style.maxHeight = `${maxHeight}px`; } }, [autosize, maxHeight, textareaNode]); const handleTextareaFocus = (event: React.FocusEvent) => { setFocused(true); if (onFocus) { onFocus(event); } }; const handleTextareaBlur = (event: React.FocusEvent) => { setFocused(false); if (onBlur) { onBlur(event); } }; const handleTextareaChange = (event: React.ChangeEvent) => { const { target: { value: newValue }, } = event; if (onChange) { onChange(event, { value: newValue }); } if (uncontrolled) { setStateValue(newValue); } }; const handleTeaxtareaScroll = (event: React.UIEvent) => { if (maxLength) { const val = (event.target as HTMLElement).scrollTop; setScrollPosition(val); } }; const getValueLength = (): number => { if (uncontrolled) { return stateValue.length; } return value.length; }; const textareaClassNameCalc = cn( styles.textarea, colorStyles[colors].textarea, styles[`size-${size}`], { [styles.overflowHidden]: autosize && !maxRows, [styles.customScrollbar]: !nativeScrollbar, [styles.hasInnerLabel]: hasInnerLabel, [colorStyles[colors].hasInnerLabel]: hasInnerLabel, [styles.filled]: filled, [styles.overflow]: hasOverflow, }, textareaClassName, ); const textareaProps = { ...restProps, className: textareaClassNameCalc, autoComplete, disabled, maxLength: allowOverflow ? undefined : maxLength, onBlur: handleTextareaBlur, onFocus: handleTextareaFocus, onChange: handleTextareaChange, value: uncontrolled ? stateValue : value, rows, ref: mergeRefs([ref, setTextareaNode]), 'data-test-id': dataTestId, onScroll: handleTeaxtareaScroll, }; const getBottomAddons = () => { const counterIsVisible = Boolean(maxLength && showCounter); return ( {counterIsVisible && ( {getCounterText(getValueLength(), maxLength)} )} {bottomAddons} ); }; return ( {hasOverflow && ( )} {autosize ? ( ) : (