Skip to content

Commit e4e08a8

Browse files
authored
feat: keepInViewport enhancements (#11199) (CP: 24.10) (#11202)
1 parent 31aecee commit e4e08a8

10 files changed

+315
-30
lines changed

packages/dialog/src/vaadin-dialog-base-mixin.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,14 @@ export declare class DialogBaseMixinClass {
7070
* If a unitless number is provided, pixels are assumed.
7171
*/
7272
height: string;
73+
74+
/**
75+
* Set to true to prevent the dialog from moving outside the viewport bounds.
76+
* When enabled, all four edges of the dialog will remain visible, for example
77+
* when dragging the dialog or when the viewport is resized. Note that the
78+
* dialog will also adjust any programmatically configured size and position
79+
* so that it stays within the viewport.
80+
* @attr {boolean} keep-in-viewport
81+
*/
82+
keepInViewport: boolean;
7383
}

packages/dialog/src/vaadin-dialog-base-mixin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ export const DialogBaseMixin = (superClass) =>
100100
type: String,
101101
value: 'dialog',
102102
},
103+
104+
/**
105+
* Set to true to prevent the dialog from moving outside the viewport bounds.
106+
* When enabled, all four edges of the dialog will remain visible, for example
107+
* when dragging the dialog or when the viewport is resized. Note that the
108+
* dialog will also adjust any programmatically configured size and position
109+
* so that it stays within the viewport.
110+
* @attr {boolean} keep-in-viewport
111+
* @type {boolean}
112+
*/
113+
keepInViewport: {
114+
type: Boolean,
115+
value: false,
116+
reflectToAttribute: true,
117+
},
103118
};
104119
}
105120

packages/dialog/src/vaadin-dialog-draggable-mixin.d.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,4 @@ export declare class DialogDraggableMixinClass {
2121
* "`draggable-leaf-only`" class name.
2222
*/
2323
draggable: boolean;
24-
25-
/**
26-
* Set to true to prevent dragging the dialog outside the viewport bounds.
27-
* When enabled, all four edges of the dialog will remain visible during dragging.
28-
* The dialog may still become partially hidden when the viewport is resized.
29-
* @attr {boolean} keep-in-viewport
30-
*/
31-
keepInViewport: boolean;
3224
}

packages/dialog/src/vaadin-dialog-draggable-mixin.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,6 @@ export const DialogDraggableMixin = (superClass) =>
3131
reflectToAttribute: true,
3232
},
3333

34-
/**
35-
* Set to true to prevent dragging the dialog outside the viewport bounds.
36-
* When enabled, all four edges of the dialog will remain visible during dragging.
37-
* The dialog may still become partially hidden when the viewport is resized.
38-
* @attr {boolean} keep-in-viewport
39-
* @type {boolean}
40-
*/
41-
keepInViewport: {
42-
type: Boolean,
43-
value: false,
44-
reflectToAttribute: true,
45-
},
46-
4734
/** @private */
4835
_touchDevice: {
4936
type: Boolean,

packages/dialog/src/vaadin-dialog-overlay-mixin.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export declare class DialogOverlayMixinClass {
3434
*/
3535
headerTitle: string;
3636

37+
/**
38+
* Whether to keep the overlay within the viewport.
39+
*/
40+
keepInViewport: boolean;
41+
3742
/**
3843
* Custom function for rendering the dialog header.
3944
*/

packages/dialog/src/vaadin-dialog-overlay-mixin.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};

packages/dialog/src/vaadin-dialog-styles.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const dialogOverlay = css`
7474
min-width: 12em; /* matches the default <vaadin-text-field> width */
7575
}
7676
77-
:host([has-bounds-set]) [part='overlay'] {
77+
:host([has-bounds-set]:not([keep-in-viewport])) [part='overlay'] {
7878
max-width: none;
7979
}
8080

packages/dialog/src/vaadin-dialog.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class Dialog extends DialogDraggableMixin(
115115
with-backdrop="[[!modeless]]"
116116
resizable$="[[resizable]]"
117117
draggable$="[[draggable]]"
118+
keep-in-viewport="[[keepInViewport]]"
118119
restore-focus-on-close
119120
focus-trap
120121
></vaadin-dialog-overlay>

0 commit comments

Comments
 (0)