@vaadin/overlay 24.0.0-alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +10 -0
- package/package.json +54 -0
- package/src/vaadin-overlay-position-mixin.js +356 -0
- package/src/vaadin-overlay.d.ts +208 -0
- package/src/vaadin-overlay.js +800 -0
- package/theme/lumo/vaadin-overlay-styles.js +4 -0
- package/theme/lumo/vaadin-overlay.js +2 -0
- package/theme/material/vaadin-overlay-styles.js +4 -0
- package/theme/material/vaadin-overlay.js +2 -0
- package/vaadin-overlay.d.ts +1 -0
- package/vaadin-overlay.js +2 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2017 - 2022 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
|
7
|
+
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
8
|
+
import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
|
|
9
|
+
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
|
|
10
|
+
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
|
|
11
|
+
import { FocusTrapController } from '@vaadin/component-base/src/focus-trap-controller.js';
|
|
12
|
+
import { processTemplates } from '@vaadin/component-base/src/templates.js';
|
|
13
|
+
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `<vaadin-overlay>` is a Web Component for creating overlays. The content of the overlay
|
|
17
|
+
* can be populated imperatively by using `renderer` callback function.
|
|
18
|
+
*
|
|
19
|
+
* ### Rendering
|
|
20
|
+
*
|
|
21
|
+
* The renderer function provides `root`, `owner`, `model` arguments when applicable.
|
|
22
|
+
* Generate DOM content by using `model` object properties if needed, append it to the `root`
|
|
23
|
+
* element and control the state of the host element by accessing `owner`. Before generating new
|
|
24
|
+
* content, users are able to check if there is already content in `root` for reusing it.
|
|
25
|
+
*
|
|
26
|
+
* ```html
|
|
27
|
+
* <vaadin-overlay id="overlay"></vaadin-overlay>
|
|
28
|
+
* ```
|
|
29
|
+
* ```js
|
|
30
|
+
* const overlay = document.querySelector('#overlay');
|
|
31
|
+
* overlay.renderer = function(root) {
|
|
32
|
+
* root.textContent = "Overlay content";
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Renderer is called on the opening of the overlay and each time the related model is updated.
|
|
37
|
+
* DOM generated during the renderer call can be reused
|
|
38
|
+
* in the next renderer call and will be provided with the `root` argument.
|
|
39
|
+
* On first call it will be empty.
|
|
40
|
+
*
|
|
41
|
+
* ### Styling
|
|
42
|
+
*
|
|
43
|
+
* The following Shadow DOM parts are available for styling:
|
|
44
|
+
*
|
|
45
|
+
* Part name | Description
|
|
46
|
+
* -----------|---------------------------------------------------------|
|
|
47
|
+
* `backdrop` | Backdrop of the overlay
|
|
48
|
+
* `overlay` | Container for position/sizing/alignment of the content
|
|
49
|
+
* `content` | Content of the overlay
|
|
50
|
+
*
|
|
51
|
+
* The following state attributes are available for styling:
|
|
52
|
+
*
|
|
53
|
+
* Attribute | Description | Part
|
|
54
|
+
* ---|---|---
|
|
55
|
+
* `opening` | Applied just after the overlay is attached to the DOM. You can apply a CSS @keyframe animation for this state. | `:host`
|
|
56
|
+
* `closing` | Applied just before the overlay is detached from the DOM. You can apply a CSS @keyframe animation for this state. | `:host`
|
|
57
|
+
*
|
|
58
|
+
* The following custom CSS properties are available for styling:
|
|
59
|
+
*
|
|
60
|
+
* Custom CSS property | Description | Default value
|
|
61
|
+
* ---|---|---
|
|
62
|
+
* `--vaadin-overlay-viewport-bottom` | Bottom offset of the visible viewport area | `0` or detected offset
|
|
63
|
+
*
|
|
64
|
+
* See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
|
|
65
|
+
*
|
|
66
|
+
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
|
|
67
|
+
* @fires {CustomEvent} vaadin-overlay-open - Fired after the overlay is opened.
|
|
68
|
+
* @fires {CustomEvent} vaadin-overlay-close - Fired before the overlay will be closed. If canceled the closing of the overlay is canceled as well.
|
|
69
|
+
* @fires {CustomEvent} vaadin-overlay-closing - Fired when the overlay will be closed.
|
|
70
|
+
* @fires {CustomEvent} vaadin-overlay-outside-click - Fired before the overlay will be closed on outside click. If canceled the closing of the overlay is canceled as well.
|
|
71
|
+
* @fires {CustomEvent} vaadin-overlay-escape-press - Fired before the overlay will be closed on ESC button press. If canceled the closing of the overlay is canceled as well.
|
|
72
|
+
*
|
|
73
|
+
* @extends HTMLElement
|
|
74
|
+
* @mixes ThemableMixin
|
|
75
|
+
* @mixes DirMixin
|
|
76
|
+
* @mixes ControllerMixin
|
|
77
|
+
*/
|
|
78
|
+
class Overlay extends ThemableMixin(DirMixin(ControllerMixin(PolymerElement))) {
|
|
79
|
+
static get template() {
|
|
80
|
+
return html`
|
|
81
|
+
<style>
|
|
82
|
+
:host {
|
|
83
|
+
z-index: 200;
|
|
84
|
+
position: fixed;
|
|
85
|
+
|
|
86
|
+
/* Despite of what the names say, <vaadin-overlay> is just a container
|
|
87
|
+
for position/sizing/alignment. The actual overlay is the overlay part. */
|
|
88
|
+
|
|
89
|
+
/* Default position constraints: the entire viewport. Note: themes can
|
|
90
|
+
override this to introduce gaps between the overlay and the viewport. */
|
|
91
|
+
top: 0;
|
|
92
|
+
right: 0;
|
|
93
|
+
bottom: var(--vaadin-overlay-viewport-bottom);
|
|
94
|
+
left: 0;
|
|
95
|
+
|
|
96
|
+
/* Use flexbox alignment for the overlay part. */
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column; /* makes dropdowns sizing easier */
|
|
99
|
+
/* Align to center by default. */
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
|
|
103
|
+
/* Allow centering when max-width/max-height applies. */
|
|
104
|
+
margin: auto;
|
|
105
|
+
|
|
106
|
+
/* The host is not clickable, only the overlay part is. */
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
|
|
109
|
+
/* Remove tap highlight on touch devices. */
|
|
110
|
+
-webkit-tap-highlight-color: transparent;
|
|
111
|
+
|
|
112
|
+
/* CSS API for host */
|
|
113
|
+
--vaadin-overlay-viewport-bottom: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
:host([hidden]),
|
|
117
|
+
:host(:not([opened]):not([closing])) {
|
|
118
|
+
display: none !important;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
[part='overlay'] {
|
|
122
|
+
-webkit-overflow-scrolling: touch;
|
|
123
|
+
overflow: auto;
|
|
124
|
+
pointer-events: auto;
|
|
125
|
+
|
|
126
|
+
/* Prevent overflowing the host in MSIE 11 */
|
|
127
|
+
max-width: 100%;
|
|
128
|
+
box-sizing: border-box;
|
|
129
|
+
|
|
130
|
+
-webkit-tap-highlight-color: initial; /* reenable tap highlight inside */
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[part='backdrop'] {
|
|
134
|
+
z-index: -1;
|
|
135
|
+
content: '';
|
|
136
|
+
background: rgba(0, 0, 0, 0.5);
|
|
137
|
+
position: fixed;
|
|
138
|
+
top: 0;
|
|
139
|
+
left: 0;
|
|
140
|
+
bottom: 0;
|
|
141
|
+
right: 0;
|
|
142
|
+
pointer-events: auto;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
145
|
+
|
|
146
|
+
<div id="backdrop" part="backdrop" hidden$="[[!withBackdrop]]"></div>
|
|
147
|
+
<div part="overlay" id="overlay" tabindex="0">
|
|
148
|
+
<div part="content" id="content">
|
|
149
|
+
<slot></slot>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static get is() {
|
|
156
|
+
return 'vaadin-overlay';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static get properties() {
|
|
160
|
+
return {
|
|
161
|
+
/**
|
|
162
|
+
* When true, the overlay is visible and attached to body.
|
|
163
|
+
*/
|
|
164
|
+
opened: {
|
|
165
|
+
type: Boolean,
|
|
166
|
+
notify: true,
|
|
167
|
+
observer: '_openedChanged',
|
|
168
|
+
reflectToAttribute: true,
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Owner element passed with renderer function
|
|
173
|
+
* @type {HTMLElement}
|
|
174
|
+
*/
|
|
175
|
+
owner: Element,
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Custom function for rendering the content of the overlay.
|
|
179
|
+
* Receives three arguments:
|
|
180
|
+
*
|
|
181
|
+
* - `root` The root container DOM element. Append your content to it.
|
|
182
|
+
* - `owner` The host element of the renderer function.
|
|
183
|
+
* - `model` The object with the properties related with rendering.
|
|
184
|
+
* @type {OverlayRenderer | null | undefined}
|
|
185
|
+
*/
|
|
186
|
+
renderer: Function,
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* When true the overlay has backdrop on top of content when opened.
|
|
190
|
+
* @type {boolean}
|
|
191
|
+
*/
|
|
192
|
+
withBackdrop: {
|
|
193
|
+
type: Boolean,
|
|
194
|
+
value: false,
|
|
195
|
+
reflectToAttribute: true,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Object with properties that is passed to `renderer` function
|
|
200
|
+
*/
|
|
201
|
+
model: Object,
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* When true the overlay won't disable the main content, showing
|
|
205
|
+
* it doesn’t change the functionality of the user interface.
|
|
206
|
+
* @type {boolean}
|
|
207
|
+
*/
|
|
208
|
+
modeless: {
|
|
209
|
+
type: Boolean,
|
|
210
|
+
value: false,
|
|
211
|
+
reflectToAttribute: true,
|
|
212
|
+
observer: '_modelessChanged',
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* When set to true, the overlay is hidden. This also closes the overlay
|
|
217
|
+
* immediately in case there is a closing animation in progress.
|
|
218
|
+
* @type {boolean}
|
|
219
|
+
*/
|
|
220
|
+
hidden: {
|
|
221
|
+
type: Boolean,
|
|
222
|
+
reflectToAttribute: true,
|
|
223
|
+
observer: '_hiddenChanged',
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* When true move focus to the first focusable element in the overlay,
|
|
228
|
+
* or to the overlay if there are no focusable elements.
|
|
229
|
+
* @type {boolean}
|
|
230
|
+
*/
|
|
231
|
+
focusTrap: {
|
|
232
|
+
type: Boolean,
|
|
233
|
+
value: false,
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Set to true to enable restoring of focus when overlay is closed.
|
|
238
|
+
* @type {boolean}
|
|
239
|
+
*/
|
|
240
|
+
restoreFocusOnClose: {
|
|
241
|
+
type: Boolean,
|
|
242
|
+
value: false,
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Set to specify the element which should be focused on overlay close,
|
|
247
|
+
* if `restoreFocusOnClose` is set to true.
|
|
248
|
+
* @type {HTMLElement}
|
|
249
|
+
*/
|
|
250
|
+
restoreFocusNode: {
|
|
251
|
+
type: HTMLElement,
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/** @private */
|
|
255
|
+
_mouseDownInside: {
|
|
256
|
+
type: Boolean,
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/** @private */
|
|
260
|
+
_mouseUpInside: {
|
|
261
|
+
type: Boolean,
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/** @private */
|
|
265
|
+
_oldOwner: Element,
|
|
266
|
+
|
|
267
|
+
/** @private */
|
|
268
|
+
_oldModel: Object,
|
|
269
|
+
|
|
270
|
+
/** @private */
|
|
271
|
+
_oldRenderer: Object,
|
|
272
|
+
|
|
273
|
+
/** @private */
|
|
274
|
+
_oldOpened: Boolean,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static get observers() {
|
|
279
|
+
return ['_rendererOrDataChanged(renderer, owner, model, opened)'];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
constructor() {
|
|
283
|
+
super();
|
|
284
|
+
this._boundMouseDownListener = this._mouseDownListener.bind(this);
|
|
285
|
+
this._boundMouseUpListener = this._mouseUpListener.bind(this);
|
|
286
|
+
this._boundOutsideClickListener = this._outsideClickListener.bind(this);
|
|
287
|
+
this._boundKeydownListener = this._keydownListener.bind(this);
|
|
288
|
+
|
|
289
|
+
/* c8 ignore next 3 */
|
|
290
|
+
if (isIOS) {
|
|
291
|
+
this._boundIosResizeListener = () => this._detectIosNavbar();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.__focusTrapController = new FocusTrapController(this);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @protected */
|
|
298
|
+
ready() {
|
|
299
|
+
super.ready();
|
|
300
|
+
|
|
301
|
+
// Need to add dummy click listeners to this and the backdrop or else
|
|
302
|
+
// the document click event listener (_outsideClickListener) may never
|
|
303
|
+
// get invoked on iOS Safari (reproducible in <vaadin-dialog>
|
|
304
|
+
// and <vaadin-context-menu>).
|
|
305
|
+
this.addEventListener('click', () => {});
|
|
306
|
+
this.$.backdrop.addEventListener('click', () => {});
|
|
307
|
+
|
|
308
|
+
this.addController(this.__focusTrapController);
|
|
309
|
+
|
|
310
|
+
processTemplates(this);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** @private */
|
|
314
|
+
_detectIosNavbar() {
|
|
315
|
+
/* c8 ignore next 15 */
|
|
316
|
+
if (!this.opened) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const innerHeight = window.innerHeight;
|
|
321
|
+
const innerWidth = window.innerWidth;
|
|
322
|
+
|
|
323
|
+
const landscape = innerWidth > innerHeight;
|
|
324
|
+
|
|
325
|
+
const clientHeight = document.documentElement.clientHeight;
|
|
326
|
+
|
|
327
|
+
if (landscape && clientHeight > innerHeight) {
|
|
328
|
+
this.style.setProperty('--vaadin-overlay-viewport-bottom', `${clientHeight - innerHeight}px`);
|
|
329
|
+
} else {
|
|
330
|
+
this.style.setProperty('--vaadin-overlay-viewport-bottom', '0');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @param {Event=} sourceEvent
|
|
336
|
+
* @event vaadin-overlay-close
|
|
337
|
+
* fired before the `vaadin-overlay` will be closed. If canceled the closing of the overlay is canceled as well.
|
|
338
|
+
*/
|
|
339
|
+
close(sourceEvent) {
|
|
340
|
+
const evt = new CustomEvent('vaadin-overlay-close', {
|
|
341
|
+
bubbles: true,
|
|
342
|
+
cancelable: true,
|
|
343
|
+
detail: { sourceEvent },
|
|
344
|
+
});
|
|
345
|
+
this.dispatchEvent(evt);
|
|
346
|
+
if (!evt.defaultPrevented) {
|
|
347
|
+
this.opened = false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @protected */
|
|
352
|
+
connectedCallback() {
|
|
353
|
+
super.connectedCallback();
|
|
354
|
+
|
|
355
|
+
/* c8 ignore next 3 */
|
|
356
|
+
if (this._boundIosResizeListener) {
|
|
357
|
+
this._detectIosNavbar();
|
|
358
|
+
window.addEventListener('resize', this._boundIosResizeListener);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** @protected */
|
|
363
|
+
disconnectedCallback() {
|
|
364
|
+
super.disconnectedCallback();
|
|
365
|
+
|
|
366
|
+
/* c8 ignore next 3 */
|
|
367
|
+
if (this._boundIosResizeListener) {
|
|
368
|
+
window.removeEventListener('resize', this._boundIosResizeListener);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Requests an update for the content of the overlay.
|
|
374
|
+
* While performing the update, it invokes the renderer passed in the `renderer` property.
|
|
375
|
+
*
|
|
376
|
+
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
|
|
377
|
+
*/
|
|
378
|
+
requestContentUpdate() {
|
|
379
|
+
if (this.renderer) {
|
|
380
|
+
this.renderer.call(this.owner, this, this.owner, this.model);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** @private */
|
|
385
|
+
_mouseDownListener(event) {
|
|
386
|
+
this._mouseDownInside = event.composedPath().indexOf(this.$.overlay) >= 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** @private */
|
|
390
|
+
_mouseUpListener(event) {
|
|
391
|
+
this._mouseUpInside = event.composedPath().indexOf(this.$.overlay) >= 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* We need to listen on 'click' / 'tap' event and capture it and close the overlay before
|
|
396
|
+
* propagating the event to the listener in the button. Otherwise, if the clicked button would call
|
|
397
|
+
* open(), this would happen: https://www.youtube.com/watch?v=Z86V_ICUCD4
|
|
398
|
+
*
|
|
399
|
+
* @event vaadin-overlay-outside-click
|
|
400
|
+
* fired before the `vaadin-overlay` will be closed on outside click. If canceled the closing of the overlay is canceled as well.
|
|
401
|
+
*
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
_outsideClickListener(event) {
|
|
405
|
+
if (event.composedPath().includes(this.$.overlay) || this._mouseDownInside || this._mouseUpInside) {
|
|
406
|
+
this._mouseDownInside = false;
|
|
407
|
+
this._mouseUpInside = false;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!this._last) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const evt = new CustomEvent('vaadin-overlay-outside-click', {
|
|
415
|
+
bubbles: true,
|
|
416
|
+
cancelable: true,
|
|
417
|
+
detail: { sourceEvent: event },
|
|
418
|
+
});
|
|
419
|
+
this.dispatchEvent(evt);
|
|
420
|
+
|
|
421
|
+
if (this.opened && !evt.defaultPrevented) {
|
|
422
|
+
this.close(event);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @event vaadin-overlay-escape-press
|
|
428
|
+
* fired before the `vaadin-overlay` will be closed on ESC button press. If canceled the closing of the overlay is canceled as well.
|
|
429
|
+
*
|
|
430
|
+
* @private
|
|
431
|
+
*/
|
|
432
|
+
_keydownListener(event) {
|
|
433
|
+
if (!this._last) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Only close modeless overlay on Esc press when it contains focus
|
|
438
|
+
if (this.modeless && !event.composedPath().includes(this.$.overlay)) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (event.key === 'Escape') {
|
|
443
|
+
const evt = new CustomEvent('vaadin-overlay-escape-press', {
|
|
444
|
+
bubbles: true,
|
|
445
|
+
cancelable: true,
|
|
446
|
+
detail: { sourceEvent: event },
|
|
447
|
+
});
|
|
448
|
+
this.dispatchEvent(evt);
|
|
449
|
+
|
|
450
|
+
if (this.opened && !evt.defaultPrevented) {
|
|
451
|
+
this.close(event);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* @event vaadin-overlay-open
|
|
458
|
+
* fired after the `vaadin-overlay` is opened.
|
|
459
|
+
*
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
_openedChanged(opened, wasOpened) {
|
|
463
|
+
if (opened) {
|
|
464
|
+
// Store focused node.
|
|
465
|
+
this.__restoreFocusNode = this._getActiveElement();
|
|
466
|
+
this._animatedOpening();
|
|
467
|
+
|
|
468
|
+
afterNextRender(this, () => {
|
|
469
|
+
if (this.focusTrap) {
|
|
470
|
+
this.__focusTrapController.trapFocus(this.$.overlay);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const evt = new CustomEvent('vaadin-overlay-open', { bubbles: true });
|
|
474
|
+
this.dispatchEvent(evt);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
document.addEventListener('keydown', this._boundKeydownListener);
|
|
478
|
+
|
|
479
|
+
if (!this.modeless) {
|
|
480
|
+
this._addGlobalListeners();
|
|
481
|
+
}
|
|
482
|
+
} else if (wasOpened) {
|
|
483
|
+
if (this.focusTrap) {
|
|
484
|
+
this.__focusTrapController.releaseFocus();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this._animatedClosing();
|
|
488
|
+
|
|
489
|
+
document.removeEventListener('keydown', this._boundKeydownListener);
|
|
490
|
+
|
|
491
|
+
if (!this.modeless) {
|
|
492
|
+
this._removeGlobalListeners();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** @private */
|
|
498
|
+
_hiddenChanged(hidden) {
|
|
499
|
+
if (hidden && this.hasAttribute('closing')) {
|
|
500
|
+
this._flushAnimation('closing');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* @return {boolean}
|
|
506
|
+
* @protected
|
|
507
|
+
*/
|
|
508
|
+
_shouldAnimate() {
|
|
509
|
+
const name = getComputedStyle(this).getPropertyValue('animation-name');
|
|
510
|
+
const hidden = getComputedStyle(this).getPropertyValue('display') === 'none';
|
|
511
|
+
return !hidden && name && name !== 'none';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @param {string} type
|
|
516
|
+
* @param {Function} callback
|
|
517
|
+
* @protected
|
|
518
|
+
*/
|
|
519
|
+
_enqueueAnimation(type, callback) {
|
|
520
|
+
const handler = `__${type}Handler`;
|
|
521
|
+
const listener = (event) => {
|
|
522
|
+
if (event && event.target !== this) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
callback();
|
|
526
|
+
this.removeEventListener('animationend', listener);
|
|
527
|
+
delete this[handler];
|
|
528
|
+
};
|
|
529
|
+
this[handler] = listener;
|
|
530
|
+
this.addEventListener('animationend', listener);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* @param {string} type
|
|
535
|
+
* @protected
|
|
536
|
+
*/
|
|
537
|
+
_flushAnimation(type) {
|
|
538
|
+
const handler = `__${type}Handler`;
|
|
539
|
+
if (typeof this[handler] === 'function') {
|
|
540
|
+
this[handler]();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** @protected */
|
|
545
|
+
_animatedOpening() {
|
|
546
|
+
if (this.parentNode === document.body && this.hasAttribute('closing')) {
|
|
547
|
+
this._flushAnimation('closing');
|
|
548
|
+
}
|
|
549
|
+
this._attachOverlay();
|
|
550
|
+
if (!this.modeless) {
|
|
551
|
+
this._enterModalState();
|
|
552
|
+
}
|
|
553
|
+
this.setAttribute('opening', '');
|
|
554
|
+
|
|
555
|
+
if (this._shouldAnimate()) {
|
|
556
|
+
this._enqueueAnimation('opening', () => {
|
|
557
|
+
this._finishOpening();
|
|
558
|
+
});
|
|
559
|
+
} else {
|
|
560
|
+
this._finishOpening();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** @protected */
|
|
565
|
+
_attachOverlay() {
|
|
566
|
+
this._placeholder = document.createComment('vaadin-overlay-placeholder');
|
|
567
|
+
this.parentNode.insertBefore(this._placeholder, this);
|
|
568
|
+
document.body.appendChild(this);
|
|
569
|
+
this.bringToFront();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** @protected */
|
|
573
|
+
_finishOpening() {
|
|
574
|
+
this.removeAttribute('opening');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** @protected */
|
|
578
|
+
_finishClosing() {
|
|
579
|
+
this._detachOverlay();
|
|
580
|
+
this.$.overlay.style.removeProperty('pointer-events');
|
|
581
|
+
this.removeAttribute('closing');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @event vaadin-overlay-closing
|
|
586
|
+
* Fired when the overlay will be closed.
|
|
587
|
+
*
|
|
588
|
+
* @protected
|
|
589
|
+
*/
|
|
590
|
+
_animatedClosing() {
|
|
591
|
+
if (this.hasAttribute('opening')) {
|
|
592
|
+
this._flushAnimation('opening');
|
|
593
|
+
}
|
|
594
|
+
if (this._placeholder) {
|
|
595
|
+
this._exitModalState();
|
|
596
|
+
|
|
597
|
+
// Use this.restoreFocusNode if specified, otherwise fallback to the node
|
|
598
|
+
// which was focused before opening the overlay.
|
|
599
|
+
const restoreFocusNode = this.restoreFocusNode || this.__restoreFocusNode;
|
|
600
|
+
|
|
601
|
+
if (this.restoreFocusOnClose && restoreFocusNode) {
|
|
602
|
+
// If the activeElement is `<body>` or inside the overlay,
|
|
603
|
+
// we are allowed to restore the focus. In all the other
|
|
604
|
+
// cases focus might have been moved elsewhere by another
|
|
605
|
+
// component or by the user interaction (e.g. click on a
|
|
606
|
+
// button outside the overlay).
|
|
607
|
+
const activeElement = this._getActiveElement();
|
|
608
|
+
|
|
609
|
+
if (activeElement === document.body || this._deepContains(activeElement)) {
|
|
610
|
+
// Focusing the restoreFocusNode doesn't always work synchronously on Firefox and Safari
|
|
611
|
+
// (e.g. combo-box overlay close on outside click).
|
|
612
|
+
setTimeout(() => restoreFocusNode.focus());
|
|
613
|
+
}
|
|
614
|
+
this.__restoreFocusNode = null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.setAttribute('closing', '');
|
|
618
|
+
this.dispatchEvent(new CustomEvent('vaadin-overlay-closing'));
|
|
619
|
+
|
|
620
|
+
if (this._shouldAnimate()) {
|
|
621
|
+
this._enqueueAnimation('closing', () => {
|
|
622
|
+
this._finishClosing();
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
this._finishClosing();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** @protected */
|
|
631
|
+
_detachOverlay() {
|
|
632
|
+
this._placeholder.parentNode.insertBefore(this, this._placeholder);
|
|
633
|
+
this._placeholder.parentNode.removeChild(this._placeholder);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Returns all attached overlays in visual stacking order.
|
|
638
|
+
* @private
|
|
639
|
+
*/
|
|
640
|
+
static get __attachedInstances() {
|
|
641
|
+
return Array.from(document.body.children)
|
|
642
|
+
.filter((el) => el instanceof Overlay && !el.hasAttribute('closing'))
|
|
643
|
+
.sort((a, b) => a.__zIndex - b.__zIndex || 0);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Returns true if this is the last one in the opened overlays stack
|
|
648
|
+
* @return {boolean}
|
|
649
|
+
* @protected
|
|
650
|
+
*/
|
|
651
|
+
get _last() {
|
|
652
|
+
return this === Overlay.__attachedInstances.pop();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** @private */
|
|
656
|
+
_modelessChanged(modeless) {
|
|
657
|
+
if (!modeless) {
|
|
658
|
+
if (this.opened) {
|
|
659
|
+
this._addGlobalListeners();
|
|
660
|
+
this._enterModalState();
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
this._removeGlobalListeners();
|
|
664
|
+
this._exitModalState();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** @protected */
|
|
669
|
+
_addGlobalListeners() {
|
|
670
|
+
document.addEventListener('mousedown', this._boundMouseDownListener);
|
|
671
|
+
document.addEventListener('mouseup', this._boundMouseUpListener);
|
|
672
|
+
// Firefox leaks click to document on contextmenu even if prevented
|
|
673
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=990614
|
|
674
|
+
document.documentElement.addEventListener('click', this._boundOutsideClickListener, true);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** @protected */
|
|
678
|
+
_enterModalState() {
|
|
679
|
+
if (document.body.style.pointerEvents !== 'none') {
|
|
680
|
+
// Set body pointer-events to 'none' to disable mouse interactions with
|
|
681
|
+
// other document nodes.
|
|
682
|
+
this._previousDocumentPointerEvents = document.body.style.pointerEvents;
|
|
683
|
+
document.body.style.pointerEvents = 'none';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Disable pointer events in other attached overlays
|
|
687
|
+
Overlay.__attachedInstances.forEach((el) => {
|
|
688
|
+
if (el !== this) {
|
|
689
|
+
el.shadowRoot.querySelector('[part="overlay"]').style.pointerEvents = 'none';
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** @protected */
|
|
695
|
+
_removeGlobalListeners() {
|
|
696
|
+
document.removeEventListener('mousedown', this._boundMouseDownListener);
|
|
697
|
+
document.removeEventListener('mouseup', this._boundMouseUpListener);
|
|
698
|
+
document.documentElement.removeEventListener('click', this._boundOutsideClickListener, true);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** @protected */
|
|
702
|
+
_exitModalState() {
|
|
703
|
+
if (this._previousDocumentPointerEvents !== undefined) {
|
|
704
|
+
// Restore body pointer-events
|
|
705
|
+
document.body.style.pointerEvents = this._previousDocumentPointerEvents;
|
|
706
|
+
delete this._previousDocumentPointerEvents;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Restore pointer events in the previous overlay(s)
|
|
710
|
+
const instances = Overlay.__attachedInstances;
|
|
711
|
+
let el;
|
|
712
|
+
// Use instances.pop() to ensure the reverse order
|
|
713
|
+
while ((el = instances.pop())) {
|
|
714
|
+
if (el === this) {
|
|
715
|
+
// Skip the current instance
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
el.shadowRoot.querySelector('[part="overlay"]').style.removeProperty('pointer-events');
|
|
719
|
+
if (!el.modeless) {
|
|
720
|
+
// Stop after the last modal
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** @private */
|
|
727
|
+
_rendererOrDataChanged(renderer, owner, model, opened) {
|
|
728
|
+
const ownerOrModelChanged = this._oldOwner !== owner || this._oldModel !== model;
|
|
729
|
+
this._oldModel = model;
|
|
730
|
+
this._oldOwner = owner;
|
|
731
|
+
|
|
732
|
+
const rendererChanged = this._oldRenderer !== renderer;
|
|
733
|
+
this._oldRenderer = renderer;
|
|
734
|
+
|
|
735
|
+
const openedChanged = this._oldOpened !== opened;
|
|
736
|
+
this._oldOpened = opened;
|
|
737
|
+
|
|
738
|
+
if (rendererChanged) {
|
|
739
|
+
this.innerHTML = '';
|
|
740
|
+
// Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
|
|
741
|
+
// When clearing the rendered content, this part needs to be manually disposed of.
|
|
742
|
+
// Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
|
|
743
|
+
delete this._$litPart$;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (opened && renderer && (rendererChanged || openedChanged || ownerOrModelChanged)) {
|
|
747
|
+
this.requestContentUpdate();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* @return {!Element}
|
|
753
|
+
* @protected
|
|
754
|
+
*/
|
|
755
|
+
_getActiveElement() {
|
|
756
|
+
// Document.activeElement can be null
|
|
757
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
|
758
|
+
let active = document.activeElement || document.body;
|
|
759
|
+
while (active.shadowRoot && active.shadowRoot.activeElement) {
|
|
760
|
+
active = active.shadowRoot.activeElement;
|
|
761
|
+
}
|
|
762
|
+
return active;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* @param {!Node} node
|
|
767
|
+
* @return {boolean}
|
|
768
|
+
* @protected
|
|
769
|
+
*/
|
|
770
|
+
_deepContains(node) {
|
|
771
|
+
if (this.contains(node)) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
let n = node;
|
|
775
|
+
const doc = node.ownerDocument;
|
|
776
|
+
// Walk from node to `this` or `document`
|
|
777
|
+
while (n && n !== doc && n !== this) {
|
|
778
|
+
n = n.parentNode || n.host;
|
|
779
|
+
}
|
|
780
|
+
return n === this;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Brings the overlay as visually the frontmost one
|
|
785
|
+
*/
|
|
786
|
+
bringToFront() {
|
|
787
|
+
let zIndex = '';
|
|
788
|
+
const frontmost = Overlay.__attachedInstances.filter((o) => o !== this).pop();
|
|
789
|
+
if (frontmost) {
|
|
790
|
+
const frontmostZIndex = frontmost.__zIndex;
|
|
791
|
+
zIndex = frontmostZIndex + 1;
|
|
792
|
+
}
|
|
793
|
+
this.style.zIndex = zIndex;
|
|
794
|
+
this.__zIndex = zIndex || parseFloat(getComputedStyle(this).zIndex);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
customElements.define(Overlay.is, Overlay);
|
|
799
|
+
|
|
800
|
+
export { Overlay };
|