UNPKG

@kadconsulting/dry

Version:
88 lines 6.84 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { v4 as uuid } from 'uuid'; import classnames from 'classnames'; import InputMask from 'react-input-mask'; import { Label as DryLabel } from './constituents/Label'; import { ErrorText as DryErrorText } from './constituents/ErrorText'; import { HintText as DryHintText } from './constituents/HintText'; import { InputAdornment as DryInputAdornment } from './constituents/InputAdornment'; import { useLayoutEffect, useMemo, useRef, useState, forwardRef } from 'react'; import './TextInput.scss'; export const BASE_INPUT_PADDING_HORIZONTAL = 8; /** * The most atomic text input consisting of a container, an accessibility label, optional helper text below that swaps out optionally with an * error message. Can be used to compose more complex text inputs, such as those with adornments for phone numbers, etc. */ const TextInput = forwardRef(({ ContainerProps, size = 'sm', Label: UserLabelNodes, LabelProps, labelRef, maskProps, InputAdornmentLeft, InputAdornmentLeftProps, InputAdornmentRight, InputAdornmentRightProps, ErrorText: UserErrorNodes, ErrorTextProps, errorTextRef, HintText: UserHintTextNodes, HintTextProps, HintTextRef, disabled, value, inputType = 'text', width, labelColor, inputTextColor, inputBackgroundColor, required, ...props }, ref) => { const leftAdornmentRef = useRef(null); const rightAdornmentRef = useRef(null); /** If an id is not provided, ensure that one is generated, so that the label is accessible */ const [id] = useState(() => props.id || `input-${uuid()}`); /** Computed pixel values for how much padding should be added to the input to prevent occlusion by the input adornment */ const [adornmentAwareLeftPadding, setAdornmentAwareLeftPadding] = useState(0); const [adornmentAwareRightPadding, setAdornmentAwareRightPadding] = useState(0); /** * rome-ignore lint/nursery/useExhaustiveDependencies: * * We need this effect to fire if InputAdornmentLeft or InputAdornmentRight change, * so that layout updates can take changes within the adornments into account. * For example, the currency symbol's length can change, $ vs. Fr (francs) */ useLayoutEffect(() => { if (!leftAdornmentRef.current && !rightAdornmentRef.current) return; // Get the computed width of the input adornment(s) const leftAdornmentWidth = leftAdornmentRef.current?.getBoundingClientRect().width || 0; const rightAdornmentWidth = rightAdornmentRef.current?.getBoundingClientRect().width || 0; // Add gap to the computed width of the input adornment setAdornmentAwareLeftPadding(leftAdornmentWidth); setAdornmentAwareRightPadding(rightAdornmentWidth); }, [ InputAdornmentLeft, InputAdornmentRight, setAdornmentAwareLeftPadding, setAdornmentAwareRightPadding, ]); const inputProps = useMemo(() => ({ id, style: { paddingLeft: `${adornmentAwareLeftPadding + BASE_INPUT_PADDING_HORIZONTAL}px`, paddingRight: `${adornmentAwareRightPadding + BASE_INPUT_PADDING_HORIZONTAL}px`, }, ...props, }), [id, props, adornmentAwareLeftPadding, adornmentAwareRightPadding]); const Label = useMemo(() => (_jsx(DryLabel, { required: required, labelColor: labelColor, htmlFor: id, ref: labelRef, className: classnames('dry-textInput__label', LabelProps?.className), ...LabelProps, children: UserLabelNodes })), [UserLabelNodes, LabelProps, id, labelRef]); const LeftAdornment = useMemo(() => InputAdornmentLeft && (_jsx(DryInputAdornment, { position: 'left', ref: leftAdornmentRef, ...InputAdornmentLeftProps, className: classnames(InputAdornmentLeftProps?.className), children: InputAdornmentLeft })), [InputAdornmentLeft]); const MaskedInput = useMemo(() => { if (!maskProps) return null; return (_jsx(InputMask, { ...maskProps, ...inputProps, className: classnames('dry-textInput__input', 'dry-textInput__input--masked', `dry-textInput__input--size-${size}`, { 'dry-textInput__input--disabled': disabled }, { 'dry-textInput__input--error': !!UserErrorNodes }, inputProps.className) })); }, [maskProps, inputProps, UserErrorNodes, size, disabled]); const Input = useMemo(() => { // TODO-dry: we could add this as a utility function in DRY const genStyle = () => { const baseStyle = inputTextColor ? { color: inputTextColor, } : {}; if (inputBackgroundColor) { baseStyle.backgroundColor = inputBackgroundColor; } const overLayStyle = {}; return { ...baseStyle, ...overLayStyle }; }; return (_jsx("input", { ...inputProps, type: inputType, value: value, disabled: disabled, style: genStyle(), className: classnames('dry-textInput__input', 'dry-textInput__input--unmasked', `dry-textInput__input--size-${size}`, { 'dry-textInput__input--error': !!UserErrorNodes }, { 'dry-textInput__input--disabled': disabled }, inputProps.className) })); }, [inputProps, UserErrorNodes, size, disabled]); const RightAdornment = useMemo(() => InputAdornmentRight && (_jsx(DryInputAdornment, { position: 'right', ref: rightAdornmentRef, ...InputAdornmentRightProps, className: classnames(InputAdornmentRightProps?.className), children: InputAdornmentRight })), [InputAdornmentRight]); const ErrorText = useMemo(() => UserErrorNodes && (_jsx(DryErrorText, { ref: errorTextRef, ...ErrorTextProps, className: classnames('dry-textInput__errorText', ErrorTextProps?.className), children: UserErrorNodes })), [UserErrorNodes, ErrorTextProps, errorTextRef]); const HintText = useMemo(() => UserHintTextNodes && (_jsx(DryHintText, { ref: HintTextRef, ...HintTextProps, className: classnames('dry-textInput__HintText', HintTextProps?.className), children: UserHintTextNodes })), [UserHintTextNodes, HintTextProps, HintTextRef]); /** * Prioritize displaying error text over helper text because it's * likely to be more specific to user's current validation situation. */ const DisplayedHintTextNode = useMemo(() => ErrorText || HintText, [ErrorText, HintText]); return (_jsxs("div", { ref: ref, ...ContainerProps, className: classnames('dry-textInput', ContainerProps?.className), style: { width: width ? `${width}px` : '100%' }, children: [Label, _jsxs("div", { className: 'dry-textInput__adornmentContext', children: [LeftAdornment, maskProps ? MaskedInput : Input, RightAdornment] }), DisplayedHintTextNode, inputTextColor && (_jsx("style", { children: ` .dry-textInput__input::placeholder {color: ${inputTextColor}; }` }))] })); }); export default TextInput; //# sourceMappingURL=TextInput.js.map