Fix dropdown z index (#12442)

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

This PR creates two different dropdown z indexes, one for normal
dropdowns, and one for the dropdowns inside modals.
This commit is contained in:
Raphaël Bosi
2025-06-04 11:14:56 +02:00
committed by GitHub
parent f7e2c1c627
commit 7046965496
8 changed files with 124 additions and 5 deletions

View File

@ -137,6 +137,7 @@ export const MatchColumnToFieldSelect = ({
)
}
onClickOutside={handleClickOutside}
isDropdownInModal
/>
);
};

View File

@ -69,6 +69,7 @@ export const SubMatchingSelectRowRightDropdown = <T extends string>({
onOptionSelected={handleSelect}
/>
}
isDropdownInModal
/>
</StyledDropdownContainer>
);

View File

@ -15,9 +15,10 @@
export enum RootStackingContextZIndices {
CommandMenu = 21,
CommandMenuButton = 22,
DropdownPortalBelowModal = 38,
RootModalBackDrop = 39,
RootModal = 40,
DropdownPortal = 50,
DropdownPortalAboveModal = 50,
Dialog = 9999,
SnackBar = 10002,
NotFound = 10001,

View File

@ -58,6 +58,7 @@ export type DropdownProps = {
onClose?: () => void;
onOpen?: () => void;
excludedClickOutsideIds?: string[];
isDropdownInModal?: boolean;
};
export const Dropdown = ({
@ -74,6 +75,7 @@ export const Dropdown = ({
onOpen,
clickableComponentWidth = 'auto',
excludedClickOutsideIds,
isDropdownInModal = false,
}: DropdownProps) => {
const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId);
@ -193,6 +195,7 @@ export const Dropdown = ({
onClickOutside={onClickOutside}
onHotkeyTriggered={toggleDropdown}
excludedClickOutsideIds={excludedClickOutsideIds}
isDropdownInModal={isDropdownInModal}
/>
)}
<DropdownOnToggleEffect

View File

@ -7,6 +7,14 @@ import { useState } from 'react';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { SetRecoilState } from 'recoil';
import { Avatar, IconChevronLeft } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
@ -15,6 +23,8 @@ import {
MenuItemSelectAvatar,
} from 'twenty-ui/navigation';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
import { Dropdown } from '../Dropdown';
import { DropdownMenuHeader } from '../DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuInput } from '../DropdownMenuInput';
@ -321,3 +331,96 @@ export const CheckableMenuItemWithAvatar: Story = {
},
play: playInteraction,
};
const modalId = 'dropdown-modal-test';
const ModalWithDropdown = () => {
return (
<>
<Modal modalId={modalId} size="medium" padding="medium" isClosable={true}>
<Modal.Header>Modal with Dropdown Test</Modal.Header>
<Modal.Content>
<p>
This modal contains a dropdown that should appear above the modal
(higher z-index).
</p>
<div style={{ marginTop: '20px' }}>
<Dropdown
clickableComponent={
<Button
dataTestId="dropdown-button"
title="Open Dropdown in Modal"
/>
}
dropdownHotkeyScope={{ scope: 'modal-dropdown' }}
dropdownOffset={{ x: 0, y: 8 }}
dropdownId="modal-dropdown-test"
isDropdownInModal={true}
dropdownComponents={
<DropdownMenuItemsContainer hasMaxHeight>
<div data-testid="dropdown-content">
<FakeSelectableMenuItemList hasAvatar />
</div>
</DropdownMenuItemsContainer>
}
/>
</div>
</Modal.Content>
</Modal>
</>
);
};
const initializeModalState = ({ set }: { set: SetRecoilState }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: modalId,
}),
true,
);
set(currentHotkeyScopeState, {
scope: ModalHotkeyScope.ModalFocus,
customScopes: {
commandMenu: true,
goto: false,
keyboardShortcutMenu: false,
},
});
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
set(focusStackState, [
{
focusId: modalId,
componentInstance: {
componentType: FocusComponentType.MODAL,
componentInstanceId: modalId,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
},
]);
};
export const DropdownInsideModal: Story = {
decorators: [I18nFrontDecorator, RootDecorator, ComponentDecorator],
parameters: {
initializeState: initializeModalState,
disableHotkeyInitialization: true,
},
render: () => <ModalWithDropdown />,
play: async () => {
const canvas = within(document.body);
const dropdownButton = await canvas.findByTestId('dropdown-button');
await userEvent.click(dropdownButton);
const dropdownContent = await canvas.findByTestId('dropdown-content');
expect(dropdownContent).toBeVisible();
},
};

View File

@ -22,9 +22,14 @@ import { Keys } from 'react-hotkeys-hook';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
export const StyledDropdownContentContainer = styled.div`
export const StyledDropdownContentContainer = styled.div<{
isDropdownInModal?: boolean;
}>`
display: flex;
z-index: ${RootStackingContextZIndices.DropdownPortal};
z-index: ${({ isDropdownInModal }) =>
isDropdownInModal
? RootStackingContextZIndices.DropdownPortalAboveModal
: RootStackingContextZIndices.DropdownPortalBelowModal};
`;
const StyledDropdownInsideContainer = styled.div`
@ -50,6 +55,7 @@ export type DropdownInternalContainerProps = {
dropdownComponents: React.ReactNode;
parentDropdownId?: string;
excludedClickOutsideIds?: string[];
isDropdownInModal?: boolean;
};
export const DropdownInternalContainer = ({
@ -63,6 +69,7 @@ export const DropdownInternalContainer = ({
onHotkeyTriggered,
dropdownComponents,
excludedClickOutsideIds,
isDropdownInModal = false,
}: DropdownInternalContainerProps) => {
const { isDropdownOpen, closeDropdown, setDropdownPlacement } =
useDropdown(dropdownId);
@ -140,6 +147,7 @@ export const DropdownInternalContainer = ({
role="listbox"
id={`${dropdownId}-options`}
data-click-outside-id={excludedClickOutsideId}
isDropdownInModal={isDropdownInModal}
>
<OverlayContainer>
<StyledDropdownInsideContainer id={dropdownId} data-select-disable>

View File

@ -6,6 +6,7 @@ import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { SetRecoilState } from 'recoil';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
@ -13,7 +14,7 @@ import { sleep } from '~/utils/sleep';
import { isModalOpenedComponentState } from '../../states/isModalOpenedComponentState';
import { ConfirmationModal } from '../ConfirmationModal';
const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => {
const initializeState = ({ set }: { set: SetRecoilState }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'confirmation-modal',

View File

@ -6,6 +6,7 @@ import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { SetRecoilState } from 'recoil';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
@ -13,7 +14,7 @@ import { sleep } from '~/utils/sleep';
import { isModalOpenedComponentState } from '../../states/isModalOpenedComponentState';
import { Modal } from '../Modal';
const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => {
const initializeState = ({ set }: { set: SetRecoilState }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'modal-id',