diff --git a/static/app/components/assigneeSelectorDropdown.tsx b/static/app/components/assigneeSelectorDropdown.tsx
index 2a42e4d45180b6..fa9ab455dba470 100644
--- a/static/app/components/assigneeSelectorDropdown.tsx
+++ b/static/app/components/assigneeSelectorDropdown.tsx
@@ -76,6 +76,10 @@ interface AssigneeSelectorDropdownProps {
* Additional styles to apply to the dropdown
*/
className?: string;
+ /**
+ * If true, places the assignee dropdown in document body
+ */
+ createPortalOnDropdown?: boolean;
/**
* Optional list of members to populate the dropdown with.
*/
@@ -214,6 +218,7 @@ export default function AssigneeSelectorDropdown({
sizeLimit = 150,
trigger,
additionalMenuFooterItems,
+ createPortalOnDropdown = false,
}: AssigneeSelectorDropdownProps) {
const memberLists = useLegacyStore(MemberListStore);
const sessionUser = useUser();
@@ -581,6 +586,7 @@ export default function AssigneeSelectorDropdown({
menuFooter={footerInviteButton}
sizeLimit={sizeLimit}
sizeLimitMessage="Use search to find more users and teams..."
+ createPortalOnDropdown={createPortalOnDropdown}
/>
);
diff --git a/static/app/components/core/compactSelect/control.tsx b/static/app/components/core/compactSelect/control.tsx
index 6a1ac06252d9cf..3a8e505d5bbe92 100644
--- a/static/app/components/core/compactSelect/control.tsx
+++ b/static/app/components/core/compactSelect/control.tsx
@@ -7,6 +7,7 @@ import {
useRef,
useState,
} from 'react';
+import {createPortal} from 'react-dom';
import isPropValid from '@emotion/is-prop-valid';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
@@ -112,6 +113,10 @@ export interface ControlProps
* If true, there will be a "Clear" button in the menu header.
*/
clearable?: boolean;
+ /**
+ * If true, places the dropdown in the document body instead.
+ */
+ createPortalOnDropdown?: boolean;
/**
* Whether to disable the search input's filter function (applicable only when
* `searchable` is true). This is useful for implementing custom search behaviors,
@@ -242,6 +247,7 @@ export function Control({
menuBody,
menuFooter,
onOpenChange,
+ createPortalOnDropdown,
// Select props
size = 'md',
@@ -504,6 +510,67 @@ export function Control({
}, [saveSelectedOptions, overlayState, overlayIsOpen, search]);
const theme = useTheme();
+ const dropdown = (
+
+
+
+ {(menuTitle || menuHeaderTrailingItems || (clearable && showClearButton)) && (
+
+ {menuTitle}
+
+ {loading && }
+ {typeof menuHeaderTrailingItems === 'function'
+ ? menuHeaderTrailingItems({closeOverlay: overlayState.close})
+ : menuHeaderTrailingItems}
+ {clearable && showClearButton && (
+
+ {t('Clear')}
+
+ )}
+
+
+ )}
+ {searchable && (
+ updateSearch(e.target.value)}
+ size="xs"
+ {...searchKeyboardProps}
+ />
+ )}
+ {typeof menuBody === 'function'
+ ? menuBody({closeOverlay: overlayState.close})
+ : menuBody}
+ {!hideOptions && {children}}
+ {menuFooter && (
+
+ {typeof menuFooter === 'function'
+ ? menuFooter({closeOverlay: overlayState.close})
+ : menuFooter}
+
+ )}
+
+
+
+ );
+
return (
@@ -519,66 +586,7 @@ export function Control({
{triggerLabel}
)}
-
-
-
- {(menuTitle ||
- menuHeaderTrailingItems ||
- (clearable && showClearButton)) && (
-
- {menuTitle}
-
- {loading && }
- {typeof menuHeaderTrailingItems === 'function'
- ? menuHeaderTrailingItems({closeOverlay: overlayState.close})
- : menuHeaderTrailingItems}
- {clearable && showClearButton && (
-
- {t('Clear')}
-
- )}
-
-
- )}
- {searchable && (
- updateSearch(e.target.value)}
- size="xs"
- {...searchKeyboardProps}
- />
- )}
- {typeof menuBody === 'function'
- ? menuBody({closeOverlay: overlayState.close})
- : menuBody}
- {!hideOptions && {children}}
- {menuFooter && (
-
- {typeof menuFooter === 'function'
- ? menuFooter({closeOverlay: overlayState.close})
- : menuFooter}
-
- )}
-
-
-
+ {createPortalOnDropdown ? createPortal(dropdown, document.body) : dropdown}
);
@@ -692,9 +700,10 @@ const StyledOverlay = styled(Overlay, {
const StyledPositionWrapper = styled(PositionWrapper, {
shouldForwardProp: prop => isPropValid(prop),
-})<{visible?: boolean}>`
+})<{visible?: boolean; zIndex?: number}>`
min-width: 100%;
display: ${p => (p.visible ? 'block' : 'none')};
+ ${p => (p.zIndex ? `z-index: ${p.zIndex} !important;` : '')}
`;
const OptionsWrap = styled('div')`
diff --git a/static/app/components/core/compactSelect/index.tsx b/static/app/components/core/compactSelect/index.tsx
index 56b6a88740287b..325864c0baca66 100644
--- a/static/app/components/core/compactSelect/index.tsx
+++ b/static/app/components/core/compactSelect/index.tsx
@@ -21,6 +21,7 @@ export type {SelectOption, SelectOptionOrSection, SelectSection, SelectKey};
interface BaseSelectProps extends ControlProps {
options: Array>;
+ createPortalOnDropdown?: boolean;
}
export interface SingleSelectProps
@@ -70,6 +71,7 @@ function CompactSelect({
sizeLimitMessage,
// Control props
+ createPortalOnDropdown = false,
grid,
disabled,
emptyMessage,
@@ -114,6 +116,7 @@ function CompactSelect({
disabled={controlDisabled}
grid={grid}
size={size}
+ createPortalOnDropdown={createPortalOnDropdown}
>
void;
additionalMenuFooterItems?: React.ReactNode;
+ createPortalOnDropdown?: boolean;
memberList?: User[];
owners?: Array>;
showLabel?: boolean;
@@ -81,6 +82,7 @@ export function AssigneeSelector({
owners,
additionalMenuFooterItems,
showLabel = false,
+ createPortalOnDropdown = false,
}: AssigneeSelectorProps) {
const theme = useTheme();
@@ -116,6 +118,7 @@ export function AssigneeSelector({
)}
additionalMenuFooterItems={additionalMenuFooterItems}
+ createPortalOnDropdown={createPortalOnDropdown}
/>
);
}
diff --git a/static/app/utils/dashboards/issueAssignee.tsx b/static/app/utils/dashboards/issueAssignee.tsx
index 4bcbf61052ee60..2b4f393ec0ab67 100644
--- a/static/app/utils/dashboards/issueAssignee.tsx
+++ b/static/app/utils/dashboards/issueAssignee.tsx
@@ -32,6 +32,7 @@ export function IssueAssignee({groupId}: IssueAssigneeProps) {
memberList={memberListState.members}
assigneeLoading={assigneeLoading}
handleAssigneeChange={handleAssigneeChange}
+ createPortalOnDropdown
/>
);
}
diff --git a/static/app/utils/useOverlay.tsx b/static/app/utils/useOverlay.tsx
index 342418b7dedb09..0943aef3204e4e 100644
--- a/static/app/utils/useOverlay.tsx
+++ b/static/app/utils/useOverlay.tsx
@@ -1,4 +1,4 @@
-import {useMemo, useRef, useState} from 'react';
+import {useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {PopperProps} from 'react-popper';
import {usePopper} from 'react-popper';
import type {Modifier} from '@popperjs/core';
@@ -309,6 +309,15 @@ function useOverlay({
overlayRef
);
+ // Force popper update when elements mount/update
+ useLayoutEffect(() => {
+ if (!openState.isOpen || !popperUpdate) {
+ return undefined;
+ }
+ popperUpdate();
+ return () => {};
+ }, [openState.isOpen, triggerElement, overlayElement, popperUpdate]);
+
return {
isOpen: openState.isOpen,
state: openState,