diff --git a/studio/frontend/src/components/ui/dropdown-menu.tsx b/studio/frontend/src/components/ui/dropdown-menu.tsx index fe0270a63e..ea6dbbb6e7 100644 --- a/studio/frontend/src/components/ui/dropdown-menu.tsx +++ b/studio/frontend/src/components/ui/dropdown-menu.tsx @@ -2,10 +2,11 @@ // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; -import type * as React from "react"; +import * as React from "react"; import { Tick02Icon } from "@/lib/tick-icon"; import { ChevronRightStandardIcon } from "@/lib/chevron-icons"; +import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -206,6 +207,14 @@ function DropdownMenuShortcut({ ); } +function assignRef(ref: React.Ref | undefined, value: T | null) { + if (typeof ref === "function") { + ref(value); + } else if (ref) { + ref.current = value; + } +} + function DropdownMenuSub({ ...props }: React.ComponentProps) { @@ -242,17 +251,63 @@ function DropdownMenuSubTrigger({ function DropdownMenuSubContent({ className, + sideOffset, + style, + ref, ...props }: React.ComponentProps) { + const isMobile = useIsMobile(); + const [contentWidth, setContentWidth] = React.useState(0); + const resizeObserverRef = React.useRef(null); + const composedRef = React.useCallback( + ( + element: React.ComponentRef< + typeof DropdownMenuPrimitive.SubContent + > | null, + ) => { + resizeObserverRef.current?.disconnect(); + resizeObserverRef.current = null; + assignRef(ref, element); + if (!element) return; + + const updateContentWidth = () => { + setContentWidth(element.offsetWidth); + }; + updateContentWidth(); + + if (typeof ResizeObserver !== "undefined") { + resizeObserverRef.current = new ResizeObserver(updateContentWidth); + resizeObserverRef.current.observe(element); + } + }, + [ref], + ); + + React.useEffect( + () => () => { + resizeObserverRef.current?.disconnect(); + }, + [], + ); + + const compactSideOffset = + isMobile && contentWidth > 0 ? -contentWidth : sideOffset; return ( // Portaled like DropdownMenuContent: rendered inline, the fixed popper // wrapper is a descendant of the parent menu's scroll container, so any // transform there turns on overflow clipping and hides the submenu. 0 ? -contentWidth : sideOffset" in source + assert "sideOffset={compactSideOffset}" in source + assert 'isMobile && contentWidth === 0 ? "hidden"' in source + assert "-248" not in source + + +def test_shared_submenu_never_exceeds_the_compact_viewport(): + source = DROPDOWN_MENU.read_text(encoding = "utf-8") + assert "max-w-[calc(100vw-2rem)]" in source + + +def test_consumers_do_not_duplicate_compact_offset_logic(): + for path in FRONTEND_SRC.rglob("*.tsx"): + if path == DROPDOWN_MENU: + continue + source = path.read_text(encoding = "utf-8") + assert "compactSubmenuOffset" not in source, path + + +def test_all_submenu_consumers_use_the_shared_primitive(): + for path in FRONTEND_SRC.rglob("*.tsx"): + if path == DROPDOWN_MENU: + continue + source = path.read_text(encoding = "utf-8") + assert "DropdownMenuPrimitive.SubContent" not in source, path