@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
JavaScript
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