Merge pull request #33542 from overleaf/ii-dropdown-flicker

Fix dropdown menu flicker on open

GitOrigin-RevId: ddf826b30fcac3322d86067e5950731e7dc1a2d4
This commit is contained in:
ilkin-overleaf
2026-05-22 08:05:59 +00:00
committed by Copybot
parent 803ba735ca
commit 192fd7c28c
2 changed files with 63 additions and 6 deletions
@@ -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 <BS5Dropdown {...props} />
// 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<DropdownInternalContextValue | null>(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 (
<DropdownInternalContext.Provider value={value}>
<BS5Dropdown {...props}>{children}</BS5Dropdown>
</DropdownInternalContext.Provider>
)
}
function DropdownItem(
@@ -124,14 +158,32 @@ DropdownToggleCustom.displayName = 'DropdownToggleCustom'
export const DropdownToggle = forwardRef<
typeof BS5DropdownToggle,
DropdownToggleProps
>((props, ref) => <BS5DropdownToggle {...props} ref={ref} />)
>((props, ref) => {
const registerToggle = useContext(DropdownInternalContext)?.registerToggle
useLayoutEffect(() => registerToggle?.(), [registerToggle])
return <BS5DropdownToggle {...props} ref={ref} />
})
DropdownToggle.displayName = 'DropdownToggle'
export const DropdownMenu = forwardRef<
typeof BS5DropdownMenu,
DropdownMenuProps
>(({ as = 'ul', ...props }, ref) => {
return <BS5DropdownMenu as={as} role="menu" {...props} ref={ref} />
>(({ as = 'ul', className, ...props }, ref) => {
const context = useContext(DropdownInternalContext)
return (
<BS5DropdownMenu
as={as}
role="menu"
className={classnames(className, {
'dropdown-menu-popper': !!context?.hasToggle,
})}
{...props}
ref={ref}
/>
)
})
DropdownMenu.displayName = 'DropdownMenu'
@@ -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 {