Refactor input arch (#1982)

This commit is contained in:
Charles Bochet
2023-10-12 17:41:50 +02:00
committed by GitHub
parent 09fd5b6454
commit 6b990c8501
47 changed files with 312 additions and 523 deletions

View File

@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
import { MainButton } from '@/ui/button/components/MainButton';
import { IconBrandGoogle } from '@/ui/icon';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { Logo } from '../../components/Logo';
@ -132,7 +132,7 @@ export const SignInUpForm = () => {
fieldState: { error },
}) => (
<StyledInputContainer>
<TextInputSettings
<TextInput
autoFocus
value={value}
placeholder="Email"
@ -170,7 +170,7 @@ export const SignInUpForm = () => {
fieldState: { error },
}) => (
<StyledInputContainer>
<TextInputSettings
<TextInput
autoFocus
value={value}
type="password"

View File

@ -10,9 +10,9 @@ import {
} from '@/people/components/PeoplePicker';
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
import { IconPlus } from '@/ui/icon';
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
export const EmailField = () => {
const currentUser = useRecoilValue(currentUserState);
return (
<TextInputSettings
<TextInput
value={currentUser?.email}
disabled
fullWidth

View File

@ -5,7 +5,7 @@ import debounce from 'lodash.debounce';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { useUpdateUserMutation } from '~/generated/graphql';
import { logError } from '~/utils/logError';
@ -87,14 +87,14 @@ export const NameFields = ({
return (
<StyledComboInputContainer>
<TextInputSettings
<TextInput
label="First Name"
value={firstName}
onChange={setFirstName}
placeholder="Tim"
fullWidth
/>
<TextInputSettings
<TextInput
label="Last Name"
value={lastName}
onChange={setLastName}

View File

@ -3,7 +3,7 @@ import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { ImageInput } from '@/ui/input/image/components/ImageInput';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import {

View File

@ -5,7 +5,7 @@ import debounce from 'lodash.debounce';
import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
import { logError } from '~/utils/logError';
@ -72,7 +72,7 @@ export const NameField = ({
return (
<StyledComboInputContainer>
<TextInputSettings
<TextInput
label="Name"
value={displayName}
onChange={setDisplayName}

View File

@ -2,7 +2,7 @@ import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { ImageInput } from '@/ui/input/image/components/ImageInput';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import {

View File

@ -5,8 +5,8 @@ import styled from '@emotion/styled';
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { Data, Fields } from '@/spreadsheet-import/types';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { TextInput } from '@/ui/input/components/TextInput';
import { Toggle } from '@/ui/input/components/Toggle';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { AppTooltip } from '@/ui/tooltip/AppTooltip';
import { Meta } from '../types';
@ -146,7 +146,7 @@ export const generateColumns = <T extends string>(
}
default:
component = (
<TextInputSettings
<TextInput
value={row[columnKey] as string}
onChange={(value: string) => {
onRowChange({ ...row, [columnKey]: value });

View File

@ -1,4 +1,4 @@
import { BooleanInput } from '@/ui/input/components/BooleanInput';
import { BooleanInput } from '@/ui/field/meta-types/input/components/internal/BooleanInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useBooleanField } from '../../hooks/useBooleanField';

View File

@ -1,4 +1,4 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useChipField } from '../../hooks/useChipField';

View File

@ -1,4 +1,4 @@
import { DateInput } from '@/ui/input/components/DateInput';
import { DateInput } from '@/ui/field/meta-types/input/components/internal/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';

View File

@ -1,5 +1,5 @@
import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';

View File

@ -1,5 +1,5 @@
import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDoubleTextField } from '../../hooks/useDoubleTextField';

View File

@ -1,4 +1,4 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useEmailField } from '../../hooks/useEmailField';

View File

@ -1,4 +1,4 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { useMoneyField } from '../../hooks/useMoneyField';

View File

@ -1,4 +1,4 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { useNumberField } from '../../hooks/useNumberField';

View File

@ -1,4 +1,4 @@
import { PhoneInput } from '@/ui/input/components/PhoneInput';
import { PhoneInput } from '@/ui/field/meta-types/input/components/internal/PhoneInput';
import { usePhoneField } from '../../hooks/usePhoneField';

View File

@ -1,4 +1,4 @@
import { ProbabilityInput } from '@/ui/input/components/ProbabilityInput';
import { ProbabilityInput } from '@/ui/field/meta-types/input/components/internal/ProbabilityInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useProbabilityField } from '../../hooks/useProbabilityField';

View File

@ -1,4 +1,4 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';

View File

@ -1,4 +1,4 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { useURLField } from '../../hooks/useURLField';

View File

@ -4,11 +4,10 @@ import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { DateDisplay } from '@/ui/field/meta-types/display/content-display/components/DateDisplay';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { Nullable } from '~/types/Nullable';
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
import { DatePicker } from './DatePicker';
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
@ -89,7 +88,7 @@ export const DateInput = ({
</div>
<div ref={refs.setFloating} style={floatingStyles}>
<StyledCalendarContainer>
<DatePicker
<InternalDatePicker
date={internalValue ?? new Date()}
onChange={handleChange}
onMouseSelect={(newDate: Date) => {

View File

@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from 'react';
import ReactPhoneNumberInput from 'react-phone-number-input';
import styled from '@emotion/styled';
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
import { CountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/CountryPickerDropdownButton';
import { CountryPickerDropdownButton } from './CountryPickerDropdownButton';
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
import 'react-phone-number-input/style.css';

View File

@ -0,0 +1,70 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/theme/constants/effects';
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
export const StyledInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
`;
type TextInputProps = {
placeholder?: string;
autoFocus?: boolean;
value: string;
onEnter: (newText: string) => void;
onEscape: (newText: string) => void;
onTab?: (newText: string) => void;
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
hotkeyScope: string;
};
export const TextInput = ({
placeholder,
autoFocus,
value,
hotkeyScope,
onEnter,
onEscape,
onTab,
onShiftTab,
onClickOutside,
}: TextInputProps) => {
const [internalText, setInternalText] = useState(value);
const wrapperRef = useRef(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalText(event.target.value);
};
useEffect(() => {
setInternalText(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalText,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
hotkeyScope,
});
return (
<StyledInput
autoComplete="off"
ref={wrapperRef}
placeholder={placeholder}
onChange={handleChange}
autoFocus={autoFocus}
value={internalText}
/>
);
};

View File

@ -8,7 +8,7 @@ import { RoundedIconButton } from '@/ui/button/components/RoundedIconButton';
import { IconArrowRight } from '@/ui/icon/index';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { InputHotkeyScope } from '../text/types/InputHotkeyScope';
import { InputHotkeyScope } from '../types/InputHotkeyScope';
const MAX_ROWS = 5;

View File

@ -1,7 +1,7 @@
import { ChangeEvent } from 'react';
import styled from '@emotion/styled';
import { StyledInput } from '@/ui/input/components/TextInput';
import { StyledInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
export type EntityTitleDoubleTextInputProps = {

View File

@ -1,70 +1,203 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import {
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 { textInputStyle } from '@/ui/theme/constants/effects';
import { IconAlertCircle } from '@/ui/icon';
import { IconEye, IconEyeOff } from '@/ui/icon/index';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
import { InputHotkeyScope } from '../types/InputHotkeyScope';
export const StyledInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
type TextInputComponentProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'onChange'
> & {
label?: string;
onChange?: (text: string) => void;
fullWidth?: boolean;
disableHotkeys?: boolean;
error?: string;
};
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
display: flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
`;
type TextInputProps = {
placeholder?: string;
autoFocus?: boolean;
value: string;
onEnter: (newText: string) => void;
onEscape: (newText: string) => void;
onTab?: (newText: string) => void;
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
hotkeyScope: string;
};
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)};
text-transform: uppercase;
`;
export const TextInput = ({
placeholder,
autoFocus,
value,
hotkeyScope,
onEnter,
onEscape,
onTab,
onShiftTab,
onClickOutside,
}: TextInputProps) => {
const [internalText, setInternalText] = useState(value);
const StyledInputContainer = styled.div`
display: flex;
flex-direction: row;
const wrapperRef = useRef(null);
width: 100%;
`;
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalText(event.target.value);
const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
background-color: ${({ theme }) => theme.background.tertiary};
border: none;
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-grow: 1;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
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};
}
`;
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.tertiary};
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
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: pointer;
display: flex;
justify-content: center;
`;
const INPUT_TYPE_PASSWORD = 'password';
const TextInputComponent = (
{
label,
value,
onChange,
onFocus,
onBlur,
fullWidth,
error,
required,
type,
disableHotkeys = false,
autoFocus,
placeholder,
disabled,
tabIndex,
}: TextInputComponentProps,
// eslint-disable-next-line twenty/component-props-naming
ref: ForwardedRef<HTMLInputElement>,
): JSX.Element => {
const theme = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
const combinedRef = useCombinedRefs(ref, inputRef);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
onFocus?.(e);
if (!disableHotkeys) {
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
}
};
useEffect(() => {
setInternalText(value);
}, [value]);
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
onBlur?.(e);
if (!disableHotkeys) {
goBackToPreviousHotkeyScope();
}
};
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalText,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
hotkeyScope,
});
useScopedHotkeys(
[Key.Escape, Key.Enter],
() => {
inputRef.current?.blur();
},
InputHotkeyScope.TextInput,
);
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTogglePasswordVisibility = () => {
setPasswordVisible(!passwordVisible);
};
return (
<StyledInput
autoComplete="off"
ref={wrapperRef}
placeholder={placeholder}
onChange={handleChange}
autoFocus={autoFocus}
value={internalText}
/>
<StyledContainer 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);
}}
{...{ 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>
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
</StyledContainer>
);
};
export const TextInput = forwardRef(TextInputComponent);

View File

@ -1,20 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { EmailDisplay } from '../../../field/meta-types/display/content-display/components/EmailDisplay';
const meta: Meta = {
title: 'UI/Input/EmailInputDisplay',
component: EmailDisplay,
decorators: [ComponentWithRouterDecorator],
args: {
value: 'mustajab.ikram@google.com',
},
};
export default meta;
type Story = StoryObj<typeof EmailDisplay>;
export const Default: Story = {};

View File

@ -220,17 +220,17 @@ const StyledContainer = styled.div`
}
`;
export type DatePickerProps = {
export type InternalDatePickerProps = {
date: Date;
onMouseSelect?: (date: Date) => void;
onChange?: (date: Date) => void;
};
export const DatePicker = ({
export const InternalDatePicker = ({
date,
onChange,
onMouseSelect,
}: DatePickerProps) => (
}: InternalDatePickerProps) => (
<StyledContainer>
<ReactDatePicker
open={true}

View File

@ -4,11 +4,11 @@ import { userEvent, within } from '@storybook/testing-library';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DatePicker } from '../DatePicker';
import { InternalDatePicker } from '../InternalDatePicker';
const meta: Meta<typeof DatePicker> = {
title: 'UI/Input/DatePicker',
component: DatePicker,
const meta: Meta<typeof InternalDatePicker> = {
title: 'UI/Input/InternalDatePicker',
component: InternalDatePicker,
decorators: [ComponentDecorator],
argTypes: {
date: { control: 'date' },
@ -17,7 +17,7 @@ const meta: Meta<typeof DatePicker> = {
};
export default meta;
type Story = StoryObj<typeof DatePicker>;
type Story = StoryObj<typeof InternalDatePicker>;
export const Default: Story = {};

View File

@ -10,9 +10,9 @@ import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { useDropdown } from '@/ui/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/dropdown/scopes/DropdownScope';
import { IconChevronDown } from '@/ui/icon';
import { IconWorld } from '@/ui/input/constants/icons';
import { IconWorld } from '../constants/icons';
import { CountryPickerHotkeyScope } from '../Types/CountryPickerHotkeyScope';
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';

View File

@ -1,48 +0,0 @@
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/theme/constants/effects';
import { overlayBackground } from '@/ui/theme/constants/effects';
const StyledInplaceInputTextInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
`;
const StyledTextInputContainer = styled.div`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
margin-left: -1px;
min-height: 32px;
width: inherit;
${overlayBackground}
z-index: 10;
`;
export type TextInputEditProps = {
placeholder?: string;
value?: string;
onChange?: (newValue: string) => void;
autoFocus?: boolean;
};
export const TextInputEdit = ({
placeholder,
value,
onChange,
autoFocus,
}: TextInputEditProps) => (
<StyledTextInputContainer>
<StyledInplaceInputTextInput
autoComplete="off"
autoFocus={autoFocus}
placeholder={placeholder}
value={value}
onChange={(e) => onChange?.(e.target.value)}
/>
</StyledTextInputContainer>
);

View File

@ -1,203 +0,0 @@
import {
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 { IconAlertCircle } from '@/ui/icon';
import { IconEye, IconEyeOff } from '@/ui/icon/index';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { InputHotkeyScope } from '../types/InputHotkeyScope';
type TextInputComponentProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'onChange'
> & {
label?: string;
onChange?: (text: string) => void;
fullWidth?: boolean;
disableHotkeys?: boolean;
error?: string;
};
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
display: 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)};
text-transform: uppercase;
`;
const StyledInputContainer = styled.div`
display: flex;
flex-direction: row;
width: 100%;
`;
const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
background-color: ${({ theme }) => theme.background.tertiary};
border: none;
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-grow: 1;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
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};
}
`;
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.tertiary};
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
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: pointer;
display: flex;
justify-content: center;
`;
const INPUT_TYPE_PASSWORD = 'password';
const TextInputComponent = (
{
label,
value,
onChange,
onFocus,
onBlur,
fullWidth,
error,
required,
type,
disableHotkeys = false,
autoFocus,
placeholder,
disabled,
tabIndex,
}: TextInputComponentProps,
// eslint-disable-next-line twenty/component-props-naming
ref: ForwardedRef<HTMLInputElement>,
): JSX.Element => {
const theme = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
const combinedRef = useCombinedRefs(ref, inputRef);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
onFocus?.(e);
if (!disableHotkeys) {
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
}
};
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
onBlur?.(e);
if (!disableHotkeys) {
goBackToPreviousHotkeyScope();
}
};
useScopedHotkeys(
[Key.Escape, Key.Enter],
() => {
inputRef.current?.blur();
},
InputHotkeyScope.TextInput,
);
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTogglePasswordVisibility = () => {
setPasswordVisible(!passwordVisible);
};
return (
<StyledContainer 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);
}}
{...{ 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>
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
</StyledContainer>
);
};
export const TextInputSettings = forwardRef(TextInputComponent);

View File

@ -1,142 +0,0 @@
import { useState } from 'react';
import { expect } from '@storybook/jest';
import { jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { TextInputSettings } from '../TextInputSettings';
const changeJestFn = jest.fn();
const meta: Meta<typeof TextInputSettings> = {
title: 'UI/Input/TextInput',
component: TextInputSettings,
decorators: [ComponentDecorator],
args: { value: '', onChange: changeJestFn, placeholder: 'Placeholder' },
};
export default meta;
type Story = StoryObj<typeof TextInputSettings>;
const FakeTextInput = ({
autoFocus,
disableHotkeys = false,
disabled,
error,
fullWidth,
label,
onBlur,
onChange,
onFocus,
placeholder,
required,
tabIndex,
type,
value: initialValue,
}: React.ComponentProps<typeof TextInputSettings>) => {
const [value, setValue] = useState(initialValue);
return (
<TextInputSettings
{...{
autoFocus,
disableHotkeys,
disabled,
error,
fullWidth,
label,
onBlur,
onFocus,
placeholder,
required,
tabIndex,
type,
}}
value={value}
onChange={(text) => {
setValue(text);
onChange?.(text);
}}
/>
);
};
export const Default: Story = {
argTypes: { value: { control: false } },
args: { value: 'A good value ' },
render: ({
autoFocus,
disableHotkeys,
disabled,
error,
fullWidth,
label,
onBlur,
onChange,
onFocus,
placeholder,
required,
tabIndex,
type,
value,
}) => (
<FakeTextInput
{...{
autoFocus,
disableHotkeys,
disabled,
error,
fullWidth,
label,
onBlur,
onChange,
onFocus,
placeholder,
required,
tabIndex,
type,
value,
}}
/>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByRole('textbox');
await userEvent.type(input, 'cou', { delay: 100 });
expect(changeJestFn).toHaveBeenNthCalledWith(1, 'A good value c');
expect(changeJestFn).toHaveBeenNthCalledWith(2, 'A good value co');
expect(changeJestFn).toHaveBeenNthCalledWith(3, 'A good value cou');
},
};
export const Placeholder: Story = {};
export const FullWidth: Story = {
args: { value: 'A good value', fullWidth: true },
};
export const WithLabel: Story = {
args: { label: 'Lorem ipsum' },
};
export const WithError: Story = {
args: { error: 'Lorem ipsum' },
};
export const PasswordInput: Story = {
args: { type: 'password', placeholder: 'Password' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByPlaceholderText('Password');
await userEvent.type(input, 'pa$$w0rd');
const revealButton = canvas.getByTestId('reveal-password-button');
await userEvent.click(revealButton);
expect(input).toHaveAttribute('type', 'text');
},
};

View File

@ -4,7 +4,7 @@ import { AnimatePresence, LayoutGroup } from 'framer-motion';
import debounce from 'lodash.debounce';
import { Button } from '@/ui/button/components/Button';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { Modal } from '@/ui/modal/components/Modal';
import {
Section,
@ -99,7 +99,7 @@ export const ConfirmationModal = ({
</Section>
{confirmationValue && (
<Section>
<TextInputSettings
<TextInput
value={inputConfirmationValue}
onChange={handleInputConfimrationValueChange}
placeholder={confirmationPlaceholder}

View File

@ -1,4 +1,4 @@
import { DatePicker } from '@/ui/input/components/DatePicker';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
@ -42,7 +42,7 @@ export const FilterDropdownDateSearchInput = () => {
};
return (
<DatePicker
<InternalDatePicker
date={new Date()}
onChange={handleChange}
onMouseSelect={handleChange}

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { Button } from '@/ui/button/components/Button';
import { IconCopy, IconLink } from '@/ui/icon';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
const StyledContainer = styled.div`
@ -31,7 +31,7 @@ export const WorkspaceInviteLink = ({
return (
<StyledContainer>
<StyledLinkContainer>
<TextInputSettings value={inviteLink} disabled fullWidth />
<TextInput value={inviteLink} disabled fullWidth />
</StyledLinkContainer>
<Button
Icon={IconLink}

View File

@ -14,7 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { MainButton } from '@/ui/button/components/MainButton';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { H2Title } from '@/ui/typography/components/H2Title';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -145,7 +145,7 @@ export const CreateProfile = () => {
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInputSettings
<TextInput
autoFocus
label="First Name"
value={value}
@ -165,7 +165,7 @@ export const CreateProfile = () => {
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInputSettings
<TextInput
label="Last Name"
value={value}
onBlur={onBlur}

View File

@ -11,7 +11,7 @@ import { Title } from '@/auth/components/Title';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { MainButton } from '@/ui/button/components/MainButton';
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
import { TextInput } from '@/ui/input/components/TextInput';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { H2Title } from '@/ui/typography/components/H2Title';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -122,7 +122,7 @@ export const CreateWorkspace = () => {
field: { onChange, onBlur, value },
fieldState: { error },
}) => (
<TextInputSettings
<TextInput
autoFocus
value={value}
placeholder="Apple"