diff --git a/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx index 46da4b2eb..5067405a4 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx @@ -5,7 +5,7 @@ import { Key } from 'ts-key-enum'; import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue'; import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata'; 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 { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { isDefined } from '~/utils/isDefined'; @@ -179,7 +179,7 @@ export const AddressInput = ({ return ( - - - - - >` - 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>` - 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, - 'onChange' | 'onKeyDown' -> & { - className?: string; - label?: string; - onChange?: (text: string) => void; - fullWidth?: boolean; +export type TextInputComponentProps = TextInputV2ComponentProps & { disableHotkeys?: boolean; - error?: string; - RightIcon?: IconComponent; - onKeyDown?: (event: React.KeyboardEvent) => void; - onBlur?: () => void; }; -const TextInputComponent = ( - { - className, - label, - value, - onChange, - 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, -): JSX.Element => { - const theme = useTheme(); - +const TextInputComponent = ({ + onFocus, + onBlur, + disableHotkeys = false, + ...props +}: TextInputComponentProps): JSX.Element => { const inputRef = useRef(null); - const combinedRef = useCombinedRefs(ref, inputRef); const { goBackToPreviousHotkeyScope, @@ -167,57 +52,8 @@ const TextInputComponent = ( { enabled: !disableHotkeys }, ); - const [passwordVisible, setPasswordVisible] = useState(false); - - const handleTogglePasswordVisibility = () => { - setPasswordVisible(!passwordVisible); - }; - - return ( - - {label && {label + (required ? '*' : '')}} - - ) => { - onChange?.(event.target.value); - }} - onKeyDown={onKeyDown} - {...{ autoFocus, disabled, placeholder, required, value }} - /> - - {error && ( - - - - )} - {!error && type === INPUT_TYPE_PASSWORD && ( - - {passwordVisible ? ( - - ) : ( - - )} - - )} - {!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && ( - - - - )} - - - {error && {error}} - - ); + // eslint-disable-next-line react/jsx-props-no-spreading + return ; }; export const TextInput = forwardRef(TextInputComponent); diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx new file mode 100644 index 000000000..395058083 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx @@ -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 +>` + 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>` + 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, + 'onChange' | 'onKeyDown' +> & { + className?: string; + label?: string; + onChange?: (text: string) => void; + fullWidth?: boolean; + error?: string; + RightIcon?: IconComponent; + onKeyDown?: (event: React.KeyboardEvent) => void; + onBlur?: FocusEventHandler; +}; + +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, +): JSX.Element => { + const theme = useTheme(); + + const inputRef = useRef(null); + const combinedRef = useCombinedRefs(ref, inputRef); + + const [passwordVisible, setPasswordVisible] = useState(false); + + const handleTogglePasswordVisibility = () => { + setPasswordVisible(!passwordVisible); + }; + + return ( + + {label && {label + (required ? '*' : '')}} + + ) => { + onChange?.(event.target.value); + }} + onKeyDown={onKeyDown} + {...{ autoFocus, disabled, placeholder, required, value }} + /> + + {error && ( + + + + )} + {!error && type === INPUT_TYPE_PASSWORD && ( + + {passwordVisible ? ( + + ) : ( + + )} + + )} + {!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && ( + + + + )} + + + {error && {error}} + + ); +}; + +export const TextInputV2 = forwardRef(TextInputV2Component); diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx new file mode 100644 index 000000000..8cd86abc2 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInputV2.stories.tsx @@ -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 ; +}; + +const meta: Meta = { + title: 'UI/Input/TextInputV2', + component: TextInputV2, + decorators: [ComponentDecorator], + args: { placeholder: 'Tim' }, + render: Render, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Filled: Story = { + args: { value: 'Tim' }, +}; + +export const Disabled: Story = { + args: { disabled: true, value: 'Tim' }, +}; diff --git a/packages/twenty-front/src/pages/auth/CreateProfile.tsx b/packages/twenty-front/src/pages/auth/CreateProfile.tsx index a5e76d433..a66f6994c 100644 --- a/packages/twenty-front/src/pages/auth/CreateProfile.tsx +++ b/packages/twenty-front/src/pages/auth/CreateProfile.tsx @@ -1,8 +1,9 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; import { useRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; import { z } from 'zod'; import { SubTitle } from '@/auth/components/SubTitle'; @@ -13,10 +14,12 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; +import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; 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'; 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) { return null; } - const onNameInputKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - event.preventDefault(); - onSubmit(getValues()); - } - }; - return ( <> Create profile @@ -157,17 +165,19 @@ export const CreateProfile = () => { field: { onChange, onBlur, value }, fieldState: { error }, }) => ( - setIsEditingMode(true)} + onBlur={() => { + onBlur(); + setIsEditingMode(false); + }} onChange={onChange} placeholder="Tim" error={error?.message} fullWidth - onKeyDown={onNameInputKeyDown} - disableHotkeys /> )} /> @@ -178,16 +188,18 @@ export const CreateProfile = () => { field: { onChange, onBlur, value }, fieldState: { error }, }) => ( - setIsEditingMode(true)} + onBlur={() => { + onBlur(); + setIsEditingMode(false); + }} onChange={onChange} placeholder="Cook" error={error?.message} fullWidth - onKeyDown={onNameInputKeyDown} - disableHotkeys /> )} /> diff --git a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx index 3313ace6b..81c93bcfe 100644 --- a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx +++ b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx @@ -20,7 +20,7 @@ import { H2Title } from '@/ui/display/typography/components/H2Title'; import { Loader } from '@/ui/feedback/loader/components/Loader.tsx'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; 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 { isDefined } from '~/utils/isDefined'; @@ -141,7 +141,7 @@ export const CreateWorkspace = () => { field: { onChange, onBlur, value }, fieldState: { error }, }) => ( - { error={error?.message} onKeyDown={handleKeyDown} fullWidth - disableHotkeys /> )} /> diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index 9de60cc46..332fb5a07 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -18,7 +18,7 @@ import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex'; import { AppPath } from '@/types/AppPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; 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 { useUpdatePasswordViaResetTokenMutation, @@ -191,12 +191,11 @@ export const PasswordReset = () => { }} > - @@ -218,7 +217,7 @@ export const PasswordReset = () => { fieldState: { error }, }) => ( - { onChange={onChange} error={error?.message} fullWidth - disableHotkeys /> )}