@hitachivantara/uikit-react-core
Version:
UI Kit Core React components.
396 lines (395 loc) • 14.8 kB
JavaScript
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 };