From b4ee7eb2501bfd44146e961f9da3feaa280d03ae Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 23 May 2025 10:55:58 -0400 Subject: [PATCH 1/2] fix(dashboards): Fix issue assignee dropdown when few issues exist in widget --- .../components/assigneeSelectorDropdown.tsx | 6 + .../components/core/compactSelect/control.tsx | 131 ++++++++++-------- .../components/core/compactSelect/index.tsx | 3 + .../app/components/group/assigneeSelector.tsx | 3 + static/app/utils/dashboards/issueAssignee.tsx | 1 + 5 files changed, 83 insertions(+), 61 deletions(-) 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 /> ); } From 5c14f83b6e78d837cade6d7a6ccab503b19a97d2 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 23 May 2025 14:19:28 -0400 Subject: [PATCH 2/2] fix(dashboards): force update to avoid flickering --- static/app/utils/useOverlay.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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,