UNPKG

@veracity/vui

Version:

Veracity UI is a React component library crafted for use within Veracity applications and pages. Based on Styled Components and @xstyled.

297 lines (291 loc) 9.76 kB
import { isString } from "../utils/assertion.js"; import { filterUndefined } from "../utils/object.js"; import { cs } from "../utils/styles.js"; import { clearIconSize, displayValueOnlyTextSize, fieldColors } from "./consts.js"; import { useStyleConfig } from "../core/theme.js"; import styled from "../core/styled.js"; import vui from "../core/vui.js"; import { Box } from "../box/box.js"; import { T } from "../t/t.js"; import { IconButton } from "../button/buttons.js"; import { TextFieldProvider } from "./context.js"; import { clampValue, createSyntheticChangeEvent, filterAutoCompleteOption, getHelpTextMargin, getInitialCount, resolveLabel } from "./helpers.js"; import { TextFieldAutoComplete } from "./textFieldAutoComplete.js"; import { TextFieldHelpText } from "./textFieldHelpText.js"; import { TextFieldIcon } from "./textFieldIcon.js"; import { useEffect, useId, useMemo, useState } from "react"; import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime"; //#region src/textField/textField.tsx const TextFieldControlBase = styled.divBox` align-items: center; color: ${fieldColors.inputText}; background-color: white; border-radius: none; border-width: 1px; display: flex; flex-shrink: 0; gap: 8px; outline: none; padding: 0 8px; position: relative; transform: translate3d(0, 0, 0); transition-duration: 0s; width: 100%; &[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; background-color: ${fieldColors.disabledBackground}; border-color: ${fieldColors.disabledBackground}; color: ${fieldColors.disabledForeground}; & > .vui-icon { color: ${fieldColors.disabledForeground}; } & > .vui-icon > path { fill: ${fieldColors.disabledForeground}; } } ${({ $multiline }) => $multiline && ` align-items: stretch; flex-direction: column; gap: 0; padding: 0; `} `; const TextFieldInputBase = styled.input` align-self: stretch; background-color: transparent; border: none; border-radius: none; color: inherit; flex: 1; font-size: inherit; min-width: 0; outline: none; &[aria-disabled='true'], &[aria-disabled='true']::placeholder { cursor: not-allowed; } `; const TextFieldTextareaBase = styled.textareaBox` background-color: transparent; border: none; color: inherit; flex: 1 1 auto; font-size: inherit; outline: none; padding: 4px; resize: none; width: 100%; &[aria-disabled='true'] { cursor: not-allowed; } `; /** * Displays a text field wrapped in a div to allow extra content, like counter, side icons or buttons. * Renders a native `<input>` when `rows` is unset or `<= 1`, and a `<textarea>` when `rows > 1`. * Forwards many relevant props to the inner field. Handles different states, like loading or error. * Exposes some props to the children via context. */ const TextField = vui((props, ref) => { const { ariaLabel, autoComplete, autoFocus, className, id: idProp, name, disabled, readOnly, required, defaultValue, value, onChange: onChangeProp, onBlur, onFocus, max, maxLength, min, minLength, pattern, placeholder, step, type = "text", cols, resize, rows, label, labelMarkOptional, labelTooltipText, helpText, errorText, loading, showCount, displayValueOnly, allowClear, startIcon, endIcon, itemLeft, itemRight, autoCompleteOptions, autoCompleteMaxHeight, field, fieldProps, fieldRef, size = "lg", variant, ...rest } = props; const isMultiline = (rows ?? 0) > 1; const generatedId = useId(); const [count, setCount] = useState(() => getInitialCount(props)); const [valueInternal, setValueInternal] = useState(value ?? defaultValue ?? ""); const minHeight = isMultiline && resize && rows ? `calc(${rows} * 1lh + 10px)` : void 0; const effectiveVariant = errorText ? "red" : loading ? "grey" : variant; const id = idProp || generatedId; const styles = useStyleConfig("Input", { ...props, variant: effectiveVariant }); const { h: _containerHeight, ...containerStylesMultiline } = styles.container ?? {}; const containerStyles = isMultiline ? containerStylesMultiline : styles.container; const context = useMemo(() => filterUndefined({ disabled, id, size, variant: effectiveVariant }), [ disabled, id, size, effectiveVariant ]); function onChange(e) { const value = clampValue(e.target.value, maxLength); setValueInternal(value); setCount(value?.length); if (value !== e.target.value) e.target.value = value; onChangeProp?.(e); } function onAutoCompleteSelect(value) { onChange(createSyntheticChangeEvent(value, "autoCompleteSelect")); } function onClear() { setValueInternal(""); setCount(0); onChange(createSyntheticChangeEvent("", "onClear")); } const aliasedProps = filterUndefined({ "aria-disabled": disabled, bg: disabled ? fieldColors.disabledBackground : readOnly ? fieldColors.readOnlyBackground : void 0, borderColor: disabled ? fieldColors.disabledBackground : readOnly && !loading ? fieldColors.readOnlyBorder : void 0 }); const fieldAliasedProps = filterUndefined({ "aria-disabled": disabled, "aria-label": ariaLabel }); const helpTextId = helpText ? `${id}-help-text` : void 0; const errorTextId = errorText ? `${id}-error-text` : void 0; const ariaDescribedBy = [helpTextId, errorTextId].filter(Boolean).join(" ") || void 0; const commonFieldProps = { value: valueInternal, ...styles.field, ...fieldAliasedProps, "aria-describedby": ariaDescribedBy, "aria-invalid": errorText ? true : void 0 }; const sharedFieldProps = { autoFocus, disabled, id, maxLength, name, onBlur, onChange, onFocus, placeholder, readOnly, required }; const textareaFieldProps = { ...sharedFieldProps, cols, rows, ...commonFieldProps, ...fieldProps }; const inputFieldProps = { ...sharedFieldProps, max, min, minLength, pattern, step, type, autoComplete: autoCompleteOptions?.length ? "off" : autoComplete, ...commonFieldProps, ...fieldProps }; useEffect(() => { if (value === void 0 && defaultValue !== void 0) return; setValueInternal(value ?? ""); setCount(value?.toString()?.length ? `${value}`.length : 0); }, [value]); const filterAutoCompleteOptions = (i) => filterAutoCompleteOption(`${valueInternal}`, i); const resolvedLabel = resolveLabel(label, id, disabled, labelMarkOptional, labelTooltipText, styles.label); const counterNode = showCount && /* @__PURE__ */ jsxs(T, { className: "vui-textFieldCount", color: maxLength && count > maxLength ? fieldColors.error : fieldColors.helpText, position: "absolute", right: 0, size: "sm", top: "100%", children: [ count, " ", maxLength ? `/ ${maxLength}` : null ] }); if (type === "hidden") return /* @__PURE__ */ jsx("input", { defaultValue, id, name, onChange, ref: fieldRef, type: "hidden", value: valueInternal, ...fieldProps }); return /* @__PURE__ */ jsx(TextFieldProvider, { value: context, children: displayValueOnly ? /* @__PURE__ */ jsx(T, { className: cs("vui-textField-value", className), size: displayValueOnlyTextSize[size], children: value || defaultValue }) : /* @__PURE__ */ jsx(TextFieldAutoComplete, { autoCompleteMaxHeight, autoCompleteOptions: isMultiline ? void 0 : autoCompleteOptions, filterAutoCompleteOptions, onAutoCompleteSelect, children: /* @__PURE__ */ jsxs(Box, { className: "vui-textFieldContainer", column: true, gap: "4px", ref, ...rest, children: [ resolvedLabel, /* @__PURE__ */ jsxs(TextFieldControlBase, { $multiline: isMultiline, className: cs("vui-textField", className), minH: isMultiline && resize ? minHeight : void 0, overflow: isMultiline && resize ? "auto" : void 0, resize: isMultiline && resize ? "vertical" : void 0, ...containerStyles, ...aliasedProps, children: [ !isMultiline && /* @__PURE__ */ jsxs(Fragment$1, { children: [itemLeft, isString(startIcon) ? /* @__PURE__ */ jsx(TextFieldIcon, { name: startIcon }) : startIcon] }), field ?? (isMultiline ? /* @__PURE__ */ jsx(TextFieldTextareaBase, { className: "vui-textFieldField", ref: fieldRef, ...textareaFieldProps }) : /* @__PURE__ */ jsx(TextFieldInputBase, { className: "vui-textFieldField", ref: fieldRef, ...inputFieldProps })), !isMultiline && /* @__PURE__ */ jsxs(Fragment$1, { children: [ isString(endIcon) ? /* @__PURE__ */ jsx(TextFieldIcon, { name: endIcon }) : endIcon, itemRight, loading && /* @__PURE__ */ jsx(TextFieldIcon, { animation: "vui-spin 0.6s linear infinite", name: "uiSpinnerThird", pathFill: [fieldColors.loading, fieldColors.loadingBar] }), allowClear && !!valueInternal && /* @__PURE__ */ jsx(IconButton, { icon: "uiTimes", onClick: onClear, size: clearIconSize[size], title: "Clear", variant: "tertiary" }) ] }), counterNode ] }), !!helpText && /* @__PURE__ */ jsx(TextFieldHelpText, { id: helpTextId, mr: getHelpTextMargin(showCount), children: helpText }), !!errorText && /* @__PURE__ */ jsx(TextFieldHelpText, { id: errorTextId, isError: true, mr: getHelpTextMargin(showCount), children: errorText }) ] }) }) }); }); TextField.AutoComplete = TextFieldAutoComplete; TextField.HelpText = TextFieldHelpText; TextField.Icon = TextFieldIcon; TextField.displayName = "TextField"; //#endregion export { TextField, TextField as default, TextFieldControlBase, TextFieldInputBase, TextFieldTextareaBase }; globalThis.__vuiVersion__ = "5.4.0-alpha" //# sourceMappingURL=textField.js.map