UNPKG

@syncfusion/react-inputs

Version:

Syncfusion React Input package is a feature-rich collection of UI components, including Textbox, Textarea, Numeric-textbox and Form, designed to capture user input in React applications.

252 lines (251 loc) 10.8 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, useLayoutEffect, useMemo } from 'react'; import { CLASS_NAMES, renderClearButton, renderFloatLabelElement, renderAdornmentElement } from '../common/inputbase'; import { preRender, useProviderContext, Variant, Size, useStableId } from '@syncfusion/react-base'; import { HelperText } from '../common/helper-text'; export { Variant, Size }; /** * Constant for multi-line input class */ const MULTILINE = 'sf-multi-line-input'; /** * Constant for auto-width class */ const AUTOWIDTH = 'sf-auto-width'; /** * Specifies the available resize modes for components that support resizing. * * @enum {string} */ export var ResizeMode; (function (ResizeMode) { /** * Disables resizing functionality. */ ResizeMode["None"] = "None"; /** * Enables resizing in both horizontal and vertical directions. */ ResizeMode["Both"] = "Both"; /** * Enables resizing only in the horizontal direction. */ ResizeMode["Horizontal"] = "Horizontal"; /** * Enables resizing only in the vertical direction. */ ResizeMode["Vertical"] = "Vertical"; })(ResizeMode || (ResizeMode = {})); const RESIZE_MAP = { /** * Constant for no resize mode */ [ResizeMode.None]: 'sf-resize-none', /** * Constant for both horizontal and vertical resize mode */ [ResizeMode.Both]: 'sf-resize-xy', /** * Constant for horizontal resize mode */ [ResizeMode.Horizontal]: 'sf-resize-x', /** * Constant for vertical resize mode */ [ResizeMode.Vertical]: 'sf-resize-y' }; /** * TextArea component that provides a multi-line text input field with enhanced functionality. * Supports both controlled and uncontrolled modes based on presence of value or defaultValue prop. * * ```typescript * import { TextArea } from '@syncfusion/react-inputs'; * * <TextArea defaultValue="Initial text" placeholder="Enter text" rows={5} cols={40} /> * ``` */ export const TextArea = forwardRef((props, ref) => { const { readOnly = false, value, defaultValue, labelMode = 'Never', placeholder = '', disabled = false, width, resizeMode = ResizeMode.Both, maxLength, cols = null, rows = null, clearButton = false, autoResize = false, className = '', id, size = Size.Medium, variant, onChange, onBlur, onFocus, prefix, suffix, helperText, helperTextOnFocus = false, helperTextDirection = 'Left', ...rest } = props; const isControlled = value !== undefined; const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue || ''); const displayValue = isControlled ? value : uncontrolledValue; const [isFocused, setIsFocused] = useState(false); const generatedId = useStableId('sf-textarea'); const textareaId = id ?? generatedId; const elementRef = useRef(null); const autoResizeFrameIdRef = useRef(null); const resizeObserverInstanceRef = useRef(null); const lastObservedInlineSizeRef = useRef(null); const { locale, dir } = useProviderContext(); const publicAPI = { clearButton, labelMode, disabled, readOnly, resizeMode, helperText, helperTextOnFocus, helperTextDirection }; useEffect(() => { preRender('textarea'); }, []); useEffect(() => { if (isControlled) { setUncontrolledValue(value || ''); } }, [isControlled, value]); const classNames = (...classes) => { return classes.filter(Boolean).join(' '); }; const containerClassNames = useMemo(() => classNames(CLASS_NAMES.INPUTGROUP, CLASS_NAMES.WRAPPER, labelMode !== 'Never' ? CLASS_NAMES.FLOATINPUT : '', MULTILINE, className, dir === 'rtl' ? CLASS_NAMES.RTL : '', disabled ? CLASS_NAMES.DISABLE : '', readOnly ? CLASS_NAMES.READONLY : '', isFocused ? CLASS_NAMES.TEXTBOX_FOCUS : '', displayValue !== '' ? CLASS_NAMES.VALIDINPUT : '', variant && variant.toLowerCase() !== 'standard' ? variant.toLowerCase() === 'outlined' ? 'sf-outline' : `sf-${variant.toLowerCase()}` : '', AUTOWIDTH, size && size.toLowerCase() !== 'small' ? `sf-${size.toLowerCase()}` : '', prefix ? 'sf-has-prefix' : '', suffix ? 'sf-has-suffix' : '', 'sf-control'), [isFocused, displayValue, disabled, readOnly, labelMode, variant, size, prefix, suffix, dir, className]); useImperativeHandle(ref, () => ({ ...publicAPI, element: elementRef.current })); const adjustHeight = useCallback((textAreaElement) => { if (!textAreaElement) { return; } textAreaElement.style.height = 'auto'; textAreaElement.style.height = `${textAreaElement.scrollHeight}px`; }, []); const requestAdjustHeight = useCallback(() => { if (!autoResize || !elementRef.current) { return; } if (autoResizeFrameIdRef.current !== null) { cancelAnimationFrame(autoResizeFrameIdRef.current); autoResizeFrameIdRef.current = null; } autoResizeFrameIdRef.current = requestAnimationFrame(() => { adjustHeight(elementRef.current); }); }, [autoResize, adjustHeight]); const getObservedInlineSize = (entry) => { const firstBox = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; if (firstBox && typeof firstBox.inlineSize === 'number') { return firstBox.inlineSize; } const width = entry.contentRect.width; return typeof width === 'number' ? width : null; }; useEffect(() => { return () => { if (autoResizeFrameIdRef.current !== null) { cancelAnimationFrame(autoResizeFrameIdRef.current); autoResizeFrameIdRef.current = null; } }; }, []); useEffect(() => { if (!autoResize || !elementRef.current) { return; } let frameScheduled = false; const scheduleAdjustHeight = () => { if (frameScheduled) { return; } frameScheduled = true; requestAnimationFrame(() => { frameScheduled = false; if (!elementRef.current) { return; } requestAdjustHeight(); }); }; if ('ResizeObserver' in window) { const handleObservedSizeChange = (entries) => { const entry = entries[0]; if (!entry) { return; } const inlineSize = getObservedInlineSize(entry); if (inlineSize !== null && lastObservedInlineSizeRef.current === inlineSize) { return; } lastObservedInlineSizeRef.current = inlineSize; scheduleAdjustHeight(); }; const observer = new ResizeObserver(handleObservedSizeChange); observer.observe(elementRef.current); resizeObserverInstanceRef.current = observer; return () => { observer.disconnect(); resizeObserverInstanceRef.current = null; lastObservedInlineSizeRef.current = null; }; } const view = elementRef.current.ownerDocument ? elementRef.current.ownerDocument.defaultView : null; if (!view) { return; } const handleWindowResize = () => { scheduleAdjustHeight(); }; view.addEventListener('resize', handleWindowResize); return () => { view.removeEventListener('resize', handleWindowResize); }; }, [autoResize, requestAdjustHeight]); useLayoutEffect(() => { if (!autoResize || !elementRef.current) { return; } requestAdjustHeight(); }, [autoResize, displayValue, width, rows, cols]); const handleChange = useCallback((event) => { const newValue = event.target.value; if (!isControlled) { setUncontrolledValue(newValue); } if (onChange) { onChange({ event, value: newValue }); } if (autoResize) { requestAdjustHeight(); } }, [isControlled, onChange, uncontrolledValue, value, autoResize, requestAdjustHeight]); const handleFocus = useCallback((event) => { setIsFocused(true); if (onFocus) { onFocus(event); } }, [onFocus]); const handleBlur = useCallback((event) => { setIsFocused(false); if (onBlur) { onBlur(event); } }, [onBlur]); const clearValue = useCallback(() => { const newValue = ''; if (!isControlled) { setUncontrolledValue(newValue); if (elementRef.current) { elementRef.current.value = newValue; } } if (onChange) { onChange({ value: newValue, event: undefined }); } if (autoResize) { requestAdjustHeight(); } }, [onChange, isControlled, requestAdjustHeight]); const getCurrentResizeClass = (resizeMode) => { return RESIZE_MAP[`${resizeMode}`]; }; return (_jsxs(_Fragment, { children: [_jsxs("div", { className: containerClassNames, children: [renderAdornmentElement(prefix), _jsx("textarea", { ref: elementRef, id: textareaId, value: isControlled ? (value) : undefined, defaultValue: !isControlled ? (defaultValue) : undefined, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, readOnly: readOnly, placeholder: labelMode === 'Never' ? placeholder : undefined, disabled: disabled, maxLength: maxLength, cols: cols ?? undefined, rows: rows ?? undefined, ...rest, style: { width: width ? (typeof width === 'number' ? `${width}px` : width) : undefined, resize: resizeMode === 'None' || autoResize ? 'none' : undefined, overflowY: autoResize ? 'hidden' : undefined }, className: `sf-textarea sf-lib sf-input ${getCurrentResizeClass(resizeMode)}`, "aria-labelledby": `label_${textareaId}` }), renderFloatLabelElement(labelMode, isFocused || (displayValue) !== '', displayValue, placeholder, textareaId), clearButton && renderClearButton((displayValue && isFocused) ? (displayValue).toString() : '', clearValue, clearButton, 'textarea', locale), renderAdornmentElement(suffix)] }), _jsx(HelperText, { helperText: helperText, helperTextOnFocus: helperTextOnFocus, isFocused: isFocused, helperTextDirection: helperTextDirection })] })); }); export default TextArea;