Skip to content

fix(dashboards): Fix issue assignee dropdown when few issues exist in… #92216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions static/app/components/assigneeSelectorDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -214,6 +218,7 @@ export default function AssigneeSelectorDropdown({
sizeLimit = 150,
trigger,
additionalMenuFooterItems,
createPortalOnDropdown = false,
}: AssigneeSelectorDropdownProps) {
const memberLists = useLegacyStore(MemberListStore);
const sessionUser = useUser();
Expand Down Expand Up @@ -581,6 +586,7 @@ export default function AssigneeSelectorDropdown({
menuFooter={footerInviteButton}
sizeLimit={sizeLimit}
sizeLimitMessage="Use search to find more users and teams..."
createPortalOnDropdown={createPortalOnDropdown}
/>
</AssigneeWrapper>
);
Expand Down
131 changes: 70 additions & 61 deletions static/app/components/core/compactSelect/control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -242,6 +247,7 @@ export function Control({
menuBody,
menuFooter,
onOpenChange,
createPortalOnDropdown,

// Select props
size = 'md',
Expand Down Expand Up @@ -504,6 +510,67 @@ export function Control({
}, [saveSelectedOptions, overlayState, overlayIsOpen, search]);

const theme = useTheme();
const dropdown = (
<StyledPositionWrapper
zIndex={theme.zIndex?.tooltip}
visible={overlayIsOpen}
{...overlayProps}
>
<StyledOverlay
width={menuWidth ?? menuFullWidth}
minWidth={overlayProps.style!.minWidth}
maxWidth={maxMenuWidth}
maxHeight={overlayProps.style!.maxHeight}
maxHeightProp={maxMenuHeight}
data-menu-has-header={!!menuTitle || clearable}
data-menu-has-search={searchable}
data-menu-has-footer={!!menuFooter}
>
<FocusScope contain={overlayIsOpen}>
{(menuTitle || menuHeaderTrailingItems || (clearable && showClearButton)) && (
<MenuHeader size={size}>
<MenuTitle>{menuTitle}</MenuTitle>
<MenuHeaderTrailingItems>
{loading && <StyledLoadingIndicator size={12} />}
{typeof menuHeaderTrailingItems === 'function'
? menuHeaderTrailingItems({closeOverlay: overlayState.close})
: menuHeaderTrailingItems}
{clearable && showClearButton && (
<ClearButton onClick={clearSelection} size="zero" borderless>
{t('Clear')}
</ClearButton>
)}
</MenuHeaderTrailingItems>
</MenuHeader>
)}
{searchable && (
<SearchInput
ref={searchRef}
placeholder={searchPlaceholder}
value={searchInputValue}
onFocus={onSearchFocus}
onBlur={onSearchBlur}
onChange={e => updateSearch(e.target.value)}
size="xs"
{...searchKeyboardProps}
/>
)}
{typeof menuBody === 'function'
? menuBody({closeOverlay: overlayState.close})
: menuBody}
{!hideOptions && <OptionsWrap>{children}</OptionsWrap>}
{menuFooter && (
<MenuFooter>
{typeof menuFooter === 'function'
? menuFooter({closeOverlay: overlayState.close})
: menuFooter}
</MenuFooter>
)}
</FocusScope>
</StyledOverlay>
</StyledPositionWrapper>
);

return (
<SelectContext value={contextValue}>
<ControlWrap {...wrapperProps}>
Expand All @@ -519,66 +586,7 @@ export function Control({
{triggerLabel}
</DropdownButton>
)}
<StyledPositionWrapper
zIndex={theme.zIndex?.tooltip}
visible={overlayIsOpen}
{...overlayProps}
>
<StyledOverlay
width={menuWidth ?? menuFullWidth}
minWidth={overlayProps.style!.minWidth}
maxWidth={maxMenuWidth}
maxHeight={overlayProps.style!.maxHeight}
maxHeightProp={maxMenuHeight}
data-menu-has-header={!!menuTitle || clearable}
data-menu-has-search={searchable}
data-menu-has-footer={!!menuFooter}
>
<FocusScope contain={overlayIsOpen}>
{(menuTitle ||
menuHeaderTrailingItems ||
(clearable && showClearButton)) && (
<MenuHeader size={size}>
<MenuTitle>{menuTitle}</MenuTitle>
<MenuHeaderTrailingItems>
{loading && <StyledLoadingIndicator size={12} />}
{typeof menuHeaderTrailingItems === 'function'
? menuHeaderTrailingItems({closeOverlay: overlayState.close})
: menuHeaderTrailingItems}
{clearable && showClearButton && (
<ClearButton onClick={clearSelection} size="zero" borderless>
{t('Clear')}
</ClearButton>
)}
</MenuHeaderTrailingItems>
</MenuHeader>
)}
{searchable && (
<SearchInput
ref={searchRef}
placeholder={searchPlaceholder}
value={searchInputValue}
onFocus={onSearchFocus}
onBlur={onSearchBlur}
onChange={e => updateSearch(e.target.value)}
size="xs"
{...searchKeyboardProps}
/>
)}
{typeof menuBody === 'function'
? menuBody({closeOverlay: overlayState.close})
: menuBody}
{!hideOptions && <OptionsWrap>{children}</OptionsWrap>}
{menuFooter && (
<MenuFooter>
{typeof menuFooter === 'function'
? menuFooter({closeOverlay: overlayState.close})
: menuFooter}
</MenuFooter>
)}
</FocusScope>
</StyledOverlay>
</StyledPositionWrapper>
{createPortalOnDropdown ? createPortal(dropdown, document.body) : dropdown}
</ControlWrap>
</SelectContext>
);
Expand Down Expand Up @@ -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')`
Expand Down
3 changes: 3 additions & 0 deletions static/app/components/core/compactSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type {SelectOption, SelectOptionOrSection, SelectSection, SelectKey};

interface BaseSelectProps<Value extends SelectKey> extends ControlProps {
options: Array<SelectOptionOrSection<Value>>;
createPortalOnDropdown?: boolean;
}

export interface SingleSelectProps<Value extends SelectKey>
Expand Down Expand Up @@ -70,6 +71,7 @@ function CompactSelect<Value extends SelectKey>({
sizeLimitMessage,

// Control props
createPortalOnDropdown = false,
grid,
disabled,
emptyMessage,
Expand Down Expand Up @@ -114,6 +116,7 @@ function CompactSelect<Value extends SelectKey>({
disabled={controlDisabled}
grid={grid}
size={size}
createPortalOnDropdown={createPortalOnDropdown}
>
<List
{...listProps}
Expand Down
3 changes: 3 additions & 0 deletions static/app/components/group/assigneeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface AssigneeSelectorProps {
group: Group;
handleAssigneeChange: (assignedActor: AssignableEntity | null) => void;
additionalMenuFooterItems?: React.ReactNode;
createPortalOnDropdown?: boolean;
memberList?: User[];
owners?: Array<Omit<SuggestedAssignee, 'assignee'>>;
showLabel?: boolean;
Expand Down Expand Up @@ -81,6 +82,7 @@ export function AssigneeSelector({
owners,
additionalMenuFooterItems,
showLabel = false,
createPortalOnDropdown = false,
}: AssigneeSelectorProps) {
const theme = useTheme();

Expand Down Expand Up @@ -116,6 +118,7 @@ export function AssigneeSelector({
</StyledDropdownButton>
)}
additionalMenuFooterItems={additionalMenuFooterItems}
createPortalOnDropdown={createPortalOnDropdown}
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/dashboards/issueAssignee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function IssueAssignee({groupId}: IssueAssigneeProps) {
memberList={memberListState.members}
assigneeLoading={assigneeLoading}
handleAssigneeChange={handleAssigneeChange}
createPortalOnDropdown
/>
);
}
11 changes: 10 additions & 1 deletion static/app/utils/useOverlay.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Loading