@@ -33,13 +33,22 @@ export const DialogOverlayMixin = (superClass) =>
3333 footerRenderer : {
3434 type : Object ,
3535 } ,
36+
37+ /**
38+ * Whether to keep the overlay within the viewport.
39+ */
40+ keepInViewport : {
41+ type : Boolean ,
42+ reflectToAttribute : true ,
43+ } ,
3644 } ;
3745 }
3846
3947 static get observers ( ) {
4048 return [
4149 '_headerFooterRendererChange(headerRenderer, footerRenderer, opened)' ,
4250 '_headerTitleChanged(headerTitle, opened)' ,
51+ '__keepInViewportChanged(keepInViewport, opened)' ,
4352 ] ;
4453 }
4554
@@ -50,13 +59,16 @@ export const DialogOverlayMixin = (superClass) =>
5059 // Update overflow attribute on resize
5160 this . __resizeObserver = new ResizeObserver ( ( ) => {
5261 this . __updateOverflow ( ) ;
62+ this . __adjustPosition ( ) ;
5363 } ) ;
5464 this . __resizeObserver . observe ( this . $ . resizerContainer ) ;
5565
5666 // Update overflow attribute on scroll
5767 this . $ . content . addEventListener ( 'scroll' , ( ) => {
5868 this . __updateOverflow ( ) ;
5969 } ) ;
70+
71+ this . __handleWindowResize = this . __handleWindowResize . bind ( this ) ;
6072 }
6173
6274 /** @private */
@@ -214,6 +226,8 @@ export const DialogOverlayMixin = (superClass) =>
214226 } ) ;
215227
216228 Object . assign ( overlay . style , parsedBounds ) ;
229+
230+ this . __adjustPosition ( ) ;
217231 }
218232
219233 /**
@@ -255,4 +269,54 @@ export const DialogOverlayMixin = (superClass) =>
255269 this . removeAttribute ( 'overflow' ) ;
256270 }
257271 }
272+
273+ /** @private */
274+ __keepInViewportChanged ( keepInViewport , opened ) {
275+ if ( opened && keepInViewport ) {
276+ window . addEventListener ( 'resize' , this . __handleWindowResize ) ;
277+ } else {
278+ window . removeEventListener ( 'resize' , this . __handleWindowResize ) ;
279+ }
280+ }
281+
282+ /** @private */
283+ __handleWindowResize ( ) {
284+ this . __adjustPosition ( ) ;
285+ }
286+
287+ /**
288+ * Adjusts the position of the overlay to keep it within the viewport if `keepInViewport` is true.
289+ * @private
290+ */
291+ __adjustPosition ( ) {
292+ if ( ! this . opened || ! this . keepInViewport ) {
293+ return ;
294+ }
295+
296+ // Centered dialogs do not use absolute positioning and automatically adjust their position / size to fit the viewport
297+ const style = getComputedStyle ( this . $ . overlay ) ;
298+ if ( style . position !== 'absolute' ) {
299+ return ;
300+ }
301+
302+ const overlayHostBounds = this . getBoundingClientRect ( ) ;
303+ const bounds = this . getBounds ( ) ;
304+ // Prefer dimensions from getComputedStyle, as bounding rect is affected
305+ // by scale transform applied by opening animation in Lumo
306+ const width = parseFloat ( style . width ) || bounds . width ;
307+ const height = parseFloat ( style . height ) || bounds . height ;
308+
309+ const maxLeft = overlayHostBounds . right - overlayHostBounds . left - width ;
310+ const maxTop = overlayHostBounds . bottom - overlayHostBounds . top - height ;
311+
312+ if ( bounds . left > maxLeft || bounds . top > maxTop ) {
313+ const left = Math . max ( 0 , Math . min ( bounds . left , maxLeft ) ) ;
314+ const top = Math . max ( 0 , Math . min ( bounds . top , maxTop ) ) ;
315+
316+ Object . assign ( this . $ . overlay . style , {
317+ left : `${ left } px` ,
318+ top : `${ top } px` ,
319+ } ) ;
320+ }
321+ }
258322 } ;
0 commit comments