Files
twenty/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx
T
Félix Malfait b627c55b9a Add blue focus on text inputs (#8343)
We removed the blue focus on phone/domain/array/email floating inputs
but we want to keep it in the app settings.

<img width="644" alt="Screenshot 2024-11-05 at 15 55 04"
src="https://github.com/user-attachments/assets/afcbe6b2-2d6b-4e0d-8397-4268d529127c">
<img width="606" alt="Screenshot 2024-11-05 at 15 55 23"
src="https://github.com/user-attachments/assets/004becf8-41e7-45d6-9ad9-9e487b8db8f3">
<img width="351" alt="Screenshot 2024-11-05 at 15 55 33"
src="https://github.com/user-attachments/assets/6a4c06e6-04d3-46bf-940b-9fd61ee91995">
<img width="330" alt="Screenshot 2024-11-05 at 15 55 41"
src="https://github.com/user-attachments/assets/e6fc8bbd-eca3-47bc-93f1-d6ff8d3d8a13">
<img width="588" alt="Screenshot 2024-11-05 at 15 56 07"
src="https://github.com/user-attachments/assets/0d0f5e80-3501-4346-94a1-6ea4b77ee7ba">
<img width="211" alt="Screenshot 2024-11-05 at 15 56 31"
src="https://github.com/user-attachments/assets/9cd85f4d-8052-4c6b-a694-84c691c6217d">
2024-11-06 14:23:34 +01:00

246 lines
6.6 KiB
TypeScript

import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
ChangeEvent,
FocusEventHandler,
ForwardedRef,
InputHTMLAttributes,
forwardRef,
useId,
useRef,
useState,
} from 'react';
import { IconComponent, IconEye, IconEyeOff, RGBA } from 'twenty-ui';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledContainer = styled.div<
Pick<TextInputV2ComponentProps, 'fullWidth'>
>`
display: inline-flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
`;
const StyledLabel = styled.label`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
`;
const StyledInputContainer = styled.div`
display: flex;
flex-direction: row;
width: 100%;
position: relative;
`;
const StyledInput = styled.input<
Pick<TextInputV2ComponentProps, 'fullWidth' | 'LeftIcon' | 'error'>
>`
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: 32px;
outline: none;
padding: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme, LeftIcon }) =>
LeftIcon ? `calc(${theme.spacing(4)} + 16px)` : theme.spacing(2)};
width: 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 }) => {
return `box-shadow: 0px 0px 0px 3px ${RGBA(theme.color.blue, 0.1)};
border-color: ${theme.color.blue};`;
}};
}
`;
const StyledErrorHelper = styled.div`
color: ${({ theme }) => theme.color.red};
font-size: ${({ theme }) => theme.font.size.xs};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledLeftIconContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
padding-left: ${({ theme }) => theme.spacing(2)};
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
`;
const StyledTrailingIconContainer = styled.div<
Pick<TextInputV2ComponentProps, 'error'>
>`
align-items: center;
display: flex;
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
position: absolute;
top: 0;
bottom: 0;
right: 0;
margin: auto 0;
`;
const StyledTrailingIcon = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex;
justify-content: center;
`;
const INPUT_TYPE_PASSWORD = 'password';
export type TextInputV2ComponentProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'onChange' | 'onKeyDown'
> & {
className?: string;
label?: string;
onChange?: (text: string) => void;
fullWidth?: boolean;
error?: string;
noErrorHelper?: boolean;
RightIcon?: IconComponent;
LeftIcon?: IconComponent;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onBlur?: FocusEventHandler<HTMLInputElement>;
dataTestId?: string;
};
const TextInputV2Component = (
{
className,
label,
value,
onChange,
onFocus,
onBlur,
onKeyDown,
fullWidth,
error,
noErrorHelper = false,
required,
type,
autoFocus,
placeholder,
disabled,
tabIndex,
RightIcon,
LeftIcon,
autoComplete,
maxLength,
dataTestId,
}: TextInputV2ComponentProps,
// eslint-disable-next-line @nx/workspace-component-props-naming
ref: ForwardedRef<HTMLInputElement>,
): JSX.Element => {
const theme = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
const combinedRef = useCombinedRefs(ref, inputRef);
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTogglePasswordVisibility = () => {
setPasswordVisible(!passwordVisible);
};
const inputId = useId();
return (
<StyledContainer className={className} fullWidth={fullWidth ?? false}>
{label && (
<StyledLabel htmlFor={inputId}>
{label + (required ? '*' : '')}
</StyledLabel>
)}
<StyledInputContainer>
{!!LeftIcon && (
<StyledLeftIconContainer>
<StyledTrailingIcon>
<LeftIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
</StyledLeftIconContainer>
)}
<StyledInput
id={inputId}
data-testid={dataTestId}
autoComplete={autoComplete || 'off'}
ref={combinedRef}
tabIndex={tabIndex ?? 0}
onFocus={onFocus}
onBlur={onBlur}
type={passwordVisible ? 'text' : type}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange?.(
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
);
}}
onKeyDown={onKeyDown}
{...{
autoFocus,
disabled,
placeholder,
required,
value,
LeftIcon,
maxLength,
error,
}}
/>
<StyledTrailingIconContainer {...{ error }}>
{!error && type === INPUT_TYPE_PASSWORD && (
<StyledTrailingIcon
onClick={handleTogglePasswordVisibility}
data-testid="reveal-password-button"
>
{passwordVisible ? (
<IconEyeOff size={theme.icon.size.md} />
) : (
<IconEye size={theme.icon.size.md} />
)}
</StyledTrailingIcon>
)}
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
<StyledTrailingIcon>
<RightIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{error && !noErrorHelper && (
<StyledErrorHelper>{error}</StyledErrorHelper>
)}
</StyledContainer>
);
};
export const TextInputV2 = forwardRef(TextInputV2Component);