diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx new file mode 100644 index 000000000..cc4678d2c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx @@ -0,0 +1,142 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { forwardRef, InputHTMLAttributes, ReactNode, useRef } from 'react'; +import { TEXT_INPUT_STYLE } from 'twenty-ui'; + +import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; +import { useCombinedRefs } from '~/hooks/useCombinedRefs'; + +const StyledInput = styled.input<{ + withRightComponent?: boolean; + hasError?: boolean; +}>` + ${TEXT_INPUT_STYLE} + + background-color: ${({ theme }) => theme.background.transparent.lighter}; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + + box-sizing: border-box; + font-weight: ${({ theme }) => theme.font.weight.medium}; + height: 32px; + position: relative; + width: 100%; + + ${({ withRightComponent }) => + withRightComponent && + css` + padding-right: 32px; + `} +`; + +const StyledInputContainer = styled.div` + background-color: transparent; + box-sizing: border-box; + position: relative; + width: 100%; + + &:not(:first-of-type) { + padding: ${({ theme }) => theme.spacing(1)}; + } +`; + +const StyledRightContainer = styled.div` + position: absolute; + right: ${({ theme }) => theme.spacing(2)}; + top: 50%; + transform: translateY(-50%); +`; + +const StyledErrorDiv = styled.div` + color: ${({ theme }) => theme.color.red}; + padding: 0 ${({ theme }) => theme.spacing(2)}; +`; + +type HTMLInputProps = InputHTMLAttributes; + +export type MultiItemBaseInputProps = HTMLInputProps & { + hotkeyScope?: string; + onClickOutside?: () => void; + onEnter?: () => void; + onEscape?: () => void; + onShiftTab?: () => void; + onTab?: () => void; + rightComponent?: ReactNode; + renderInput?: (props: { + value: HTMLInputProps['value']; + onChange: HTMLInputProps['onChange']; + autoFocus: HTMLInputProps['autoFocus']; + placeholder: HTMLInputProps['placeholder']; + }) => React.ReactNode; + error?: string | null; + hasError?: boolean; +}; + +export const MultiItemBaseInput = forwardRef< + HTMLInputElement, + MultiItemBaseInputProps +>( + ( + { + autoFocus, + className, + value, + placeholder, + hotkeyScope = 'dropdown-menu-input', + onChange, + onClickOutside, + onEnter = () => {}, + onEscape = () => {}, + onShiftTab, + onTab, + rightComponent, + renderInput, + error = '', + hasError = false, + }, + ref, + ) => { + const inputRef = useRef(null); + const combinedRef = useCombinedRefs(ref, inputRef); + + useRegisterInputEvents({ + inputRef, + inputValue: value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, + hotkeyScope, + }); + + return ( + <> + + {renderInput ? ( + renderInput({ + value, + onChange, + autoFocus, + placeholder, + }) + ) : ( + + )} + {!!rightComponent && ( + {rightComponent} + )} + + {error && {error}} + + ); + }, +); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index 91b160253..224936435 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -2,12 +2,12 @@ import React, { useRef, useState } from 'react'; import { Key } from 'ts-key-enum'; import { IconCheck, IconPlus, LightIconButton, MenuItem } from 'twenty-ui'; +import { + MultiItemBaseInput, + MultiItemBaseInputProps, +} from '@/object-record/record-field/meta-types/input/components/MultiItemBaseInput'; import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; -import { - DropdownMenuInput, - DropdownMenuInputProps, -} from '@/ui/layout/dropdown/components/DropdownMenuInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -34,7 +34,7 @@ type MultiItemFieldInputProps = { hotkeyScope: string; newItemLabel?: string; fieldMetadataType: FieldMetadataType; - renderInput?: DropdownMenuInputProps['renderInput']; + renderInput?: MultiItemBaseInputProps['renderInput']; onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; @@ -176,7 +176,7 @@ export const MultiItemFieldInput = ({ )} {isInputDisplayed || !items.length ? ( - theme.background.transparent.lighter}; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + height: 30px; +`; + const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` - font-family: ${({ theme }) => theme.font.family}; ${TEXT_INPUT_STYLE} padding: 0; + height: 100%; .PhoneInputInput { background: none; @@ -123,16 +130,18 @@ export const PhonesFieldInput = ({ )} renderInput={({ value, onChange, autoFocus, placeholder }) => { return ( - void} - international={true} - withCountryCallingCode={true} - countrySelectComponent={PhoneCountryPickerDropdownButton} - defaultCountry={defaultCountry} - /> + + void} + international={true} + withCountryCallingCode={true} + countrySelectComponent={PhoneCountryPickerDropdownButton} + defaultCountry={defaultCountry} + /> + ); }} hotkeyScope={hotkeyScope} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/MultiItemBaseInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/MultiItemBaseInput.stories.tsx new file mode 100644 index 000000000..eac534cf5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/MultiItemBaseInput.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { ComponentDecorator } from 'twenty-ui'; + +import { MultiItemBaseInput } from '../MultiItemBaseInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Input/BaseFieldInput', + component: MultiItemBaseInput, + decorators: [ComponentDecorator], + args: { value: 'Lorem ipsum' }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Focused: Story = { + args: { autoFocus: true }, +}; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx deleted file mode 100644 index ea297940d..000000000 --- a/packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import styled from '@emotion/styled'; -import { useEffect, useRef, useState } from 'react'; -import ReactPhoneNumberInput from 'react-phone-number-input'; -import { TEXT_INPUT_STYLE } from 'twenty-ui'; - -import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; -import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; -import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton'; - -import { E164Number } from 'libphonenumber-js'; -import 'react-phone-number-input/style.css'; - -const StyledContainer = styled.div` - align-items: center; - - border: none; - border-radius: ${({ theme }) => theme.border.radius.sm}; - box-shadow: ${({ theme }) => theme.boxShadow.strong}; - width: 100%; - - display: flex; - justify-content: start; -`; - -const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` - font-family: ${({ theme }) => theme.font.family}; - height: 32px; - ${TEXT_INPUT_STYLE} - padding: 0; - - .PhoneInputInput { - background: none; - border: none; - color: ${({ theme }) => theme.font.color.primary}; - - &::placeholder, - &::-webkit-input-placeholder { - color: ${({ theme }) => theme.font.color.light}; - font-family: ${({ theme }) => theme.font.family}; - font-weight: ${({ theme }) => theme.font.weight.medium}; - } - - :focus { - outline: none; - } - } - - & svg { - border-radius: ${({ theme }) => theme.border.radius.xs}; - height: 12px; - } - width: calc(100% - ${({ theme }) => theme.spacing(8)}); -`; - -const StyledLightIconButtonContainer = styled.div` - position: absolute; - top: 50%; - transform: translateY(-50%); - right: 0; -`; - -export type PhoneInputProps = { - 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; - onChange?: (newText: string) => void; - hotkeyScope: string; - copyButton?: boolean; -}; - -export const PhoneInput = ({ - autoFocus, - value, - onEnter, - onEscape, - onTab, - onShiftTab, - onClickOutside, - hotkeyScope, - onChange, - copyButton = true, -}: PhoneInputProps) => { - const [internalValue, setInternalValue] = useState(value); - - const wrapperRef = useRef(null); - const copyRef = useRef(null); - - const handleChange = (newValue: E164Number) => { - setInternalValue(newValue); - onChange?.(newValue as string); - }; - - useEffect(() => { - setInternalValue(value); - }, [value]); - - useRegisterInputEvents({ - inputRef: wrapperRef, - copyRef: copyRef, - inputValue: internalValue ?? '', - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, - hotkeyScope, - }); - - return ( - - - {copyButton && ( - - - - )} - - ); -}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx index f46e546be..feaa9d417 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx @@ -21,7 +21,7 @@ const StyledDropdownButtonContainer = styled.div` padding-right: ${({ theme }) => theme.spacing(2)}; user-select: none; &:hover { - filter: brightness(0.95); + background-color: ${({ theme }) => theme.background.transparent.light}; } `; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx index 2a550c6d9..7a1c62681 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx @@ -27,16 +27,16 @@ const StyledDropdownButtonContainer = styled.div` cursor: pointer; display: flex; - height: 32px; + height: 30px; padding-left: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(1)}; user-select: none; - border-right: 1px solid ${({ theme }) => theme.border.color.light}; + border-right: 1px solid ${({ theme }) => theme.border.color.medium}; &:hover { - filter: brightness(0.95); + background-color: ${({ theme }) => theme.background.transparent.light}; } `;