diff --git a/services/web/frontend/js/shared/components/dropdown/dropdown-menu.tsx b/services/web/frontend/js/shared/components/dropdown/dropdown-menu.tsx index 291a2ac07d..c978085ff0 100644 --- a/services/web/frontend/js/shared/components/dropdown/dropdown-menu.tsx +++ b/services/web/frontend/js/shared/components/dropdown/dropdown-menu.tsx @@ -1,4 +1,12 @@ -import React, { forwardRef } from 'react' +import React, { + createContext, + forwardRef, + useCallback, + useContext, + useLayoutEffect, + useMemo, + useState, +} from 'react' import { Dropdown as BS5Dropdown, DropdownToggle as BS5DropdownToggle, @@ -20,8 +28,34 @@ import MaterialIcon from '@/shared/components/material-icon' import { fixedForwardRef } from '@/utils/react' import classnames from 'classnames' -export function Dropdown({ ...props }: DropdownProps) { - return +// Lets DropdownToggle register its presence with the parent Dropdown so that +// DropdownMenu can know whether a toggle exists (and therefore whether Popper +// will run to position the menu). +type DropdownInternalContextValue = { + registerToggle: () => () => void + hasToggle: boolean +} +const DropdownInternalContext = + createContext(null) + +export function Dropdown({ children, ...props }: DropdownProps) { + const [toggleCount, setToggleCount] = useState(0) + + const registerToggle = useCallback(() => { + setToggleCount(c => c + 1) + return () => setToggleCount(c => c - 1) + }, []) + + const value = useMemo( + () => ({ registerToggle, hasToggle: toggleCount > 0 }), + [registerToggle, toggleCount] + ) + + return ( + + {children} + + ) } function DropdownItem( @@ -124,14 +158,32 @@ DropdownToggleCustom.displayName = 'DropdownToggleCustom' export const DropdownToggle = forwardRef< typeof BS5DropdownToggle, DropdownToggleProps ->((props, ref) => ) +>((props, ref) => { + const registerToggle = useContext(DropdownInternalContext)?.registerToggle + + useLayoutEffect(() => registerToggle?.(), [registerToggle]) + + return +}) DropdownToggle.displayName = 'DropdownToggle' export const DropdownMenu = forwardRef< typeof BS5DropdownMenu, DropdownMenuProps ->(({ as = 'ul', ...props }, ref) => { - return +>(({ as = 'ul', className, ...props }, ref) => { + const context = useContext(DropdownInternalContext) + + return ( + + ) }) DropdownMenu.displayName = 'DropdownMenu' diff --git a/services/web/frontend/stylesheets/components/dropdown-menu.scss b/services/web/frontend/stylesheets/components/dropdown-menu.scss index aa2161375a..c5ba8d387e 100644 --- a/services/web/frontend/stylesheets/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/components/dropdown-menu.scss @@ -103,6 +103,11 @@ $dropdown-item-min-height: 36px; .subdued { color: var(--content-disabled); } + + &.show.dropdown-menu-popper:not([data-popper-placement], [data-bs-popper]) { + // Prevents flickering before Popper positions the menu + visibility: hidden; + } } .dropdown-item {