Bladeren bron

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

Fix dropdown menu flicker on open

GitOrigin-RevId: ddf826b30fcac3322d86067e5950731e7dc1a2d4
ilkin-overleaf 2 maanden geleden
bovenliggende
commit
192fd7c28c

+ 58 - 6
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 {
 import {
   Dropdown as BS5Dropdown,
   Dropdown as BS5Dropdown,
   DropdownToggle as BS5DropdownToggle,
   DropdownToggle as BS5DropdownToggle,
@@ -20,8 +28,34 @@ import MaterialIcon from '@/shared/components/material-icon'
 import { fixedForwardRef } from '@/utils/react'
 import { fixedForwardRef } from '@/utils/react'
 import classnames from 'classnames'
 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(
 function DropdownItem(
@@ -124,14 +158,32 @@ DropdownToggleCustom.displayName = 'DropdownToggleCustom'
 export const DropdownToggle = forwardRef<
 export const DropdownToggle = forwardRef<
   typeof BS5DropdownToggle,
   typeof BS5DropdownToggle,
   DropdownToggleProps
   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'
 DropdownToggle.displayName = 'DropdownToggle'
 
 
 export const DropdownMenu = forwardRef<
 export const DropdownMenu = forwardRef<
   typeof BS5DropdownMenu,
   typeof BS5DropdownMenu,
   DropdownMenuProps
   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'
 DropdownMenu.displayName = 'DropdownMenu'
 
 

+ 5 - 0
services/web/frontend/stylesheets/components/dropdown-menu.scss

@@ -103,6 +103,11 @@ $dropdown-item-min-height: 36px;
   .subdued {
   .subdued {
     color: var(--content-disabled);
     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 {
 .dropdown-item {