import { cloneElement, forwardRef, Fragment, ReactElement, ReactNode, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react'; import classnames from 'classnames'; import { TOKEN_SPACE_100 } from '@swipebox/morphe-design-tokens'; import styles from './TagArea.css'; import Box from '../Box'; import boxStyles from '../Box.css'; import Flex from '../Flex'; import FormErrorMessage from '../sharedSubcomponents/FormErrorMessage'; import FormHelperText from '../sharedSubcomponents/FormHelperText'; import { MaxLength } from '../TextField'; import TextUI from '../TextUI'; import useInteractiveStates from '../utils/useInteractiveStates'; type Props = { // REQUIRED id: string; onChange: (arg1: { event: React.ChangeEvent; value: string }) => void; // OPTIONAL accessibilityControls?: string; accessibilityActiveDescendant?: string; dataTestId?: string; disabled?: boolean; errorMessage?: ReactNode; hasError?: boolean; helperText?: string; iconButton?: ReactElement; label?: string; labelDisplay?: 'visible' | 'hidden'; maxLength?: MaxLength | null | undefined; name?: string; onBlur?: (arg1: { event: React.FocusEvent; value: string }) => void; onClick?: (arg1: { event: React.MouseEvent; value: string }) => void; onFocus?: (arg1: { event: React.FocusEvent; value: string }) => void; onKeyDown?: (arg1: { event: React.KeyboardEvent; value: string }) => void; placeholder?: string; readOnly?: boolean; readOnlyNoIconButton?: boolean; size: 'sm' | 'md' | 'lg'; tags: ReadonlyArray; value?: string; }; const TagAreaWithForwardRef = forwardRef(function TagArea( { accessibilityControls, accessibilityActiveDescendant, dataTestId, disabled = false, errorMessage, hasError = false, helperText, iconButton, id, label, labelDisplay, maxLength, name, onBlur, onChange, onClick, onFocus, onKeyDown, placeholder, readOnly, readOnlyNoIconButton, size, tags, value, }: Props, ref, ) { const innerRef = useRef(null); const labelRef = useRef(null); // @ts-expect-error - TS2322 - Type 'HTMLDivElement | HTMLTextAreaElement | null' is not assignable to type 'HTMLTextAreaElement'. useImperativeHandle(ref, () => innerRef.current); // ==== STATE ==== const [currentLength, setCurrentLength] = useState(value?.length ?? 0); const [ellipsisActive, setEllipsisActive] = useState(false); const { handleOnBlur, handleOnFocus, handleOnMouseEnter, handleOnMouseLeave, isHovered, isFocused, } = useInteractiveStates(); const isEllipsisActive = (element: HTMLElement) => element.offsetHeight < element.scrollHeight || element.offsetWidth < element.scrollWidth; const checkEllipsisActive = useCallback(() => { if (labelRef.current && !ellipsisActive && isEllipsisActive(labelRef?.current)) { setEllipsisActive(true); } else if (labelRef.current && ellipsisActive && !isEllipsisActive(labelRef?.current)) { setEllipsisActive(false); } }, [ellipsisActive]); // ==== VARIABLES ==== const hasErrorMessage = Boolean(errorMessage); const isLabelVisible = labelDisplay === 'visible'; const isSM = size === 'sm'; const isMD = size === 'md'; const isLG = size === 'lg'; let ariaDescribedby; if (hasErrorMessage) { ariaDescribedby = `${id}-error`; } if (helperText || maxLength) { ariaDescribedby = `${id}-helperText`; } useEffect(() => { if (!label) return () => {}; checkEllipsisActive(); if (typeof window !== 'undefined') window.addEventListener('resize', checkEllipsisActive); return () => { if (typeof window !== 'undefined') window?.removeEventListener('resize', checkEllipsisActive); }; }, [label, checkEllipsisActive]); const inputElement = ( { handleOnBlur(); onBlur?.({ event, value: event.currentTarget.value }); }} onChange={(event) => { setCurrentLength(event.currentTarget.value?.length ?? 0); onChange({ event, value: event.currentTarget.value }); }} onClick={(event) => onClick?.({ event, value: event.currentTarget.value })} onFocus={(event) => { handleOnFocus(); onFocus?.({ event, value: event.currentTarget.value }); }} onKeyDown={(event) => onKeyDown?.({ event, value: event.currentTarget.value })} placeholder={placeholder} readOnly={readOnly} value={value} /> ); return (
{label && ( )} {tags.length > 0 ? ( {tags.map((tag, tagIndex) => ( {cloneElement(tag, { size: size === 'lg' ? 'md' : 'sm', disabled: disabled || readOnly, })} ))} {inputElement} ) : ( inputElement )} {!disabled && !readOnlyNoIconButton && iconButton}
{(helperText || maxLength) && !hasErrorMessage ? ( ) : null} {!disabled && hasErrorMessage ? ( ) : null}
); }); TagAreaWithForwardRef.displayName = 'TagArea'; export default TagAreaWithForwardRef;