import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ChangeEvent, FocusEventHandler, InputHTMLAttributes, forwardRef, useId, useRef, useState, } from 'react'; import { AutogrowWrapper, IconComponent, IconEye, IconEyeOff, Loader, } from 'twenty-ui'; import { useCombinedRefs } from '~/hooks/useCombinedRefs'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; const StyledContainer = styled.div< Pick >` box-sizing: border-box; display: inline-flex; flex-direction: column; width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')}; `; const StyledInputContainer = styled.div` align-items: center; background-color: inherit; display: flex; flex-direction: row; position: relative; `; const StyledAdornmentContainer = styled.div<{ sizeVariant: TextInputV2Size; position: 'left' | 'right'; }>` align-items: center; background-color: ${({ theme }) => theme.background.transparent.light}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme, position }) => position === 'left' ? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}` : `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0`}; box-sizing: border-box; color: ${({ theme }) => theme.font.color.tertiary}; display: flex; font-size: ${({ theme }) => theme.font.size.md}; font-weight: ${({ theme }) => theme.font.weight.medium}; height: ${({ sizeVariant }) => sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; justify-content: center; min-width: fit-content; padding: ${({ theme }) => theme.spacing(2)}; width: auto; line-height: ${({ sizeVariant }) => sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; ${({ position }) => position === 'left' ? 'border-right: none;' : 'border-left: none;'} `; const StyledInput = styled.input< Pick< TextInputV2ComponentProps, | 'LeftIcon' | 'error' | 'sizeVariant' | 'width' | 'inheritFontStyles' | 'autoGrow' | 'rightAdornment' | 'leftAdornment' > >` background-color: ${({ theme }) => theme.background.transparent.lighter}; border-radius: ${({ theme, leftAdornment, rightAdornment }) => leftAdornment ? `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0` : rightAdornment ? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}` : theme.border.radius.sm}; border: 1px solid ${({ theme, error }) => error ? theme.border.color.danger : theme.border.color.medium}; box-sizing: border-box; color: ${({ theme }) => theme.font.color.primary}; display: flex; flex-grow: 1; font-family: ${({ theme, inheritFontStyles }) => inheritFontStyles ? 'inherit' : theme.font.family}; font-size: ${({ theme, inheritFontStyles }) => inheritFontStyles ? 'inherit' : theme.font.size.md}; font-weight: ${({ theme, inheritFontStyles }) => inheritFontStyles ? 'inherit' : theme.font.weight.regular}; height: ${({ sizeVariant }) => sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; line-height: ${({ sizeVariant }) => sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; outline: none; padding: ${({ theme, sizeVariant, autoGrow }) => autoGrow ? theme.spacing(1) : sizeVariant === 'sm' ? `${theme.spacing(2)} 0` : theme.spacing(2)}; padding-left: ${({ theme, LeftIcon, autoGrow }) => autoGrow ? theme.spacing(1) : LeftIcon ? `calc(${theme.spacing(3)} + 16px)` : theme.spacing(2)}; width: ${({ theme, width }) => width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'}; max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')}; &::placeholder, &::-webkit-input-placeholder { color: ${({ theme }) => theme.font.color.light}; font-family: ${({ theme }) => theme.font.family}; font-weight: ${({ theme }) => theme.font.weight.medium}; } &:disabled { color: ${({ theme }) => theme.font.color.tertiary}; } &:focus { ${({ theme, error }) => { return ` border-color: ${error ? theme.border.color.danger : theme.color.blue}; `; }}; } `; const StyledLeftIconContainer = styled.div<{ sizeVariant: TextInputV2Size }>` align-items: center; display: flex; justify-content: center; padding-left: ${({ theme, sizeVariant }) => sizeVariant === 'sm' ? theme.spacing(0.5) : sizeVariant === 'md' ? theme.spacing(1) : theme.spacing(2)}; position: absolute; top: 0; bottom: 0; margin: auto 0; `; const StyledTrailingIconContainer = styled.div< Pick >` align-items: center; display: flex; justify-content: center; padding-right: ${({ theme }) => theme.spacing(2)}; position: absolute; top: 0; bottom: 0; right: 0; margin: auto 0; `; const StyledTrailingIcon = styled.div<{ isFocused?: boolean }>` align-items: center; color: ${({ theme, isFocused }) => isFocused ? theme.font.color.secondary : theme.font.color.light}; cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')}; display: flex; justify-content: center; `; const INPUT_TYPE_PASSWORD = 'password'; export type TextInputV2Size = 'sm' | 'md' | 'lg'; export type TextInputV2ComponentProps = Omit< InputHTMLAttributes, 'onChange' | 'onKeyDown' > & { className?: string; label?: string; onChange?: (text: string) => void; fullWidth?: boolean; error?: string; noErrorHelper?: boolean; RightIcon?: IconComponent; LeftIcon?: IconComponent; autoGrow?: boolean; onKeyDown?: (event: React.KeyboardEvent) => void; onBlur?: FocusEventHandler; dataTestId?: string; sizeVariant?: TextInputV2Size; inheritFontStyles?: boolean; loading?: boolean; rightAdornment?: string; leftAdornment?: string; }; type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps; const TextInputV2Component = forwardRef< HTMLInputElement, TextInputV2ComponentProps >( ( { className, label, value, onChange, onFocus, onBlur, onKeyDown, fullWidth, width, error, noErrorHelper = false, required, type, autoFocus, placeholder, disabled, tabIndex, RightIcon, LeftIcon, autoComplete, maxLength, sizeVariant = 'lg', inheritFontStyles = false, dataTestId, autoGrow = false, loading = false, rightAdornment, leftAdornment, }, ref, ) => { const theme = useTheme(); const inputRef = useRef(null); const combinedRef = useCombinedRefs(ref, inputRef); const [passwordVisible, setPasswordVisible] = useState(false); const [isFocused, setIsFocused] = useState(false); const handleTogglePasswordVisibility = () => { setPasswordVisible(!passwordVisible); }; const handleFocus: FocusEventHandler = (event) => { setIsFocused(true); onFocus?.(event); }; const handleBlur: FocusEventHandler = (event) => { setIsFocused(false); onBlur?.(event); }; const inputId = useId(); return ( {label && ( {label + (required ? '*' : '')} )} {leftAdornment && ( {leftAdornment} )} {!!LeftIcon && ( )} ) => { onChange?.( turnIntoEmptyStringIfWhitespacesOnly(event.target.value), ); }} onKeyDown={onKeyDown} {...{ autoFocus, disabled, placeholder, required, value, LeftIcon, maxLength, error, sizeVariant, inheritFontStyles, autoGrow, leftAdornment, rightAdornment, }} /> {rightAdornment && ( {rightAdornment} )} {!error && type === INPUT_TYPE_PASSWORD && ( {passwordVisible ? ( ) : ( )} )} {!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && ( )} {!error && type !== INPUT_TYPE_PASSWORD && !!loading && ( )} {error} ); }, ); const StyledAutogrowWrapper = styled(AutogrowWrapper)<{ sizeVariant?: TextInputV2Size; }>` border: 1px solid transparent; height: ${({ sizeVariant }) => sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; padding: 0 ${({ theme }) => theme.spacing(1.25)}; box-sizing: border-box; `; const TextInputV2WithAutoGrowWrapper = forwardRef< HTMLInputElement, TextInputV2WithAutoGrowWrapperProps >((props, ref) => { return ( <> {props.autoGrow ? ( ) : ( )} ); }); export const TextInputV2 = TextInputV2WithAutoGrowWrapper;