[refacto] Introduce stateless TextInputV2 (#5013)
## Context As discussed with @lucasbordeau and @charlesBochet we are looking at making low level UI components stateless when possible. Therefore TextInput should not handle a hotkey state. Instead hotkeys should be defined in the parent component (as done here in CreateProfile). Introducing here TextInputV2 that is stateless and that can already replace TextInput without any behaviour change everywhere it is used with `disableHotkey` prop. ## How was it tested? Locally + Storybook --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -5,7 +5,7 @@ import { Key } from 'ts-key-enum';
|
|||||||
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||||
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { CountrySelect } from '@/ui/input/components/internal/country/components/CountrySelect';
|
import { CountrySelect } from '@/ui/input/components/internal/country/components/CountrySelect';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -179,7 +179,7 @@ export const AddressInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAddressContainer ref={wrapperRef}>
|
<StyledAddressContainer ref={wrapperRef}>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoFocus
|
autoFocus
|
||||||
value={internalValue.addressStreet1 ?? ''}
|
value={internalValue.addressStreet1 ?? ''}
|
||||||
ref={inputRefs['addressStreet1']}
|
ref={inputRefs['addressStreet1']}
|
||||||
@ -187,46 +187,41 @@ export const AddressInput = ({
|
|||||||
fullWidth
|
fullWidth
|
||||||
onChange={getChangeHandler('addressStreet1')}
|
onChange={getChangeHandler('addressStreet1')}
|
||||||
onFocus={getFocusHandler('addressStreet1')}
|
onFocus={getFocusHandler('addressStreet1')}
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
value={internalValue.addressStreet2 ?? ''}
|
value={internalValue.addressStreet2 ?? ''}
|
||||||
ref={inputRefs['addressStreet2']}
|
ref={inputRefs['addressStreet2']}
|
||||||
label="ADDRESS 2"
|
label="ADDRESS 2"
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={getChangeHandler('addressStreet2')}
|
onChange={getChangeHandler('addressStreet2')}
|
||||||
onFocus={getFocusHandler('addressStreet2')}
|
onFocus={getFocusHandler('addressStreet2')}
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
<StyledHalfRowContainer>
|
<StyledHalfRowContainer>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
value={internalValue.addressCity ?? ''}
|
value={internalValue.addressCity ?? ''}
|
||||||
ref={inputRefs['addressCity']}
|
ref={inputRefs['addressCity']}
|
||||||
label="CITY"
|
label="CITY"
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={getChangeHandler('addressCity')}
|
onChange={getChangeHandler('addressCity')}
|
||||||
onFocus={getFocusHandler('addressCity')}
|
onFocus={getFocusHandler('addressCity')}
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
value={internalValue.addressState ?? ''}
|
value={internalValue.addressState ?? ''}
|
||||||
ref={inputRefs['addressState']}
|
ref={inputRefs['addressState']}
|
||||||
label="STATE"
|
label="STATE"
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={getChangeHandler('addressState')}
|
onChange={getChangeHandler('addressState')}
|
||||||
onFocus={getFocusHandler('addressState')}
|
onFocus={getFocusHandler('addressState')}
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
</StyledHalfRowContainer>
|
</StyledHalfRowContainer>
|
||||||
<StyledHalfRowContainer>
|
<StyledHalfRowContainer>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
value={internalValue.addressPostcode ?? ''}
|
value={internalValue.addressPostcode ?? ''}
|
||||||
ref={inputRefs['addressPostcode']}
|
ref={inputRefs['addressPostcode']}
|
||||||
label="POST CODE"
|
label="POST CODE"
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={getChangeHandler('addressPostcode')}
|
onChange={getChangeHandler('addressPostcode')}
|
||||||
onFocus={getFocusHandler('addressPostcode')}
|
onFocus={getFocusHandler('addressPostcode')}
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
<CountrySelect
|
<CountrySelect
|
||||||
onChange={getChangeHandler('addressCountry')}
|
onChange={getChangeHandler('addressCountry')}
|
||||||
|
|||||||
@ -1,141 +1,26 @@
|
|||||||
import {
|
import { FocusEventHandler, forwardRef, useRef } from 'react';
|
||||||
ChangeEvent,
|
|
||||||
FocusEventHandler,
|
|
||||||
ForwardedRef,
|
|
||||||
forwardRef,
|
|
||||||
InputHTMLAttributes,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { IconAlertCircle, IconComponent, IconEye, IconEyeOff } from 'twenty-ui';
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
TextInputV2,
|
||||||
|
TextInputV2ComponentProps,
|
||||||
|
} from '@/ui/input/components/TextInputV2';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
|
||||||
|
|
||||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
|
|
||||||
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
|
export type TextInputComponentProps = TextInputV2ComponentProps & {
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
|
||||||
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%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
|
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
border-right: none;
|
|
||||||
border-top-left-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)};
|
|
||||||
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};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledErrorHelper = styled.div`
|
|
||||||
color: ${({ theme }) => theme.color.red};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.xs};
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTrailingIconContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
border-left: none;
|
|
||||||
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
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 TextInputComponentProps = Omit<
|
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
|
||||||
'onChange' | 'onKeyDown'
|
|
||||||
> & {
|
|
||||||
className?: string;
|
|
||||||
label?: string;
|
|
||||||
onChange?: (text: string) => void;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
disableHotkeys?: boolean;
|
disableHotkeys?: boolean;
|
||||||
error?: string;
|
|
||||||
RightIcon?: IconComponent;
|
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextInputComponent = (
|
const TextInputComponent = ({
|
||||||
{
|
onFocus,
|
||||||
className,
|
onBlur,
|
||||||
label,
|
disableHotkeys = false,
|
||||||
value,
|
...props
|
||||||
onChange,
|
}: TextInputComponentProps): JSX.Element => {
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
onKeyDown,
|
|
||||||
fullWidth,
|
|
||||||
error,
|
|
||||||
required,
|
|
||||||
type,
|
|
||||||
disableHotkeys = false,
|
|
||||||
autoFocus,
|
|
||||||
placeholder,
|
|
||||||
disabled,
|
|
||||||
tabIndex,
|
|
||||||
RightIcon,
|
|
||||||
}: TextInputComponentProps,
|
|
||||||
// eslint-disable-next-line @nx/workspace-component-props-naming
|
|
||||||
ref: ForwardedRef<HTMLInputElement>,
|
|
||||||
): JSX.Element => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const combinedRef = useCombinedRefs(ref, inputRef);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
@ -167,57 +52,8 @@ const TextInputComponent = (
|
|||||||
{ enabled: !disableHotkeys },
|
{ enabled: !disableHotkeys },
|
||||||
);
|
);
|
||||||
|
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <TextInputV2 {...props} onFocus={handleFocus} onBlur={handleBlur} />;
|
||||||
const handleTogglePasswordVisibility = () => {
|
|
||||||
setPasswordVisible(!passwordVisible);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer className={className} fullWidth={fullWidth ?? false}>
|
|
||||||
{label && <StyledLabel>{label + (required ? '*' : '')}</StyledLabel>}
|
|
||||||
<StyledInputContainer>
|
|
||||||
<StyledInput
|
|
||||||
autoComplete="off"
|
|
||||||
ref={combinedRef}
|
|
||||||
tabIndex={tabIndex ?? 0}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
type={passwordVisible ? 'text' : type}
|
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange?.(event.target.value);
|
|
||||||
}}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
{...{ autoFocus, disabled, placeholder, required, value }}
|
|
||||||
/>
|
|
||||||
<StyledTrailingIconContainer>
|
|
||||||
{error && (
|
|
||||||
<StyledTrailingIcon>
|
|
||||||
<IconAlertCircle size={16} color={theme.color.red} />
|
|
||||||
</StyledTrailingIcon>
|
|
||||||
)}
|
|
||||||
{!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 && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextInput = forwardRef(TextInputComponent);
|
export const TextInput = forwardRef(TextInputComponent);
|
||||||
|
|||||||
@ -0,0 +1,188 @@
|
|||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
FocusEventHandler,
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconAlertCircle, IconComponent, IconEye, IconEyeOff } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<
|
||||||
|
Pick<TextInputV2ComponentProps, 'fullWidth'>
|
||||||
|
>`
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabel = styled.span`
|
||||||
|
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%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInput = styled.input<Pick<TextInputV2ComponentProps, 'fullWidth'>>`
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-right: none;
|
||||||
|
border-top-left-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)};
|
||||||
|
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};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledErrorHelper = styled.div`
|
||||||
|
color: ${({ theme }) => theme.color.red};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTrailingIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-left: none;
|
||||||
|
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
RightIcon?: IconComponent;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextInputV2Component = (
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
onKeyDown,
|
||||||
|
fullWidth,
|
||||||
|
error,
|
||||||
|
required,
|
||||||
|
type,
|
||||||
|
autoFocus,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
tabIndex,
|
||||||
|
RightIcon,
|
||||||
|
}: 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer className={className} fullWidth={fullWidth ?? false}>
|
||||||
|
{label && <StyledLabel>{label + (required ? '*' : '')}</StyledLabel>}
|
||||||
|
<StyledInputContainer>
|
||||||
|
<StyledInput
|
||||||
|
autoComplete="off"
|
||||||
|
ref={combinedRef}
|
||||||
|
tabIndex={tabIndex ?? 0}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
type={passwordVisible ? 'text' : type}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange?.(event.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
{...{ autoFocus, disabled, placeholder, required, value }}
|
||||||
|
/>
|
||||||
|
<StyledTrailingIconContainer>
|
||||||
|
{error && (
|
||||||
|
<StyledTrailingIcon>
|
||||||
|
<IconAlertCircle size={16} color={theme.color.red} />
|
||||||
|
</StyledTrailingIcon>
|
||||||
|
)}
|
||||||
|
{!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 && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextInputV2 = forwardRef(TextInputV2Component);
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TextInputV2,
|
||||||
|
TextInputV2ComponentProps,
|
||||||
|
} from '@/ui/input/components/TextInputV2';
|
||||||
|
|
||||||
|
type RenderProps = TextInputV2ComponentProps;
|
||||||
|
|
||||||
|
const Render = (args: RenderProps) => {
|
||||||
|
const [value, setValue] = useState(args.value);
|
||||||
|
const handleChange = (text: string) => {
|
||||||
|
args.onChange?.(text);
|
||||||
|
setValue(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <TextInputV2 {...args} value={value} onChange={handleChange} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta<typeof TextInputV2> = {
|
||||||
|
title: 'UI/Input/TextInputV2',
|
||||||
|
component: TextInputV2,
|
||||||
|
decorators: [ComponentDecorator],
|
||||||
|
args: { placeholder: 'Tim' },
|
||||||
|
render: Render,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TextInputV2>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Filled: Story = {
|
||||||
|
args: { value: 'Tim' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: { disabled: true, value: 'Tim' },
|
||||||
|
};
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { SubTitle } from '@/auth/components/SubTitle';
|
import { SubTitle } from '@/auth/components/SubTitle';
|
||||||
@ -13,10 +14,12 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||||
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
@ -123,17 +126,22 @@ export const CreateProfile = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isEditingMode, setIsEditingMode] = useState(false);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
Key.Enter,
|
||||||
|
() => {
|
||||||
|
if (isEditingMode) {
|
||||||
|
onSubmit(getValues());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PageHotkeyScope.CreateProfile,
|
||||||
|
);
|
||||||
|
|
||||||
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
|
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNameInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
onSubmit(getValues());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title withMarginTop={false}>Create profile</Title>
|
<Title withMarginTop={false}>Create profile</Title>
|
||||||
@ -157,17 +165,19 @@ export const CreateProfile = () => {
|
|||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoFocus
|
autoFocus
|
||||||
label="First Name"
|
label="First Name"
|
||||||
value={value}
|
value={value}
|
||||||
onBlur={onBlur}
|
onFocus={() => setIsEditingMode(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
onBlur();
|
||||||
|
setIsEditingMode(false);
|
||||||
|
}}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder="Tim"
|
placeholder="Tim"
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
onKeyDown={onNameInputKeyDown}
|
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -178,16 +188,18 @@ export const CreateProfile = () => {
|
|||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInputV2
|
||||||
label="Last Name"
|
label="Last Name"
|
||||||
value={value}
|
value={value}
|
||||||
onBlur={onBlur}
|
onFocus={() => setIsEditingMode(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
onBlur();
|
||||||
|
setIsEditingMode(false);
|
||||||
|
}}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder="Cook"
|
placeholder="Cook"
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
onKeyDown={onNameInputKeyDown}
|
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { H2Title } from '@/ui/display/typography/components/H2Title';
|
|||||||
import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
|
import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
import { useActivateWorkspaceMutation } from '~/generated/graphql';
|
import { useActivateWorkspaceMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ export const CreateWorkspace = () => {
|
|||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Apple"
|
placeholder="Apple"
|
||||||
@ -150,7 +150,6 @@ export const CreateWorkspace = () => {
|
|||||||
error={error?.message}
|
error={error?.message}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
fullWidth
|
fullWidth
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
|||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||||
import {
|
import {
|
||||||
useUpdatePasswordViaResetTokenMutation,
|
useUpdatePasswordViaResetTokenMutation,
|
||||||
@ -191,12 +191,11 @@ export const PasswordReset = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoFocus
|
autoFocus
|
||||||
value={email}
|
value={email}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
fullWidth
|
fullWidth
|
||||||
disableHotkeys
|
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
@ -218,7 +217,7 @@ export const PasswordReset = () => {
|
|||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value}
|
value={value}
|
||||||
type="password"
|
type="password"
|
||||||
@ -227,7 +226,6 @@ export const PasswordReset = () => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
disableHotkeys
|
|
||||||
/>
|
/>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user