11/**
22 * MobileSelectionPopover — floating menu when text is selected in the reader.
33 * Supports: highlight (6 colors + underline), note, copy, translate, ask AI, TTS, edit/delete.
4+ *
5+ * Uses position: absolute within the reader container (like Readest) so the
6+ * popover shares the same stacking context as the iframe content and reliably
7+ * renders above the selection highlight.
48 */
5- import { useCallback , useMemo , useState } from "react" ;
9+ import { useCallback , useState , type RefObject } from "react" ;
610import { useTranslation } from "react-i18next" ;
711import {
812 Copy ,
@@ -27,6 +31,7 @@ export interface BookSelection {
2731
2832interface MobileSelectionPopoverProps {
2933 selection : BookSelection ;
34+ containerRef : RefObject < HTMLDivElement | null > ;
3035 isPdf ?: boolean ;
3136 onHighlight : ( color : string ) => void ;
3237 onNote : ( ) => void ;
@@ -49,6 +54,7 @@ const HIGHLIGHT_COLORS = [
4954
5055export function MobileSelectionPopover ( {
5156 selection,
57+ containerRef,
5258 isPdf,
5359 onHighlight,
5460 onNote,
@@ -71,72 +77,66 @@ export function MobileSelectionPopover({
7177 }
7278 } , [ isPdf , showColors ] ) ;
7379
74- // Position: smart above/below placement like desktop
80+ // Popover dimensions
7581 const popoverWidth = 220 ;
7682 const popoverH = 44 ; // single row height
7783 const colorRowH = 40 ; // color picker row height
7884 const gap = 8 ;
7985 const padding = 8 ;
8086 const totalH = showColors ? popoverH + colorRowH + 6 : popoverH ; // 6 for mb-1.5
8187
82- // Read safe-area-inset-top for top boundary clamping
83- const safeAreaTop = useMemo ( ( ) => {
84- const el = document . documentElement ;
85- const val = getComputedStyle ( el ) . getPropertyValue ( "--safe-area-top" ) ?. trim ( ) ;
86- if ( val ) return parseInt ( val , 10 ) || 0 ;
87- // Fallback: read env(safe-area-inset-top) via a temp element
88- const tmp = document . createElement ( "div" ) ;
89- tmp . style . position = "fixed" ;
90- tmp . style . top = "env(safe-area-inset-top, 0px)" ;
91- tmp . style . visibility = "hidden" ;
92- document . body . appendChild ( tmp ) ;
93- const top = tmp . getBoundingClientRect ( ) . top ;
94- document . body . removeChild ( tmp ) ;
95- return top ;
96- } , [ ] ) ;
88+ // Get container rect for coordinate conversion (viewport → container-relative)
89+ const containerRect = containerRef . current ?. getBoundingClientRect ( ) ;
90+ const containerTop = containerRect ?. top ?? 0 ;
91+ const containerLeft = containerRect ?. left ?? 0 ;
92+ const containerW = containerRect ?. width ?? window . innerWidth ;
93+ const containerH = containerRect ?. height ?? window . innerHeight ;
9794
98- const topPadding = padding + safeAreaTop ;
99- const { selectionTop, selectionBottom, direction } = selection . position ;
95+ // Convert viewport coordinates to container-relative coordinates
96+ const { selectionTop : vpSelTop , selectionBottom : vpSelBot , direction } = selection . position ;
97+ const selTop = vpSelTop - containerTop ;
98+ const selBot = vpSelBot - containerTop ;
99+ const selCenterX = selection . position . x - containerLeft ;
100100
101- // X: centered on selection, clamped to viewport
101+ // X: centered on selection, clamped within container
102102 const x = Math . max ( padding , Math . min (
103- selection . position . x - popoverWidth / 2 ,
104- window . innerWidth - popoverWidth - padding ,
103+ selCenterX - popoverWidth / 2 ,
104+ containerW - popoverWidth - padding ,
105105 ) ) ;
106106
107- // Y positioning: direction-aware with viewport clamp
108- const yAbove = selectionTop - totalH - gap ;
109- const yBelow = selectionBottom + gap ;
110- const aboveValid = yAbove >= topPadding ;
111- const belowValid = yBelow + totalH + padding <= window . innerHeight ;
107+ // Y positioning: direction-aware, coordinates relative to container
108+ const yAbove = selTop - totalH - gap ;
109+ const yBelow = selBot + gap ;
110+ const aboveValid = yAbove >= padding ;
111+ const belowValid = yBelow + totalH + padding <= containerH ;
112112
113113 let y : number ;
114114 if ( direction === "backward" ) {
115115 // User selected upward — prefer placing above the selection top
116116 if ( aboveValid ) {
117117 y = yAbove ;
118118 } else {
119- // Can't fit above — place at the top of the visible area
120- y = topPadding ;
119+ // Can't fit above — place at the top of the container
120+ y = padding ;
121121 }
122122 } else {
123- // User selected downward (or unknown ) — prefer placing below the selection bottom
123+ // User selected downward (or default ) — prefer placing below the selection bottom
124124 if ( belowValid ) {
125125 y = yBelow ;
126126 } else {
127- // Can't fit below — place at the bottom of the visible area
128- y = window . innerHeight - totalH - padding ;
127+ // Can't fit below — place at the bottom of the container
128+ y = containerH - totalH - padding ;
129129 }
130130 }
131131
132- // Final clamp: always keep fully visible within the safe viewport
133- y = Math . max ( topPadding , Math . min ( y , window . innerHeight - totalH - padding ) ) ;
132+ // Final clamp: always keep fully visible within the container
133+ y = Math . max ( padding , Math . min ( y , containerH - totalH - padding ) ) ;
134134
135135 const style : React . CSSProperties = {
136- position : "fixed " ,
136+ position : "absolute " ,
137137 left : x ,
138138 top : y ,
139- zIndex : 9999 ,
139+ zIndex : 50 ,
140140 } ;
141141
142142 return (
0 commit comments