UNPKG

@carbon/react

Version:

React components for the Carbon Design System

229 lines (227 loc) 8.86 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. */ import { usePrefix } from "../../internal/usePrefix.js"; import { Text } from "../Text/Text.js"; import { deprecate } from "../../prop-types/deprecate.js"; import { isComponentElement } from "../../internal/utils.js"; import { useMergedRefs } from "../../internal/useMergedRefs.js"; import { useNormalizedInputProps } from "../../internal/useNormalizedInputProps.js"; import { AILabel } from "../AILabel/index.js"; import { FormContext } from "../FluidForm/FormContext.js"; import { getTextInputProps } from "./util.js"; import { getAnnouncement } from "../../internal/getAnnouncement.js"; import classNames from "classnames"; import { cloneElement, forwardRef, useContext, useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { jsx, jsxs } from "react/jsx-runtime"; //#region src/components/TextInput/TextInput.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 TextInput = forwardRef(({ className, decorator, disabled = false, helperText, hideLabel, id, inline = false, invalid = false, invalidText, labelText, light, onChange = () => {}, onClick = () => {}, placeholder, readOnly, size, type = "text", warn = false, warnText, enableCounter = false, maxCount, slug, ...rest }, ref) => { const prefix = usePrefix(); const { defaultValue, value } = rest; const inputRef = useRef(null); const mergedRef = useMergedRefs([ref, inputRef]); function getInitialTextCount() { return (defaultValue || value || inputRef.current?.value || "").toString().length; } const [textCount, setTextCount] = useState(getInitialTextCount()); useEffect(() => { setTextCount(getInitialTextCount()); }, [ value, defaultValue, enableCounter ]); const normalizedProps = useNormalizedInputProps({ id, readOnly, disabled, invalid, invalidText, warn, warnText }); const sharedTextInputProps = { id, onChange: (evt) => { if (!normalizedProps.disabled) { setTextCount(evt.target.value?.length); onChange(evt); } }, onClick: (evt) => { if (!normalizedProps.disabled) onClick(evt); }, placeholder, type, ref: mergedRef, className: classNames(`${prefix}--text-input`, { [`${prefix}--text-input--light`]: light, [`${prefix}--text-input--invalid`]: normalizedProps.invalid, [`${prefix}--text-input--warning`]: normalizedProps.warn, [`${prefix}--text-input--${size}`]: size, [`${prefix}--layout--size-${size}`]: size }), title: placeholder, disabled: normalizedProps.disabled, readOnly, ["aria-describedby"]: helperText && normalizedProps.helperId, ...rest }; if (enableCounter) sharedTextInputProps.maxLength = maxCount; const inputWrapperClasses = classNames([classNames(`${prefix}--form-item`, className)], `${prefix}--text-input-wrapper`, { [`${prefix}--text-input-wrapper--readonly`]: readOnly, [`${prefix}--text-input-wrapper--light`]: light, [`${prefix}--text-input-wrapper--inline`]: inline, [`${prefix}--text-input-wrapper--inline--invalid`]: inline && normalizedProps.invalid }); const labelClasses = classNames(`${prefix}--label`, { [`${prefix}--visually-hidden`]: hideLabel, [`${prefix}--label--disabled`]: normalizedProps.disabled, [`${prefix}--label--inline`]: inline, [`${prefix}--label--inline--${size}`]: inline && !!size }); const helperTextClasses = classNames(`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: normalizedProps.disabled, [`${prefix}--form__helper-text--inline`]: inline }); const fieldOuterWrapperClasses = classNames(`${prefix}--text-input__field-outer-wrapper`, { [`${prefix}--text-input__field-outer-wrapper--inline`]: inline }); const fieldWrapperClasses = classNames(`${prefix}--text-input__field-wrapper`, { [`${prefix}--text-input__field-wrapper--warning`]: normalizedProps.warn, [`${prefix}--text-input__field-wrapper--slug`]: slug, [`${prefix}--text-input__field-wrapper--decorator`]: decorator }); const iconClasses = classNames({ [`${prefix}--text-input__invalid-icon`]: normalizedProps.invalid || normalizedProps.warn, [`${prefix}--text-input__invalid-icon--warning`]: normalizedProps.warn }); const counterClasses = classNames(`${prefix}--label`, { [`${prefix}--label--disabled`]: disabled, [`${prefix}--text-input__label-counter`]: true }); const counter = enableCounter && maxCount ? /* @__PURE__ */ jsx(Text, { as: "div", className: counterClasses, children: `${textCount}/${maxCount}` }) : null; const label = typeof labelText !== "undefined" && labelText !== null && /* @__PURE__ */ jsx(Text, { as: "label", htmlFor: id, className: labelClasses, children: labelText }); const labelWrapper = /* @__PURE__ */ jsxs("div", { className: `${prefix}--text-input__label-wrapper`, children: [label, counter] }); const helper = typeof helperText !== "undefined" && helperText !== null && /* @__PURE__ */ jsx(Text, { as: "div", id: normalizedProps.helperId, className: helperTextClasses, children: helperText }); const input = /* @__PURE__ */ jsx("input", { ...getTextInputProps({ sharedTextInputProps, invalid: normalizedProps.invalid, invalidId: normalizedProps.invalidId, warn: normalizedProps.warn, warnId: normalizedProps.warnId }) }); const { isFluid } = useContext(FormContext); const announcerRef = useRef(null); const [prevAnnouncement, setPrevAnnouncement] = useState(""); const ariaAnnouncement = getAnnouncement(textCount, maxCount); useEffect(() => { if (ariaAnnouncement && ariaAnnouncement !== prevAnnouncement) { const announcer = announcerRef.current; if (announcer) { announcer.textContent = ""; const timeoutId = setTimeout(() => { if (announcer) { announcer.textContent = ariaAnnouncement; setPrevAnnouncement(ariaAnnouncement); } }, 1e3); return () => { if (timeoutId) clearTimeout(timeoutId); }; } } }, [ariaAnnouncement, prevAnnouncement]); const Icon = normalizedProps.icon; const candidate = slug ?? decorator; const normalizedDecorator = isComponentElement(candidate, AILabel) ? cloneElement(candidate, { size: "mini" }) : candidate; return /* @__PURE__ */ jsxs("div", { className: inputWrapperClasses, children: [!inline ? labelWrapper : /* @__PURE__ */ jsxs("div", { className: `${prefix}--text-input__label-helper-wrapper`, children: [labelWrapper, !isFluid && (normalizedProps.validation || helper)] }), /* @__PURE__ */ jsxs("div", { className: fieldOuterWrapperClasses, children: [/* @__PURE__ */ jsxs("div", { className: fieldWrapperClasses, "data-invalid": normalizedProps.invalid || null, children: [ Icon && /* @__PURE__ */ jsx(Icon, { className: iconClasses }), input, slug ? normalizedDecorator : decorator ? /* @__PURE__ */ jsx("div", { className: `${prefix}--text-input__field-inner-wrapper--decorator`, children: normalizedDecorator }) : "", /* @__PURE__ */ jsx("span", { className: `${prefix}--text-input__counter-alert`, role: "alert", "aria-live": "assertive", "aria-atomic": "true", ref: announcerRef, children: ariaAnnouncement }), isFluid && /* @__PURE__ */ jsx("hr", { className: `${prefix}--text-input__divider` }), isFluid && !inline && normalizedProps.validation ] }), !isFluid && !inline && (normalizedProps.validation || helper)] })] }); }); TextInput.displayName = "TextInput"; TextInput.propTypes = { className: PropTypes.string, decorator: PropTypes.node, defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), disabled: PropTypes.bool, enableCounter: PropTypes.bool, helperText: PropTypes.node, hideLabel: PropTypes.bool, id: PropTypes.string.isRequired, inline: PropTypes.bool, invalid: PropTypes.bool, invalidText: PropTypes.node, labelText: PropTypes.node.isRequired, light: deprecate(PropTypes.bool, "The `light` prop for `TextInput` has been deprecated in favor of the new `Layer` component. It will be removed in the next major release."), maxCount: PropTypes.number, onChange: PropTypes.func, onClick: PropTypes.func, placeholder: PropTypes.string, readOnly: PropTypes.bool, size: PropTypes.oneOf([ "sm", "md", "lg" ]), slug: deprecate(PropTypes.node, "The `slug` prop for `TextInput` has been deprecated in favor of the new `decorator` prop. It will be removed in the next major release."), type: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), warn: PropTypes.bool, warnText: PropTypes.node }; //#endregion export { TextInput as default };