import isNumber from 'is-number'; import React, { ElementType, InputHTMLAttributes, ReactNode, Ref, useCallback, useRef, useState } from 'react'; import classNames from 'classnames'; import type { CSSModule } from 'reactstrap/types/lib/utils'; import { Icon } from '../Icon/Icon'; import { notifyDeprecation } from '../utils'; import { InputContainer } from './InputContainer'; import { getClasses, getFormControlClass, getTag, getValidationTextControlClass, useFocus } from './utils'; // taken from reactstrap types type InputType = | 'text' | 'email' | 'select' | 'file' | 'radio' | 'checkbox' | 'textarea' | 'button' | 'reset' | 'submit' | 'date' | 'datetime-local' | 'hidden' | 'image' | 'month' | 'number' | 'range' | 'search' | 'tel' | 'url' | 'week' | 'password' | 'datetime' | 'time' | 'color' | 'adaptive' | 'currency' | 'percentage'; export interface InputProps extends InputHTMLAttributes { /** Il tipo specifico di input da utilizzare. Il valore di default è `text`. */ type?: InputType; /** Dimensione personalizzate del campo di Input secondo le classi Bootstrap/Bootstrap Italia. */ bsSize?: 'lg' | 'sm'; size?: number; /** Etichetta del campo Input. */ label?: string | ReactNode; /** Etichetta del pulsante incremento. */ incrementLabel?: string | ReactNode; /** Etichetta del pulsante decremento. */ decrementLabel?: string | ReactNode; /** Testo di esempio da utilizzare per il campo. */ placeholder?: string; /** Testo di validazione per l'elemento del modulo form. */ validationText?: string; /** Testo di aiuto per l'elemento del moduleo form. Richiede che il componente `Input` abbia la prop `id` impostata. */ infoText?: string; /** Il valore nel campo Input. */ value?: string | number; /** Da utilizzare per impedire la modifica del valore contenuto. */ readOnly?: boolean; /** Associato all'attributo readOnly mostra il campo con lo stile classico, mantenendo lo stato di sola lettura. */ normalized?: boolean; /** Utilizzare per mostrare il successo nella validazione del valore nel campo Input */ valid?: boolean; innerRef?: Ref; /** Utilizzare per mostrare testo statico non modificabile. */ plaintext?: boolean; /** Utilizzare per mostrare un elemento un simbolo attivando la proprietà addon nel campo input all'interno del componente */ addonText?: string; /** Oggetto contenente la nuova mappatura per le classi CSS. */ cssModule?: CSSModule; /** Classi aggiuntive da usare per il wrapper del componente Input */ wrapperClassName?: string; /** * Classi aggiuntive da usare per il wrapper del componente Input. * @deprecated. Usare `wrapperClassName`. * */ wrapperClass?: string; /** Utilizzarlo in caso di utilizzo di componenti personalizzati */ tag?: ElementType; /** Classi aggiuntive da usare per il componente Input */ className?: string; /** * Usare "plaintext". * @deprecated */ static?: boolean; /** Quando attivo rimuove il componente contenitore dell'Input. Utile per un controllo maggiore dello styling */ noWrapper?: boolean; /** Indica che il componente ha un bottone a destra rispetto all'input */ hasButtonRight?: boolean; /** Componente per il bottone */ buttonRight?: ReactNode; /** Indica che il componente ha una icona a sinistra rispetto all'input */ hasIconLeft?: boolean; /** Componente per l'icona */ iconLeft?: ReactNode; testId?: string; } export const Input = ({ id, className, cssModule, type = 'text', tag, addonText, static: staticInput, plaintext, innerRef, label, incrementLabel, decrementLabel, validationText, infoText, placeholder, normalized, value, wrapperClass: originalWrapperClassOld, wrapperClassName: originalWrapperClass, size, testId, noWrapper = false, hasButtonRight, buttonRight, hasIconLeft, iconLeft, ...attributes }: InputProps) => { const [isHidden, setHidden] = useState(true); const [hasIcon, toggleIcon] = useState(true); const { toggleFocusLabel, toggleBlurLabel, isFocused } = useFocus({ onFocus: attributes.onFocus, onBlur: attributes.onBlur }); const toggleShow = useCallback(() => { setHidden(!isHidden); toggleIcon(!hasIcon); }, [hasIcon, isHidden]); const inputRef = useRef(null); // eslint-disable-next-line prefer-const let { bsSize, valid, ...rest } = attributes; const Tag = getTag({ tag, plaintext, staticInput, type }); const formControlClass = getFormControlClass( { plaintext, staticInput, type, normalized }, cssModule ); const validationTextControlClass = getValidationTextControlClass({ valid }, cssModule); const extraAttributes: { type?: InputType; size?: number; ['aria-describedby']?: string; } = {}; if (size && !isNumber(size)) { notifyDeprecation('Please use the prop "bsSize" instead of the "size" to bootstrap\'s input sizing.'); bsSize = size as unknown as InputProps['bsSize']; } else { extraAttributes.size = size; } if (Tag === 'input' || typeof tag !== 'string') { extraAttributes.type = type; } // associate the input field with the help text const infoId = id && infoText ? `${id}Description` : undefined; if (infoId) { extraAttributes['aria-describedby'] = infoId; } if ( attributes.children && !(plaintext || staticInput || type === 'select' || typeof Tag !== 'string' || Tag === 'select') ) { notifyDeprecation( `Input with a type of "${type}" cannot have children. Please use "value"/"defaultValue" instead.` ); delete attributes.children; } const inputPassword = extraAttributes.type === 'password'; const indeterminateCheckboxInput = type === 'checkbox' && className?.includes('semi-checked'); // Styling const { activeClass, extraLabelClass, validationTextClass, inputClasses, wrapperClass } = getClasses( className, type, { valid, bsSize, placeholder, value, label, validationText, normalized: Boolean(normalized), inputPassword, formControlClass, validationTextControlClass, isFocused: isFocused, originalWrapperClass: originalWrapperClass || originalWrapperClassOld }, cssModule ); // set of attributes always shared by the Input components const sharedAttributes = { id, onFocus: toggleFocusLabel, onBlur: toggleBlurLabel, value: value, ref: innerRef }; // set of attributes always shared by the wrapper component const containerProps = { id, infoId, infoText, activeClass, extraLabelClass, label, validationTextClass, validationText, wrapperClass, hasButtonRight, buttonRight, hasIconLeft, iconLeft }; if (noWrapper) { return ( ); } const clickIncrDecr = (mode: number) => { let step = parseFloat(inputRef.current?.step ? inputRef.current.step : '1'); const min = parseFloat(inputRef.current?.min ? inputRef.current.min : 'Nan'); const max = parseFloat(inputRef.current?.max ? inputRef.current.max : 'Nan'); step = isNaN(step) ? 1 : step; const newValue = parseFloat(inputRef.current?.value ? inputRef.current.value : '0') + mode * step; if (!isNaN(max) && newValue > max) { return; } if (!isNaN(min) && newValue < min) { return; } const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; nativeInputValueSetter?.call(inputRef.current, `${newValue}`); const ev1 = new Event('change', { bubbles: true }); const ev2 = new Event('input', { bubbles: true }); inputRef.current?.dispatchEvent(ev1); inputRef.current?.dispatchEvent(ev2); inputRef.current?.focus(); }; if (['currency', 'percentage', 'adaptive', 'number'].includes(type)) { if (containerProps.extraLabelClass && ['currency', 'percentage'].includes(type)) { containerProps.extraLabelClass = containerProps.extraLabelClass + ' input-symbol-label'; } return (
{['currency', 'percentage'].includes(type) && ( {addonText} )}
); } if (placeholder) { return ( ); } if (indeterminateCheckboxInput) { return ( ); } if (inputPassword) { return ( ); } if (normalized) { return ( ); } if (label || validationText) { return ( ); } return ; };