UNPKG

@carbon/react

Version:

React components for the Carbon Design System

329 lines (327 loc) 13.8 kB
/** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const require_runtime = require("../../_virtual/_rolldown/runtime.js"); const require_usePrefix = require("../../internal/usePrefix.js"); const require_Text = require("../Text/Text.js"); const require_useIsomorphicEffect = require("../../internal/useIsomorphicEffect.js"); const require_useId = require("../../internal/useId.js"); const require_noopFn = require("../../internal/noopFn.js"); const require_deprecate = require("../../prop-types/deprecate.js"); const require_utils = require("../../internal/utils.js"); const require_useMergedRefs = require("../../internal/useMergedRefs.js"); const require_index = require("../AILabel/index.js"); const require_FormContext = require("../FluidForm/FormContext.js"); const require_getAnnouncement = require("../../internal/getAnnouncement.js"); let classnames = require("classnames"); classnames = require_runtime.__toESM(classnames); let react = require("react"); react = require_runtime.__toESM(react); let prop_types = require("prop-types"); prop_types = require_runtime.__toESM(prop_types); let react_jsx_runtime = require("react/jsx-runtime"); let _carbon_icons_react = require("@carbon/icons-react"); //#region src/components/TextArea/TextArea.tsx /** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const TextArea = (0, react.forwardRef)((props, forwardRef) => { const { className, decorator, disabled = false, id, labelText, hideLabel, onChange = require_noopFn.noopFn, onClick = require_noopFn.noopFn, onKeyDown = require_noopFn.noopFn, invalid = false, invalidText = "", helperText, light, placeholder = "", enableCounter = false, maxCount, counterMode = "character", warn = false, warnText = "", rows = 4, slug, ...other } = props; const prefix = require_usePrefix.usePrefix(); const { isFluid } = (0, react.useContext)(require_FormContext.FormContext); const { defaultValue, value } = other; const textAreaInstanceId = require_useId.useId(); const wrapperRef = (0, react.useRef)(null); const textareaRef = (0, react.useRef)(null); const helperTextRef = (0, react.useRef)(null); const errorTextRef = (0, react.useRef)(null); const warnTextRef = (0, react.useRef)(null); const ref = require_useMergedRefs.useMergedRefs([forwardRef, textareaRef]); function getInitialTextCount() { const strValue = (defaultValue || value || textareaRef.current?.value || "").toString(); if (counterMode === "character") return strValue.length; else return strValue.match(/\p{L}+/gu)?.length || 0; } const [textCount, setTextCount] = (0, react.useState)(getInitialTextCount()); (0, react.useEffect)(() => { setTextCount(getInitialTextCount()); }, [ value, defaultValue, counterMode ]); require_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) || []; if (existingWords.length + pastedWords.length > 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?.(); 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 = (0, classnames.default)(`${prefix}--form-item`, className); const textAreaWrapperClasses = (0, classnames.default)(`${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 = (0, classnames.default)(`${prefix}--label`, { [`${prefix}--visually-hidden`]: hideLabel && !isFluid, [`${prefix}--label--disabled`]: disabled }); const textareaClasses = (0, classnames.default)(`${prefix}--text-area`, { [`${prefix}--text-area--light`]: light, [`${prefix}--text-area--invalid`]: invalid, [`${prefix}--text-area--warn`]: warn }); const counterClasses = (0, classnames.default)(`${prefix}--label`, { [`${prefix}--label--disabled`]: disabled, [`${prefix}--text-area__label-counter`]: true }); const helperTextClasses = (0, classnames.default)(`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: disabled }); const label = typeof labelText !== "undefined" && labelText !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, { as: "label", htmlFor: id, className: labelClasses, children: labelText }); const counter = enableCounter && maxCount && (counterMode === "character" || counterMode === "word") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, { as: "div", className: counterClasses, "aria-hidden": "true", children: `${textCount}/${maxCount}` }) : null; const counterDescriptionId = enableCounter && maxCount ? `${id}-counter-desc` : void 0; const hasHelper = typeof helperText !== "undefined" && helperText !== null; const helperId = !hasHelper ? void 0 : `text-area-helper-text-${textAreaInstanceId}`; const helper = hasHelper && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, { as: "div", id: helperId, className: helperTextClasses, ref: helperTextRef, children: helperText }); const errorId = id + "-error-msg"; const error = invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_Text.Text, { as: "div", role: "alert", className: `${prefix}--form-requirement`, id: errorId, ref: errorTextRef, children: [invalidText, isFluid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.WarningFilled, { className: `${prefix}--text-area__invalid-icon` })] }) : null; const warnId = id + "-warn-msg"; const warning = warn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_Text.Text, { as: "div", role: "alert", className: `${prefix}--form-requirement`, id: warnId, ref: warnTextRef, children: [warnText, isFluid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.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 && hasHelper && helperId) ids.push(helperId); if (counterDescriptionId) ids.push(counterDescriptionId); ariaDescribedBy = ids.length > 0 ? ids.join(" ") : void 0; } if (enableCounter) { if (counterMode == "character") textareaProps.maxLength = maxCount; } const announcerRef = (0, react.useRef)(null); const [prevAnnouncement, setPrevAnnouncement] = (0, react.useState)(""); const ariaAnnouncement = require_getAnnouncement.getAnnouncement(textCount, maxCount, counterMode === "word" ? "word" : void 0, counterMode === "word" ? "words" : void 0); (0, react.useEffect)(() => { if (ariaAnnouncement && ariaAnnouncement !== prevAnnouncement) { const announcer = announcerRef.current; if (announcer) { announcer.textContent = ""; const timeoutId = setTimeout(() => { if (announcer) { announcer.textContent = ariaAnnouncement; setPrevAnnouncement(ariaAnnouncement); } }, counterMode === "word" ? 2e3 : 1e3); return () => { if (timeoutId) clearTimeout(timeoutId); }; } } }, [ ariaAnnouncement, prevAnnouncement, counterMode ]); const input = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", { ...other, ...textareaProps, placeholder, "aria-readonly": other.readOnly, className: textareaClasses, "aria-invalid": invalid, "aria-describedby": ariaDescribedBy, disabled, rows, readOnly: other.readOnly, ref }); const candidate = slug ?? decorator; const normalizedDecorator = require_utils.isComponentElement(candidate, require_index.AILabel) ? (0, react.cloneElement)(candidate, { size: "mini" }) : candidate; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: formItemClasses, children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `${prefix}--text-area__label-wrapper`, children: [label, counter] }), enableCounter && maxCount && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { id: counterDescriptionId, className: `${prefix}--visually-hidden`, children: counterMode === "word" ? `Word limit ${maxCount}` : `Character limit ${maxCount}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { ref: wrapperRef, className: textAreaWrapperClasses, "data-invalid": invalid || null, children: [ invalid && !isFluid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.WarningFilled, { className: `${prefix}--text-area__invalid-icon` }), warn && !invalid && !isFluid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.WarningAltFilled, { className: `${prefix}--text-area__invalid-icon ${prefix}--text-area__invalid-icon--warning` }), input, slug ? normalizedDecorator : decorator ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `${prefix}--text-area__inner-wrapper--decorator`, children: normalizedDecorator }) : "", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${prefix}--text-area__counter-alert`, role: "alert", "aria-live": "assertive", "aria-atomic": "true", ref: announcerRef, children: ariaAnnouncement }), isFluid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("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 = { className: prop_types.default.string, cols: prop_types.default.number, counterMode: prop_types.default.oneOf(["character", "word"]), decorator: prop_types.default.node, defaultValue: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number]), disabled: prop_types.default.bool, enableCounter: prop_types.default.bool, helperText: prop_types.default.node, hideLabel: prop_types.default.bool, id: prop_types.default.string, invalid: prop_types.default.bool, invalidText: prop_types.default.node, labelText: prop_types.default.node.isRequired, light: require_deprecate.deprecate(prop_types.default.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."), maxCount: prop_types.default.number, onChange: prop_types.default.func, onClick: prop_types.default.func, onKeyDown: prop_types.default.func, placeholder: prop_types.default.string, readOnly: prop_types.default.bool, rows: prop_types.default.number, slug: require_deprecate.deprecate(prop_types.default.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."), value: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number]), warn: prop_types.default.bool, warnText: prop_types.default.node }; //#endregion exports.default = TextArea;