import { InputLabel } from '@/ui/input/components/InputLabel'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ChangeEvent, FocusEventHandler, ForwardedRef, InputHTMLAttributes, forwardRef, useId, useRef, useState, } from 'react'; import { ComputeNodeDimensions, IconComponent, IconEye, IconEyeOff, } from 'twenty-ui'; import { useCombinedRefs } from '~/hooks/useCombinedRefs'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper'; const StyledContainer = styled.div< Pick >` box-sizing: border-box; display: inline-flex; flex-direction: column; width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')}; `; const StyledInputContainer = styled.div` background-color: inherit; display: flex; flex-direction: row; position: relative; `; const StyledInput = styled.input< Pick< TextInputV2ComponentProps, 'LeftIcon' | 'error' | 'sizeVariant' | 'width' > >` background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme, error }) => error ? theme.border.color.danger : theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.sm}; box-sizing: border-box; color: ${({ theme }) => theme.font.color.primary}; display: flex; flex-grow: 1; font-family: ${({ theme }) => theme.font.family}; font-weight: ${({ theme }) => theme.font.weight.regular}; height: ${({ sizeVariant }) => sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; outline: none; padding: ${({ theme, sizeVariant }) => sizeVariant === 'sm' ? `${theme.spacing(2)} 0` : theme.spacing(2)}; padding-left: ${({ theme, LeftIcon }) => LeftIcon ? `calc(${theme.spacing(3)} + 16px)` : theme.spacing(2)}; width: ${({ theme, width }) => width ? `calc(${width}px + ${theme.spacing(5)})` : '100%'}; &::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; }; type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps; const TextInputV2Component = ( { className, label, value, onChange, onFocus, onBlur, onKeyDown, fullWidth, width, error, noErrorHelper = false, required, type, autoFocus, placeholder, disabled, tabIndex, RightIcon, LeftIcon, autoComplete, maxLength, sizeVariant = 'lg', dataTestId, }: TextInputV2ComponentProps, // eslint-disable-next-line @nx/workspace-component-props-naming ref: ForwardedRef, ): JSX.Element => { 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 ? '*' : '')} )} {!!LeftIcon && ( )} ) => { onChange?.( turnIntoEmptyStringIfWhitespacesOnly(event.target.value), ); }} onKeyDown={onKeyDown} {...{ autoFocus, disabled, placeholder, required, value, LeftIcon, maxLength, error, sizeVariant, }} /> {!error && type === INPUT_TYPE_PASSWORD && ( {passwordVisible ? ( ) : ( )} )} {!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && ( )} {error} ); }; const TextInputV2WithAutoGrowWrapper = ( props: TextInputV2WithAutoGrowWrapperProps, ) => ( <> {props.autoGrow ? ( {(nodeDimensions) => ( // eslint-disable-next-line )} ) : ( // eslint-disable-next-line )} ); export const TextInputV2 = forwardRef(TextInputV2WithAutoGrowWrapper);