Refactored and simplified DropdownMenuItemsContainer height management (#12547)

This PR refactors the `DropdownMenuItemsContainer` component and
simplifies its inner parts, which have been modified over months for
different needs without taking the time to have a global approach.

It should however be noted that due to the recent refactor of the
`DropdownContent`, it is now much easier to refactor
`DropdownMenuItemsContainer`, mainly because of the width management
being nicely handled by `DropdownContent` now.

Fixes https://github.com/twentyhq/twenty/issues/11766

# Changes

The `width` props of `DropdownMenuItemsContainer` and its usage in
calling components have been removed.

The multiple ternaries inside `DropdownMenuItemsContainer` have been
reduced to one ternary on `scrollable` props.

The `ScrollWrapper` usage has been removed from
`DropdownMenuItemsContainer`, because the only thing we need is to have
a simple `overflow-y: scroll;` CSS property.

Why ? Because it was previously relevant to have a `ScrollWrapper`, when
we were using an external library, but now that `ScrollWrapper` is a
simple `div` with overflowing, which only benefit is to expose a hook to
imperatively toggle this overflowing behavior from outside (mainly
useful for table fixed row and column), and that we don’t need this for
`DropdownMenuItemsContainer`, then it follows that we just need a simple
overflowing `div` container, which simplifies everything and boils down
our `DropdownMenuItemsContainer` to a straightforward and standard CSS
stack.

We remove the temporary `scrollWrapperHeightAuto` props that was used to
fix a bug in a previous PR, we also rollback `ScrollWrapper` to its
previous state with `width: 100%` and `height: 100%` and removed
`heightAuto` props.

The `hasMaxHeight` props is kept, but the `168` pixels value is
extracted in a constant.

# QA


Component | Comment
-- | --
CommandMenuActionDropdown | Reported bug
https://github.com/twentyhq/twenty/issues/12541
RecordIndexActionMenuDropdown |  
AttachmentDropdown | Cannot test because cannot add a file (currently
broken, maybe because of permissions ?)
CommandMenuContextChipGroups |  
FavoriteFolderNavigationDrawerItemDropdown |  
FavoriteFolderPicker |  
FavoriteFolderPickerFooter |  
AdvancedFilterAddFilterRuleSelect |  
AdvancedFilterFieldSelectMenu |  
AdvancedFilterRecordFilterGroupOptionsDropdown |  
AdvancedFilterRecordFilterOperandSelect |  
AdvancedFilterRecordFilterOptionsDropdown |  
AdvancedFilterSubFieldSelectMenu |  
ObjectFilterDropdownBooleanSelect |  
ObjectFilterDropdownCountrySelect |  
ObjectFilterDropdownCurrencySelect |  
ObjectFilterDropdownNumberInput |  
ObjectFilterDropdownOptionSelect | Fixed “No result” case
ObjectFilterDropdownRecordRemoveFilterMenuItem | Removed because unused
ObjectFilterDropdownTextInput |  
ObjectOptionsDropdownFieldsContent | Spotted bug with icon eye
https://github.com/twentyhq/twenty/issues/12545
ObjectOptionsDropdownHiddenFieldsContent | Spotted bug with icon eye
https://github.com/twentyhq/twenty/issues/12545
ObjectOptionsDropdownLayoutContent | Refactored
DropdownMenuItemsContainer usage with DropdownMenuSeparator, spotted bug
switch view type https://github.com/twentyhq/twenty/issues/12546
ObjectOptionsDropdownMenuContent | Refactored DropdownMenuItemsContainer
usage with DropdownMenuSeparator
ObjectOptionsDropdownLayoutOpenInContent |  
ObjectOptionsDropdownMenuViewName |  
ObjectOptionsDropdownRecordGroupFieldsContent |  
ObjectOptionsDropdownRecordGroupSortContent |  
ObjectSortDropdownButton |  
RecordBoardColumnDropdownMenu |  
RecordBoardColumnDropdownMenu |  
RecordBoardColumnHeaderAggregateDropdownFieldsContent |  
RecordBoardColumnHeaderAggregateDropdownMenuContent |  
RecordBoardColumnHeaderAggregateDropdownOptionsContent |  
MultiItemFieldInput | Added hasMaxHeight on list of items
MultiItemFieldMenuItem |  
RecordGroupsVisibilityDropdownSection |  
MultipleRecordPicker |  
MultipleRecordPickerMenuItems |  
SingleRecordPickerMenuItems |  
SingleRecordPickerMenuItemsWithSearch |  
RecordDetailRelationRecordsListItem |  
RecordTableColumnAggregateFooterDropdownSubmenuContent |  
RecordTableColumnAggregateFooterMenuContent |  
RecordTableHeaderPlusButtonContent |  
RecordTableHeaderPlusButtonContent |  
MultipleSelectDropdown |  
SettingsAccountsRowDropdownMenu |  
ConfigVariableDatabaseInput |  
ConfigVariableOptionsDropdownContent |  
SettingsDataModelNewFieldBreadcrumbDropDown |  
SettingsDataModelFieldSelectFormOptionRow |  
SettingsObjectFieldActiveActionDropdown |  
SettingsObjectFieldInactiveActionDropdown |  
SettingsObjectInactiveMenuDropDown |  
SettingsIntegrationDatabaseConnectionSummaryCard |  
SettingsRoleAssignmentWorkspaceMemberPickerDropdown |  
SettingsRolePermissionsObjectLevelObjectPickerDropdownContent | Cannot
test
SettingsSecurityApprovedAccessDomainRowDropdownMenu | Cannot test
SettingsSecuritySSORowDropdownMenu | Cannot test
SettingsServerlessFunctionTabEnvironmentVariableTableRow | Cannot test
MatchColumnSelectFieldSelectDropdownContent |  
MatchColumnSelectSubFieldSelectDropdownContent |  
SubMatchingSelectInput |  
SupportDropdown |  
IconPicker |  
Select |  
SelectInput |  
CurrencyPickerDropdownSelect |  
PhoneCountryPickerDropdownSelect |  
CustomSlashMenu |  
TabListDropdown | Cannot test
MultiWorkspaceDropdownDefaultComponents | Removed unnecessary
StyledDropdownMenuItemsContainer
MultiWorkspaceDropdownThemesComponents |  
MultiWorkspaceDropdownWorkspacesListComponents |  
UpdateViewButtonGroup |  
ViewBarFilterDropdownFieldSelectMenu |  
ViewFieldsVisibilityDropdownSection |  
ViewPickerContentCreateMode |  
ViewPickerContentEditMode |  
ViewPickerListContent | Add hasMaxHeight to limit the height of view
list
ViewPickerOptionDropdown |  
WorkflowEditTriggerDatabaseEventForm |  
WorkflowVariablesDropdownFieldItems |  
WorkflowVariablesDropdownObjectItems |  
WorkflowVariablesDropdownWorkflowStepItems |  


<!-- notionvc: a3a87101-9944-4b03-a29d-b2974d5ffa9d -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2025-06-12 12:22:26 +02:00
committed by GitHub
parent 6b0517943f
commit 333d7081ef
18 changed files with 81 additions and 143 deletions

View File

@ -150,7 +150,7 @@ export const AdvancedFilterFieldSelectMenu = ({
{shouldShowVisibleFields && (
<>
<DropdownMenuSectionLabel label={t`Visible fields`} />
<DropdownMenuItemsContainer scrollWrapperHeightAuto>
<DropdownMenuItemsContainer>
{visibleColumnsFieldMetadataItems.map(
(visibleFieldMetadataItem, index) => (
<SelectableListItem
@ -174,7 +174,7 @@ export const AdvancedFilterFieldSelectMenu = ({
{shouldShowHiddenFields && (
<>
<DropdownMenuSectionLabel label={t`Hidden fields`} />
<DropdownMenuItemsContainer scrollWrapperHeightAuto>
<DropdownMenuItemsContainer>
{hiddenColumnsFieldMetadataItems.map(
(hiddenFieldMetadataItem, index) => (
<SelectableListItem

View File

@ -58,7 +58,7 @@ export const ObjectFilterDropdownBooleanSelect = () => {
selectableItemIdArray={options.map((option) => option.toString())}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
>
<DropdownMenuItemsContainer hasMaxHeight width="auto">
<DropdownMenuItemsContainer hasMaxHeight>
{options.map((option) => (
<StyledBooleanSelectContainer
key={String(option)}

View File

@ -107,7 +107,7 @@ export const ObjectFilterDropdownCurrencySelect = () => {
}}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight width="auto">
<DropdownMenuItemsContainer hasMaxHeight>
{filteredSelectedItems?.map((item) => {
return (
<MenuItemMultiSelectAvatar

View File

@ -38,7 +38,7 @@ export const ObjectFilterDropdownNumberInput = () => {
};
return (
<DropdownMenuItemsContainer width="auto">
<DropdownMenuItemsContainer>
<DropdownMenuInput
ref={handleInputRef}
value={objectFilterDropdownFilterValue}

View File

@ -151,21 +151,24 @@ export const ObjectFilterDropdownOptionSelect = () => {
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
>
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
{showNoResult ? (
<MenuItem text="No results" />
) : (
optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))
)}
</DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No results" />}
</SelectableList>
);
};

View File

@ -1,26 +0,0 @@
import { useEmptyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { IconFilterOff } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
export const ObjectFilterDropdownRecordRemoveFilterMenuItem = () => {
const { emptyRecordFilter } = useEmptyRecordFilter();
const { closeDropdown } = useDropdown();
const handleRemoveFilter = () => {
emptyRecordFilter();
closeDropdown();
};
return (
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleRemoveFilter}
LeftIcon={IconFilterOff}
text={'Remove filter'}
/>
</DropdownMenuItemsContainer>
);
};

View File

@ -38,7 +38,7 @@ export const ObjectFilterDropdownTextInput = () => {
};
return (
<DropdownMenuItemsContainer width="auto">
<DropdownMenuItemsContainer>
<DropdownMenuInput
ref={handleInputRef}
value={objectFilterDropdownFilterValue}

View File

@ -106,12 +106,12 @@ export const ObjectOptionsDropdownLayoutContent = () => {
</DropdownMenuHeader>
{!!currentView && (
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem
itemId={ViewType.Table}
onEnter={() => {
@ -157,7 +157,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
onClick={handleSelectKanbanViewType}
/>
</SelectableListItem>
<DropdownMenuSeparator />
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem
itemId={ViewOpenRecordInType.SIDE_PANEL}
onEnter={() => {
@ -232,8 +234,8 @@ export const ObjectOptionsDropdownLayoutContent = () => {
</SelectableListItem>
</>
)}
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownMenuItemsContainer>
</SelectableList>
)}
</DropdownContent>
);

View File

@ -173,7 +173,9 @@ export const ObjectOptionsDropdownMenuContent = () => {
width="100%"
/>
)}
<DropdownMenuSeparator />
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem
itemId="Copy link to view"
onEnter={() => {

View File

@ -286,7 +286,7 @@ export const ObjectSortDropdownButton = ({
{shouldShowVisibleFields && (
<>
<DropdownMenuSectionLabel label={t`Visible fields`} />
<DropdownMenuItemsContainer scrollWrapperHeightAuto>
<DropdownMenuItemsContainer>
{visibleFieldMetadataItems.map(
(visibleFieldMetadataItem, index) => (
<SelectableListItem
@ -315,7 +315,7 @@ export const ObjectSortDropdownButton = ({
{shouldShowHiddenFields && (
<>
<DropdownMenuSectionLabel label={t`Hidden fields`} />
<DropdownMenuItemsContainer scrollWrapperHeightAuto>
<DropdownMenuItemsContainer>
{hiddenFieldMetadataItems.map(
(hiddenFieldMetadataItem, index) => (
<SelectableListItem

View File

@ -184,7 +184,7 @@ export const MultiItemFieldInput = <T,>({
<DropdownContent ref={containerRef}>
{!!items.length && (
<>
<DropdownMenuItemsContainer>
<DropdownMenuItemsContainer hasMaxHeight>
{items.map((item, index) =>
renderItem({
value: item,

View File

@ -87,7 +87,7 @@ export const MultipleSelectDropdown = ({
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
>
<DropdownMenuItemsContainer hasMaxHeight width="auto">
<DropdownMenuItemsContainer hasMaxHeight>
{itemsInDropdown?.map((item) => {
return (
<SelectableListItem

View File

@ -1,12 +1,8 @@
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { css } from '@emotion/react';
import { DROPDOWN_MENU_ITEMS_CONTAINER_MAX_HEIGHT } from '@/ui/layout/dropdown/constants/DropdownMenuItemsContainerMaxHeight';
import styled from '@emotion/styled';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMaxHeight?: boolean;
width: number | 'auto' | '100%';
const StyledExternalContainer = styled.div<{
maxHeight?: number;
}>`
--padding: ${({ theme }) => theme.spacing(1)};
@ -14,22 +10,27 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
display: flex;
flex-direction: column;
max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '168px' : 'none')};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'none')};
width: 100%;
height: fit-content;
padding: var(--padding);
${({ width }) =>
isDefined(width) && width === '100%'
? css`
width: 100%;
`
: css`
width: ${width}px;
`}
box-sizing: border-box;
`;
const StyledDropdownMenuItemsInternalContainer = styled.div`
align-items: stretch;
const StyledScrollableContainer = styled.div<{ maxHeight?: number }>`
box-sizing: border-box;
display: flex;
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'none')};
width: 100%;
overflow-y: scroll;
`;
const StyledInternalContainer = styled.div`
display: flex;
flex-direction: column;
@ -39,64 +40,28 @@ const StyledDropdownMenuItemsInternalContainer = styled.div`
width: 100%;
`;
const StyledScrollWrapper = styled(ScrollWrapper)`
width: 100%;
`;
export const DropdownMenuItemsContainer = ({
children,
hasMaxHeight,
className,
scrollable = true,
width = 'auto',
scrollWrapperHeightAuto,
}: {
children: React.ReactNode;
hasMaxHeight?: boolean;
className?: string;
scrollable?: boolean;
width?: number | 'auto' | '100%';
scrollWrapperHeightAuto?: boolean;
}) => {
const id = useId();
return scrollable !== true ? (
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
className={className}
role="listbox"
width={width}
return scrollable === true ? (
<StyledScrollableContainer
maxHeight={
hasMaxHeight ? DROPDOWN_MENU_ITEMS_CONTAINER_MAX_HEIGHT : undefined
}
>
{hasMaxHeight ? (
<StyledScrollWrapper
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
heightAuto={scrollWrapperHeightAuto}
>
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
</StyledScrollWrapper>
) : (
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
)}
</StyledDropdownMenuItemsExternalContainer>
<StyledExternalContainer role="listbox">
<StyledInternalContainer>{children}</StyledInternalContainer>
</StyledExternalContainer>
</StyledScrollableContainer>
) : (
<ScrollWrapper
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
heightAuto={scrollWrapperHeightAuto}
>
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
className={className}
role="listbox"
width={width}
>
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
</StyledDropdownMenuItemsExternalContainer>
</ScrollWrapper>
<StyledExternalContainer role="listbox">
<StyledInternalContainer>{children}</StyledInternalContainer>
</StyledExternalContainer>
);
};

View File

@ -0,0 +1 @@
export const DROPDOWN_MENU_ITEMS_CONTAINER_MAX_HEIGHT = 168;

View File

@ -46,11 +46,6 @@ const StyledDescription = styled.div`
padding-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledDropdownMenuItemsContainer = styled.div`
margin: ${({ theme }) => theme.spacing(1)} 0;
padding: 0 ${({ theme }) => theme.spacing(1)};
`;
export const MultiWorkspaceDropdownDefaultComponents = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { t } = useLingui();
@ -134,7 +129,7 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
</DropdownMenuHeader>
{workspaces.length > 1 && (
<>
<StyledDropdownMenuItemsContainer>
<DropdownMenuItemsContainer>
{workspaces
.filter(({ id }) => id !== currentWorkspace?.id)
.slice(0, 3)
@ -171,7 +166,7 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
hasSubMenu={true}
/>
)}
</StyledDropdownMenuItemsContainer>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}

View File

@ -7,7 +7,7 @@ import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/sta
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
const StyledScrollWrapper = styled.div<{ height: string }>`
const StyledScrollWrapper = styled.div`
&.scroll-wrapper-x-enabled {
overflow-x: overlay;
}
@ -17,7 +17,7 @@ const StyledScrollWrapper = styled.div<{ height: string }>`
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
height: ${({ height }) => height};
height: 100%;
`;
export type ScrollWrapperProps = {
@ -26,7 +26,6 @@ export type ScrollWrapperProps = {
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
componentInstanceId: string;
heightAuto?: boolean;
};
export const ScrollWrapper = ({
@ -35,7 +34,6 @@ export const ScrollWrapper = ({
className,
defaultEnableXScroll = true,
defaultEnableYScroll = true,
heightAuto = false,
}: ScrollWrapperProps) => {
const setScrollTop = useSetRecoilComponentStateV2(
scrollWrapperScrollTopComponentState,
@ -73,7 +71,6 @@ export const ScrollWrapper = ({
id={`scroll-wrapper-${componentInstanceId}`}
className={className}
onScroll={handleScroll}
height={heightAuto ? 'auto' : '100%'}
>
{children}
</StyledScrollWrapper>

View File

@ -98,8 +98,7 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
{shouldShowVisibleFields && (
<>
<DropdownMenuSectionLabel label={t`Visible fields`} />
<DropdownMenuItemsContainer scrollWrapperHeightAuto>
<DropdownMenuItemsContainer>
{selectableVisibleFieldMetadataItems.map(
(visibleFieldMetadataItem) => (
<ViewBarFilterDropdownFieldSelectMenuItem
@ -115,7 +114,7 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
{shouldShowHiddenFields && (
<>
<DropdownMenuSectionLabel label={t`Hidden fields`} />
<DropdownMenuItemsContainer scrollWrapperHeightAuto>
<DropdownMenuItemsContainer>
{selectableHiddenFieldMetadataItems.map(
(hiddenFieldMetadataItem) => (
<ViewBarFilterDropdownFieldSelectMenuItem

View File

@ -96,7 +96,7 @@ export const ViewPickerListContent = () => {
return (
<DropdownContent>
<DropdownMenuItemsContainer>
<DropdownMenuItemsContainer hasMaxHeight>
<DraggableList
onDragEnd={handleDragEnd}
draggableItems={viewsOnCurrentObject.map((view, index) => {