1- import { memo , useState , useRef , useEffect , useId } from "react" ;
1+ import { memo , useState , useRef , useEffect , useLayoutEffect , useId } from "react" ;
2+ import { createPortal } from "react-dom" ;
23import { CANVAS_DIMENSIONS } from "@hyperframes/parsers" ;
34import { RenderQueueItem } from "./RenderQueueItem" ;
45import { Button } from "../ui/Button" ;
6+ import { resolveFloatingPanelPosition , type FloatingPosition } from "../editor/floatingPanel" ;
57import type { RenderJob , ResolutionPreset } from "./useRenderQueue" ;
68import { getPersistedRenderSettings , persistRenderSettings } from "./renderSettings" ;
79import { trackStudioEvent } from "../../utils/studioTelemetry" ;
@@ -132,12 +134,20 @@ const FORMAT_INFO: Record<"mp4" | "webm" | "mov", { label: string; desc: string
132134 } ,
133135} ;
134136
137+ // Estimated, like COLOR_PICKER_SIZE in propertyPanelColor: only the flip
138+ // decision uses the height, and the clamp keeps the panel on screen either way.
139+ const FORMAT_PANEL_SIZE = { width : 208 , height : 150 } ;
140+
135141// Rich format guidance in a keyboard-reachable disclosure: the trigger is a
136142// real button (focusable, labelled), the panel is tied to it via
137143// aria-describedby, and Escape dismisses (WCAG 1.4.13). Content is too rich
138144// for the one-line ui/Tooltip primitive, so this stays a local popover.
145+ // It renders in a portal because the right panel is overflow-hidden: an
146+ // in-flow absolute panel gets clipped at the panel edge.
139147function FormatInfoTooltip ( { format } : { format : "mp4" | "webm" | "mov" } ) {
140148 const [ open , setOpen ] = useState ( false ) ;
149+ const [ position , setPosition ] = useState < FloatingPosition | null > ( null ) ;
150+ const triggerRef = useRef < HTMLDivElement > ( null ) ;
141151 const timeoutRef = useRef < ReturnType < typeof setTimeout > > ( undefined ) ;
142152 const panelId = useId ( ) ;
143153
@@ -151,6 +161,22 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
151161
152162 useEffect ( ( ) => ( ) => clearTimeout ( timeoutRef . current ) , [ ] ) ;
153163
164+ // Positioned once on open, so it does not follow panel scroll. The popover
165+ // is hover-lived; add a scroll listener only if that ever shows up.
166+ useLayoutEffect ( ( ) => {
167+ if ( ! open ) return ;
168+ const el = triggerRef . current ;
169+ if ( ! el ) return ;
170+ setPosition (
171+ resolveFloatingPanelPosition (
172+ el . getBoundingClientRect ( ) ,
173+ { width : window . innerWidth , height : window . innerHeight } ,
174+ FORMAT_PANEL_SIZE ,
175+ { offset : 6 } ,
176+ ) ,
177+ ) ;
178+ } , [ open ] ) ;
179+
154180 useEffect ( ( ) => {
155181 if ( ! open ) return ;
156182 const onKeyDown = ( e : KeyboardEvent ) => {
@@ -163,7 +189,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
163189 const info = FORMAT_INFO [ format ] ;
164190
165191 return (
166- < div className = "relative" onPointerEnter = { show } onPointerLeave = { hide } >
192+ < div ref = { triggerRef } className = "relative" onPointerEnter = { show } onPointerLeave = { hide } >
167193 < button
168194 type = "button"
169195 aria-label = "About video formats"
@@ -190,27 +216,32 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
190216 < line x1 = "12" y1 = "17" x2 = "12.01" y2 = "17" />
191217 </ svg >
192218 </ button >
193- { open && (
194- < div
195- id = { panelId }
196- role = "tooltip"
197- className = "absolute top-full right-0 mt-1.5 w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-50"
198- >
199- < p className = "text-[10px] font-semibold text-panel-text-1 mb-0.5" > { info . label } </ p >
200- < p className = "text-[9px] text-panel-text-3 leading-tight" > { info . desc } </ p >
201- < div className = "mt-1.5 pt-1.5 border-t border-neutral-800" >
202- { ( [ "mp4" , "mov" , "webm" ] as const )
203- . filter ( ( f ) => f !== format )
204- . map ( ( f ) => (
205- < p key = { f } className = "text-[9px] text-panel-text-4 leading-relaxed" >
206- < span className = "text-panel-text-3 font-medium" > { FORMAT_INFO [ f ] . label } </ span >
207- { " — " }
208- { FORMAT_INFO [ f ] . desc }
209- </ p >
210- ) ) }
211- </ div >
212- </ div >
213- ) }
219+ { open &&
220+ createPortal (
221+ < div
222+ id = { panelId }
223+ role = "tooltip"
224+ onPointerEnter = { show }
225+ onPointerLeave = { hide }
226+ className = "fixed w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-[200]"
227+ style = { { left : position ?. left ?? - 9999 , top : position ?. top ?? - 9999 } }
228+ >
229+ < p className = "text-[10px] font-semibold text-panel-text-1 mb-0.5" > { info . label } </ p >
230+ < p className = "text-[9px] text-panel-text-3 leading-tight" > { info . desc } </ p >
231+ < div className = "mt-1.5 pt-1.5 border-t border-neutral-800" >
232+ { ( [ "mp4" , "mov" , "webm" ] as const )
233+ . filter ( ( f ) => f !== format )
234+ . map ( ( f ) => (
235+ < p key = { f } className = "text-[9px] text-panel-text-4 leading-relaxed" >
236+ < span className = "text-panel-text-3 font-medium" > { FORMAT_INFO [ f ] . label } </ span >
237+ { " — " }
238+ { FORMAT_INFO [ f ] . desc }
239+ </ p >
240+ ) ) }
241+ </ div >
242+ </ div > ,
243+ document . body ,
244+ ) }
214245 </ div >
215246 ) ;
216247}
0 commit comments