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

@ -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,
});