@spaced-out/ui-design-system
Version:
Sense UI components library
326 lines (304 loc) • 8.34 kB
Flow
// @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>
{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>);