UNPKG

@hitachivantara/uikit-react-core

Version:
396 lines (395 loc) 14.8 kB
import { fixedForwardRef } from "../types/generic.js"; import { isKey } from "../utils/keyboardUtils.js"; import { setId } from "../utils/setId.js"; import { HvAdornment } from "../FormElement/Adornment/Adornment.js"; import { useUniqueId } from "../hooks/useUniqueId.js"; import { isInvalid, isValid } from "../FormElement/utils.js"; import { HvFormElement } from "../FormElement/FormElement.js"; import { HvInfoMessage } from "../FormElement/InfoMessage/InfoMessage.js"; import { HvIcon } from "../icons.js"; import { HvWarningText } from "../FormElement/WarningText/WarningText.js"; import { useControlled as useControlled$1 } from "../hooks/useControlled.js"; import { HvLabelContainer } from "../FormElement/LabelContainer.js"; import { HvSuggestions } from "../FormElement/Suggestions/Suggestions.js"; import { HvBaseInput } from "../BaseInput/BaseInput.js"; import { useLabels } from "../hooks/useLabels.js"; import { HvTooltip } from "../Tooltip/Tooltip.js"; import { DEFAULT_ERROR_MESSAGES, computeValidationMessage, computeValidationState, hasBuiltInValidations, validateInput } from "../BaseInput/validations.js"; import { useIsMounted } from "../hooks/useIsMounted.js"; import { EyeIcon } from "./icons.js"; import { useClasses } from "./Input.styles.js"; import { changeInputValue } from "./utils.js"; import { useDefaultProps, useTheme } from "@hitachivantara/uikit-react-utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; import { useForkRef } from "@mui/material/utils"; //#region src/Input/Input.tsx var DEFAULT_LABELS = { /** The label of the clear button. */ clearButtonLabel: "Clear the text", /** The tooltip of the reveal password button when the password is hidden. */ revealPasswordButtonClickToShowTooltip: "Click to show password.", /** The tooltip of the reveal password button when the password is revealed. */ revealPasswordButtonClickToHideTooltip: "Click to hide password.", /** The label of the search button. */ searchButtonLabel: "Search" }; function eventTargetIsInsideContainer(container, event) { return !!container?.contains(event.relatedTarget); } /** The Input is a UI control that allows users to enter and edit text, typically used for collecting user-provided information. */ var HvInput = fixedForwardRef(function HvInput(props, ref) { const { classes: classesProp, className, id, name, value: valueProp, defaultValue, required, readOnly, disabled, enablePortal, suggestOnFocus, label, description: descriptionProp, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, onChange, onEnter, status, statusMessage, infoMessage: infoMessageProp, "aria-errormessage": ariaErrorMessage, type = "text", placeholder, autoFocus, labels: labelsProp, validationMessages, disableClear, disableRevealPassword, disableSearchButton, endAdornment, maxCharQuantity, minCharQuantity, validation, showValidationIcon, suggestionListCallback, inputRef: inputRefProp, onBlur, onFocus, onKeyDown, inputProps = {}, ...others } = useDefaultProps("HvInput", props); const { classes, cx } = useClasses(classesProp); const labels = useLabels(DEFAULT_LABELS, labelsProp); const elementId = useUniqueId(id); const { activeTheme } = useTheme(); const inputRef = useRef(null); const forkedRef = useForkRef(ref, inputRef, inputRefProp); const suggestionsRef = useRef(null); const [description, infoMessage] = activeTheme?.name === "pentahoPlus" ? [infoMessageProp, descriptionProp] : [descriptionProp, infoMessageProp]; const [focused, setFocused] = useState(false); const isDirty = useRef(false); const isEmptyValue = !inputRef.current?.value; const [validationState, setValidationState] = useControlled$1(status, "standBy"); const [validationMessage, setValidationMessage] = useControlled$1(statusMessage, ""); const errorMessages = useMemo(() => ({ ...DEFAULT_ERROR_MESSAGES, ...validationMessages }), [ validationMessages?.error, validationMessages?.requiredError, validationMessages?.minCharError, validationMessages?.maxCharError, validationMessages?.typeMismatchError ]); const performValidation = useCallback(() => { const inputValidity = validateInput(inputRef.current, required, minCharQuantity, maxCharQuantity, validation); setValidationState(computeValidationState(inputValidity, isEmptyValue)); setValidationMessage(computeValidationMessage(inputValidity, errorMessages) || ""); return inputValidity; }, [ errorMessages, isEmptyValue, maxCharQuantity, minCharQuantity, required, setValidationMessage, setValidationState, validation ]); const canShowError = ariaErrorMessage == null && (status !== void 0 && statusMessage !== void 0 || status === void 0 && hasBuiltInValidations(required, type, minCharQuantity, maxCharQuantity, validation, inputProps)); const isStateInvalid = isInvalid(validationState); const canShowInfo = !!infoMessage && !(canShowError && isStateInvalid); const [revealPassword, setRevealPassword] = useState(false); const realType = useMemo(() => { if (type === "password") return revealPassword ? "text" : "password"; if ([ "search", "number", "email" ].includes(type)) return type; return "text"; }, [revealPassword, type]); const [suggestionValues, setSuggestionValues] = useState(null); const canShowSuggestions = suggestionListCallback != null; const hasSuggestions = !!suggestionValues; const hasLabel = label != null; /** * Looks for the node that represent the input inside the material tree and focus it. */ const focusInput = () => { inputRef.current?.focus(); }; const isMounted = useIsMounted(); /** * Clears the suggestion array. */ const suggestionClearHandler = () => { if (isMounted.current) setSuggestionValues(null); }; /** * Fills of the suggestion array. */ const suggestionHandler = (val) => { const suggestionsArray = suggestionListCallback?.(val); if (suggestionsArray?.[0]?.label) setSuggestionValues(suggestionsArray); else suggestionClearHandler(); }; /** * Executes the user callback adds the selection to the state and clears the suggestions. */ const suggestionSelectedHandler = (event, item) => { const newValue = item.value || item.label; changeInputValue(inputRef.current, newValue); focusInput(); suggestionClearHandler(); if (type === "search") onEnter?.(event, newValue); }; const onChangeHandler = (event, newValue) => { isDirty.current = true; onChange?.(event, newValue); if (canShowSuggestions) suggestionHandler(newValue); }; /** * Validates the input updating the state and modifying the info text, also executes * the user provided onBlur passing the current validation status and value. */ const onInputBlurHandler = (event) => { if (eventTargetIsInsideContainer(suggestionsRef.current, event)) return; setFocused(false); const inputValidity = performValidation(); onBlur?.(event, event.target?.value, inputValidity); }; /** * Updates the state putting again the value from the state because the input value is * not automatically manage, it also executes the onFocus function from the user passing the value */ const onFocusHandler = (event) => { setFocused(true); setValidationState("standBy"); onFocus?.(event, event.target.value); }; const getSuggestions = (li) => { const listEl = document.getElementById(setId(elementId, "suggestions-list") || ""); return li != null ? listEl?.getElementsByTagName("li")?.[li] : listEl; }; const onSuggestionKeyDown = (event) => { if (isKey(event, "Esc")) { suggestionClearHandler(); focusInput(); } else if (isKey(event, "Tab")) suggestionClearHandler(); }; /** Focus the suggestion list when the arrow down is pressed. */ const onKeyDownHandler = (event) => { const { value } = event.currentTarget; if (isKey(event, "ArrowDown") && hasSuggestions) getSuggestions(0)?.focus(); else if (isKey(event, "Enter")) onEnter?.(event, value); onKeyDown?.(event, value); }; /** Clears the suggestion list on blur. */ const onContainerBlurHandler = (event) => { if (event.relatedTarget) setTimeout(() => { if (!getSuggestions(null)?.contains(document.activeElement)) suggestionClearHandler(); }, 10); }; const showClear = !disabled && !readOnly && !disableClear && !isEmptyValue && (!onEnter || type !== "search" || disableSearchButton || validationState !== "standBy"); const showSearchIcon = type === "search" && !disableSearchButton; const showRevealPasswordButton = type === "password" && !disableRevealPassword; /** * Clears the input value from the state and refocus the input. */ const handleClear = useCallback((event) => { setValidationState("standBy"); changeInputValue(inputRef.current, ""); if (canShowSuggestions && suggestOnFocus) event.stopPropagation(); else setTimeout(focusInput); }, [ canShowSuggestions, setValidationState, suggestOnFocus ]); const clearButton = useMemo(() => { if (!showClear) return null; return /* @__PURE__ */ jsx(HvAdornment, { className: cx(classes.adornmentButton, { [classes.iconClear]: !showSearchIcon }), onClick: handleClear, "aria-label": labels?.clearButtonLabel, "aria-controls": setId(elementId, "input"), icon: /* @__PURE__ */ jsx(HvIcon, { compact: true, name: "Close", size: "xs" }) }); }, [ showClear, classes.adornmentButton, classes.iconClear, showSearchIcon, handleClear, labels?.clearButtonLabel, elementId, cx ]); const searchButton = useMemo(() => { if (!(showSearchIcon && (isEmptyValue || onEnter && validationState === "standBy"))) return null; return /* @__PURE__ */ jsx(HvAdornment, { className: classes.adornmentButton, onClick: onEnter && ((evt) => onEnter?.(evt, inputRef.current?.value ?? "")), icon: /* @__PURE__ */ jsx(HvIcon, { compact: true, name: "Search", title: labels.searchButtonLabel }) }); }, [ showSearchIcon, isEmptyValue, onEnter, validationState, classes.adornmentButton, labels.searchButtonLabel ]); const revealPasswordButton = useMemo(() => { if (!showRevealPasswordButton) return null; return /* @__PURE__ */ jsx(HvTooltip, { title: revealPassword ? labels?.revealPasswordButtonClickToHideTooltip : labels?.revealPasswordButtonClickToShowTooltip, children: /* @__PURE__ */ jsx(HvAdornment, { className: classes.adornmentButton, onClick: () => setRevealPassword((s) => !s), "aria-controls": setId(elementId, "input"), icon: /* @__PURE__ */ jsx(EyeIcon, { selected: revealPassword }), tabIndex: 0, selected: revealPassword }) }); }, [ showRevealPasswordButton, revealPassword, labels?.revealPasswordButtonClickToHideTooltip, labels?.revealPasswordButtonClickToShowTooltip, classes.adornmentButton, elementId ]); const validationIcon = useMemo(() => { if (!showValidationIcon) return null; if (!isValid(validationState)) return null; return /* @__PURE__ */ jsx(HvIcon, { name: "Success", color: "positive", className: classes.icon }); }, [ showValidationIcon, validationState, classes.icon ]); const adornments = useMemo(() => { if (!clearButton && !revealPasswordButton && !searchButton && !validationIcon && !endAdornment) return null; return /* @__PURE__ */ jsxs("div", { className: classes.adornmentsBox, children: [ clearButton, revealPasswordButton, searchButton, validationIcon || endAdornment ] }); }, [ classes.adornmentsBox, clearButton, endAdornment, revealPasswordButton, searchButton, validationIcon ]); useEffect(() => { if (focused || !isDirty.current && isEmptyValue) return; performValidation(); }, [ focused, isEmptyValue, performValidation ]); const errorMessageId = isStateInvalid ? canShowError ? setId(elementId, "error") : ariaErrorMessage : void 0; return /* @__PURE__ */ jsxs(HvFormElement, { id, name, status: validationState, disabled, required, readOnly, className: cx(classes.root, { [classes.hasSuggestions]: hasSuggestions }, className), onBlur: onContainerBlurHandler, children: [ /* @__PURE__ */ jsx(HvLabelContainer, { label, description, inputId: setId(elementId, "input"), labelId: setId(elementId, "label"), descriptionId: setId(elementId, "description"), classes: { root: classes.labelContainer, label: classes.label, description: classes.description } }), /* @__PURE__ */ jsx(HvBaseInput, { id: hasLabel || showClear || showRevealPasswordButton ? setId(elementId, "input") : setId(id, "input"), name, value: valueProp, defaultValue, required, readOnly, disabled, onChange: onChangeHandler, autoFocus, onKeyDown: onKeyDownHandler, onBlur: onInputBlurHandler, onFocus: onFocusHandler, placeholder, type: realType, classes: { input: classes.input, root: classes.inputRoot, focused: classes.inputRootFocused, disabled: classes.inputRootDisabled, multiline: classes.inputRootMultiline }, invalid: isStateInvalid, inputProps: { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-invalid": isStateInvalid ? true : void 0, "aria-errormessage": errorMessageId, "aria-describedby": ariaDescribedBy != null ? ariaDescribedBy : description ? setId(elementId, "description") : void 0, "aria-controls": canShowSuggestions ? setId(elementId, "suggestions") : void 0, autoComplete: canShowSuggestions ? "off" : void 0, onFocus: (event) => { inputProps.onFocus?.(event); if (canShowSuggestions && suggestOnFocus) suggestionHandler(event.currentTarget.value); }, onClick: (event) => { inputProps.onClick?.(event); if (canShowSuggestions && suggestOnFocus) event.stopPropagation(); }, ...inputProps }, ref: forkedRef, endAdornment: adornments, ...others }), canShowSuggestions && /* @__PURE__ */ jsx(HvSuggestions, { id: setId(elementId, "suggestions"), classes: { root: classes.suggestionsContainer, list: classes.suggestionList }, open: hasSuggestions, anchorEl: inputRef.current?.parentElement, onClose: suggestionClearHandler, onKeyDown: onSuggestionKeyDown, onSuggestionSelected: suggestionSelectedHandler, suggestionValues, enablePortal, popperProps: { ref: suggestionsRef } }), canShowError && /* @__PURE__ */ jsx(HvWarningText, { id: setId(elementId, "error"), disableBorder: true, className: classes.error, children: validationMessage }), canShowInfo && /* @__PURE__ */ jsx(HvInfoMessage, { disableGutter: true, variant: "caption1", children: infoMessage }) ] }); }); //#endregion export { HvInput };