UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

560 lines (559 loc) 17.5 kB
"use client"; var _AlignmentHelper; import React, { useCallback, useContext, useEffect, useRef, useState, useMemo } from 'react'; import clsx from 'clsx'; import useCombinedRef from "../../shared/helpers/useCombinedRef.js"; import useMountEffect from "../../shared/helpers/useMountEffect.js"; import withComponentMarkers from "../../shared/helpers/withComponentMarkers.js"; import { extendPropsWithContext } from "../../shared/helpers/extendPropsWithContext.js"; import { pickFormElementProps } from "../../shared/helpers/filterValidProps.js"; import useId from "../../shared/helpers/useId.js"; import Suffix from "../../shared/helpers/Suffix.js"; import { warn, removeUndefinedProps, validateDOMAttributes, processChildren, getStatusState, combineDescribedBy, dispatchCustomElementEvent, convertJsxToString } from "../../shared/component-helper.js"; import AlignmentHelper from "../../shared/AlignmentHelper.js"; import { applySpacing } from "../space/SpacingUtils.js"; import { skeletonDOMAttributes, createSkeletonClass } from "../skeleton/SkeletonHelper.js"; import Button from "../button/Button.js"; import FormLabel from "../form-label/FormLabel.js"; import FormStatus from "../form-status/FormStatus.js"; import IconPrimary from "../icon-primary/IconPrimary.js"; import Context from "../../shared/Context.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; export const inputDefaultProps = { type: 'text', size: null, value: 'initval', id: null, label: null, labelDirection: 'vertical', labelSrOnly: null, status: null, globalStatus: null, statusState: 'error', statusProps: null, statusNoAnimation: null, inputState: null, autocomplete: 'off', placeholder: null, showClearButton: null, keepPlaceholder: null, suffix: null, align: null, selectAll: null, stretch: null, disabled: null, skeleton: null, inputClassName: null, inputAttributes: null, inputElement: null, ref: null, icon: null, iconSize: null, iconPosition: 'left', readOnly: false, innerElement: null, submitElement: null, submitButtonTitle: null, clearButtonTitle: null, submitButtonVariant: 'secondary', submitButtonIcon: 'loupe', submitButtonStatus: null, className: null, children: null, onChange: null, onKeyDown: null, onSubmit: null, onFocus: null, onBlur: null, onSubmitFocus: null, onSubmitBlur: null, onClear: null }; function hasValue(value) { return (typeof value === 'string' || typeof value === 'number') && String(value).length > 0 || false; } function getValue(props) { const value = processChildren(props); if (value === '' || hasValue(value)) { return value; } return props.value; } function InputComponent({ ref, ...restProps }) { const context = useContext(Context); const inputRef = useRef(null); const combinedRef = useCombinedRef(ref, inputRef); const formElement = context?.formElement; const _id = useId(restProps.id || formElement?.useId?.()); const selectAllTimeoutRef = useRef(undefined); const initialValue = useMemo(() => { const v = getValue(restProps); if (v !== 'initval' && hasValue(v)) { return v; } return null; }, []); const [value, setValue] = useState(initialValue); const [inputState, setInputState] = useState(restProps.inputState || 'virgin'); const prevValuePropRef = useRef(restProps.value); const props = extendPropsWithContext({ ...inputDefaultProps, ...removeUndefinedProps({ ...restProps }) }, inputDefaultProps, { skeleton: context?.skeleton }, context.getTranslation(restProps).Input, pickFormElementProps(context?.formElement), context.Input); const propValue = getValue(restProps); if (propValue !== 'initval' && propValue !== value && propValue !== prevValuePropRef.current) { setValue(propValue); } prevValuePropRef.current = restProps.value; if (restProps.inputState && restProps.inputState !== inputState) { setInputState(restProps.inputState); } const updateInputValue = useCallback(() => { if (inputRef.current && !restProps.inputElement) { const hasVal = hasValue(value); const newValue = hasVal ? String(value) : ''; if (inputRef.current.value !== newValue) { inputRef.current.value = newValue; } } }, [value, restProps.inputElement]); useEffect(() => { updateInputValue(); }); useMountEffect(() => { if (restProps.showClearButton && restProps.iconPosition === 'right') { warn('You cannot have a clear button and iconPosition="right"'); } return () => { clearTimeout(selectAllTimeoutRef.current); }; }); const onFocusHandler = useCallback(event => { const { value: eventValue } = event.target; setInputState('focus'); dispatchCustomElementEvent(props, 'onFocus', { value: eventValue, event }); if (props.selectAll && inputRef.current) { clearTimeout(selectAllTimeoutRef.current); selectAllTimeoutRef.current = setTimeout(() => { try { inputRef.current.select(); } catch (e) { warn(e); } }, 1); } }, [props.selectAll, props.onFocus]); const onBlurHandler = useCallback(event => { const { value: eventValue } = event.target; const result = dispatchCustomElementEvent(props, 'onBlur', { value: eventValue, event }); if (result !== false) { setInputState(hasValue(eventValue) && eventValue !== String(prevValuePropRef.current) ? 'dirty' : 'initial'); } }, [props.onBlur]); const onChangeHandler = useCallback(event => { const { value: eventValue } = event.target; const result = dispatchCustomElementEvent(props, 'onChange', { value: eventValue, event }); if (result === false) { updateInputValue(); return; } if (typeof result === 'string') { setValue(result); } else { setValue(eventValue); } }, [props.onChange, updateInputValue]); const onKeyDownHandler = useCallback(event => { const eventValue = event.target.value; dispatchCustomElementEvent(props, 'onKeyDown', { value: eventValue, event }); if (event.key === 'Enter') { dispatchCustomElementEvent(props, 'onSubmit', { value: eventValue, event }); } }, [props.onKeyDown, props.onSubmit]); const clearValueHandler = useCallback(event => { const previousValue = value; const clearedValue = ''; setValue(clearedValue); dispatchCustomElementEvent(props, 'onChange', { value: clearedValue, event }); dispatchCustomElementEvent(props, 'onClear', { value: clearedValue, previousValue, event }); inputRef.current.focus({ preventScroll: true }); }, [value, props.onChange, props.onClear]); const { type, size, label, labelDirection, labelSrOnly, status, globalStatus, statusState, statusProps, statusNoAnimation, disabled, skeleton, placeholder, showClearButton, keepPlaceholder, _omitInputShellClass, suffix, align, inputClassName, submitButtonTitle, clearButtonTitle, submitButtonVariant, submitButtonIcon, submitButtonStatus, submitElement, innerElement, autocomplete, readOnly, stretch, inputAttributes, icon, iconPosition, iconSize, className, id: _id_unused, children, value: _value, selectAll, inputElement: _inputElement, ref: _ref, inputState: _inputState, onSubmit, onClear, ...inputSubmitButtonAttributes } = props; const { onSubmitBlur, onSubmitFocus, ...attributes } = inputSubmitButtonAttributes; let usedInputState = inputState; if (disabled || skeleton) { usedInputState = 'disabled'; } const sizeIsNumber = parseFloat(size) > 0; const id = _id; const showStatus = getStatusState(status); const hasSubmitButton = submitElement || submitElement !== false && type === 'search'; const hasVal = hasValue(value); const usedIconSize = size === 'large' && (iconSize === 'default' || !iconSize) ? 'medium' : iconSize; const mainParams = applySpacing(props, { className: clsx("dnb-input dnb-input__border--tokens dnb-form-component", className, icon && `dnb-input--icon-position-${iconPosition} dnb-input--has-icon` + (usedIconSize ? ` dnb-input--icon-size-${usedIconSize}` : ""), type && `dnb-input--${type}`, size && !sizeIsNumber && `dnb-input--${size}`, hasSubmitButton && 'dnb-input--has-submit-element', innerElement && 'dnb-input--has-inner-element', showClearButton && 'dnb-input--has-clear-button', align && `dnb-input__align--${align}`, status && `dnb-input__status--${statusState}`, disabled && 'dnb-input--disabled', labelDirection && `dnb-input--${labelDirection}`, stretch && `dnb-input--stretch`, keepPlaceholder && 'dnb-input--keep-placeholder'), 'data-input-state': usedInputState, 'data-has-content': hasVal ? 'true' : 'false' }); const innerParams = { className: 'dnb-input__inner' }; let { inputElement: InputElement } = props; const usedInputAttributes = inputAttributes ? typeof inputAttributes === 'string' ? JSON.parse(inputAttributes) : inputAttributes : {}; const inputParams = { className: clsx('dnb-input__input', inputClassName), autoComplete: autocomplete, type, id, disabled: disabled, name: id, 'aria-placeholder': placeholder ? convertJsxToString(placeholder) : undefined, ...attributes, ...usedInputAttributes, onChange: onChangeHandler, onKeyDown: onKeyDownHandler, onFocus: onFocusHandler, onBlur: onBlurHandler }; if (inputParams['role'] && inputParams['role'] !== 'textbox' && inputParams['role'] !== 'searchbox') { delete inputParams['aria-placeholder']; } if (sizeIsNumber) { inputParams.size = size; } if (showStatus || suffix || hasSubmitButton) { inputParams['aria-describedby'] = combineDescribedBy(inputParams, hasSubmitButton && !submitElement ? id + '-submit-button' : null, showStatus ? id + '-status' : null, suffix ? id + '-suffix' : null); } if (readOnly) { inputParams['aria-readonly'] = inputParams.readOnly = true; } const shellParams = { className: clsx(createSkeletonClass('shape', skeleton, context), !_omitInputShellClass && "dnb-input__shell dnb-input__border") }; skeletonDOMAttributes(inputParams, skeleton, context); validateDOMAttributes(restProps, inputParams); validateDOMAttributes(null, shellParams); if (InputElement && typeof InputElement === 'function') { InputElement = InputElement({ ...inputParams, value }, inputRef); } else if (!InputElement && _inputElement) { InputElement = _inputElement; } return _jsxs("span", { ...mainParams, children: [label && _jsx(FormLabel, { id: id + '-label', forId: id, text: label, labelDirection: labelDirection, srOnly: labelSrOnly, disabled: disabled, skeleton: skeleton }), _jsxs("span", { ...innerParams, children: [_AlignmentHelper || (_AlignmentHelper = _jsx(AlignmentHelper, {})), _jsx(FormStatus, { show: showStatus, id: id + '-form-status', globalStatus: globalStatus, label: label, text: status, state: statusState, textId: id + '-status', noAnimation: statusNoAnimation, skeleton: skeleton, ...statusProps }), _jsxs("span", { className: "dnb-input__row", children: [_jsxs("span", { ...shellParams, children: [InputElement || _jsx("input", { ref: combinedRef, ...inputParams }), innerElement && _jsx("span", { className: "dnb-input__inner__element dnb-p", children: innerElement }), icon && _jsx(InputIcon, { className: "dnb-input__icon", icon: icon, size: usedIconSize }), !hasVal && placeholder && _jsx("span", { id: id + '-placeholder', className: 'dnb-input__placeholder' + (align ? ` dnb-input__align--${align}` : ""), role: "presentation", "aria-hidden": true, children: placeholder }), showClearButton && iconPosition !== 'right' && _jsx("span", { className: "dnb-input--clear dnb-input__submit-element", children: _jsx(InputSubmitButton, { "aria-hidden": !hasVal, attributes: { className: 'dnb-input__clear-button' }, id: id + '-clear-button', type: "button", variant: "tertiary", "aria-controls": id, "aria-label": clearButtonTitle, tooltip: hasVal && clearButtonTitle, icon: "close", iconSize: size === 'small' ? 'small' : undefined, skeleton: skeleton, disabled: disabled || !hasVal, onClick: clearValueHandler }) })] }), hasSubmitButton && _jsx("span", { className: "dnb-input__submit-element", children: submitElement ? submitElement : _jsx(InputSubmitButton, { ...inputSubmitButtonAttributes, id: id + '-submit-button', value: hasVal ? value : '', icon: submitButtonIcon, status: submitButtonStatus || status ? statusState : null, statusState: statusState, iconSize: size === 'medium' || size === 'large' ? 'medium' : 'default', title: submitButtonTitle, variant: submitButtonVariant, disabled: disabled, skeleton: skeleton, size: size, onSubmit: onSubmit, ...statusProps }) }), suffix && _jsx(Suffix, { className: "dnb-input__suffix", id: id + '-suffix', context: props, children: suffix })] })] })] }); } const inputSubmitButtonDefaultProps = { id: null, value: null, title: null, disabled: false, skeleton: false, variant: 'secondary', icon: 'loupe', iconSize: null, status: null, statusState: 'error', statusProps: null, className: null, onSubmit: null, onSubmitFocus: null, onSubmitBlur: null }; function InputSubmitButton({ ref, ...ownProps }) { const context = useContext(Context); const buttonRef = useRef(null); const combinedButtonRef = useCombinedRef(ref, buttonRef); const [focusState, setFocusState] = useState('virgin'); const props = { ...inputSubmitButtonDefaultProps, ...removeUndefinedProps({ ...ownProps }) }; const onSubmitFocusHandler = useCallback(event => { const submitValue = props.value; setFocusState('focus'); dispatchCustomElementEvent(props, 'onSubmitFocus', { value: submitValue, event }); }, [props.value, props.onSubmitFocus]); const onSubmitBlurHandler = useCallback(event => { const submitValue = props.value; setFocusState('dirty'); dispatchCustomElementEvent(props, 'onSubmitBlur', { value: submitValue, event }); }, [props.value, props.onSubmitBlur]); const onSubmitHandler = useCallback(event => { const submitValue = props.value; dispatchCustomElementEvent(props, 'onSubmit', { value: submitValue, event }); }, [props.value, props.onSubmit]); const { id, title, disabled, skeleton, variant, icon, iconSize, status, statusState, statusProps, className, onSubmitBlur: _onSubmitBlur, onSubmitFocus: _onSubmitFocus, ...rest } = props; const params = { id, type: 'submit', 'aria-label': title, disabled, ...rest }; skeletonDOMAttributes(params, skeleton, context); validateDOMAttributes(ownProps, params); return _jsx("span", { className: "dnb-input__submit-button", "data-input-state": focusState, children: _jsx(Button, { className: clsx("dnb-input__submit-button__button dnb-button--input-button", className), variant: variant, icon: icon, iconSize: iconSize, status: status, statusState: statusState, onClick: onSubmitHandler, onFocus: onSubmitFocusHandler, onBlur: onSubmitBlurHandler, ref: combinedButtonRef, ...params, ...statusProps }) }); } export { InputSubmitButton as SubmitButton }; const InputIcon = React.memo(props => _jsx(IconPrimary, { ...props }), ({ icon: prev }, { icon: next }) => { if (typeof prev === 'string' && typeof next === 'string') { return prev === next; } const isProgressIndicator = icon => { if (!React.isValidElement(icon)) { return false; } const type = icon.type; return type?.displayName === 'ProgressIndicator'; }; if (isProgressIndicator(prev) && isProgressIndicator(next)) { return typeof prev === typeof next; } return false; }); InputIcon.displayName = 'InputIcon'; const MemoizedInputComponent = React.memo(InputComponent); const Input = Object.assign(function Input(props) { return _jsx(MemoizedInputComponent, { ...props }); }, { getValue, hasValue }); withComponentMarkers(Input, { _formElement: true, _supportsSpacingProps: true }); export default Input; //# sourceMappingURL=Input.js.map