@@ -35,6 +35,14 @@ export const DialogOverlayMixin = (superClass) =>
3535 footerRenderer : {
3636 type : Object ,
3737 } ,
38+
39+ /**
40+ * Whether to keep the overlay within the viewport.
41+ */
42+ keepInViewport : {
43+ type : Boolean ,
44+ reflectToAttribute : true ,
45+ } ,
3846 } ;
3947 }
4048
@@ -77,6 +85,7 @@ export const DialogOverlayMixin = (superClass) =>
7785 // Update overflow attribute on resize
7886 this . __resizeObserver = new ResizeObserver ( ( ) => {
7987 requestAnimationFrame ( ( ) => {
88+ this . __adjustPosition ( ) ;
8089 this . __updateOverflow ( ) ;
8190 } ) ;
8291 } ) ;
@@ -104,6 +113,20 @@ export const DialogOverlayMixin = (superClass) =>
104113 setOverlayStateAttribute ( this , 'has-footer' , currentNodes . length > 0 ) ;
105114 this . __updateOverflow ( ) ;
106115 } ) ;
116+
117+ this . __handleWindowResize = this . __handleWindowResize . bind ( this ) ;
118+ }
119+
120+ updated ( props ) {
121+ super . updated ( props ) ;
122+
123+ if ( props . has ( 'opened' ) || props . has ( 'keepInViewport' ) ) {
124+ if ( this . opened && this . keepInViewport ) {
125+ window . addEventListener ( 'resize' , this . __handleWindowResize ) ;
126+ } else {
127+ window . removeEventListener ( 'resize' , this . __handleWindowResize ) ;
128+ }
129+ }
107130 }
108131
109132 /** @private */
@@ -233,6 +256,15 @@ export const DialogOverlayMixin = (superClass) =>
233256 return { top, left, width, height } ;
234257 }
235258
259+ /**
260+ * Override method from OverlayMixin to adjust the position of the overlay if `keepInViewport` is true.
261+ * @override
262+ */
263+ setBounds ( bounds , absolute = true ) {
264+ super . setBounds ( bounds , absolute ) ;
265+ this . __adjustPosition ( ) ;
266+ }
267+
236268 /** @private */
237269 __updateOverflow ( ) {
238270 let overflow = '' ;
@@ -254,4 +286,45 @@ export const DialogOverlayMixin = (superClass) =>
254286 setOverlayStateAttribute ( this , 'overflow' , null ) ;
255287 }
256288 }
289+
290+ /** @private */
291+ __handleWindowResize ( ) {
292+ this . __adjustPosition ( ) ;
293+ }
294+
295+ /**
296+ * Adjusts the position of the overlay to keep it within the viewport if `keepInViewport` is true.
297+ * @private
298+ */
299+ __adjustPosition ( ) {
300+ if ( ! this . opened || ! this . keepInViewport ) {
301+ return ;
302+ }
303+
304+ // Centered dialogs do not use absolute positioning and automatically adjust their position / size to fit the viewport
305+ const style = getComputedStyle ( this . $ . overlay ) ;
306+ if ( style . position !== 'absolute' ) {
307+ return ;
308+ }
309+
310+ const overlayHostBounds = this . getBoundingClientRect ( ) ;
311+ const bounds = this . getBounds ( ) ;
312+ // Prefer dimensions from getComputedStyle, as bounding rect is affected
313+ // by scale transform applied by opening animation in Lumo
314+ const width = parseFloat ( style . width ) || bounds . width ;
315+ const height = parseFloat ( style . height ) || bounds . height ;
316+
317+ const maxLeft = overlayHostBounds . right - overlayHostBounds . left - width ;
318+ const maxTop = overlayHostBounds . bottom - overlayHostBounds . top - height ;
319+
320+ if ( bounds . left > maxLeft || bounds . top > maxTop ) {
321+ const left = Math . max ( 0 , Math . min ( bounds . left , maxLeft ) ) ;
322+ const top = Math . max ( 0 , Math . min ( bounds . top , maxTop ) ) ;
323+
324+ Object . assign ( this . $ . overlay . style , {
325+ left : `${ left } px` ,
326+ top : `${ top } px` ,
327+ } ) ;
328+ }
329+ }
257330 } ;
0 commit comments