From 7036a8ccc3ecdc6d944c04ba57c6283572f1c4b2 Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Wed, 8 Jan 2025 16:16:46 +0100
Subject: [PATCH] Fix dropdown input design (#9439)
### Context
Update to match Figma design for dropdown input - [standard
input](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=28562-75034&t=4FdGFZfPLtvNq8La-4)
/ [custom phone
input](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=28562-74922&t=YIxM3jgx8kC9qiyQ-4)
### Preview
closes #8904
---------
Co-authored-by: etiennejouan
---
.../input/components/MultiItemBaseInput.tsx | 142 ++++++++++++++++++
.../input/components/MultiItemFieldInput.tsx | 12 +-
.../input/components/PhonesFieldInput.tsx | 31 ++--
.../MultiItemBaseInput.stories.tsx | 20 +++
.../ui/field/input/components/PhoneInput.tsx | 132 ----------------
.../CurrencyPickerDropdownButton.tsx | 2 +-
.../PhoneCountryPickerDropdownButton.tsx | 6 +-
7 files changed, 192 insertions(+), 153 deletions(-)
create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/MultiItemBaseInput.stories.tsx
delete mode 100644 packages/twenty-front/src/modules/ui/field/input/components/PhoneInput.tsx
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};
}
`;