UNPKG

@spaced-out/ui-design-system

Version:
326 lines (304 loc) 8.34 kB
// @flow strict import * as React from 'react'; import {classify} from '../../utils/classify'; import type {IconType} from '../Icon'; import {Icon} from '../Icon'; import {BodySmall, FormLabelSmall} from '../Text'; import css from './Input.module.css'; type ClassNames = $ReadOnly<{ box?: string, iconLeft?: string, iconRight?: string, wrapper?: string, }>; export const EXPONENT_CHARACTER_LIST = ['E', 'e']; export const INPUT_TYPES = Object.freeze({ text: 'text', number: 'number', password: 'password', email: 'email', tel: 'tel', url: 'url', date: 'date', 'datetime-local': 'datetime-local', time: 'time', week: 'week', month: 'month', color: 'color', search: 'search', }); export type InputType = $Values<typeof INPUT_TYPES>; export type InputOnChangeParamsType = { evt: SyntheticInputEvent<HTMLInputElement>, isEnter?: boolean, }; export type InputProps = { value?: string, onChange?: ( evt: SyntheticInputEvent<HTMLInputElement>, isEnter?: boolean, ) => mixed, classNames?: ClassNames, onFocus?: (e: SyntheticInputEvent<HTMLInputElement>) => mixed, onBlur?: (e: SyntheticInputEvent<HTMLInputElement>) => mixed, onKeyDown?: (e: SyntheticKeyboardEvent<HTMLInputElement>) => mixed, onPaste?: (e: ClipboardEvent) => mixed, onIconRightClick?: ?(SyntheticEvent<HTMLElement>) => mixed, onContainerClick?: ?(SyntheticEvent<HTMLElement>) => mixed, name?: string, disabled?: boolean, placeholder?: string, locked?: boolean, error?: boolean, errorText?: string, label?: string | React.Node, helperText?: string | React.Node, type?: InputType, size?: 'medium' | 'small', iconLeftName?: string, iconLeftType?: IconType, iconRightName?: string, iconRightType?: IconType, required?: boolean, readOnly?: boolean, boxRef?: (?HTMLElement) => mixed, minLength?: string, maxLength?: string, pattern?: string, min?: string, max?: string, autoComplete?: string, /* Note(Nishant): Restricts typing `e` and `E` in the number input when set to true. We have baked this condition in the keydown handler itself so this would restrict and not show these exponent characters when typed **/ disallowExponents?: boolean, /** The step attribute is a number that specifies the granularity that the value must adhere to, or the special value any. * Only values which are equal to the basis for stepping (min if specified, value otherwise, and an * appropriate default value if neither of those is provided) are valid. */ step?: string, hideNumberSpinner?: boolean, hidePasswordToggleIcon?: boolean, ... }; const Input_ = (props: InputProps, ref): React.Node => { const { value, type, onChange, onFocus, onBlur, onIconRightClick, onContainerClick, name, disabled, placeholder, error, locked, errorText, label, helperText, classNames, size = 'medium', iconLeftName = '', iconLeftType = 'regular', iconRightName = '', iconRightType = 'regular', required, readOnly, boxRef, onKeyDown, disallowExponents, hideNumberSpinner, hidePasswordToggleIcon, ...inputProps } = props; const [showPassword, setShowPassword] = React.useState(false); const controlledInputFilled = value !== ''; const handleRightIconClick = (e: SyntheticEvent<HTMLElement>) => { if (locked || disabled) { return; } if (type === 'password') { setShowPassword(!showPassword); } onIconRightClick && onIconRightClick(e); }; const handleKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => { if (type === INPUT_TYPES.number && disallowExponents) { if (EXPONENT_CHARACTER_LIST.includes(e.key)) { e.preventDefault(); } } onKeyDown?.(e); }; return ( <div className={classify( css.wrapper, { [css.filled]: controlledInputFilled ?? false, [css.withError]: error ?? false, }, classNames?.wrapper, )} > {Boolean(label) && ( <div className={css.info}> <div className={css.infoContent}> <FormLabelSmall color="secondary">{label ?? ''}</FormLabelSmall> &nbsp; {required && <FormLabelSmall color="danger">{'*'}</FormLabelSmall>} </div> </div> )} <div className={classify( css.box, { [css.inputDisabled]: disabled ?? false, [css.medium]: size === 'medium', [css.small]: size === 'small', [css.locked]: locked, [css.color]: type === 'color', [css.hideNumberSpinner]: hideNumberSpinner, }, classNames?.box, )} onClick={!(disabled || locked) ? onContainerClick : null} ref={boxRef} > {iconLeftName && ( <Icon className={classify(classNames?.iconLeft)} name={iconLeftName} color={disabled ? 'disabled' : 'secondary'} size="small" type={iconLeftType} /> )} <input {...inputProps} disabled={locked || disabled} name={name} ref={ref} placeholder={placeholder} value={value} onChange={onChange} onFocus={onFocus} onBlur={onBlur} type={showPassword ? 'text' : type} readOnly={readOnly && 'readOnly'} onKeyDown={handleKeyDown} /> {type === 'color' && ( <div className={classify(css.colorText, css[size], { [css.hasValue]: value, })} > {value ? value : placeholder} </div> )} <RightInputIcon isEmail={type === 'email'} isPassword={type === 'password'} showPassword={showPassword} showPasswordToggleIcon={!hidePasswordToggleIcon} isLocked={locked} isDisabled={disabled} onClick={handleRightIconClick} iconRightName={iconRightName} iconRightType={iconRightType} className={classNames?.iconRight} /> </div> {(Boolean(helperText) || error) && ( <div className={css.info}> {error && errorText ? ( <BodySmall color="danger">{errorText}</BodySmall> ) : typeof helperText === 'string' ? ( <BodySmall color={disabled ? 'disabled' : 'secondary'}> {helperText} </BodySmall> ) : ( helperText )} </div> )} </div> ); }; const RightInputIcon = ({ isEmail, isPassword, showPassword, showPasswordToggleIcon, isLocked, isDisabled, iconRightName, iconRightType, ...rightIconProps }: { isEmail?: boolean, isPassword?: boolean, showPassword?: boolean, showPasswordToggleIcon?: boolean, isLocked?: boolean, isDisabled?: boolean, onClick?: ?(SyntheticEvent<HTMLElement>) => mixed, iconRightName?: string, iconRightType?: IconType, className?: string, }): React.Node => { if (isLocked) { return ( <Icon name="lock" color={isDisabled ? 'disabled' : 'secondary'} size="small" {...rightIconProps} /> ); } if (isEmail) { return ( <Icon name="at" color={isDisabled ? 'disabled' : 'secondary'} size="small" type={iconRightType} {...rightIconProps} /> ); } if (isPassword && showPasswordToggleIcon) { return ( <Icon name={showPassword ? 'eye-slash' : 'eye'} color={isDisabled ? 'disabled' : 'secondary'} size="small" type={iconRightType} {...rightIconProps} className={classify(css.rightClickableIcon, { [css.disabled]: isDisabled || isLocked, })} /> ); } if (iconRightName) { return ( <Icon name={iconRightName || ''} color={isDisabled ? 'disabled' : 'secondary'} size="small" type={iconRightType} {...rightIconProps} /> ); } return <></>; }; export const Input = (React.forwardRef<InputProps, HTMLInputElement>( Input_, ): React$AbstractComponent<InputProps, HTMLInputElement>);