11"use client"
22
3- import { memo , useState , useRef , useEffect , useCallback , useMemo } from "react"
3+ import { memo , useState , useRef , useEffect , useLayoutEffect , useCallback , useMemo } from "react"
4+ import { createPortal } from "react-dom"
45import {
56 CONTENT_TYPE_OPTIONS ,
67 CONTENT_TYPE_ICONS ,
@@ -53,6 +54,7 @@ const ContentTypePicker = memo(function ContentTypePicker({
5354 const triggeredRef = useRef ( false )
5455 const itemRefs = useRef < Map < number , HTMLButtonElement > > ( new Map ( ) )
5556 const submenuTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
57+ const submenuAnchorRef = useRef < HTMLDivElement | null > ( null )
5658
5759 const hasSelection = selected . length > 0
5860
@@ -377,6 +379,7 @@ const ContentTypePicker = memo(function ContentTypePicker({
377379 return (
378380 < div
379381 key = { t . id }
382+ ref = { activeSubmenu === t . id ? submenuAnchorRef : undefined }
380383 style = { { position : "relative" } }
381384 onMouseEnter = { ( ) => {
382385 setHighlightIdx ( idx )
@@ -469,9 +472,10 @@ const ContentTypePicker = memo(function ContentTypePicker({
469472 ) }
470473 </ button >
471474
472- { /* Sub-menu for variants */ }
475+ { /* Sub-menu for variants (rendered in portal so it's not clipped by overflow) */ }
473476 { hasVariants && activeSubmenu === t . id && (
474477 < VariantSubmenu
478+ anchorRef = { submenuAnchorRef }
475479 parent = { t }
476480 variants = { getFilteredVariants ( t ) }
477481 selected = { parentSelected }
@@ -503,9 +507,12 @@ const ContentTypePicker = memo(function ContentTypePicker({
503507 )
504508} )
505509
506- /* ── Variant Sub-menu ── */
510+ /* ── Variant Sub-menu (portal so it isn't clipped by list overflow) ── */
511+
512+ const SUBMENU_OFFSET = 4
507513
508514const VariantSubmenu = memo ( function VariantSubmenu ( {
515+ anchorRef,
509516 parent,
510517 variants,
511518 selected,
@@ -520,6 +527,7 @@ const VariantSubmenu = memo(function VariantSubmenu({
520527 popupBorder,
521528 itemHoverBg,
522529} : {
530+ anchorRef : React . RefObject < HTMLDivElement | null >
523531 parent : ContentTypeOption
524532 variants : { id : string ; label : string } [ ]
525533 selected : string [ ]
@@ -535,14 +543,26 @@ const VariantSubmenu = memo(function VariantSubmenu({
535543 popupBorder : string
536544 itemHoverBg : string
537545} ) {
538- return (
546+ const [ position , setPosition ] = useState ( { top : 0 , left : 0 } )
547+
548+ useLayoutEffect ( ( ) => {
549+ const anchor = anchorRef . current
550+ if ( ! anchor ) return
551+ const rect = anchor . getBoundingClientRect ( )
552+ setPosition ( {
553+ top : rect . top ,
554+ left : rect . right + SUBMENU_OFFSET ,
555+ } )
556+ } , [ anchorRef ] )
557+
558+ const submenuEl = (
539559 < div
540560 onMouseEnter = { onMouseEnter }
541561 onMouseLeave = { onMouseLeave }
542562 style = { {
543- position : "absolute " ,
544- left : "calc(100% + 4px)" ,
545- top : 0 ,
563+ position : "fixed " ,
564+ top : position . top ,
565+ left : position . left ,
546566 background : popupBg ,
547567 border : `1px solid ${ popupBorder } ` ,
548568 borderRadius : "12px" ,
@@ -554,7 +574,7 @@ const VariantSubmenu = memo(function VariantSubmenu({
554574 boxShadow : isDark
555575 ? "0 8px 30px rgba(0,0,0,0.5)"
556576 : "0 6px 24px rgba(0,0,0,0.12)" ,
557- zIndex : 101 ,
577+ zIndex : 10002 ,
558578 } }
559579 >
560580 { /* Sub-menu header */ }
@@ -620,6 +640,9 @@ const VariantSubmenu = memo(function VariantSubmenu({
620640 } ) }
621641 </ div >
622642 )
643+
644+ if ( typeof document === "undefined" ) return null
645+ return createPortal ( submenuEl , document . body )
623646} )
624647
625648export default ContentTypePicker
0 commit comments