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:
@ -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"
|
||||
|
||||
@ -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}`}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const dropdownWidthComponentState = createComponentState<
|
||||
number | undefined
|
||||
>({
|
||||
key: 'dropdownWidthComponentState',
|
||||
defaultValue: 200,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user