@@ -11,12 +11,15 @@ type Params = {
1111 settings ?: MapLegendControlSettings ;
1212 onChangeSettings : ( settings : Partial < MapLegendControlSettings > ) => void ;
1313 theme : Record < string , any > ;
14+ mapHeight ?: number ;
15+ mapWidth ?: number ;
1416} ;
1517
1618type ReturnType = {
1719 positionStyles : Record < string , unknown > ;
1820 updatePosition : ( ) => void ;
1921 contentHeight : number ;
22+ maxContentHeight ?: number ;
2023 startResize : ( ) => void ;
2124 resize : ( deltaY : number ) => void ;
2225} ;
@@ -87,14 +90,23 @@ export default function useLegendPosition({
8790 isSidePanelShown,
8891 settings,
8992 onChangeSettings,
90- theme
93+ theme,
94+ mapHeight,
95+ mapWidth
9196} : Params ) : ReturnType {
9297 const pos = settings ?. position ?? DEFAULT_POSITION ;
9398 const contentHeight = settings ?. contentHeight ?? - 1 ;
9499 const positionStyles = useMemo ( ( ) => ( { [ pos . anchorX ] : pos . x , [ pos . anchorY ] : pos . y } ) , [ pos ] ) ;
95100 const startHeightRef = useRef ( 0 ) ;
96101 const sidePanelWidth = theme . sidePanel ?. width || 0 ;
97102
103+ // Calculate dynamic max content height based on map root dimensions
104+ const maxContentHeight = useMemo ( ( ) => {
105+ if ( ! mapHeight ) return undefined ;
106+ // Available height minus margins and header
107+ return mapHeight - MARGIN . top - MARGIN . bottom - MAP_CONTROL_HEADER_FULL_HEIGHT ;
108+ } , [ mapHeight ] ) ;
109+
98110 const calcPosition = useCalcLegendPosition ( {
99111 legendContentRef,
100112 isSidePanelShown,
@@ -119,8 +131,17 @@ export default function useLegendPosition({
119131 if ( root instanceof HTMLElement && legendContent ) {
120132 const mapRootBounds = root . getBoundingClientRect ( ) ;
121133 const legendRect = legendContent . getBoundingClientRect ( ) ;
134+ // Use maxContentHeight if available, otherwise fall back to viewport-based calculation
135+ const maxHeight = maxContentHeight
136+ ? Math . min (
137+ maxContentHeight ,
138+ mapRootBounds . bottom -
139+ ( legendRect . top + MAP_CONTROL_HEADER_FULL_HEIGHT + MARGIN . bottom )
140+ )
141+ : mapRootBounds . bottom -
142+ ( legendRect . top + MAP_CONTROL_HEADER_FULL_HEIGHT + MARGIN . bottom ) ;
122143 const nextHeight = Math . min (
123- mapRootBounds . bottom - ( legendRect . top + MAP_CONTROL_HEADER_FULL_HEIGHT + MARGIN . bottom ) ,
144+ maxHeight ,
124145 Math . max ( MIN_CONTENT_HEIGHT , startHeightRef . current + deltaY )
125146 ) ;
126147 onChangeSettings ( { contentHeight : nextHeight } ) ;
@@ -130,7 +151,7 @@ export default function useLegendPosition({
130151 }
131152 } ,
132153 // eslint-disable-next-line react-hooks/exhaustive-deps
133- [ contentHeight , pos , onChangeSettings ]
154+ [ contentHeight , pos , onChangeSettings , maxContentHeight ]
134155 ) ;
135156
136157 // Shift when side panel is shown/hidden
@@ -151,5 +172,61 @@ export default function useLegendPosition({
151172 }
152173 } , [ isSidePanelShown , onChangeSettings , sidePanelWidth ] ) ;
153174
154- return { positionStyles, updatePosition, contentHeight, startResize, resize} ;
175+ // Clamp position when map resizes to ensure legend stays within viewport
176+ useEffect ( ( ) => {
177+ if ( ! mapWidth || ! mapHeight || ! legendContentRef . current ) return ;
178+
179+ const legendContent = legendContentRef . current ;
180+ const legendRect = legendContent . getBoundingClientRect ( ) ;
181+ const currentPos = posRef . current ;
182+ const leftSidebarOffset = isSidePanelShown ? sidePanelWidth : 0 ;
183+
184+ let needsUpdate = false ;
185+ const newPos = { ...currentPos } ;
186+
187+ // Clamp horizontal position
188+ if ( currentPos . anchorX === 'left' ) {
189+ const maxX = mapWidth - legendRect . width - MARGIN . right ;
190+ const minX = leftSidebarOffset + MARGIN . left ;
191+ if ( currentPos . x < minX ) {
192+ newPos . x = minX ;
193+ needsUpdate = true ;
194+ } else if ( currentPos . x > maxX ) {
195+ newPos . x = maxX ;
196+ needsUpdate = true ;
197+ }
198+ } else {
199+ // anchorX === 'right'
200+ const maxX = mapWidth - MARGIN . right ;
201+ const minX = legendRect . width + MARGIN . left ;
202+ if ( currentPos . x < minX ) {
203+ newPos . x = minX ;
204+ needsUpdate = true ;
205+ } else if ( currentPos . x > maxX ) {
206+ newPos . x = maxX ;
207+ needsUpdate = true ;
208+ }
209+ }
210+
211+ // Clamp contentHeight if it exceeds available space
212+ if ( maxContentHeight && contentHeight > 0 && contentHeight > maxContentHeight ) {
213+ onChangeSettings ( { contentHeight : maxContentHeight } ) ;
214+ needsUpdate = true ;
215+ }
216+
217+ if ( needsUpdate ) {
218+ onChangeSettings ( { position : newPos } ) ;
219+ }
220+ // eslint-disable-next-line react-hooks/exhaustive-deps
221+ } , [
222+ mapWidth ,
223+ mapHeight ,
224+ contentHeight ,
225+ isSidePanelShown ,
226+ sidePanelWidth ,
227+ onChangeSettings ,
228+ maxContentHeight
229+ ] ) ;
230+
231+ return { positionStyles, updatePosition, contentHeight, maxContentHeight, startResize, resize} ;
155232}
0 commit comments