Fix broken dropdown auto resize behavior (#11423)

This PR was originally about fixing advanced filter dropdown auto resize
to avoid breaking the app main container, but the regression is not
limited to advanced filter dropdown, so this PR fixes the regression for
every dropdown in the app.

This PR adds a max dropdown max width to allow resizing dropdowns
horizontally also, which can happen easily for the advanced filter
dropdown.

In this PR we also start removing `fieldMetadataItemUsedInDropdown` in
component `AdvancedFilterDropdownTextInput` because it has no impact
outside of this component which is used only once.

The autoresize behavior determines the right padding-bottom between
mobile and PC.

Mobile : 

<img width="604" alt="Capture d’écran 2025-04-07 à 16 03 12"
src="https://github.com/user-attachments/assets/fbdd8020-1bfc-4e01-8a05-3a9f114cdd40"
/>

PC :

<img width="757" alt="Capture d’écran 2025-04-07 à 16 03 30"
src="https://github.com/user-attachments/assets/f80a5967-8f60-40bb-ae3c-fa9eb4c65707"
/>

Fixes https://github.com/twentyhq/core-team-issues/issues/725
Fixes https://github.com/twentyhq/twenty/issues/11409

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2025-04-08 11:03:10 +02:00
committed by GitHub
parent 89abf3db4f
commit 3d90eb4eb9
38 changed files with 174 additions and 213 deletions

View File

@ -72,7 +72,7 @@ export const RecordIndexActionMenuDropdown = () => {
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
}}
data-select-disable
dropdownMenuWidth={width}
dropdownWidth={width}
dropdownPlacement="bottom-start"
dropdownStrategy="absolute"
dropdownOffset={{

View File

@ -48,7 +48,7 @@ export const AttachmentDropdown = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -5,43 +5,30 @@ import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-d
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { AdvancedFilterDropdownDateInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownDateInput';
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
type AdvancedFilterDropdownFilterInputProps = {
filterDropdownId?: string;
recordFilterId?: string;
recordFilter: RecordFilter;
};
export const AdvancedFilterDropdownFilterInput = ({
filterDropdownId,
recordFilterId,
recordFilter,
}: AdvancedFilterDropdownFilterInputProps) => {
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
filterDropdownId,
);
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
subFieldNameUsedInDropdownComponentState,
filterDropdownId,
);
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
return null;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
const filterType = recordFilter.type;
const isActorSourceCompositeFilter = isFilterOnActorSourceSubField(
subFieldNameUsedInDropdown,
@ -57,7 +44,7 @@ export const AdvancedFilterDropdownFilterInput = ({
<>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownRecordSelect recordFilterId={recordFilterId} />
<ObjectFilterDropdownRecordSelect recordFilterId={recordFilter.id} />
</>
)}
{filterType === 'ACTOR' &&

View File

@ -1,57 +1,36 @@
import { useState } from 'react';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { v4 } from 'uuid';
export const AdvancedFilterDropdownTextInput = () => {
const selectedOperandInDropdown = useRecoilComponentValueV2(
selectedOperandInDropdownComponentState,
);
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedFilter = useRecoilComponentValueV2(
selectedFilterComponentState,
);
type AdvancedFilterDropdownTextInputProps = {
recordFilter: RecordFilter;
};
export const AdvancedFilterDropdownTextInput = ({
recordFilter,
}: AdvancedFilterDropdownTextInputProps) => {
const { applyRecordFilter } = useApplyRecordFilter();
const [inputValue, setInputValue] = useState(
() => selectedFilter?.value || '',
);
const [inputValue, setInputValue] = useState(() => recordFilter?.value || '');
const handleChange = (newValue: string) => {
if (!fieldMetadataItemUsedInDropdown || !selectedOperandInDropdown) {
return;
}
setInputValue(newValue);
applyRecordFilter({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: fieldMetadataItemUsedInDropdown?.id ?? '',
id: recordFilter.id,
fieldMetadataId: recordFilter?.fieldMetadataId ?? '',
value: newValue,
operand: selectedOperandInDropdown,
operand: recordFilter.operand,
displayValue: newValue,
type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type),
label: fieldMetadataItemUsedInDropdown.label,
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup,
type: recordFilter.type,
label: recordFilter.label,
recordFilterGroupId: recordFilter?.recordFilterGroupId,
positionInRecordFilterGroup: recordFilter?.positionInRecordFilterGroup,
});
};
if (!selectedOperandInDropdown || !fieldMetadataItemUsedInDropdown) {
return null;
}
return (
<TextInputV2
value={inputValue}

View File

@ -5,8 +5,10 @@ import { AdvancedFilterRecordFilterRow } from '@/object-record/advanced-filter/c
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector';
import { isRecordFilterGroupChildARecordFilterGroup } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilterGroup';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { id } from 'date-fns/locale';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
@ -16,7 +18,7 @@ const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
min-width: 650px;
`;
export const AdvancedFilterRootRecordFilterGroup = () => {
@ -34,28 +36,32 @@ export const AdvancedFilterRootRecordFilterGroup = () => {
}
return (
<StyledContainer>
{childRecordFiltersAndRecordFilterGroups.map(
(recordFilterGroupChild, recordFilterGroupChildIndex) =>
isRecordFilterGroupChildARecordFilterGroup(recordFilterGroupChild) ? (
<AdvancedFilterRecordFilterGroupRow
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
/>
) : (
<AdvancedFilterRecordFilterRow
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}
recordFilterIndex={recordFilterGroupChildIndex}
/>
),
)}
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={rootRecordFilterGroup}
/>
</StyledContainer>
<ScrollWrapper componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}>
<StyledContainer>
{childRecordFiltersAndRecordFilterGroups.map(
(recordFilterGroupChild, recordFilterGroupChildIndex) =>
isRecordFilterGroupChildARecordFilterGroup(
recordFilterGroupChild,
) ? (
<AdvancedFilterRecordFilterGroupRow
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
/>
) : (
<AdvancedFilterRecordFilterRow
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}
recordFilterIndex={recordFilterGroupChildIndex}
/>
),
)}
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={rootRecordFilterGroup}
/>
</StyledContainer>
</ScrollWrapper>
);
};

View File

@ -45,11 +45,15 @@ export const AdvancedFilterValueInput = ({
const operandHasNoInput =
recordFilter && !configurableViewFilterOperands.has(recordFilter.operand);
if (!isDefined(recordFilter)) {
return null;
}
const handleFilterValueDropdownClose = () => {
setObjectFilterDropdownSearchInput('');
};
const filterType = recordFilter?.type;
const filterType = recordFilter.type;
const dropdownContentOffset =
filterType === 'DATE' || filterType === 'DATE_TIME'
@ -67,7 +71,7 @@ export const AdvancedFilterValueInput = ({
) : isDefined(filterType) &&
(TEXT_FILTER_TYPES.includes(filterType) ||
NUMBER_FILTER_TYPES.includes(filterType)) ? (
<AdvancedFilterDropdownTextInput />
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
) : (
<Dropdown
dropdownId={dropdownId}
@ -77,14 +81,12 @@ export const AdvancedFilterValueInput = ({
/>
}
dropdownComponents={
<AdvancedFilterDropdownFilterInput
recordFilterId={recordFilter.id}
/>
<AdvancedFilterDropdownFilterInput recordFilter={recordFilter} />
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={dropdownContentOffset}
dropdownPlacement="bottom-start"
dropdownMenuWidth={280}
dropdownWidth={280}
onClose={handleFilterValueDropdownClose}
/>
)}

View File

@ -3,7 +3,6 @@ import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
type MultipleFiltersDropdownContentProps = {
@ -40,7 +39,6 @@ export const MultipleFiltersDropdownContent = ({
) : (
<ObjectFilterDropdownFilterSelect isAdvancedFilterButtonVisible />
)}
<MultipleFiltersDropdownFilterOnFilterChangedEffect />
</>
);
};

View File

@ -1,36 +0,0 @@
import { useEffect } from 'react';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
export const MultipleFiltersDropdownFilterOnFilterChangedEffect = () => {
const { setDropdownWidth } = useDropdown();
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
useEffect(() => {
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
return;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
switch (filterType) {
case 'DATE':
case 'DATE_TIME':
setDropdownWidth(280);
break;
default:
setDropdownWidth(200);
}
}, [fieldMetadataItemUsedInDropdown, setDropdownWidth]);
return null;
};

View File

@ -33,7 +33,7 @@ export const ObjectOptionsDropdown = ({
<Dropdown
dropdownId={OBJECT_OPTIONS_DROPDOWN_ID}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownMenuWidth={DROPDOWN_WIDTH}
dropdownWidth={DROPDOWN_WIDTH}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<StyledHeaderDropdownButton isUnfolded={isDropdownOpen}>

View File

@ -26,8 +26,6 @@ describe('useOptionsDropdown', () => {
closeDropdown: mockCloseDropdown,
toggleDropdown: jest.fn(),
openDropdown: jest.fn(),
dropdownWidth: undefined,
setDropdownWidth: jest.fn(),
dropdownPlacement: null,
setDropdownPlacement: jest.fn(),
});

View File

@ -46,7 +46,7 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
dropdownHotkeyScope={{
scope: RecordBoardColumnHotkeyScope.ColumnHeader,
}}
dropdownMenuWidth={DROPDOWN_WIDTH}
dropdownWidth={DROPDOWN_WIDTH}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<RecordBoardColumnHeaderAggregateDropdownButton

View File

@ -78,7 +78,7 @@ export const RecordIndexAddRecordInGroupDropdown = ({
return (
<Dropdown
dropdownMenuWidth="200px"
dropdownWidth="200px"
dropdownPlacement="bottom-start"
clickableComponent={clickableComponent}
dropdownId={dropdownId}

View File

@ -10,7 +10,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { Trans, useLingui } from '@lingui/react/macro';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import {
IconCalendarEvent,
IconDotsVertical,
@ -20,6 +19,7 @@ import {
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
type SettingsAccountsRowDropdownMenuProps = {
account: ConnectedAccount;
@ -55,7 +55,7 @@ export const SettingsAccountsRowDropdownMenu = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -4,14 +4,14 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { Select } from '@/ui/input/components/Select';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { useLingui } from '@lingui/react/macro';
import { CountryCode } from 'libphonenumber-js';
import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui/display';
import { z } from 'zod';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { useLingui } from '@lingui/react/macro';
import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui/display';
type SettingsDataModelFieldPhonesFormProps = {
disabled?: boolean;
@ -74,7 +74,6 @@ export const SettingsDataModelFieldPhonesForm = ({
description={t`The default country code for new phone numbers.`}
>
<Select<string>
dropdownWidth={'auto'}
dropdownId="selectDefaultCountryCode"
value={stripSimpleQuotesFromString(
value?.primaryPhoneCountryCode,

View File

@ -55,7 +55,7 @@ export const SettingsObjectFieldActiveActionDropdown = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -2,7 +2,6 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { t } from '@lingui/core/macro';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
IconArchiveOff,
IconDotsVertical,
@ -12,6 +11,7 @@ import {
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SettingsObjectFieldInactiveActionDropdownProps = {
isCustomField?: boolean;
@ -60,7 +60,7 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -77,7 +77,7 @@ export const SettingsObjectSummaryCard = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -42,7 +42,7 @@ export const SettingsObjectInactiveMenuDropDown = ({
accent="tertiary"
/>
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -8,11 +8,11 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useLingui } from '@lingui/react/macro';
import { UnwrapRecoilValue } from 'recoil';
import { SsoIdentityProviderStatus } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
import { IconArchive, IconDotsVertical, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { SsoIdentityProviderStatus } from '~/generated/graphql';
type SettingsSecuritySSORowDropdownMenuProps = {
SSOIdp: UnwrapRecoilValue<typeof SSOIdentitiesProvidersState>[0];
@ -72,7 +72,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -1,15 +1,15 @@
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { UnwrapRecoilValue, useSetRecoilState } from 'recoil';
import { useDeleteApprovedAccessDomainMutation } from '~/generated/graphql';
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
import { isDefined } from 'twenty-shared/utils';
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useDeleteApprovedAccessDomainMutation } from '~/generated/graphql';
type SettingsSecurityApprovedAccessDomainRowDropdownMenuProps = {
approvedAccessDomain: UnwrapRecoilValue<typeof approvedAccessDomainsState>[0];
@ -61,7 +61,7 @@ export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -26,7 +26,7 @@ export const SupportDropdown = () => {
dropdownPlacement="top-start"
dropdownOffset={{ x: 0, y: -28 }}
clickableComponent={<SupportButton />}
dropdownMenuWidth={160}
dropdownWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem

View File

@ -14,7 +14,6 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { t } from '@lingui/core/macro';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
import { IconApps, IconComponent, useIcons } from 'twenty-ui/display';
import {
IconButton,
@ -22,6 +21,7 @@ import {
IconButtonVariant,
LightIconButton,
} from 'twenty-ui/input';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
export type IconPickerProps = {
disabled?: boolean;
@ -172,7 +172,7 @@ export const IconPicker = ({
size={size}
/>
}
dropdownMenuWidth={176}
dropdownWidth={176}
dropdownComponents={
<SelectableList
selectableListId="icon-list"

View File

@ -30,7 +30,7 @@ export type SelectProps<Value extends SelectValue> = {
disabled?: boolean;
selectSizeVariant?: SelectSizeVariant;
dropdownId: string;
dropdownWidth?: `${string}px` | 'auto' | number;
dropdownWidth?: number;
dropdownWidthAuto?: boolean;
emptyOption?: SelectOption<Value>;
fullWidth?: boolean;
@ -128,7 +128,7 @@ export const Select = <Value extends SelectValue>({
) : (
<Dropdown
dropdownId={dropdownId}
dropdownMenuWidth={dropDownMenuWidth}
dropdownWidth={dropDownMenuWidth}
dropdownPlacement="bottom-start"
dropdownOffset={dropdownOffset}
clickableComponent={
@ -152,7 +152,10 @@ export const Select = <Value extends SelectValue>({
<DropdownMenuSeparator />
)}
{!!filteredOptions.length && (
<DropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuItemsContainer
hasMaxHeight
width={dropDownMenuWidth}
>
{filteredOptions.map((option) => (
<MenuItemSelect
key={`${option.value}-${option.label}`}

View File

@ -30,6 +30,8 @@ export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
'date-picker-month-and-year-dropdown-year-select';
const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
width: 280px;
& .react-datepicker {
border-color: ${({ theme }) => theme.border.color.light};
background: transparent;

View File

@ -3,9 +3,12 @@ import { DropdownOnToggleEffect } from '@/ui/layout/dropdown/components/Dropdown
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import {
Placement,
@ -20,6 +23,7 @@ import { flushSync } from 'react-dom';
import { Keys } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
import { useDropdown } from '../hooks/useDropdown';
const StyledDropdownFallbackAnchor = styled.div`
@ -43,7 +47,7 @@ export type DropdownProps = {
dropdownHotkeyScope: HotkeyScope;
dropdownId: string;
dropdownPlacement?: Placement;
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownOffset?: DropdownOffset;
dropdownStrategy?: 'fixed' | 'absolute';
onClickOutside?: () => void;
@ -56,7 +60,7 @@ export const Dropdown = ({
className,
clickableComponent,
dropdownComponents,
dropdownMenuWidth,
dropdownWidth,
hotkey,
dropdownId,
dropdownHotkeyScope,
@ -82,17 +86,39 @@ export const Dropdown = ({
]
: [];
const setDropdownMaxHeight = useSetRecoilComponentStateV2(
dropdownMaxHeightComponentState,
dropdownId,
);
const setDropdownMaxWidth = useSetRecoilComponentStateV2(
dropdownMaxWidthComponentState,
dropdownId,
);
const isMobile = useIsMobile();
const bottomAutoresizePadding = isMobile ? 64 : 32;
const { refs, floatingStyles, placement } = useFloating({
placement: dropdownPlacement,
middleware: [
...offsetMiddleware,
flip(),
size({
padding: 32,
apply: () => {
padding: {
right: 32,
bottom: bottomAutoresizePadding,
},
/**
* DO NOT TOUCH THIS apply() MIDDLEWARE PLEASE
* THIS IS MANDATORY FOR KEEPING AUTORESIZING FOR ALL DROPDOWNS
* IT'S THE STANDARD WAY OF WORKING RECOMMENDED BY THE LIBRARY
* See https://floating-ui.com/docs/size#usage
*/
apply: ({ availableHeight, availableWidth }) => {
flushSync(() => {
// TODO: I think this is not needed anymore let's remove it if not used for a few weeks
// setDropdownMaxHeight(availableHeight);
setDropdownMaxHeight(availableHeight);
setDropdownMaxWidth(availableWidth);
});
},
boundary: document.querySelector('#root') ?? undefined,
@ -144,7 +170,7 @@ export const Dropdown = ({
<DropdownContent
className={className}
floatingStyles={floatingStyles}
dropdownMenuWidth={dropdownMenuWidth}
dropdownWidth={dropdownWidth}
dropdownComponents={dropdownComponents}
dropdownId={dropdownId}
dropdownPlacement={placement}

View File

@ -3,7 +3,8 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -40,7 +41,7 @@ export type DropdownContentProps = {
scope: string;
};
onHotkeyTriggered?: () => void;
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownComponents: React.ReactNode;
parentDropdownId?: string;
avoidPortal?: boolean;
@ -56,17 +57,22 @@ export const DropdownContent = ({
floatingStyles,
hotkey,
onHotkeyTriggered,
dropdownMenuWidth,
dropdownWidth,
dropdownComponents,
avoidPortal,
}: DropdownContentProps) => {
const { isDropdownOpen, closeDropdown, dropdownWidth, setDropdownPlacement } =
const { isDropdownOpen, closeDropdown, setDropdownPlacement } =
useDropdown(dropdownId);
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
const dropdownMaxHeight = useRecoilComponentValueV2(
dropdownMaxHeightComponentStateV2,
dropdownMaxHeightComponentState,
dropdownId,
);
const dropdownMaxWidth = useRecoilComponentValueV2(
dropdownMaxWidthComponentState,
dropdownId,
);
@ -112,6 +118,7 @@ export const DropdownContent = ({
const dropdownMenuStyles = {
...floatingStyles,
maxHeight: dropdownMaxHeight,
maxWidth: dropdownMaxWidth,
};
return (
@ -129,7 +136,7 @@ export const DropdownContent = ({
<OverlayContainer>
<DropdownMenu
className={className}
width={dropdownMenuWidth ?? dropdownWidth}
width={dropdownWidth}
data-select-disable
>
{dropdownComponents}
@ -148,7 +155,7 @@ export const DropdownContent = ({
<DropdownMenu
id={dropdownId}
className={className}
width={dropdownMenuWidth ?? dropdownWidth}
width={dropdownWidth}
data-select-disable
>
{dropdownComponents}

View File

@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
const StyledDropdownMenu = styled.div<{
width?: `${string}px` | `${number}%` | 'auto' | number;
@ -7,8 +8,12 @@ const StyledDropdownMenu = styled.div<{
flex-direction: column;
height: 100%;
width: ${({ width = 200 }) =>
typeof width === 'number' ? `${width}px` : width};
width: ${({ width }) =>
isDefined(width)
? typeof width === 'number'
? `${width}px`
: width
: 'auto'};
`;
export const DropdownMenu = StyledDropdownMenu;

View File

@ -1,9 +1,12 @@
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMaxHeight?: boolean;
width: number;
}>`
--padding: ${({ theme }) => theme.spacing(1)};
@ -15,7 +18,11 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
padding: var(--padding);
width: calc(100% - 2 * var(--padding));
${({ width }) =>
isDefined(width) &&
css`
width: ${width}px;
`}
`;
const StyledDropdownMenuItemsInternalContainer = styled.div`
@ -38,12 +45,14 @@ export const DropdownMenuItemsContainer = ({
children,
hasMaxHeight,
className,
width = 200,
scrollable = true,
}: {
children: React.ReactNode;
hasMaxHeight?: boolean;
className?: string;
scrollable?: boolean;
width?: number;
}) => {
const id = useId();
@ -52,6 +61,7 @@ export const DropdownMenuItemsContainer = ({
hasMaxHeight={hasMaxHeight}
className={className}
role="listbox"
width={width}
>
{hasMaxHeight ? (
<StyledScrollWrapper
@ -73,6 +83,7 @@ export const DropdownMenuItemsContainer = ({
hasMaxHeight={hasMaxHeight}
className={className}
role="listbox"
width={width}
>
<StyledDropdownMenuItemsInternalContainer>
{children}

View File

@ -51,18 +51,4 @@ describe('useDropdown', () => {
expect(result.current.isDropdownOpen).toBe(false);
});
it('should change dropdownWidth', async () => {
const { result } = renderHook(() => useDropdown(dropdownId), {
wrapper: Wrapper,
});
expect(result.current.dropdownWidth).toBe(200);
await act(async () => {
result.current.setDropdownWidth(220);
});
expect(result.current.dropdownWidth).toEqual(220);
});
});

View File

@ -1,7 +1,6 @@
import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState';
import { dropdownWidthComponentState } from '@/ui/layout/dropdown/states/dropdownWidthComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -28,10 +27,6 @@ export const useDropdownStates = ({
dropdownHotkeyComponentState,
scopeId,
),
dropdownWidthState: extractComponentState(
dropdownWidthComponentState,
scopeId,
),
isDropdownOpenState: extractComponentState(
isDropdownOpenComponentState,
scopeId,

View File

@ -12,14 +12,10 @@ import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useDropdown = (dropdownId?: string) => {
const {
scopeId,
dropdownWidthState,
isDropdownOpenState,
dropdownPlacementState,
} = useDropdownStates({
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
});
const { scopeId, isDropdownOpenState, dropdownPlacementState } =
useDropdownStates({
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
});
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
@ -32,8 +28,6 @@ export const useDropdown = (dropdownId?: string) => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState);
const [dropdownPlacement, setDropdownPlacement] = useRecoilState(
dropdownPlacementState,
);
@ -103,8 +97,6 @@ export const useDropdown = (dropdownId?: string) => {
closeDropdown,
toggleDropdown,
openDropdown,
dropdownWidth,
setDropdownWidth,
dropdownPlacement,
setDropdownPlacement,
};

View File

@ -1,8 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const dropdownWidthComponentState = createComponentState<
number | undefined
>({
key: 'dropdownWidthComponentState',
defaultValue: 200,
});

View File

@ -1,10 +1,10 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxHeightComponentStateV2 = createComponentStateV2<
export const dropdownMaxHeightComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxHeightComponentStateV2',
key: 'dropdownMaxHeightComponentState',
componentInstanceContext: DropdownComponentInstanceContext,
defaultValue: undefined,
});

View File

@ -0,0 +1,10 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxWidthComponentState = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxWidthComponentState',
componentInstanceContext: DropdownComponentInstanceContext,
defaultValue: undefined,
});

View File

@ -24,7 +24,7 @@ export const AdvancedFilterDropdownButton = () => {
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
dropdownMenuWidth={650}
dropdownWidth="100%"
/>
);
};

View File

@ -74,7 +74,6 @@ export const ViewPickerDropdown = () => {
dropdownHotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
dropdownOffset={{ x: 0, y: 8 }}
dropdownPlacement="bottom-start"
dropdownMenuWidth={200}
onClickOutside={handleClickOutside}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isViewsListDropdownOpen}>

View File

@ -174,7 +174,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
<StyledLabel>Record Type</StyledLabel>
<Dropdown
dropdownId="workflow-edit-trigger-record-type"
dropdownMenuWidth={300}
dropdownWidth={300}
dropdownPlacement="bottom-start"
clickableComponent={
<SelectControl

View File

@ -92,7 +92,7 @@ export const WorkflowVariablesDropdown = ({
return (
<Dropdown
dropdownMenuWidth={320}
dropdownWidth={320}
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: dropdownId,