UNPKG

@carbon/react

Version:

React components for the Carbon Design System

439 lines (430 loc) 16.1 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var PropTypes = require('prop-types'); var React = require('react'); var cx = require('classnames'); var deprecate = require('../../prop-types/deprecate.js'); var iconsReact = require('@carbon/icons-react'); var usePrefix = require('../../internal/usePrefix.js'); require('../FluidForm/FluidForm.js'); var FormContext = require('../FluidForm/FormContext.js'); var getAnnouncement = require('../../internal/getAnnouncement.js'); var useIsomorphicEffect = require('../../internal/useIsomorphicEffect.js'); var useMergedRefs = require('../../internal/useMergedRefs.js'); var useId = require('../../internal/useId.js'); var noopFn = require('../../internal/noopFn.js'); var Text = require('../Text/Text.js'); require('../Text/TextDirection.js'); var index = require('../AILabel/index.js'); var utils = require('../../internal/utils.js'); const frFn = React.forwardRef; const TextArea = frFn((props, forwardRef) => { const { className, decorator, disabled = false, id, labelText, hideLabel, onChange = noopFn.noopFn, onClick = noopFn.noopFn, onKeyDown = noopFn.noopFn, invalid = false, invalidText = '', helperText = '', light, placeholder = '', enableCounter = false, maxCount, counterMode = 'character', warn = false, warnText = '', rows = 4, slug, ...other } = props; const prefix = usePrefix.usePrefix(); const { isFluid } = React.useContext(FormContext.FormContext); const { defaultValue, value } = other; const textAreaInstanceId = useId.useId(); const wrapperRef = React.useRef(null); const textareaRef = React.useRef(null); const helperTextRef = React.useRef(null); const errorTextRef = React.useRef(null); const warnTextRef = React.useRef(null); const ref = useMergedRefs.useMergedRefs([forwardRef, textareaRef]); function getInitialTextCount() { const targetValue = defaultValue || value || textareaRef.current?.value || ''; const strValue = targetValue.toString(); if (counterMode === 'character') { return strValue.length; } else { return strValue.match(/\p{L}+/gu)?.length || 0; } } const [textCount, setTextCount] = React.useState(getInitialTextCount()); React.useEffect(() => { setTextCount(getInitialTextCount()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, defaultValue, counterMode]); useIsomorphicEffect.default(() => { if (other.cols && textareaRef.current) { textareaRef.current.style.width = ''; textareaRef.current.style.resize = 'none'; } else if (textareaRef.current) { textareaRef.current.style.width = `100%`; } if (!wrapperRef.current) return; const applyWidth = width => { [helperTextRef, errorTextRef, warnTextRef].forEach(r => { if (r.current) { r.current.style.maxWidth = `${width}px`; r.current.style.overflowWrap = 'break-word'; } }); }; const resizeObserver = new ResizeObserver(([entry]) => { applyWidth(entry.contentRect.width); }); resizeObserver.observe(wrapperRef.current); return () => resizeObserver && resizeObserver.disconnect(); }, [other.cols, invalid, warn]); const textareaProps = { id, onKeyDown: evt => { if (!disabled && enableCounter && counterMode === 'word') { const key = evt.which; if (maxCount && textCount >= maxCount && key === 32 || maxCount && textCount >= maxCount && key === 13) { evt.preventDefault(); } } if (!disabled && onKeyDown) { onKeyDown(evt); } }, onPaste: evt => { if (!disabled) { if (counterMode === 'word' && enableCounter && typeof maxCount !== 'undefined' && textareaRef.current !== null) { const existingWords = textareaRef.current.value.match(/\p{L}+/gu) || []; const pastedWords = evt.clipboardData.getData('Text').match(/\p{L}+/gu) || []; const totalWords = existingWords.length + pastedWords.length; if (totalWords > maxCount) { evt.preventDefault(); const allowedWords = existingWords.concat(pastedWords).slice(0, maxCount); setTimeout(() => { setTextCount(maxCount); }, 0); textareaRef.current.value = allowedWords.join(' '); } } } }, onChange: evt => { if (!disabled) { if (counterMode == 'character') { evt?.persist?.(); // delay textCount assignation to give the textarea element value time to catch up if is a controlled input setTimeout(() => { setTextCount(evt.target?.value?.length); }, 0); } else if (counterMode == 'word') { if (!evt.target.value) { setTimeout(() => { setTextCount(0); }, 0); } else if (enableCounter && typeof maxCount !== 'undefined' && textareaRef.current !== null) { const matchedWords = evt.target?.value?.match(/\p{L}+/gu); if (matchedWords && matchedWords.length <= maxCount) { textareaRef.current.removeAttribute('maxLength'); setTimeout(() => { setTextCount(matchedWords.length); }, 0); } else if (matchedWords && matchedWords.length > maxCount) { setTimeout(() => { setTextCount(matchedWords.length); }, 0); } } } if (onChange) { onChange(evt); } } }, onClick: evt => { if (!disabled && onClick) { onClick(evt); } } }; const formItemClasses = cx(`${prefix}--form-item`, className); const textAreaWrapperClasses = cx(`${prefix}--text-area__wrapper`, { [`${prefix}--text-area__wrapper--cols`]: other.cols, [`${prefix}--text-area__wrapper--readonly`]: other.readOnly, [`${prefix}--text-area__wrapper--warn`]: warn, [`${prefix}--text-area__wrapper--slug`]: slug, [`${prefix}--text-area__wrapper--decorator`]: decorator }); const labelClasses = cx(`${prefix}--label`, { [`${prefix}--visually-hidden`]: hideLabel && !isFluid, [`${prefix}--label--disabled`]: disabled }); const textareaClasses = cx(`${prefix}--text-area`, { [`${prefix}--text-area--light`]: light, [`${prefix}--text-area--invalid`]: invalid, [`${prefix}--text-area--warn`]: warn }); const counterClasses = cx(`${prefix}--label`, { [`${prefix}--label--disabled`]: disabled, [`${prefix}--text-area__label-counter`]: true }); const helperTextClasses = cx(`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: disabled }); const label = typeof labelText !== 'undefined' && labelText !== null && /*#__PURE__*/React.createElement(Text.Text, { as: "label", htmlFor: id, className: labelClasses }, labelText); const counter = enableCounter && maxCount && (counterMode === 'character' || counterMode === 'word') ? /*#__PURE__*/React.createElement(Text.Text, { as: "div", className: counterClasses, "aria-hidden": "true" }, `${textCount}/${maxCount}`) : null; const counterDescriptionId = enableCounter && maxCount ? `${id}-counter-desc` : undefined; const hasHelper = typeof helperText !== 'undefined' && helperText !== null; const helperId = !hasHelper ? undefined : `text-area-helper-text-${textAreaInstanceId}`; const helper = hasHelper && /*#__PURE__*/React.createElement(Text.Text, { as: "div", id: helperId, className: helperTextClasses, ref: helperTextRef }, helperText); const errorId = id + '-error-msg'; const error = invalid ? /*#__PURE__*/React.createElement(Text.Text, { as: "div", role: "alert", className: `${prefix}--form-requirement`, id: errorId, ref: errorTextRef }, invalidText, isFluid && /*#__PURE__*/React.createElement(iconsReact.WarningFilled, { className: `${prefix}--text-area__invalid-icon` })) : null; const warnId = id + '-warn-msg'; const warning = warn ? /*#__PURE__*/React.createElement(Text.Text, { as: "div", role: "alert", className: `${prefix}--form-requirement`, id: warnId, ref: warnTextRef }, warnText, isFluid && /*#__PURE__*/React.createElement(iconsReact.WarningAltFilled, { className: `${prefix}--text-area__invalid-icon ${prefix}--text-area__invalid-icon--warning` })) : null; let ariaDescribedBy; if (invalid) { ariaDescribedBy = errorId; } else if (warn && !isFluid) { ariaDescribedBy = warnId; } else { const ids = []; if (!isFluid && helperText && helperId) ids.push(helperId); if (counterDescriptionId) ids.push(counterDescriptionId); ariaDescribedBy = ids.length > 0 ? ids.join(' ') : undefined; } if (enableCounter) { // handle different counter mode if (counterMode == 'character') { textareaProps.maxLength = maxCount; } } const announcerRef = React.useRef(null); const [prevAnnouncement, setPrevAnnouncement] = React.useState(''); const ariaAnnouncement = getAnnouncement.getAnnouncement(textCount, maxCount, counterMode === 'word' ? 'word' : undefined, counterMode === 'word' ? 'words' : undefined); React.useEffect(() => { if (ariaAnnouncement && ariaAnnouncement !== prevAnnouncement) { const announcer = announcerRef.current; if (announcer) { // Clear the content first announcer.textContent = ''; // Set the new content after a small delay const timeoutId = setTimeout(() => { if (announcer) { announcer.textContent = ariaAnnouncement; setPrevAnnouncement(ariaAnnouncement); } }, counterMode === 'word' ? 2000 : 1000); //clear the timeout return () => { if (timeoutId) { clearTimeout(timeoutId); } }; } } }, [ariaAnnouncement, prevAnnouncement, counterMode]); const input = /*#__PURE__*/React.createElement("textarea", _rollupPluginBabelHelpers.extends({}, other, textareaProps, { placeholder: placeholder, "aria-readonly": Boolean(other.readOnly), className: textareaClasses, "aria-invalid": invalid, "aria-describedby": ariaDescribedBy, disabled: disabled, rows: rows, readOnly: other.readOnly, ref: ref })); // AILabel is always size `mini` const candidate = slug ?? decorator; const candidateIsAILabel = utils.isComponentElement(candidate, index.AILabel); const normalizedDecorator = candidateIsAILabel ? /*#__PURE__*/React.cloneElement(candidate, { size: 'mini' }) : candidate; return /*#__PURE__*/React.createElement("div", { className: formItemClasses }, /*#__PURE__*/React.createElement("div", { className: `${prefix}--text-area__label-wrapper` }, label, counter), enableCounter && maxCount && /*#__PURE__*/React.createElement("span", { id: counterDescriptionId, className: `${prefix}--visually-hidden` }, counterMode === 'word' ? `Word limit ${maxCount}` : `Character limit ${maxCount}`), /*#__PURE__*/React.createElement("div", { ref: wrapperRef, className: textAreaWrapperClasses, "data-invalid": invalid || null }, invalid && !isFluid && /*#__PURE__*/React.createElement(iconsReact.WarningFilled, { className: `${prefix}--text-area__invalid-icon` }), warn && !invalid && !isFluid && /*#__PURE__*/React.createElement(iconsReact.WarningAltFilled, { className: `${prefix}--text-area__invalid-icon ${prefix}--text-area__invalid-icon--warning` }), input, slug ? normalizedDecorator : decorator ? /*#__PURE__*/React.createElement("div", { className: `${prefix}--text-area__inner-wrapper--decorator` }, normalizedDecorator) : '', /*#__PURE__*/React.createElement("span", { className: `${prefix}--text-area__counter-alert`, role: "alert", "aria-live": "assertive", "aria-atomic": "true", ref: announcerRef }, ariaAnnouncement), isFluid && /*#__PURE__*/React.createElement("hr", { className: `${prefix}--text-area__divider` }), isFluid && invalid ? error : null, isFluid && warn && !invalid ? warning : null), !invalid && !warn && !isFluid ? helper : null, invalid && !isFluid ? error : null, warn && !invalid && !isFluid ? warning : null); }); TextArea.displayName = 'TextArea'; TextArea.propTypes = { /** * Provide a custom className that is applied directly to the underlying * `<textarea>` node */ className: PropTypes.string, /** * Specify the `cols` attribute for the underlying `<textarea>` node */ cols: PropTypes.number, /** * Specify the method used for calculating the counter number */ counterMode: PropTypes.oneOf(['character', 'word']), /** * **Experimental**: Provide a `decorator` component to be rendered inside the `TextArea` component */ decorator: PropTypes.node, /** * Optionally provide the default value of the `<textarea>` */ defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Specify whether the control is disabled */ disabled: PropTypes.bool, /** * Specify whether to display the counter */ enableCounter: PropTypes.bool, /** * Provide text that is used alongside the control label for additional help */ helperText: PropTypes.node, /** * Specify whether you want the underlying label to be visually hidden */ hideLabel: PropTypes.bool, /** * Provide a unique identifier for the control */ id: PropTypes.string, /** * Specify whether the control is currently invalid */ invalid: PropTypes.bool, /** * Provide the text that is displayed when the control is in an invalid state */ invalidText: PropTypes.node, /** * Provide the text that will be read by a screen reader when visiting this * control */ labelText: PropTypes.node.isRequired, /** * `true` to use the light version. For use on $ui-01 backgrounds only. * Don't use this to make tile background color same as container background color. */ light: deprecate.deprecate(PropTypes.bool, 'The `light` prop for `TextArea` has ' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'), /** * Max entity count allowed for the textarea. This is needed in order for enableCounter to display */ maxCount: PropTypes.number, /** * Optionally provide an `onChange` handler that is called whenever `<textarea>` * is updated */ onChange: PropTypes.func, /** * Optionally provide an `onClick` handler that is called whenever the * `<textarea>` is clicked */ onClick: PropTypes.func, /** * Optionally provide an `onKeyDown` handler that is called whenever `<textarea>` * is keyed */ onKeyDown: PropTypes.func, /** * Specify the placeholder attribute for the `<textarea>` */ placeholder: PropTypes.string, /** * Whether the textarea should be read-only */ readOnly: PropTypes.bool, /** * Specify the rows attribute for the `<textarea>` */ rows: PropTypes.number, /** * **Experimental**: Provide a `Slug` component to be rendered inside the `TextArea` component */ slug: deprecate.deprecate(PropTypes.node, 'The `slug` prop for `TextArea` has ' + 'been deprecated in favor of the new `decorator` prop. It will be removed in the next major release.'), /** * Provide the current value of the `<textarea>` */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Specify whether the control is currently in warning state */ warn: PropTypes.bool, /** * Provide the text that is displayed when the control is in warning state */ warnText: PropTypes.node }; exports.default = TextArea;