@everymatrix/general-input 1.22.0 → 1.22.1

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.
@@ -1,9 +1,8 @@
1
- import { i, r as registerStyles, d as dedupingMixin, B as PropertyEffects, G as strictTemplatePolicy, w as wrap, H as legacyWarnings, J as getFocusableElements, b as isElementFocused, T as ThemableMixin, A as DirMixin, C as ControllerMixin, P as PolymerElement, h as html, m as FlattenedNodesObserver, M as getAncestorRootNodes } from './field-mixin.js';
2
- import { b as isIOS } from './input-field-shared-styles.js';
1
+ import { i, y as registerStyles, z as getDeepActiveElement, A as getFocusableElements, b as isElementFocused, d as dedupingMixin } from './field-mixin.js';
3
2
 
4
3
  /**
5
4
  * @license
6
- * Copyright (c) 2017 - 2022 Vaadin Ltd.
5
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
7
6
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
8
7
  */
9
8
 
@@ -74,7 +73,113 @@ const overlay = i`
74
73
 
75
74
  registerStyles('', overlay, { moduleId: 'lumo-overlay' });
76
75
 
77
- registerStyles('vaadin-overlay', overlay, { moduleId: 'lumo-vaadin-overlay' });
76
+ /**
77
+ * @license
78
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
79
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
80
+ */
81
+
82
+ const menuOverlayCore = i`
83
+ :host([opening]),
84
+ :host([closing]) {
85
+ animation: 0.14s lumo-overlay-dummy-animation;
86
+ }
87
+
88
+ [part='overlay'] {
89
+ will-change: opacity, transform;
90
+ }
91
+
92
+ :host([opening]) [part='overlay'] {
93
+ animation: 0.1s lumo-menu-overlay-enter ease-out both;
94
+ }
95
+
96
+ @keyframes lumo-menu-overlay-enter {
97
+ 0% {
98
+ opacity: 0;
99
+ transform: translateY(-4px);
100
+ }
101
+ }
102
+
103
+ :host([closing]) [part='overlay'] {
104
+ animation: 0.1s lumo-menu-overlay-exit both;
105
+ }
106
+
107
+ @keyframes lumo-menu-overlay-exit {
108
+ 100% {
109
+ opacity: 0;
110
+ }
111
+ }
112
+ `;
113
+
114
+ registerStyles('', menuOverlayCore, { moduleId: 'lumo-menu-overlay-core' });
115
+
116
+ const menuOverlayExt = i`
117
+ /* Small viewport (bottom sheet) styles */
118
+ /* Use direct media queries instead of the state attributes ([phone] and [fullscreen]) provided by the elements */
119
+ @media (max-width: 420px), (max-height: 420px) {
120
+ :host {
121
+ top: 0 !important;
122
+ right: 0 !important;
123
+ bottom: var(--vaadin-overlay-viewport-bottom, 0) !important;
124
+ left: 0 !important;
125
+ align-items: stretch !important;
126
+ justify-content: flex-end !important;
127
+ }
128
+
129
+ [part='overlay'] {
130
+ max-height: 50vh;
131
+ width: 100vw;
132
+ border-radius: 0;
133
+ box-shadow: var(--lumo-box-shadow-xl);
134
+ }
135
+
136
+ /* The content part scrolls instead of the overlay part, because of the gradient fade-out */
137
+ [part='content'] {
138
+ padding: 30px var(--lumo-space-m);
139
+ max-height: inherit;
140
+ box-sizing: border-box;
141
+ -webkit-overflow-scrolling: touch;
142
+ overflow: auto;
143
+ -webkit-mask-image: linear-gradient(transparent, #000 40px, #000 calc(100% - 40px), transparent);
144
+ mask-image: linear-gradient(transparent, #000 40px, #000 calc(100% - 40px), transparent);
145
+ }
146
+
147
+ [part='backdrop'] {
148
+ display: block;
149
+ }
150
+
151
+ /* Animations */
152
+
153
+ :host([opening]) [part='overlay'] {
154
+ animation: 0.2s lumo-mobile-menu-overlay-enter cubic-bezier(0.215, 0.61, 0.355, 1) both;
155
+ }
156
+
157
+ :host([closing]),
158
+ :host([closing]) [part='backdrop'] {
159
+ animation-delay: 0.14s;
160
+ }
161
+
162
+ :host([closing]) [part='overlay'] {
163
+ animation: 0.14s 0.14s lumo-mobile-menu-overlay-exit cubic-bezier(0.55, 0.055, 0.675, 0.19) both;
164
+ }
165
+ }
166
+
167
+ @keyframes lumo-mobile-menu-overlay-enter {
168
+ 0% {
169
+ transform: translateY(150%);
170
+ }
171
+ }
172
+
173
+ @keyframes lumo-mobile-menu-overlay-exit {
174
+ 100% {
175
+ transform: translateY(150%);
176
+ }
177
+ }
178
+ `;
179
+
180
+ const menuOverlay = [overlay, menuOverlayCore, menuOverlayExt];
181
+
182
+ registerStyles('', menuOverlay, { moduleId: 'lumo-menu-overlay' });
78
183
 
79
184
  /**
80
185
  @license
@@ -150,851 +255,362 @@ function afterNextRender(context, callback, args) {
150
255
  }
151
256
 
152
257
  /**
153
- @license
154
- Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
155
- This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
156
- The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
157
- The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
158
- Code distributed by Google as part of the polymer project is also
159
- subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
160
- */
161
-
162
- // Common implementation for mixin & behavior
163
- function mutablePropertyChange(inst, property, value, old, mutableData) {
164
- let isObject;
165
- if (mutableData) {
166
- isObject = (typeof value === 'object' && value !== null);
167
- // Pull `old` for Objects from temp cache, but treat `null` as a primitive
168
- if (isObject) {
169
- old = inst.__dataTemp[property];
170
- }
171
- }
172
- // Strict equality check, but return false for NaN===NaN
173
- let shouldChange = (old !== value && (old === old || value === value));
174
- // Objects are stored in temporary cache (cleared at end of
175
- // turn), which is used for dirty-checking
176
- if (isObject && shouldChange) {
177
- inst.__dataTemp[property] = value;
178
- }
179
- return shouldChange;
180
- }
181
-
182
- /**
183
- * Element class mixin to skip strict dirty-checking for objects and arrays
184
- * (always consider them to be "dirty"), for use on elements utilizing
185
- * `PropertyEffects`
186
- *
187
- * By default, `PropertyEffects` performs strict dirty checking on
188
- * objects, which means that any deep modifications to an object or array will
189
- * not be propagated unless "immutable" data patterns are used (i.e. all object
190
- * references from the root to the mutation were changed).
191
- *
192
- * Polymer also provides a proprietary data mutation and path notification API
193
- * (e.g. `notifyPath`, `set`, and array mutation API's) that allow efficient
194
- * mutation and notification of deep changes in an object graph to all elements
195
- * bound to the same object graph.
196
- *
197
- * In cases where neither immutable patterns nor the data mutation API can be
198
- * used, applying this mixin will cause Polymer to skip dirty checking for
199
- * objects and arrays (always consider them to be "dirty"). This allows a
200
- * user to make a deep modification to a bound object graph, and then either
201
- * simply re-set the object (e.g. `this.items = this.items`) or call `notifyPath`
202
- * (e.g. `this.notifyPath('items')`) to update the tree. Note that all
203
- * elements that wish to be updated based on deep mutations must apply this
204
- * mixin or otherwise skip strict dirty checking for objects/arrays.
205
- * Specifically, any elements in the binding tree between the source of a
206
- * mutation and the consumption of it must apply this mixin or enable the
207
- * `OptionalMutableData` mixin.
208
- *
209
- * In order to make the dirty check strategy configurable, see
210
- * `OptionalMutableData`.
211
- *
212
- * Note, the performance characteristics of propagating large object graphs
213
- * will be worse as opposed to using strict dirty checking with immutable
214
- * patterns or Polymer's path notification API.
215
- *
216
- * @mixinFunction
217
- * @polymer
218
- * @summary Element class mixin to skip strict dirty-checking for objects
219
- * and arrays
220
- * @template T
221
- * @param {function(new:T)} superClass Class to apply mixin to.
222
- * @return {function(new:T)} superClass with mixin applied.
258
+ * @license
259
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
260
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
223
261
  */
224
- const MutableData = dedupingMixin(superClass => {
225
262
 
226
- /**
227
- * @polymer
228
- * @mixinClass
229
- * @implements {Polymer_MutableData}
230
- */
231
- class MutableData extends superClass {
232
- /**
233
- * Overrides `PropertyEffects` to provide option for skipping
234
- * strict equality checking for Objects and Arrays.
235
- *
236
- * This method pulls the value to dirty check against from the `__dataTemp`
237
- * cache (rather than the normal `__data` cache) for Objects. Since the temp
238
- * cache is cleared at the end of a turn, this implementation allows
239
- * side-effects of deep object changes to be processed by re-setting the
240
- * same object (using the temp cache as an in-turn backstop to prevent
241
- * cycles due to 2-way notification).
242
- *
243
- * @param {string} property Property name
244
- * @param {*} value New property value
245
- * @param {*} old Previous property value
246
- * @return {boolean} Whether the property should be considered a change
247
- * @protected
248
- */
249
- _shouldPropertyChange(property, value, old) {
250
- return mutablePropertyChange(this, property, value, old, true);
251
- }
263
+ const testUserAgent = (regexp) => regexp.test(navigator.userAgent);
252
264
 
253
- }
265
+ const testPlatform = (regexp) => regexp.test(navigator.platform);
254
266
 
255
- return MutableData;
267
+ const testVendor = (regexp) => regexp.test(navigator.vendor);
256
268
 
257
- });
269
+ testUserAgent(/Android/u);
258
270
 
259
- /**
260
- * Element class mixin to add the optional ability to skip strict
261
- * dirty-checking for objects and arrays (always consider them to be
262
- * "dirty") by setting a `mutable-data` attribute on an element instance.
263
- *
264
- * By default, `PropertyEffects` performs strict dirty checking on
265
- * objects, which means that any deep modifications to an object or array will
266
- * not be propagated unless "immutable" data patterns are used (i.e. all object
267
- * references from the root to the mutation were changed).
268
- *
269
- * Polymer also provides a proprietary data mutation and path notification API
270
- * (e.g. `notifyPath`, `set`, and array mutation API's) that allow efficient
271
- * mutation and notification of deep changes in an object graph to all elements
272
- * bound to the same object graph.
273
- *
274
- * In cases where neither immutable patterns nor the data mutation API can be
275
- * used, applying this mixin will allow Polymer to skip dirty checking for
276
- * objects and arrays (always consider them to be "dirty"). This allows a
277
- * user to make a deep modification to a bound object graph, and then either
278
- * simply re-set the object (e.g. `this.items = this.items`) or call `notifyPath`
279
- * (e.g. `this.notifyPath('items')`) to update the tree. Note that all
280
- * elements that wish to be updated based on deep mutations must apply this
281
- * mixin or otherwise skip strict dirty checking for objects/arrays.
282
- * Specifically, any elements in the binding tree between the source of a
283
- * mutation and the consumption of it must enable this mixin or apply the
284
- * `MutableData` mixin.
285
- *
286
- * While this mixin adds the ability to forgo Object/Array dirty checking,
287
- * the `mutableData` flag defaults to false and must be set on the instance.
288
- *
289
- * Note, the performance characteristics of propagating large object graphs
290
- * will be worse by relying on `mutableData: true` as opposed to using
291
- * strict dirty checking with immutable patterns or Polymer's path notification
292
- * API.
293
- *
294
- * @mixinFunction
295
- * @polymer
296
- * @summary Element class mixin to optionally skip strict dirty-checking
297
- * for objects and arrays
298
- */
299
- const OptionalMutableData = dedupingMixin(superClass => {
271
+ testUserAgent(/Chrome/u) && testVendor(/Google Inc/u);
300
272
 
301
- /**
302
- * @mixinClass
303
- * @polymer
304
- * @implements {Polymer_OptionalMutableData}
305
- */
306
- class OptionalMutableData extends superClass {
273
+ testUserAgent(/Firefox/u);
307
274
 
308
- /** @nocollapse */
309
- static get properties() {
310
- return {
311
- /**
312
- * Instance-level flag for configuring the dirty-checking strategy
313
- * for this element. When true, Objects and Arrays will skip dirty
314
- * checking, otherwise strict equality checking will be used.
315
- */
316
- mutableData: Boolean
317
- };
318
- }
275
+ // IPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
276
+ const isIPad = testPlatform(/^iPad/u) || (testPlatform(/^Mac/u) && navigator.maxTouchPoints > 1);
319
277
 
320
- /**
321
- * Overrides `PropertyEffects` to provide option for skipping
322
- * strict equality checking for Objects and Arrays.
323
- *
324
- * When `this.mutableData` is true on this instance, this method
325
- * pulls the value to dirty check against from the `__dataTemp` cache
326
- * (rather than the normal `__data` cache) for Objects. Since the temp
327
- * cache is cleared at the end of a turn, this implementation allows
328
- * side-effects of deep object changes to be processed by re-setting the
329
- * same object (using the temp cache as an in-turn backstop to prevent
330
- * cycles due to 2-way notification).
331
- *
332
- * @param {string} property Property name
333
- * @param {*} value New property value
334
- * @param {*} old Previous property value
335
- * @return {boolean} Whether the property should be considered a change
336
- * @protected
337
- */
338
- _shouldPropertyChange(property, value, old) {
339
- return mutablePropertyChange(this, property, value, old, this.mutableData);
340
- }
341
- }
278
+ const isIPhone = testPlatform(/^iPhone/u);
342
279
 
343
- return OptionalMutableData;
280
+ const isIOS = isIPhone || isIPad;
344
281
 
345
- });
282
+ testUserAgent(/^((?!chrome|android).)*safari/iu);
346
283
 
347
- // Export for use by legacy behavior
348
- MutableData._mutablePropertyChange = mutablePropertyChange;
284
+ (() => {
285
+ try {
286
+ document.createEvent('TouchEvent');
287
+ return true;
288
+ } catch (e) {
289
+ return false;
290
+ }
291
+ })();
349
292
 
350
293
  /**
351
- @license
352
- Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
353
- This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
354
- The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
355
- The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
356
- Code distributed by Google as part of the polymer project is also
357
- subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
358
- */
359
-
360
- // Base class for HTMLTemplateElement extension that has property effects
361
- // machinery for propagating host properties to children. This is an ES5
362
- // class only because Babel (incorrectly) requires super() in the class
363
- // constructor even though no `this` is used and it returns an instance.
364
- let newInstance = null;
294
+ * @license
295
+ * Copyright (c) 2017 Anton Korzunov
296
+ * SPDX-License-Identifier: MIT
297
+ */
365
298
 
366
299
  /**
367
- * @constructor
368
- * @extends {HTMLTemplateElement}
369
- * @private
300
+ * @fileoverview
301
+ *
302
+ * This module includes JS code copied from the `aria-hidden` package:
303
+ * https://github.com/theKashey/aria-hidden/blob/master/src/index.ts
370
304
  */
371
- function HTMLTemplateElementExtension() { return newInstance; }
372
- HTMLTemplateElementExtension.prototype = Object.create(HTMLTemplateElement.prototype, {
373
- constructor: {
374
- value: HTMLTemplateElementExtension,
375
- writable: true
376
- }
377
- });
305
+
306
+ /** @type {WeakMap<Element, number>} */
307
+ let counterMap = new WeakMap();
308
+
309
+ /** @type {WeakMap<Element, boolean>} */
310
+ let uncontrolledNodes = new WeakMap();
311
+
312
+ /** @type {Record<string, WeakMap<Element, number>>} */
313
+ let markerMap = {};
314
+
315
+ /** @type {number} */
316
+ let lockCount = 0;
378
317
 
379
318
  /**
380
- * @constructor
381
- * @implements {Polymer_PropertyEffects}
382
- * @extends {HTMLTemplateElementExtension}
383
- * @private
319
+ * @param {?Node} node
320
+ * @return {boolean}
384
321
  */
385
- const DataTemplate = PropertyEffects(HTMLTemplateElementExtension);
322
+ const isElement = (node) => node && node.nodeType === Node.ELEMENT_NODE;
386
323
 
387
324
  /**
388
- * @constructor
389
- * @implements {Polymer_MutableData}
390
- * @extends {DataTemplate}
391
- * @private
325
+ * @param {...unknown} args
392
326
  */
393
- const MutableDataTemplate = MutableData(DataTemplate);
394
-
395
- // Applies a DataTemplate subclass to a <template> instance
396
- function upgradeTemplate(template, constructor) {
397
- newInstance = template;
398
- Object.setPrototypeOf(template, constructor.prototype);
399
- new constructor();
400
- newInstance = null;
401
- }
327
+ const logError = (...args) => {
328
+ console.error(`Error: ${args.join(' ')}. Skip setting aria-hidden.`);
329
+ };
402
330
 
403
331
  /**
404
- * Base class for TemplateInstance.
405
- * @constructor
406
- * @extends {HTMLElement}
407
- * @implements {Polymer_PropertyEffects}
408
- * @private
332
+ * @param {HTMLElement} parent
333
+ * @param {Element[]} targets
334
+ * @return {Element[]}
409
335
  */
410
- const templateInstanceBase = PropertyEffects(class {});
411
-
412
- function showHideChildren(hide, children) {
413
- for (let i=0; i<children.length; i++) {
414
- let n = children[i];
415
- // Ignore non-changes
416
- if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) {
417
- // clear and restore text
418
- if (n.nodeType === Node.TEXT_NODE) {
419
- if (hide) {
420
- n.__polymerTextContent__ = n.textContent;
421
- n.textContent = '';
422
- } else {
423
- n.textContent = n.__polymerTextContent__;
424
- }
425
- // remove and replace slot
426
- } else if (n.localName === 'slot') {
427
- if (hide) {
428
- n.__polymerReplaced__ = document.createComment('hidden-slot');
429
- wrap(wrap(n).parentNode).replaceChild(n.__polymerReplaced__, n);
430
- } else {
431
- const replace = n.__polymerReplaced__;
432
- if (replace) {
433
- wrap(wrap(replace).parentNode).replaceChild(n, replace);
434
- }
435
- }
336
+ const correctTargets = (parent, targets) => {
337
+ if (!isElement(parent)) {
338
+ logError(parent, 'is not a valid element');
339
+ return [];
340
+ }
341
+
342
+ return targets
343
+ .map((target) => {
344
+ if (!isElement(target)) {
345
+ logError(target, 'is not a valid element');
346
+ return null;
436
347
  }
437
- // hide and show nodes
438
- else if (n.style) {
439
- if (hide) {
440
- n.__polymerDisplay__ = n.style.display;
441
- n.style.display = 'none';
442
- } else {
443
- n.style.display = n.__polymerDisplay__;
348
+
349
+ let node = target;
350
+ while (node && node !== parent) {
351
+ if (parent.contains(node)) {
352
+ return target;
444
353
  }
354
+ node = node.getRootNode().host;
445
355
  }
446
- }
447
- n.__hideTemplateChildren__ = hide;
448
- if (n._showHideChildren) {
449
- n._showHideChildren(hide);
450
- }
451
- }
452
- }
356
+
357
+ logError(target, 'is not contained inside', parent);
358
+ return null;
359
+ })
360
+ .filter((x) => Boolean(x));
361
+ };
453
362
 
454
363
  /**
455
- * @polymer
456
- * @customElement
457
- * @appliesMixin PropertyEffects
458
- * @unrestricted
364
+ * Marks everything except given node(or nodes) as aria-hidden
365
+ * @param {Element | Element[]} originalTarget - elements to keep on the page
366
+ * @param {HTMLElement} [parentNode] - top element, defaults to document.body
367
+ * @param {String} [markerName] - a special attribute to mark every node
368
+ * @param {String} [controlAttribute] - html Attribute to control
369
+ * @return {Function}
459
370
  */
460
- class TemplateInstanceBase extends templateInstanceBase {
461
- constructor(props) {
462
- super();
463
- this._configureProperties(props);
464
- /** @type {!StampedTemplate} */
465
- this.root = this._stampTemplate(this.__dataHost);
466
- // Save list of stamped children
467
- let children = [];
468
- /** @suppress {invalidCasts} */
469
- this.children = /** @type {!NodeList} */ (children);
470
- // Polymer 1.x did not use `Polymer.dom` here so not bothering.
471
- for (let n = this.root.firstChild; n; n=n.nextSibling) {
472
- children.push(n);
473
- n.__templatizeInstance = this;
474
- }
475
- if (this.__templatizeOwner &&
476
- this.__templatizeOwner.__hideTemplateChildren__) {
477
- this._showHideChildren(true);
478
- }
479
- // Flush props only when props are passed if instance props exist
480
- // or when there isn't instance props.
481
- let options = this.__templatizeOptions;
482
- if ((props && options.instanceProps) || !options.instanceProps) {
483
- this._enableProperties();
484
- }
371
+ const applyAttributeToOthers = (originalTarget, parentNode, markerName, controlAttribute) => {
372
+ const targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
373
+
374
+ if (!markerMap[markerName]) {
375
+ markerMap[markerName] = new WeakMap();
485
376
  }
377
+
378
+ const markerCounter = markerMap[markerName];
379
+
380
+ /** @type {Element[]} */
381
+ const hiddenNodes = [];
382
+
383
+ /** @type {Set<Node>} */
384
+ const elementsToKeep = new Set();
385
+
386
+ /** @type {Set<Node>} */
387
+ const elementsToStop = new Set(targets);
388
+
486
389
  /**
487
- * Configure the given `props` by calling `_setPendingProperty`. Also
488
- * sets any properties stored in `__hostProps`.
489
- * @private
490
- * @param {Object} props Object of property name-value pairs to set.
491
- * @return {void}
390
+ * @param {?Node} el
492
391
  */
493
- _configureProperties(props) {
494
- let options = this.__templatizeOptions;
495
- if (options.forwardHostProp) {
496
- for (let hprop in this.__hostProps) {
497
- this._setPendingProperty(hprop, this.__dataHost['_host_' + hprop]);
498
- }
392
+ const keep = (el) => {
393
+ if (!el || elementsToKeep.has(el)) {
394
+ return;
499
395
  }
500
- // Any instance props passed in the constructor will overwrite host props;
501
- // normally this would be a user error but we don't specifically filter them
502
- for (let iprop in props) {
503
- this._setPendingProperty(iprop, props[iprop]);
396
+
397
+ elementsToKeep.add(el);
398
+
399
+ const slot = el.assignedSlot;
400
+ if (slot) {
401
+ keep(slot);
504
402
  }
505
- }
403
+
404
+ keep(el.parentNode || el.host);
405
+ };
406
+
407
+ targets.forEach(keep);
408
+
506
409
  /**
507
- * Forwards a host property to this instance. This method should be
508
- * called on instances from the `options.forwardHostProp` callback
509
- * to propagate changes of host properties to each instance.
510
- *
511
- * Note this method enqueues the change, which are flushed as a batch.
512
- *
513
- * @param {string} prop Property or path name
514
- * @param {*} value Value of the property to forward
515
- * @return {void}
410
+ * @param {?Node} el
516
411
  */
517
- forwardHostProp(prop, value) {
518
- if (this._setPendingPropertyOrPath(prop, value, false, true)) {
519
- this.__dataHost._enqueueClient(this);
412
+ const deep = (parent) => {
413
+ if (!parent || elementsToStop.has(parent)) {
414
+ return;
520
415
  }
521
- }
522
416
 
523
- /**
524
- * Override point for adding custom or simulated event handling.
525
- *
526
- * @override
527
- * @param {!Node} node Node to add event listener to
528
- * @param {string} eventName Name of event
529
- * @param {function(!Event):void} handler Listener function to add
530
- * @return {void}
531
- */
532
- _addEventListenerToNode(node, eventName, handler) {
533
- if (this._methodHost && this.__templatizeOptions.parentModel) {
534
- // If this instance should be considered a parent model, decorate
535
- // events this template instance as `model`
536
- this._methodHost._addEventListenerToNode(node, eventName, (e) => {
537
- e.model = this;
538
- handler(e);
539
- });
540
- } else {
541
- // Otherwise delegate to the template's host (which could be)
542
- // another template instance
543
- let templateHost = this.__dataHost.__dataHost;
544
- if (templateHost) {
545
- templateHost._addEventListenerToNode(node, eventName, handler);
417
+ const root = parent.shadowRoot;
418
+ const children = root ? [...parent.children, ...root.children] : [...parent.children];
419
+ children.forEach((node) => {
420
+ // Skip elements that don't need to be hidden
421
+ if (['template', 'script', 'style'].includes(node.localName)) {
422
+ return;
546
423
  }
547
- }
548
- }
549
- /**
550
- * Shows or hides the template instance top level child elements. For
551
- * text nodes, `textContent` is removed while "hidden" and replaced when
552
- * "shown."
553
- * @param {boolean} hide Set to true to hide the children;
554
- * set to false to show them.
555
- * @return {void}
556
- * @protected
557
- */
558
- _showHideChildren(hide) {
559
- showHideChildren(hide, this.children);
560
- }
561
- /**
562
- * Overrides default property-effects implementation to intercept
563
- * textContent bindings while children are "hidden" and cache in
564
- * private storage for later retrieval.
565
- *
566
- * @override
567
- * @param {!Node} node The node to set a property on
568
- * @param {string} prop The property to set
569
- * @param {*} value The value to set
570
- * @return {void}
571
- * @protected
572
- */
573
- _setUnmanagedPropertyToNode(node, prop, value) {
574
- if (node.__hideTemplateChildren__ &&
575
- node.nodeType == Node.TEXT_NODE && prop == 'textContent') {
576
- node.__polymerTextContent__ = value;
577
- } else {
578
- super._setUnmanagedPropertyToNode(node, prop, value);
579
- }
580
- }
581
- /**
582
- * Find the parent model of this template instance. The parent model
583
- * is either another templatize instance that had option `parentModel: true`,
584
- * or else the host element.
585
- *
586
- * @return {!Polymer_PropertyEffects} The parent model of this instance
587
- */
588
- get parentModel() {
589
- let model = this.__parentModel;
590
- if (!model) {
591
- let options;
592
- model = this;
593
- do {
594
- // A template instance's `__dataHost` is a <template>
595
- // `model.__dataHost.__dataHost` is the template's host
596
- model = model.__dataHost.__dataHost;
597
- } while ((options = model.__templatizeOptions) && !options.parentModel);
598
- this.__parentModel = model;
599
- }
600
- return model;
601
- }
602
424
 
603
- /**
604
- * Stub of HTMLElement's `dispatchEvent`, so that effects that may
605
- * dispatch events safely no-op.
606
- *
607
- * @param {Event} event Event to dispatch
608
- * @return {boolean} Always true.
609
- * @override
610
- */
611
- dispatchEvent(event) { // eslint-disable-line no-unused-vars
612
- return true;
613
- }
614
- }
425
+ if (elementsToKeep.has(node)) {
426
+ deep(node);
427
+ } else {
428
+ const attr = node.getAttribute(controlAttribute);
429
+ const alreadyHidden = attr !== null && attr !== 'false';
430
+ const counterValue = (counterMap.get(node) || 0) + 1;
431
+ const markerValue = (markerCounter.get(node) || 0) + 1;
615
432
 
616
- /**
617
- * @constructor
618
- * @extends {TemplateInstanceBase}
619
- * @implements {Polymer_MutableData}
620
- * @private
621
- */
622
- const MutableTemplateInstanceBase = MutableData(
623
- // This cast shouldn't be neccessary, but Closure doesn't understand that
624
- // TemplateInstanceBase is a constructor function.
625
- /** @type {function(new:TemplateInstanceBase)} */ (TemplateInstanceBase));
626
-
627
- function findMethodHost(template) {
628
- // Technically this should be the owner of the outermost template.
629
- // In shadow dom, this is always getRootNode().host, but we can
630
- // approximate this via cooperation with our dataHost always setting
631
- // `_methodHost` as long as there were bindings (or id's) on this
632
- // instance causing it to get a dataHost.
633
- let templateHost = template.__dataHost;
634
- return templateHost && templateHost._methodHost || templateHost;
635
- }
433
+ counterMap.set(node, counterValue);
434
+ markerCounter.set(node, markerValue);
435
+ hiddenNodes.push(node);
636
436
 
637
- /* eslint-disable valid-jsdoc */
638
- /**
639
- * @suppress {missingProperties} class.prototype is not defined for some reason
640
- */
641
- function createTemplatizerClass(template, templateInfo, options) {
642
- /**
643
- * @constructor
644
- * @extends {TemplateInstanceBase}
645
- */
646
- let templatizerBase = options.mutableData ?
647
- MutableTemplateInstanceBase : TemplateInstanceBase;
648
-
649
- // Affordance for global mixins onto TemplatizeInstance
650
- if (templatize.mixin) {
651
- templatizerBase = templatize.mixin(templatizerBase);
652
- }
437
+ if (counterValue === 1 && alreadyHidden) {
438
+ uncontrolledNodes.set(node, true);
439
+ }
653
440
 
654
- /**
655
- * Anonymous class created by the templatize
656
- * @constructor
657
- * @private
658
- */
659
- let klass = class extends templatizerBase { };
660
- /** @override */
661
- klass.prototype.__templatizeOptions = options;
662
- klass.prototype._bindTemplate(template);
663
- addNotifyEffects(klass, template, templateInfo, options);
664
- return klass;
665
- }
441
+ if (markerValue === 1) {
442
+ node.setAttribute(markerName, 'true');
443
+ }
666
444
 
667
- /**
668
- * Adds propagate effects from the template to the template instance for
669
- * properties that the host binds to the template using the `_host_` prefix.
670
- *
671
- * @suppress {missingProperties} class.prototype is not defined for some reason
672
- */
673
- function addPropagateEffects(target, templateInfo, options, methodHost) {
674
- let userForwardHostProp = options.forwardHostProp;
675
- if (userForwardHostProp && templateInfo.hasHostProps) {
676
- // Under the `removeNestedTemplates` optimization, a custom element like
677
- // `dom-if` or `dom-repeat` can itself be treated as the "template"; this
678
- // flag is used to switch between upgrading a `<template>` to be a property
679
- // effects client vs. adding the effects directly to the custom element
680
- const isTemplate = target.localName == 'template';
681
- // Provide data API and property effects on memoized template class
682
- let klass = templateInfo.templatizeTemplateClass;
683
- if (!klass) {
684
- if (isTemplate) {
685
- /**
686
- * @constructor
687
- * @extends {DataTemplate}
688
- */
689
- let templatizedBase =
690
- options.mutableData ? MutableDataTemplate : DataTemplate;
691
-
692
- // NOTE: due to https://github.com/google/closure-compiler/issues/2928,
693
- // combining the next two lines into one assignment causes a spurious
694
- // type error.
695
- /** @private */
696
- class TemplatizedTemplate extends templatizedBase {}
697
- klass = templateInfo.templatizeTemplateClass = TemplatizedTemplate;
698
- } else {
699
- /**
700
- * @constructor
701
- * @extends {PolymerElement}
702
- */
703
- const templatizedBase = target.constructor;
704
-
705
- // Create a cached subclass of the base custom element class onto which
706
- // to put the template-specific propagate effects
707
- // NOTE: due to https://github.com/google/closure-compiler/issues/2928,
708
- // combining the next two lines into one assignment causes a spurious
709
- // type error.
710
- /** @private */
711
- class TemplatizedTemplateExtension extends templatizedBase {}
712
- klass = templateInfo.templatizeTemplateClass =
713
- TemplatizedTemplateExtension;
714
- }
715
- // Add template - >instances effects
716
- // and host <- template effects
717
- let hostProps = templateInfo.hostProps;
718
- for (let prop in hostProps) {
719
- klass.prototype._addPropertyEffect('_host_' + prop,
720
- klass.prototype.PROPERTY_EFFECT_TYPES.PROPAGATE,
721
- {fn: createForwardHostPropEffect(prop, userForwardHostProp)});
722
- klass.prototype._createNotifyingProperty('_host_' + prop);
723
- }
724
- if (legacyWarnings && methodHost) {
725
- warnOnUndeclaredProperties(templateInfo, options, methodHost);
726
- }
727
- }
728
- // Mix any pre-bound data into __data; no need to flush this to
729
- // instances since they pull from the template at instance-time
730
- if (target.__dataProto) {
731
- // Note, generally `__dataProto` could be chained, but it's guaranteed
732
- // to not be since this is a vanilla template we just added effects to
733
- Object.assign(target.__data, target.__dataProto);
734
- }
735
- if (isTemplate) {
736
- upgradeTemplate(target, klass);
737
- // Clear any pending data for performance
738
- target.__dataTemp = {};
739
- target.__dataPending = null;
740
- target.__dataOld = null;
741
- target._enableProperties();
742
- } else {
743
- // Swizzle the cached subclass prototype onto the custom element
744
- Object.setPrototypeOf(target, klass.prototype);
745
- // Check for any pre-bound instance host properties, and do the
746
- // instance property delete/assign dance for those (directly into data;
747
- // not need to go through accessor since they are pulled at instance time)
748
- const hostProps = templateInfo.hostProps;
749
- for (let prop in hostProps) {
750
- prop = '_host_' + prop;
751
- if (prop in target) {
752
- const val = target[prop];
753
- delete target[prop];
754
- target.__data[prop] = val;
445
+ if (!alreadyHidden) {
446
+ node.setAttribute(controlAttribute, 'true');
755
447
  }
756
448
  }
757
- }
758
- }
759
- }
760
- /* eslint-enable valid-jsdoc */
761
-
762
- function createForwardHostPropEffect(hostProp, userForwardHostProp) {
763
- return function forwardHostProp(template, prop, props) {
764
- userForwardHostProp.call(template.__templatizeOwner,
765
- prop.substring('_host_'.length), props[prop]);
449
+ });
766
450
  };
767
- }
768
451
 
769
- function addNotifyEffects(klass, template, templateInfo, options) {
770
- let hostProps = templateInfo.hostProps || {};
771
- for (let iprop in options.instanceProps) {
772
- delete hostProps[iprop];
773
- let userNotifyInstanceProp = options.notifyInstanceProp;
774
- if (userNotifyInstanceProp) {
775
- klass.prototype._addPropertyEffect(iprop,
776
- klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY,
777
- {fn: createNotifyInstancePropEffect(iprop, userNotifyInstanceProp)});
778
- }
779
- }
780
- if (options.forwardHostProp && template.__dataHost) {
781
- for (let hprop in hostProps) {
782
- // As we're iterating hostProps in this function, note whether
783
- // there were any, for an optimization in addPropagateEffects
784
- if (!templateInfo.hasHostProps) {
785
- templateInfo.hasHostProps = true;
452
+ deep(parentNode);
453
+
454
+ elementsToKeep.clear();
455
+
456
+ lockCount += 1;
457
+
458
+ return () => {
459
+ hiddenNodes.forEach((node) => {
460
+ const counterValue = counterMap.get(node) - 1;
461
+ const markerValue = markerCounter.get(node) - 1;
462
+
463
+ counterMap.set(node, counterValue);
464
+ markerCounter.set(node, markerValue);
465
+
466
+ if (!counterValue) {
467
+ if (uncontrolledNodes.has(node)) {
468
+ uncontrolledNodes.delete(node);
469
+ } else {
470
+ node.removeAttribute(controlAttribute);
471
+ }
786
472
  }
787
- klass.prototype._addPropertyEffect(hprop,
788
- klass.prototype.PROPERTY_EFFECT_TYPES.NOTIFY,
789
- {fn: createNotifyHostPropEffect()});
790
- }
791
- }
792
- }
793
473
 
794
- function createNotifyInstancePropEffect(instProp, userNotifyInstanceProp) {
795
- return function notifyInstanceProp(inst, prop, props) {
796
- userNotifyInstanceProp.call(inst.__templatizeOwner,
797
- inst, prop, props[prop]);
798
- };
799
- }
474
+ if (!markerValue) {
475
+ node.removeAttribute(markerName);
476
+ }
477
+ });
478
+
479
+ lockCount -= 1;
800
480
 
801
- function createNotifyHostPropEffect() {
802
- return function notifyHostProp(inst, prop, props) {
803
- inst.__dataHost._setPendingPropertyOrPath('_host_' + prop, props[prop], true, true);
481
+ if (!lockCount) {
482
+ // clear
483
+ counterMap = new WeakMap();
484
+ counterMap = new WeakMap();
485
+ uncontrolledNodes = new WeakMap();
486
+ markerMap = {};
487
+ }
804
488
  };
805
- }
489
+ };
490
+
491
+ /**
492
+ * Marks everything except given node(or nodes) as aria-hidden
493
+ * @param {Element | Element[]} originalTarget - elements to keep on the page
494
+ * @param {HTMLElement} [parentNode] - top element, defaults to document.body
495
+ * @param {String} [markerName] - a special attribute to mark every node
496
+ * @return {Function} undo command
497
+ */
498
+ const hideOthers = (originalTarget, parentNode = document.body, markerName = 'data-aria-hidden') => {
499
+ const targets = Array.from(Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
500
+
501
+ if (parentNode) {
502
+ // We should not hide ariaLive elements - https://github.com/theKashey/aria-hidden/issues/10
503
+ targets.push(...Array.from(parentNode.querySelectorAll('[aria-live]')));
504
+ }
806
505
 
506
+ return applyAttributeToOthers(targets, parentNode, markerName, 'aria-hidden');
507
+ };
807
508
 
808
509
  /**
809
- * Returns an anonymous `PropertyEffects` class bound to the
810
- * `<template>` provided. Instancing the class will result in the
811
- * template being stamped into a document fragment stored as the instance's
812
- * `root` property, after which it can be appended to the DOM.
813
- *
814
- * Templates may utilize all Polymer data-binding features as well as
815
- * declarative event listeners. Event listeners and inline computing
816
- * functions in the template will be called on the host of the template.
817
- *
818
- * The constructor returned takes a single argument dictionary of initial
819
- * property values to propagate into template bindings. Additionally
820
- * host properties can be forwarded in, and instance properties can be
821
- * notified out by providing optional callbacks in the `options` dictionary.
822
- *
823
- * Valid configuration in `options` are as follows:
824
- *
825
- * - `forwardHostProp(property, value)`: Called when a property referenced
826
- * in the template changed on the template's host. As this library does
827
- * not retain references to templates instanced by the user, it is the
828
- * templatize owner's responsibility to forward host property changes into
829
- * user-stamped instances. The `instance.forwardHostProp(property, value)`
830
- * method on the generated class should be called to forward host
831
- * properties into the template to prevent unnecessary property-changed
832
- * notifications. Any properties referenced in the template that are not
833
- * defined in `instanceProps` will be notified up to the template's host
834
- * automatically.
835
- * - `instanceProps`: Dictionary of property names that will be added
836
- * to the instance by the templatize owner. These properties shadow any
837
- * host properties, and changes within the template to these properties
838
- * will result in `notifyInstanceProp` being called.
839
- * - `mutableData`: When `true`, the generated class will skip strict
840
- * dirty-checking for objects and arrays (always consider them to be
841
- * "dirty").
842
- * - `notifyInstanceProp(instance, property, value)`: Called when
843
- * an instance property changes. Users may choose to call `notifyPath`
844
- * on e.g. the owner to notify the change.
845
- * - `parentModel`: When `true`, events handled by declarative event listeners
846
- * (`on-event="handler"`) will be decorated with a `model` property pointing
847
- * to the template instance that stamped it. It will also be returned
848
- * from `instance.parentModel` in cases where template instance nesting
849
- * causes an inner model to shadow an outer model.
850
- *
851
- * All callbacks are called bound to the `owner`. Any context
852
- * needed for the callbacks (such as references to `instances` stamped)
853
- * should be stored on the `owner` such that they can be retrieved via
854
- * `this`.
855
- *
856
- * When `options.forwardHostProp` is declared as an option, any properties
857
- * referenced in the template will be automatically forwarded from the host of
858
- * the `<template>` to instances, with the exception of any properties listed in
859
- * the `options.instanceProps` object. `instanceProps` are assumed to be
860
- * managed by the owner of the instances, either passed into the constructor
861
- * or set after the fact. Note, any properties passed into the constructor will
862
- * always be set to the instance (regardless of whether they would normally
863
- * be forwarded from the host).
864
- *
865
- * Note that `templatize()` can be run only once for a given `<template>`.
866
- * Further calls will result in an error. Also, there is a special
867
- * behavior if the template was duplicated through a mechanism such as
868
- * `<dom-repeat>` or `<test-fixture>`. In this case, all calls to
869
- * `templatize()` return the same class for all duplicates of a template.
870
- * The class returned from `templatize()` is generated only once using
871
- * the `options` from the first call. This means that any `options`
872
- * provided to subsequent calls will be ignored. Therefore, it is very
873
- * important not to close over any variables inside the callbacks. Also,
874
- * arrow functions must be avoided because they bind the outer `this`.
875
- * Inside the callbacks, any contextual information can be accessed
876
- * through `this`, which points to the `owner`.
510
+ * @license
511
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
512
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
513
+ */
514
+
515
+ /**
516
+ * A controller for handling modal state on the elements with `dialog` and `alertdialog` role.
517
+ * See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal
877
518
  *
878
- * @param {!HTMLTemplateElement} template Template to templatize
879
- * @param {Polymer_PropertyEffects=} owner Owner of the template instances;
880
- * any optional callbacks will be bound to this owner.
881
- * @param {Object=} options Options dictionary (see summary for details)
882
- * @return {function(new:TemplateInstanceBase, Object=)} Generated class bound
883
- * to the template provided
884
- * @suppress {invalidCasts}
519
+ * Note, the actual `role` and `aria-modal` attributes are supposed to be handled by the
520
+ * consumer web component. This is done in to ensure the controller only does one thing.
885
521
  */
886
- function templatize(template, owner, options) {
887
- // Under strictTemplatePolicy, the templatized element must be owned
888
- // by a (trusted) Polymer element, indicated by existence of _methodHost;
889
- // e.g. for dom-if & dom-repeat in main document, _methodHost is null
890
- if (strictTemplatePolicy && !findMethodHost(template)) {
891
- throw new Error('strictTemplatePolicy: template owner not trusted');
892
- }
893
- options = /** @type {!TemplatizeOptions} */(options || {});
894
- if (template.__templatizeOwner) {
895
- throw new Error('A <template> can only be templatized once');
522
+ class AriaModalController {
523
+ /**
524
+ * @param {HTMLElement} host
525
+ */
526
+ constructor(host, callback) {
527
+ /**
528
+ * The controller host element.
529
+ *
530
+ * @type {HTMLElement}
531
+ */
532
+ this.host = host;
533
+
534
+ /**
535
+ * The callback used to detect which element
536
+ * to use as a target. Defaults to the host.
537
+ *
538
+ * @type {Function}
539
+ */
540
+ this.callback = typeof callback === 'function' ? callback : () => host;
896
541
  }
897
- template.__templatizeOwner = owner;
898
- const ctor = owner ? owner.constructor : TemplateInstanceBase;
899
- let templateInfo = ctor._parseTemplate(template);
900
- // Get memoized base class for the prototypical template, which
901
- // includes property effects for binding template & forwarding
542
+
902
543
  /**
903
- * @constructor
904
- * @extends {TemplateInstanceBase}
544
+ * Make the controller host modal by hiding other elements from screen readers
545
+ * using `aria-hidden` attribute (can be replaced with `inert` in the future).
546
+ *
547
+ * The method name is chosen to align with the one provided by native `<dialog>`:
548
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal
905
549
  */
906
- let baseClass = templateInfo.templatizeInstanceClass;
907
- if (!baseClass) {
908
- baseClass = createTemplatizerClass(template, templateInfo, options);
909
- templateInfo.templatizeInstanceClass = baseClass;
550
+ showModal() {
551
+ const targets = this.callback();
552
+ this.__showOthers = hideOthers(targets);
910
553
  }
911
- const methodHost = findMethodHost(template);
912
- // Host property forwarding must be installed onto template instance
913
- addPropagateEffects(template, templateInfo, options, methodHost);
914
- // Subclass base class and add reference for this specific template
915
- /** @private */
916
- let klass = class TemplateInstance extends baseClass {};
917
- /** @override */
918
- klass.prototype._methodHost = methodHost;
919
- /** @override */
920
- klass.prototype.__dataHost = /** @type {!DataTemplate} */ (template);
921
- /** @override */
922
- klass.prototype.__templatizeOwner = /** @type {!Object} */ (owner);
923
- /** @override */
924
- klass.prototype.__hostProps = templateInfo.hostProps;
925
- klass = /** @type {function(new:TemplateInstanceBase)} */(klass); //eslint-disable-line no-self-assign
926
- return klass;
927
- }
928
554
 
929
- function warnOnUndeclaredProperties(templateInfo, options, methodHost) {
930
- const declaredProps = methodHost.constructor._properties;
931
- const {propertyEffects} = templateInfo;
932
- const {instanceProps} = options;
933
- for (let prop in propertyEffects) {
934
- // Ensure properties with template effects are declared on the outermost
935
- // host (`methodHost`), unless they are instance props or static functions
936
- if (!declaredProps[prop] && !(instanceProps && instanceProps[prop])) {
937
- const effects = propertyEffects[prop];
938
- for (let i=0; i<effects.length; i++) {
939
- const {part} = effects[i].info;
940
- if (!(part.signature && part.signature.static)) {
941
- console.warn(`Property '${prop}' used in template but not ` +
942
- `declared in 'properties'; attribute will not be observed.`);
943
- break;
944
- }
945
- }
555
+ /**
556
+ * Remove `aria-hidden` from other elements unless there are any other
557
+ * controller hosts on the page activated by using `showModal()` call.
558
+ */
559
+ close() {
560
+ if (this.__showOthers) {
561
+ this.__showOthers();
562
+ this.__showOthers = null;
946
563
  }
947
564
  }
948
565
  }
949
566
 
950
567
  /**
951
- * Returns the template "model" associated with a given element, which
952
- * serves as the binding scope for the template instance the element is
953
- * contained in. A template model is an instance of
954
- * `TemplateInstanceBase`, and should be used to manipulate data
955
- * associated with this template instance.
956
- *
957
- * Example:
958
- *
959
- * let model = modelForElement(el);
960
- * if (model.index < 10) {
961
- * model.set('item.checked', true);
962
- * }
963
- *
964
- * @param {HTMLElement} template The model will be returned for
965
- * elements stamped from this template (accepts either an HTMLTemplateElement)
966
- * or a `<dom-if>`/`<dom-repeat>` element when using `removeNestedTemplates`
967
- * optimization.
968
- * @param {Node=} node Node for which to return a template model.
969
- * @return {TemplateInstanceBase} Template instance representing the
970
- * binding scope for the element
568
+ * @license
569
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
570
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
971
571
  */
972
- function modelForElement(template, node) {
973
- let model;
974
- while (node) {
975
- // An element with a __templatizeInstance marks the top boundary
976
- // of a scope; walk up until we find one, and then ensure that
977
- // its __dataHost matches `this`, meaning this dom-repeat stamped it
978
- if ((model = node.__dataHost ? node : node.__templatizeInstance)) {
979
- // Found an element stamped by another template; keep walking up
980
- // from its __dataHost
981
- if (model.__dataHost != template) {
982
- node = model.__dataHost;
983
- } else {
984
- return model;
985
- }
572
+
573
+ /**
574
+ * A controller for saving a focused node and restoring focus to it later.
575
+ */
576
+ class FocusRestorationController {
577
+ /**
578
+ * Saves the given node as a target for restoring focus to
579
+ * when `restoreFocus()` is called. If no node is provided,
580
+ * the currently focused node in the DOM is saved as a target.
581
+ *
582
+ * @param {Node | null | undefined} focusNode
583
+ */
584
+ saveFocus(focusNode) {
585
+ this.focusNode = focusNode || getDeepActiveElement();
586
+ }
587
+
588
+ /**
589
+ * Restores focus to the target node that was saved previously with `saveFocus()`.
590
+ */
591
+ restoreFocus() {
592
+ const focusNode = this.focusNode;
593
+ if (!focusNode) {
594
+ return;
595
+ }
596
+
597
+ if (getDeepActiveElement() === document.body) {
598
+ // In Firefox and Safari, focusing the node synchronously
599
+ // doesn't work as expected when the overlay is closing on outside click.
600
+ // These browsers force focus to move to the body element and retain it
601
+ // there until the next event loop iteration.
602
+ setTimeout(() => focusNode.focus());
986
603
  } else {
987
- // Still in a template scope, keep going up until
988
- // a __templatizeInstance is found
989
- node = wrap(node).parentNode;
604
+ focusNode.focus();
990
605
  }
606
+
607
+ this.focusNode = null;
991
608
  }
992
- return null;
993
609
  }
994
610
 
995
611
  /**
996
612
  * @license
997
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
613
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
998
614
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
999
615
  */
1000
616
 
@@ -1026,6 +642,27 @@ class FocusTrapController {
1026
642
  this.__onKeyDown = this.__onKeyDown.bind(this);
1027
643
  }
1028
644
 
645
+ /**
646
+ * An array of tab-ordered focusable elements inside the trap node.
647
+ *
648
+ * @return {HTMLElement[]}
649
+ * @private
650
+ */
651
+ get __focusableElements() {
652
+ return getFocusableElements(this.__trapNode);
653
+ }
654
+
655
+ /**
656
+ * The index of the element inside the trap node that currently has focus.
657
+ *
658
+ * @return {HTMLElement | undefined}
659
+ * @private
660
+ */
661
+ get __focusedElementIndex() {
662
+ const focusableElements = this.__focusableElements;
663
+ return focusableElements.indexOf(focusableElements.filter(isElementFocused).pop());
664
+ }
665
+
1029
666
  hostConnected() {
1030
667
  document.addEventListener('keydown', this.__onKeyDown);
1031
668
  }
@@ -1124,1123 +761,845 @@ class FocusTrapController {
1124
761
  element.select();
1125
762
  }
1126
763
  }
1127
-
1128
- /**
1129
- * An array of tab-ordered focusable elements inside the trap node.
1130
- *
1131
- * @return {HTMLElement[]}
1132
- * @private
1133
- */
1134
- get __focusableElements() {
1135
- return getFocusableElements(this.__trapNode);
1136
- }
1137
-
1138
- /**
1139
- * The index of the element inside the trap node that currently has focus.
1140
- *
1141
- * @return {HTMLElement | undefined}
1142
- * @private
1143
- */
1144
- get __focusedElementIndex() {
1145
- const focusableElements = this.__focusableElements;
1146
- return focusableElements.indexOf(focusableElements.filter(isElementFocused).pop());
1147
- }
1148
764
  }
1149
765
 
1150
766
  /**
1151
767
  * @license
1152
- * Copyright (c) 2017 - 2022 Vaadin Ltd.
768
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
1153
769
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1154
770
  */
1155
771
 
1156
772
  /**
773
+ * @typedef ReactiveController
774
+ * @type {import('lit').ReactiveController}
775
+ */
776
+
777
+ /**
778
+ * A mixin for connecting controllers to the element.
1157
779
  *
1158
- * `<vaadin-overlay>` is a Web Component for creating overlays. The content of the overlay
1159
- * can be populated in two ways: imperatively by using renderer callback function and
1160
- * declaratively by using Polymer's Templates.
1161
- *
1162
- * ### Rendering
1163
- *
1164
- * By default, the overlay uses the content provided by using the renderer callback function.
1165
- *
1166
- * The renderer function provides `root`, `owner`, `model` arguments when applicable.
1167
- * Generate DOM content by using `model` object properties if needed, append it to the `root`
1168
- * element and control the state of the host element by accessing `owner`. Before generating new
1169
- * content, users are able to check if there is already content in `root` for reusing it.
1170
- *
1171
- * ```html
1172
- * <vaadin-overlay id="overlay"></vaadin-overlay>
1173
- * ```
1174
- * ```js
1175
- * const overlay = document.querySelector('#overlay');
1176
- * overlay.renderer = function(root) {
1177
- * root.textContent = "Overlay content";
1178
- * };
1179
- * ```
1180
- *
1181
- * Renderer is called on the opening of the overlay and each time the related model is updated.
1182
- * DOM generated during the renderer call can be reused
1183
- * in the next renderer call and will be provided with the `root` argument.
1184
- * On first call it will be empty.
1185
- *
1186
- * **NOTE:** when the renderer property is defined, the `<template>` content is not used.
1187
- *
1188
- * ### Templating
1189
- *
1190
- * Alternatively, the content can be provided with Polymer Template.
1191
- * Overlay finds the first child template and uses that in case renderer callback function
1192
- * is not provided. You can also set a custom template using the `template` property.
1193
- *
1194
- * After the content from the template is stamped, the `content` property
1195
- * points to the content container.
1196
- *
1197
- * The overlay provides `forwardHostProp` when calling
1198
- * `Polymer.Templatize.templatize` for the template, so that the bindings
1199
- * from the parent scope propagate to the content.
1200
- *
1201
- * ### Styling
1202
- *
1203
- * To style the overlay content, use styles in the parent scope:
1204
- *
1205
- * - If the overlay is used in a component, then the component styles
1206
- * apply the overlay content.
1207
- * - If the overlay is used in the global DOM scope, then global styles
1208
- * apply to the overlay content.
1209
- *
1210
- * See examples for styling the overlay content in the live demos.
1211
- *
1212
- * The following Shadow DOM parts are available for styling the overlay component itself:
1213
- *
1214
- * Part name | Description
1215
- * -----------|---------------------------------------------------------|
1216
- * `backdrop` | Backdrop of the overlay
1217
- * `overlay` | Container for position/sizing/alignment of the content
1218
- * `content` | Content of the overlay
1219
- *
1220
- * The following state attributes are available for styling:
1221
- *
1222
- * Attribute | Description | Part
1223
- * ---|---|---
1224
- * `opening` | Applied just after the overlay is attached to the DOM. You can apply a CSS @keyframe animation for this state. | `:host`
1225
- * `closing` | Applied just before the overlay is detached from the DOM. You can apply a CSS @keyframe animation for this state. | `:host`
1226
- *
1227
- * The following custom CSS properties are available for styling:
1228
- *
1229
- * Custom CSS property | Description | Default value
1230
- * ---|---|---
1231
- * `--vaadin-overlay-viewport-bottom` | Bottom offset of the visible viewport area | `0` or detected offset
1232
- *
1233
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
1234
- *
1235
- * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
1236
- * @fires {CustomEvent} vaadin-overlay-open - Fired after the overlay is opened.
1237
- * @fires {CustomEvent} vaadin-overlay-close - Fired before the overlay will be closed. If canceled the closing of the overlay is canceled as well.
1238
- * @fires {CustomEvent} vaadin-overlay-closing - Fired when the overlay will be closed.
1239
- * @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.
1240
- * @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.
1241
- *
1242
- * @extends HTMLElement
1243
- * @mixes ThemableMixin
1244
- * @mixes DirMixin
1245
- * @mixes ControllerMixin
780
+ * @polymerMixin
1246
781
  */
1247
- class Overlay extends ThemableMixin(DirMixin(ControllerMixin(PolymerElement))) {
1248
- static get template() {
1249
- return html`
1250
- <style>
1251
- :host {
1252
- z-index: 200;
1253
- position: fixed;
1254
-
1255
- /* Despite of what the names say, <vaadin-overlay> is just a container
1256
- for position/sizing/alignment. The actual overlay is the overlay part. */
782
+ const ControllerMixin = dedupingMixin((superClass) => {
783
+ // If the superclass extends from LitElement,
784
+ // use its own controllers implementation.
785
+ if (typeof superClass.prototype.addController === 'function') {
786
+ return superClass;
787
+ }
1257
788
 
1258
- /* Default position constraints: the entire viewport. Note: themes can
1259
- override this to introduce gaps between the overlay and the viewport. */
1260
- top: 0;
1261
- right: 0;
1262
- bottom: var(--vaadin-overlay-viewport-bottom);
1263
- left: 0;
789
+ return class ControllerMixinClass extends superClass {
790
+ constructor() {
791
+ super();
1264
792
 
1265
- /* Use flexbox alignment for the overlay part. */
1266
- display: flex;
1267
- flex-direction: column; /* makes dropdowns sizing easier */
1268
- /* Align to center by default. */
1269
- align-items: center;
1270
- justify-content: center;
793
+ /**
794
+ * @type {Set<ReactiveController>}
795
+ */
796
+ this.__controllers = new Set();
797
+ }
1271
798
 
1272
- /* Allow centering when max-width/max-height applies. */
1273
- margin: auto;
799
+ /** @protected */
800
+ connectedCallback() {
801
+ super.connectedCallback();
1274
802
 
1275
- /* The host is not clickable, only the overlay part is. */
1276
- pointer-events: none;
803
+ this.__controllers.forEach((c) => {
804
+ if (c.hostConnected) {
805
+ c.hostConnected();
806
+ }
807
+ });
808
+ }
1277
809
 
1278
- /* Remove tap highlight on touch devices. */
1279
- -webkit-tap-highlight-color: transparent;
810
+ /** @protected */
811
+ disconnectedCallback() {
812
+ super.disconnectedCallback();
1280
813
 
1281
- /* CSS API for host */
1282
- --vaadin-overlay-viewport-bottom: 0;
814
+ this.__controllers.forEach((c) => {
815
+ if (c.hostDisconnected) {
816
+ c.hostDisconnected();
1283
817
  }
818
+ });
819
+ }
1284
820
 
1285
- :host([hidden]),
1286
- :host(:not([opened]):not([closing])) {
1287
- display: none !important;
1288
- }
821
+ /**
822
+ * Registers a controller to participate in the element update cycle.
823
+ *
824
+ * @param {ReactiveController} controller
825
+ * @protected
826
+ */
827
+ addController(controller) {
828
+ this.__controllers.add(controller);
829
+ // Call hostConnected if a controller is added after the element is attached.
830
+ if (this.$ !== undefined && this.isConnected && controller.hostConnected) {
831
+ controller.hostConnected();
832
+ }
833
+ }
1289
834
 
1290
- [part='overlay'] {
1291
- -webkit-overflow-scrolling: touch;
1292
- overflow: auto;
1293
- pointer-events: auto;
835
+ /**
836
+ * Removes a controller from the element.
837
+ *
838
+ * @param {ReactiveController} controller
839
+ * @protected
840
+ */
841
+ removeController(controller) {
842
+ this.__controllers.delete(controller);
843
+ }
844
+ };
845
+ });
1294
846
 
1295
- /* Prevent overflowing the host in MSIE 11 */
1296
- max-width: 100%;
1297
- box-sizing: border-box;
847
+ /**
848
+ * @license
849
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
850
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
851
+ */
1298
852
 
1299
- -webkit-tap-highlight-color: initial; /* reenable tap highlight inside */
1300
- }
853
+ /**
854
+ * @polymerMixin
855
+ * @mixes ControllerMixin
856
+ */
857
+ const OverlayFocusMixin = (superClass) =>
858
+ class OverlayFocusMixin extends ControllerMixin(superClass) {
859
+ static get properties() {
860
+ return {
861
+ /**
862
+ * When true, opening the overlay moves focus to the first focusable child,
863
+ * or to the overlay part with tabindex if there are no focusable children.
864
+ * @attr {boolean} focus-trap
865
+ */
866
+ focusTrap: {
867
+ type: Boolean,
868
+ value: false,
869
+ },
1301
870
 
1302
- [part='backdrop'] {
1303
- z-index: -1;
1304
- content: '';
1305
- background: rgba(0, 0, 0, 0.5);
1306
- position: fixed;
1307
- top: 0;
1308
- left: 0;
1309
- bottom: 0;
1310
- right: 0;
1311
- pointer-events: auto;
1312
- }
1313
- </style>
1314
-
1315
- <div id="backdrop" part="backdrop" hidden$="[[!withBackdrop]]"></div>
1316
- <div part="overlay" id="overlay" tabindex="0">
1317
- <div part="content" id="content">
1318
- <slot></slot>
1319
- </div>
1320
- </div>
1321
- `;
1322
- }
871
+ /**
872
+ * Set to true to enable restoring of focus when overlay is closed.
873
+ * @attr {boolean} restore-focus-on-close
874
+ */
875
+ restoreFocusOnClose: {
876
+ type: Boolean,
877
+ value: false,
878
+ },
1323
879
 
1324
- static get is() {
1325
- return 'vaadin-overlay';
1326
- }
880
+ /**
881
+ * Set to specify the element which should be focused on overlay close,
882
+ * if `restoreFocusOnClose` is set to true.
883
+ * @type {HTMLElement}
884
+ */
885
+ restoreFocusNode: {
886
+ type: HTMLElement,
887
+ },
888
+ };
889
+ }
1327
890
 
1328
- static get properties() {
1329
- return {
1330
- /**
1331
- * When true, the overlay is visible and attached to body.
1332
- */
1333
- opened: {
1334
- type: Boolean,
1335
- notify: true,
1336
- observer: '_openedChanged',
1337
- reflectToAttribute: true,
1338
- },
891
+ constructor() {
892
+ super();
1339
893
 
1340
- /**
1341
- * Owner element passed with renderer function
1342
- * @type {HTMLElement}
1343
- */
1344
- owner: Element,
894
+ this.__ariaModalController = new AriaModalController(this);
895
+ this.__focusTrapController = new FocusTrapController(this);
896
+ this.__focusRestorationController = new FocusRestorationController();
897
+ }
1345
898
 
1346
- /**
1347
- * Custom function for rendering the content of the overlay.
1348
- * Receives three arguments:
1349
- *
1350
- * - `root` The root container DOM element. Append your content to it.
1351
- * - `owner` The host element of the renderer function.
1352
- * - `model` The object with the properties related with rendering.
1353
- * @type {OverlayRenderer | null | undefined}
1354
- */
1355
- renderer: Function,
899
+ /** @protected */
900
+ ready() {
901
+ super.ready();
1356
902
 
1357
- /**
1358
- * The template of the overlay content.
1359
- * @type {HTMLTemplateElement | null | undefined}
1360
- */
1361
- template: {
1362
- type: Object,
1363
- notify: true,
1364
- },
1365
-
1366
- /**
1367
- * References the content container after the template is stamped.
1368
- * @type {!HTMLElement | undefined}
1369
- */
1370
- content: {
1371
- type: Object,
1372
- notify: true,
1373
- },
1374
-
1375
- /**
1376
- * When true the overlay has backdrop on top of content when opened.
1377
- * @type {boolean}
1378
- */
1379
- withBackdrop: {
1380
- type: Boolean,
1381
- value: false,
1382
- reflectToAttribute: true,
1383
- },
1384
-
1385
- /**
1386
- * Object with properties that is passed to `renderer` function
1387
- */
1388
- model: Object,
1389
-
1390
- /**
1391
- * When true the overlay won't disable the main content, showing
1392
- * it doesn’t change the functionality of the user interface.
1393
- * @type {boolean}
1394
- */
1395
- modeless: {
1396
- type: Boolean,
1397
- value: false,
1398
- reflectToAttribute: true,
1399
- observer: '_modelessChanged',
1400
- },
1401
-
1402
- /**
1403
- * When set to true, the overlay is hidden. This also closes the overlay
1404
- * immediately in case there is a closing animation in progress.
1405
- * @type {boolean}
1406
- */
1407
- hidden: {
1408
- type: Boolean,
1409
- reflectToAttribute: true,
1410
- observer: '_hiddenChanged',
1411
- },
1412
-
1413
- /**
1414
- * When true move focus to the first focusable element in the overlay,
1415
- * or to the overlay if there are no focusable elements.
1416
- * @type {boolean}
1417
- */
1418
- focusTrap: {
1419
- type: Boolean,
1420
- value: false,
1421
- },
1422
-
1423
- /**
1424
- * Set to true to enable restoring of focus when overlay is closed.
1425
- * @type {boolean}
1426
- */
1427
- restoreFocusOnClose: {
1428
- type: Boolean,
1429
- value: false,
1430
- },
1431
-
1432
- /**
1433
- * Set to specify the element which should be focused on overlay close,
1434
- * if `restoreFocusOnClose` is set to true.
1435
- * @type {HTMLElement}
1436
- */
1437
- restoreFocusNode: {
1438
- type: HTMLElement,
1439
- },
1440
-
1441
- /** @private */
1442
- _mouseDownInside: {
1443
- type: Boolean,
1444
- },
1445
-
1446
- /** @private */
1447
- _mouseUpInside: {
1448
- type: Boolean,
1449
- },
1450
-
1451
- /** @private */
1452
- _instance: {
1453
- type: Object,
1454
- },
1455
-
1456
- /** @private */
1457
- _originalContentPart: Object,
1458
-
1459
- /** @private */
1460
- _contentNodes: Array,
1461
-
1462
- /** @private */
1463
- _oldOwner: Element,
1464
-
1465
- /** @private */
1466
- _oldModel: Object,
1467
-
1468
- /** @private */
1469
- _oldTemplate: Object,
1470
-
1471
- /** @private */
1472
- _oldRenderer: Object,
1473
-
1474
- /** @private */
1475
- _oldOpened: Boolean,
1476
- };
1477
- }
1478
-
1479
- static get observers() {
1480
- return ['_templateOrRendererChanged(template, renderer, owner, model, opened)'];
1481
- }
1482
-
1483
- constructor() {
1484
- super();
1485
- this._boundMouseDownListener = this._mouseDownListener.bind(this);
1486
- this._boundMouseUpListener = this._mouseUpListener.bind(this);
1487
- this._boundOutsideClickListener = this._outsideClickListener.bind(this);
1488
- this._boundKeydownListener = this._keydownListener.bind(this);
1489
-
1490
- this._observer = new FlattenedNodesObserver(this, (info) => {
1491
- this._setTemplateFromNodes(info.addedNodes);
1492
- });
1493
-
1494
- // Listener for preventing closing of the paper-dialog and all components extending `iron-overlay-behavior`.
1495
- this._boundIronOverlayCanceledListener = this._ironOverlayCanceled.bind(this);
1496
-
1497
- /* c8 ignore next 3 */
1498
- if (isIOS) {
1499
- this._boundIosResizeListener = () => this._detectIosNavbar();
903
+ this.addController(this.__ariaModalController);
904
+ this.addController(this.__focusTrapController);
905
+ this.addController(this.__focusRestorationController);
1500
906
  }
1501
907
 
1502
- this.__focusTrapController = new FocusTrapController(this);
1503
- }
1504
-
1505
- /** @protected */
1506
- ready() {
1507
- super.ready();
1508
-
1509
- this._observer.flush();
1510
-
1511
- // Need to add dummy click listeners to this and the backdrop or else
1512
- // the document click event listener (_outsideClickListener) may never
1513
- // get invoked on iOS Safari (reproducible in <vaadin-dialog>
1514
- // and <vaadin-context-menu>).
1515
- this.addEventListener('click', () => {});
1516
- this.$.backdrop.addEventListener('click', () => {});
1517
-
1518
- this.addController(this.__focusTrapController);
1519
- }
908
+ /**
909
+ * Release focus and restore focus after the overlay is closed.
910
+ *
911
+ * @protected
912
+ */
913
+ _resetFocus() {
914
+ if (this.focusTrap) {
915
+ this.__ariaModalController.close();
916
+ this.__focusTrapController.releaseFocus();
917
+ }
1520
918
 
1521
- /** @private */
1522
- _detectIosNavbar() {
1523
- /* c8 ignore next 15 */
1524
- if (!this.opened) {
1525
- return;
919
+ if (this.restoreFocusOnClose && this._shouldRestoreFocus()) {
920
+ this.__focusRestorationController.restoreFocus();
921
+ }
1526
922
  }
1527
923
 
1528
- const innerHeight = window.innerHeight;
1529
- const innerWidth = window.innerWidth;
1530
-
1531
- const landscape = innerWidth > innerHeight;
1532
-
1533
- const clientHeight = document.documentElement.clientHeight;
1534
-
1535
- if (landscape && clientHeight > innerHeight) {
1536
- this.style.setProperty('--vaadin-overlay-viewport-bottom', `${clientHeight - innerHeight}px`);
1537
- } else {
1538
- this.style.setProperty('--vaadin-overlay-viewport-bottom', '0');
924
+ /**
925
+ * Save the previously focused node when the overlay starts to open.
926
+ *
927
+ * @protected
928
+ */
929
+ _saveFocus() {
930
+ if (this.restoreFocusOnClose) {
931
+ this.__focusRestorationController.saveFocus(this.restoreFocusNode);
932
+ }
1539
933
  }
1540
- }
1541
934
 
1542
- /**
1543
- * @param {!Array<!Element>} nodes
1544
- * @protected
1545
- */
1546
- _setTemplateFromNodes(nodes) {
1547
- this.template = nodes.find((node) => node.localName && node.localName === 'template') || this.template;
1548
- }
1549
-
1550
- /**
1551
- * @param {Event=} sourceEvent
1552
- * @event vaadin-overlay-close
1553
- * fired before the `vaadin-overlay` will be closed. If canceled the closing of the overlay is canceled as well.
1554
- */
1555
- close(sourceEvent) {
1556
- const evt = new CustomEvent('vaadin-overlay-close', {
1557
- bubbles: true,
1558
- cancelable: true,
1559
- detail: { sourceEvent },
1560
- });
1561
- this.dispatchEvent(evt);
1562
- if (!evt.defaultPrevented) {
1563
- this.opened = false;
935
+ /**
936
+ * Trap focus within the overlay after opening has completed.
937
+ *
938
+ * @protected
939
+ */
940
+ _trapFocus() {
941
+ if (this.focusTrap) {
942
+ this.__ariaModalController.showModal();
943
+ this.__focusTrapController.trapFocus(this.$.overlay);
944
+ }
1564
945
  }
1565
- }
1566
946
 
1567
- /** @protected */
1568
- connectedCallback() {
1569
- super.connectedCallback();
1570
-
1571
- /* c8 ignore next 3 */
1572
- if (this._boundIosResizeListener) {
1573
- this._detectIosNavbar();
1574
- window.addEventListener('resize', this._boundIosResizeListener);
947
+ /**
948
+ * Returns true if focus is still inside the overlay or on the body element,
949
+ * otherwise false.
950
+ *
951
+ * Focus shouldn't be restored if it's been moved elsewhere by another
952
+ * component or as a result of a user interaction e.g. the user clicked
953
+ * on a button outside the overlay while the overlay was open.
954
+ *
955
+ * @protected
956
+ * @return {boolean}
957
+ */
958
+ _shouldRestoreFocus() {
959
+ const activeElement = getDeepActiveElement();
960
+ return activeElement === document.body || this._deepContains(activeElement);
1575
961
  }
1576
- }
1577
962
 
1578
- /** @protected */
1579
- disconnectedCallback() {
1580
- super.disconnectedCallback();
1581
-
1582
- /* c8 ignore next 3 */
1583
- if (this._boundIosResizeListener) {
1584
- window.removeEventListener('resize', this._boundIosResizeListener);
963
+ /**
964
+ * Returns true if the overlay contains the given node,
965
+ * including those within shadow DOM trees.
966
+ *
967
+ * @param {Node} node
968
+ * @return {boolean}
969
+ * @protected
970
+ */
971
+ _deepContains(node) {
972
+ if (this.contains(node)) {
973
+ return true;
974
+ }
975
+ let n = node;
976
+ const doc = node.ownerDocument;
977
+ // Walk from node to `this` or `document`
978
+ while (n && n !== doc && n !== this) {
979
+ n = n.parentNode || n.host;
980
+ }
981
+ return n === this;
1585
982
  }
1586
- }
983
+ };
1587
984
 
1588
- /**
1589
- * Requests an update for the content of the overlay.
1590
- * While performing the update, it invokes the renderer passed in the `renderer` property.
1591
- *
1592
- * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
1593
- */
1594
- requestContentUpdate() {
1595
- if (this.renderer) {
1596
- this.renderer.call(this.owner, this.content, this.owner, this.model);
1597
- }
1598
- }
985
+ /**
986
+ * @license
987
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
988
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
989
+ */
1599
990
 
1600
- /** @private */
1601
- _ironOverlayCanceled(event) {
1602
- event.preventDefault();
1603
- }
991
+ /**
992
+ * Returns all attached overlays in visual stacking order.
993
+ * @private
994
+ */
995
+ const getAttachedInstances = () =>
996
+ Array.from(document.body.children)
997
+ .filter((el) => el instanceof HTMLElement && el._hasOverlayStackMixin && !el.hasAttribute('closing'))
998
+ .sort((a, b) => a.__zIndex - b.__zIndex || 0);
1604
999
 
1605
- /** @private */
1606
- _mouseDownListener(event) {
1607
- this._mouseDownInside = event.composedPath().indexOf(this.$.overlay) >= 0;
1608
- }
1000
+ /**
1001
+ * Returns true if the overlay is the last one in the opened overlays stack.
1002
+ * @param {HTMLElement} overlay
1003
+ * @return {boolean}
1004
+ * @protected
1005
+ */
1006
+ const isLastOverlay = (overlay) => overlay === getAttachedInstances().pop();
1609
1007
 
1610
- /** @private */
1611
- _mouseUpListener(event) {
1612
- this._mouseUpInside = event.composedPath().indexOf(this.$.overlay) >= 0;
1613
- }
1008
+ /**
1009
+ * @polymerMixin
1010
+ */
1011
+ const OverlayStackMixin = (superClass) =>
1012
+ class OverlayStackMixin extends superClass {
1013
+ constructor() {
1014
+ super();
1614
1015
 
1615
- /**
1616
- * We need to listen on 'click' / 'tap' event and capture it and close the overlay before
1617
- * propagating the event to the listener in the button. Otherwise, if the clicked button would call
1618
- * open(), this would happen: https://www.youtube.com/watch?v=Z86V_ICUCD4
1619
- *
1620
- * @event vaadin-overlay-outside-click
1621
- * fired before the `vaadin-overlay` will be closed on outside click. If canceled the closing of the overlay is canceled as well.
1622
- *
1623
- * @private
1624
- */
1625
- _outsideClickListener(event) {
1626
- if (event.composedPath().includes(this.$.overlay) || this._mouseDownInside || this._mouseUpInside) {
1627
- this._mouseDownInside = false;
1628
- this._mouseUpInside = false;
1629
- return;
1630
- }
1631
- if (!this._last) {
1632
- return;
1016
+ this._hasOverlayStackMixin = true;
1633
1017
  }
1634
1018
 
1635
- const evt = new CustomEvent('vaadin-overlay-outside-click', {
1636
- bubbles: true,
1637
- cancelable: true,
1638
- detail: { sourceEvent: event },
1639
- });
1640
- this.dispatchEvent(evt);
1641
-
1642
- if (this.opened && !evt.defaultPrevented) {
1643
- this.close(event);
1019
+ /**
1020
+ * Returns true if this is the last one in the opened overlays stack.
1021
+ *
1022
+ * @return {boolean}
1023
+ * @protected
1024
+ */
1025
+ get _last() {
1026
+ return isLastOverlay(this);
1644
1027
  }
1645
- }
1646
1028
 
1647
- /**
1648
- * @event vaadin-overlay-escape-press
1649
- * fired before the `vaadin-overlay` will be closed on ESC button press. If canceled the closing of the overlay is canceled as well.
1650
- *
1651
- * @private
1652
- */
1653
- _keydownListener(event) {
1654
- if (!this._last) {
1655
- return;
1029
+ /**
1030
+ * Brings the overlay as visually the frontmost one.
1031
+ */
1032
+ bringToFront() {
1033
+ let zIndex = '';
1034
+ const frontmost = getAttachedInstances()
1035
+ .filter((o) => o !== this)
1036
+ .pop();
1037
+ if (frontmost) {
1038
+ const frontmostZIndex = frontmost.__zIndex;
1039
+ zIndex = frontmostZIndex + 1;
1040
+ }
1041
+ this.style.zIndex = zIndex;
1042
+ this.__zIndex = zIndex || parseFloat(getComputedStyle(this).zIndex);
1656
1043
  }
1657
1044
 
1658
- // Only close modeless overlay on Esc press when it contains focus
1659
- if (this.modeless && !event.composedPath().includes(this.$.overlay)) {
1660
- return;
1661
- }
1045
+ /** @protected */
1046
+ _enterModalState() {
1047
+ if (document.body.style.pointerEvents !== 'none') {
1048
+ // Set body pointer-events to 'none' to disable mouse interactions with
1049
+ // other document nodes.
1050
+ this._previousDocumentPointerEvents = document.body.style.pointerEvents;
1051
+ document.body.style.pointerEvents = 'none';
1052
+ }
1662
1053
 
1663
- if (event.key === 'Escape') {
1664
- const evt = new CustomEvent('vaadin-overlay-escape-press', {
1665
- bubbles: true,
1666
- cancelable: true,
1667
- detail: { sourceEvent: event },
1054
+ // Disable pointer events in other attached overlays
1055
+ getAttachedInstances().forEach((el) => {
1056
+ if (el !== this) {
1057
+ el.$.overlay.style.pointerEvents = 'none';
1058
+ }
1668
1059
  });
1669
- this.dispatchEvent(evt);
1060
+ }
1670
1061
 
1671
- if (this.opened && !evt.defaultPrevented) {
1672
- this.close(event);
1062
+ /** @protected */
1063
+ _exitModalState() {
1064
+ if (this._previousDocumentPointerEvents !== undefined) {
1065
+ // Restore body pointer-events
1066
+ document.body.style.pointerEvents = this._previousDocumentPointerEvents;
1067
+ delete this._previousDocumentPointerEvents;
1673
1068
  }
1674
- }
1675
- }
1676
1069
 
1677
- /** @protected */
1678
- _ensureTemplatized() {
1679
- this._setTemplateFromNodes(Array.from(this.children));
1680
- }
1070
+ // Restore pointer events in the previous overlay(s)
1071
+ const instances = getAttachedInstances();
1681
1072
 
1682
- /**
1683
- * @event vaadin-overlay-open
1684
- * fired after the `vaadin-overlay` is opened.
1685
- *
1686
- * @private
1687
- */
1688
- _openedChanged(opened, wasOpened) {
1689
- if (!this._instance) {
1690
- this._ensureTemplatized();
1073
+ let el;
1074
+ // Use instances.pop() to ensure the reverse order
1075
+ while ((el = instances.pop())) {
1076
+ if (el === this) {
1077
+ // Skip the current instance
1078
+ continue;
1079
+ }
1080
+ el.$.overlay.style.removeProperty('pointer-events');
1081
+ if (!el.modeless) {
1082
+ // Stop after the last modal
1083
+ break;
1084
+ }
1085
+ }
1691
1086
  }
1087
+ };
1692
1088
 
1693
- if (opened) {
1694
- // Store focused node.
1695
- this.__restoreFocusNode = this._getActiveElement();
1696
- this._animatedOpening();
1089
+ /**
1090
+ * @license
1091
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
1092
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1093
+ */
1697
1094
 
1698
- afterNextRender(this, () => {
1699
- if (this.focusTrap) {
1700
- this.__focusTrapController.trapFocus(this.$.overlay);
1701
- }
1095
+ /**
1096
+ * @polymerMixin
1097
+ * @mixes OverlayFocusMixin
1098
+ * @mixes OverlayStackMixin
1099
+ */
1100
+ const OverlayMixin = (superClass) =>
1101
+ class OverlayMixin extends OverlayFocusMixin(OverlayStackMixin(superClass)) {
1102
+ static get properties() {
1103
+ return {
1104
+ /**
1105
+ * When true, the overlay is visible and attached to body.
1106
+ */
1107
+ opened: {
1108
+ type: Boolean,
1109
+ notify: true,
1110
+ observer: '_openedChanged',
1111
+ reflectToAttribute: true,
1112
+ },
1702
1113
 
1703
- const evt = new CustomEvent('vaadin-overlay-open', { bubbles: true });
1704
- this.dispatchEvent(evt);
1705
- });
1114
+ /**
1115
+ * Owner element passed with renderer function
1116
+ * @type {HTMLElement}
1117
+ */
1118
+ owner: {
1119
+ type: Object,
1120
+ },
1706
1121
 
1707
- document.addEventListener('keydown', this._boundKeydownListener);
1122
+ /**
1123
+ * Object with properties that is passed to `renderer` function
1124
+ */
1125
+ model: {
1126
+ type: Object,
1127
+ },
1708
1128
 
1709
- if (!this.modeless) {
1710
- this._addGlobalListeners();
1711
- }
1712
- } else if (wasOpened) {
1713
- if (this.focusTrap) {
1714
- this.__focusTrapController.releaseFocus();
1715
- }
1129
+ /**
1130
+ * Custom function for rendering the content of the overlay.
1131
+ * Receives three arguments:
1132
+ *
1133
+ * - `root` The root container DOM element. Append your content to it.
1134
+ * - `owner` The host element of the renderer function.
1135
+ * - `model` The object with the properties related with rendering.
1136
+ * @type {OverlayRenderer | null | undefined}
1137
+ */
1138
+ renderer: {
1139
+ type: Object,
1140
+ },
1716
1141
 
1717
- this._animatedClosing();
1142
+ /**
1143
+ * When true the overlay won't disable the main content, showing
1144
+ * it doesn't change the functionality of the user interface.
1145
+ * @type {boolean}
1146
+ */
1147
+ modeless: {
1148
+ type: Boolean,
1149
+ value: false,
1150
+ reflectToAttribute: true,
1151
+ observer: '_modelessChanged',
1152
+ },
1718
1153
 
1719
- document.removeEventListener('keydown', this._boundKeydownListener);
1154
+ /**
1155
+ * When set to true, the overlay is hidden. This also closes the overlay
1156
+ * immediately in case there is a closing animation in progress.
1157
+ * @type {boolean}
1158
+ */
1159
+ hidden: {
1160
+ type: Boolean,
1161
+ reflectToAttribute: true,
1162
+ observer: '_hiddenChanged',
1163
+ },
1720
1164
 
1721
- if (!this.modeless) {
1722
- this._removeGlobalListeners();
1723
- }
1165
+ /**
1166
+ * When true the overlay has backdrop on top of content when opened.
1167
+ * @type {boolean}
1168
+ */
1169
+ withBackdrop: {
1170
+ type: Boolean,
1171
+ value: false,
1172
+ reflectToAttribute: true,
1173
+ },
1174
+ };
1724
1175
  }
1725
- }
1726
1176
 
1727
- /** @private */
1728
- _hiddenChanged(hidden) {
1729
- if (hidden && this.hasAttribute('closing')) {
1730
- this._flushAnimation('closing');
1177
+ static get observers() {
1178
+ return ['_rendererOrDataChanged(renderer, owner, model, opened)'];
1731
1179
  }
1732
- }
1733
1180
 
1734
- /**
1735
- * @return {boolean}
1736
- * @protected
1737
- */
1738
- _shouldAnimate() {
1739
- const name = getComputedStyle(this).getPropertyValue('animation-name');
1740
- const hidden = getComputedStyle(this).getPropertyValue('display') === 'none';
1741
- return !hidden && name && name !== 'none';
1742
- }
1181
+ constructor() {
1182
+ super();
1743
1183
 
1744
- /**
1745
- * @param {string} type
1746
- * @param {Function} callback
1747
- * @protected
1748
- */
1749
- _enqueueAnimation(type, callback) {
1750
- const handler = `__${type}Handler`;
1751
- const listener = (event) => {
1752
- if (event && event.target !== this) {
1753
- return;
1754
- }
1755
- callback();
1756
- this.removeEventListener('animationend', listener);
1757
- delete this[handler];
1758
- };
1759
- this[handler] = listener;
1760
- this.addEventListener('animationend', listener);
1761
- }
1184
+ this._boundMouseDownListener = this._mouseDownListener.bind(this);
1185
+ this._boundMouseUpListener = this._mouseUpListener.bind(this);
1186
+ this._boundOutsideClickListener = this._outsideClickListener.bind(this);
1187
+ this._boundKeydownListener = this._keydownListener.bind(this);
1762
1188
 
1763
- /**
1764
- * @param {string} type
1765
- * @protected
1766
- */
1767
- _flushAnimation(type) {
1768
- const handler = `__${type}Handler`;
1769
- if (typeof this[handler] === 'function') {
1770
- this[handler]();
1189
+ /* c8 ignore next 3 */
1190
+ if (isIOS) {
1191
+ this._boundIosResizeListener = () => this._detectIosNavbar();
1192
+ }
1771
1193
  }
1772
- }
1773
1194
 
1774
- /** @protected */
1775
- _animatedOpening() {
1776
- if (this.parentNode === document.body && this.hasAttribute('closing')) {
1777
- this._flushAnimation('closing');
1778
- }
1779
- this._attachOverlay();
1780
- if (!this.modeless) {
1781
- this._enterModalState();
1782
- }
1783
- this.setAttribute('opening', '');
1195
+ /** @protected */
1196
+ ready() {
1197
+ super.ready();
1784
1198
 
1785
- if (this._shouldAnimate()) {
1786
- this._enqueueAnimation('opening', () => {
1787
- this._finishOpening();
1788
- });
1789
- } else {
1790
- this._finishOpening();
1199
+ // Need to add dummy click listeners to this and the backdrop or else
1200
+ // the document click event listener (_outsideClickListener) may never
1201
+ // get invoked on iOS Safari (reproducible in <vaadin-dialog>
1202
+ // and <vaadin-context-menu>).
1203
+ this.addEventListener('click', () => {});
1204
+ this.$.backdrop.addEventListener('click', () => {});
1791
1205
  }
1792
- }
1793
-
1794
- /** @protected */
1795
- _attachOverlay() {
1796
- this._placeholder = document.createComment('vaadin-overlay-placeholder');
1797
- this.parentNode.insertBefore(this._placeholder, this);
1798
- document.body.appendChild(this);
1799
- this.bringToFront();
1800
- }
1801
-
1802
- /** @protected */
1803
- _finishOpening() {
1804
- document.addEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
1805
- this.removeAttribute('opening');
1806
- }
1807
1206
 
1808
- /** @protected */
1809
- _finishClosing() {
1810
- document.removeEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
1811
- this._detachOverlay();
1812
- this.$.overlay.style.removeProperty('pointer-events');
1813
- this.removeAttribute('closing');
1814
- }
1207
+ /** @protected */
1208
+ connectedCallback() {
1209
+ super.connectedCallback();
1815
1210
 
1816
- /**
1817
- * @event vaadin-overlay-closing
1818
- * Fired when the overlay will be closed.
1819
- *
1820
- * @protected
1821
- */
1822
- _animatedClosing() {
1823
- if (this.hasAttribute('opening')) {
1824
- this._flushAnimation('opening');
1825
- }
1826
- if (this._placeholder) {
1827
- this._exitModalState();
1828
-
1829
- // Use this.restoreFocusNode if specified, otherwise fallback to the node
1830
- // which was focused before opening the overlay.
1831
- const restoreFocusNode = this.restoreFocusNode || this.__restoreFocusNode;
1832
-
1833
- if (this.restoreFocusOnClose && restoreFocusNode) {
1834
- // If the activeElement is `<body>` or inside the overlay,
1835
- // we are allowed to restore the focus. In all the other
1836
- // cases focus might have been moved elsewhere by another
1837
- // component or by the user interaction (e.g. click on a
1838
- // button outside the overlay).
1839
- const activeElement = this._getActiveElement();
1840
-
1841
- if (activeElement === document.body || this._deepContains(activeElement)) {
1842
- // Focusing the restoreFocusNode doesn't always work synchronously on Firefox and Safari
1843
- // (e.g. combo-box overlay close on outside click).
1844
- setTimeout(() => restoreFocusNode.focus());
1845
- }
1846
- this.__restoreFocusNode = null;
1211
+ /* c8 ignore next 3 */
1212
+ if (this._boundIosResizeListener) {
1213
+ this._detectIosNavbar();
1214
+ window.addEventListener('resize', this._boundIosResizeListener);
1847
1215
  }
1216
+ }
1848
1217
 
1849
- this.setAttribute('closing', '');
1850
- this.dispatchEvent(new CustomEvent('vaadin-overlay-closing'));
1218
+ /** @protected */
1219
+ disconnectedCallback() {
1220
+ super.disconnectedCallback();
1851
1221
 
1852
- if (this._shouldAnimate()) {
1853
- this._enqueueAnimation('closing', () => {
1854
- this._finishClosing();
1855
- });
1856
- } else {
1857
- this._finishClosing();
1222
+ /* c8 ignore next 3 */
1223
+ if (this._boundIosResizeListener) {
1224
+ window.removeEventListener('resize', this._boundIosResizeListener);
1858
1225
  }
1859
1226
  }
1860
- }
1861
-
1862
- /** @protected */
1863
- _detachOverlay() {
1864
- this._placeholder.parentNode.insertBefore(this, this._placeholder);
1865
- this._placeholder.parentNode.removeChild(this._placeholder);
1866
- }
1867
1227
 
1868
- /**
1869
- * Returns all attached overlays in visual stacking order.
1870
- * @private
1871
- */
1872
- static get __attachedInstances() {
1873
- return Array.from(document.body.children)
1874
- .filter((el) => el instanceof Overlay && !el.hasAttribute('closing'))
1875
- .sort((a, b) => a.__zIndex - b.__zIndex || 0);
1876
- }
1877
-
1878
- /**
1879
- * Returns true if this is the last one in the opened overlays stack
1880
- * @return {boolean}
1881
- * @protected
1882
- */
1883
- get _last() {
1884
- return this === Overlay.__attachedInstances.pop();
1885
- }
1228
+ /**
1229
+ * Requests an update for the content of the overlay.
1230
+ * While performing the update, it invokes the renderer passed in the `renderer` property.
1231
+ *
1232
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
1233
+ */
1234
+ requestContentUpdate() {
1235
+ if (this.renderer) {
1236
+ this.renderer.call(this.owner, this, this.owner, this.model);
1237
+ }
1238
+ }
1886
1239
 
1887
- /** @private */
1888
- _modelessChanged(modeless) {
1889
- if (!modeless) {
1890
- if (this.opened) {
1891
- this._addGlobalListeners();
1892
- this._enterModalState();
1240
+ /**
1241
+ * @param {Event=} sourceEvent
1242
+ */
1243
+ close(sourceEvent) {
1244
+ const evt = new CustomEvent('vaadin-overlay-close', {
1245
+ bubbles: true,
1246
+ cancelable: true,
1247
+ detail: { sourceEvent },
1248
+ });
1249
+ this.dispatchEvent(evt);
1250
+ if (!evt.defaultPrevented) {
1251
+ this.opened = false;
1893
1252
  }
1894
- } else {
1895
- this._removeGlobalListeners();
1896
- this._exitModalState();
1897
1253
  }
1898
- }
1899
1254
 
1900
- /** @protected */
1901
- _addGlobalListeners() {
1902
- document.addEventListener('mousedown', this._boundMouseDownListener);
1903
- document.addEventListener('mouseup', this._boundMouseUpListener);
1904
- // Firefox leaks click to document on contextmenu even if prevented
1905
- // https://bugzilla.mozilla.org/show_bug.cgi?id=990614
1906
- document.documentElement.addEventListener('click', this._boundOutsideClickListener, true);
1907
- }
1255
+ /** @private */
1256
+ _detectIosNavbar() {
1257
+ /* c8 ignore next 15 */
1258
+ if (!this.opened) {
1259
+ return;
1260
+ }
1908
1261
 
1909
- /** @protected */
1910
- _enterModalState() {
1911
- if (document.body.style.pointerEvents !== 'none') {
1912
- // Set body pointer-events to 'none' to disable mouse interactions with
1913
- // other document nodes.
1914
- this._previousDocumentPointerEvents = document.body.style.pointerEvents;
1915
- document.body.style.pointerEvents = 'none';
1916
- }
1262
+ const innerHeight = window.innerHeight;
1263
+ const innerWidth = window.innerWidth;
1917
1264
 
1918
- // Disable pointer events in other attached overlays
1919
- Overlay.__attachedInstances.forEach((el) => {
1920
- if (el !== this) {
1921
- el.shadowRoot.querySelector('[part="overlay"]').style.pointerEvents = 'none';
1922
- }
1923
- });
1924
- }
1265
+ const landscape = innerWidth > innerHeight;
1925
1266
 
1926
- /** @protected */
1927
- _removeGlobalListeners() {
1928
- document.removeEventListener('mousedown', this._boundMouseDownListener);
1929
- document.removeEventListener('mouseup', this._boundMouseUpListener);
1930
- document.documentElement.removeEventListener('click', this._boundOutsideClickListener, true);
1931
- }
1267
+ const clientHeight = document.documentElement.clientHeight;
1932
1268
 
1933
- /** @protected */
1934
- _exitModalState() {
1935
- if (this._previousDocumentPointerEvents !== undefined) {
1936
- // Restore body pointer-events
1937
- document.body.style.pointerEvents = this._previousDocumentPointerEvents;
1938
- delete this._previousDocumentPointerEvents;
1939
- }
1940
-
1941
- // Restore pointer events in the previous overlay(s)
1942
- const instances = Overlay.__attachedInstances;
1943
- let el;
1944
- // Use instances.pop() to ensure the reverse order
1945
- while ((el = instances.pop())) {
1946
- if (el === this) {
1947
- // Skip the current instance
1948
- continue;
1949
- }
1950
- el.shadowRoot.querySelector('[part="overlay"]').style.removeProperty('pointer-events');
1951
- if (!el.modeless) {
1952
- // Stop after the last modal
1953
- break;
1269
+ if (landscape && clientHeight > innerHeight) {
1270
+ this.style.setProperty('--vaadin-overlay-viewport-bottom', `${clientHeight - innerHeight}px`);
1271
+ } else {
1272
+ this.style.setProperty('--vaadin-overlay-viewport-bottom', '0');
1954
1273
  }
1955
1274
  }
1956
- }
1957
1275
 
1958
- /** @protected */
1959
- _removeOldContent() {
1960
- if (!this.content || !this._contentNodes) {
1961
- return;
1276
+ /** @private */
1277
+ _addGlobalListeners() {
1278
+ document.addEventListener('mousedown', this._boundMouseDownListener);
1279
+ document.addEventListener('mouseup', this._boundMouseUpListener);
1280
+ // Firefox leaks click to document on contextmenu even if prevented
1281
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=990614
1282
+ document.documentElement.addEventListener('click', this._boundOutsideClickListener, true);
1962
1283
  }
1963
1284
 
1964
- this._observer.disconnect();
1285
+ /** @private */
1286
+ _removeGlobalListeners() {
1287
+ document.removeEventListener('mousedown', this._boundMouseDownListener);
1288
+ document.removeEventListener('mouseup', this._boundMouseUpListener);
1289
+ document.documentElement.removeEventListener('click', this._boundOutsideClickListener, true);
1290
+ }
1965
1291
 
1966
- this._contentNodes.forEach((node) => {
1967
- if (node.parentNode === this.content) {
1968
- this.content.removeChild(node);
1292
+ /** @private */
1293
+ _rendererOrDataChanged(renderer, owner, model, opened) {
1294
+ const ownerOrModelChanged = this._oldOwner !== owner || this._oldModel !== model;
1295
+ this._oldModel = model;
1296
+ this._oldOwner = owner;
1297
+
1298
+ const rendererChanged = this._oldRenderer !== renderer;
1299
+ this._oldRenderer = renderer;
1300
+
1301
+ const openedChanged = this._oldOpened !== opened;
1302
+ this._oldOpened = opened;
1303
+
1304
+ if (rendererChanged) {
1305
+ this.innerHTML = '';
1306
+ // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
1307
+ // When clearing the rendered content, this part needs to be manually disposed of.
1308
+ // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
1309
+ delete this._$litPart$;
1969
1310
  }
1970
- });
1971
1311
 
1972
- if (this._originalContentPart) {
1973
- // Restore the original <div part="content">
1974
- this.$.content.parentNode.replaceChild(this._originalContentPart, this.$.content);
1975
- this.$.content = this._originalContentPart;
1976
- this._originalContentPart = undefined;
1312
+ if (opened && renderer && (rendererChanged || openedChanged || ownerOrModelChanged)) {
1313
+ this.requestContentUpdate();
1314
+ }
1977
1315
  }
1978
1316
 
1979
- this._observer.connect();
1317
+ /** @private */
1318
+ _modelessChanged(modeless) {
1319
+ if (!modeless) {
1320
+ if (this.opened) {
1321
+ this._addGlobalListeners();
1322
+ this._enterModalState();
1323
+ }
1324
+ } else {
1325
+ this._removeGlobalListeners();
1326
+ this._exitModalState();
1327
+ }
1328
+ }
1980
1329
 
1981
- this._contentNodes = undefined;
1982
- this.content = undefined;
1983
- }
1330
+ /** @private */
1331
+ _openedChanged(opened, wasOpened) {
1332
+ if (opened) {
1333
+ this._saveFocus();
1984
1334
 
1985
- /**
1986
- * @param {!HTMLTemplateElement} template
1987
- * @protected
1988
- */
1989
- _stampOverlayTemplate(template) {
1990
- this._removeOldContent();
1991
-
1992
- if (!template._Templatizer) {
1993
- template._Templatizer = templatize(template, this, {
1994
- forwardHostProp(prop, value) {
1995
- if (this._instance) {
1996
- this._instance.forwardHostProp(prop, value);
1997
- }
1998
- },
1999
- });
2000
- }
1335
+ this._animatedOpening();
2001
1336
 
2002
- this._instance = new template._Templatizer({});
2003
- this._contentNodes = Array.from(this._instance.root.childNodes);
1337
+ afterNextRender(this, () => {
1338
+ this._trapFocus();
2004
1339
 
2005
- const templateRoot = template._templateRoot || (template._templateRoot = template.getRootNode());
1340
+ const evt = new CustomEvent('vaadin-overlay-open', { bubbles: true });
1341
+ this.dispatchEvent(evt);
1342
+ });
2006
1343
 
2007
- if (templateRoot !== document) {
2008
- if (!this.$.content.shadowRoot) {
2009
- this.$.content.attachShadow({ mode: 'open' });
2010
- }
1344
+ document.addEventListener('keydown', this._boundKeydownListener);
2011
1345
 
2012
- let scopeCssText = Array.from(templateRoot.querySelectorAll('style')).reduce(
2013
- (result, style) => result + style.textContent,
2014
- '',
2015
- );
1346
+ if (!this.modeless) {
1347
+ this._addGlobalListeners();
1348
+ }
1349
+ } else if (wasOpened) {
1350
+ this._resetFocus();
2016
1351
 
2017
- // The overlay root’s :host styles should not apply inside the overlay
2018
- scopeCssText = scopeCssText.replace(/:host/g, ':host-nomatch');
1352
+ this._animatedClosing();
2019
1353
 
2020
- if (scopeCssText) {
2021
- // Append a style to the content shadowRoot
2022
- const style = document.createElement('style');
2023
- style.textContent = scopeCssText;
2024
- this.$.content.shadowRoot.appendChild(style);
2025
- this._contentNodes.unshift(style);
2026
- }
1354
+ document.removeEventListener('keydown', this._boundKeydownListener);
2027
1355
 
2028
- this.$.content.shadowRoot.appendChild(this._instance.root);
2029
- this.content = this.$.content.shadowRoot;
2030
- } else {
2031
- this.appendChild(this._instance.root);
2032
- this.content = this;
1356
+ if (!this.modeless) {
1357
+ this._removeGlobalListeners();
1358
+ }
1359
+ }
2033
1360
  }
2034
- }
2035
1361
 
2036
- /** @private */
2037
- _removeNewRendererOrTemplate(template, oldTemplate, renderer, oldRenderer) {
2038
- if (template !== oldTemplate) {
2039
- this.template = undefined;
2040
- } else if (renderer !== oldRenderer) {
2041
- this.renderer = undefined;
1362
+ /** @private */
1363
+ _hiddenChanged(hidden) {
1364
+ if (hidden && this.hasAttribute('closing')) {
1365
+ this._flushAnimation('closing');
1366
+ }
2042
1367
  }
2043
- }
2044
1368
 
2045
- /** @private */
2046
- // eslint-disable-next-line max-params
2047
- _templateOrRendererChanged(template, renderer, owner, model, opened) {
2048
- if (template && renderer) {
2049
- this._removeNewRendererOrTemplate(template, this._oldTemplate, renderer, this._oldRenderer);
2050
- throw new Error('You should only use either a renderer or a template for overlay content');
1369
+ /**
1370
+ * @return {boolean}
1371
+ * @private
1372
+ */
1373
+ _shouldAnimate() {
1374
+ const style = getComputedStyle(this);
1375
+ const name = style.getPropertyValue('animation-name');
1376
+ const hidden = style.getPropertyValue('display') === 'none';
1377
+ return !hidden && name && name !== 'none';
2051
1378
  }
2052
1379
 
2053
- const ownerOrModelChanged = this._oldOwner !== owner || this._oldModel !== model;
2054
- this._oldModel = model;
2055
- this._oldOwner = owner;
2056
-
2057
- const templateChanged = this._oldTemplate !== template;
2058
- this._oldTemplate = template;
2059
-
2060
- const rendererChanged = this._oldRenderer !== renderer;
2061
- this._oldRenderer = renderer;
2062
-
2063
- const openedChanged = this._oldOpened !== opened;
2064
- this._oldOpened = opened;
2065
-
2066
- if (rendererChanged) {
2067
- this.content = this;
2068
- this.content.innerHTML = '';
2069
- // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
2070
- // When clearing the rendered content, this part needs to be manually disposed of.
2071
- // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
2072
- delete this.content._$litPart$;
1380
+ /**
1381
+ * @param {string} type
1382
+ * @param {Function} callback
1383
+ * @private
1384
+ */
1385
+ _enqueueAnimation(type, callback) {
1386
+ const handler = `__${type}Handler`;
1387
+ const listener = (event) => {
1388
+ if (event && event.target !== this) {
1389
+ return;
1390
+ }
1391
+ callback();
1392
+ this.removeEventListener('animationend', listener);
1393
+ delete this[handler];
1394
+ };
1395
+ this[handler] = listener;
1396
+ this.addEventListener('animationend', listener);
2073
1397
  }
2074
1398
 
2075
- if (template && templateChanged) {
2076
- this._stampOverlayTemplate(template);
2077
- } else if (renderer && (rendererChanged || openedChanged || ownerOrModelChanged)) {
2078
- if (opened) {
2079
- this.requestContentUpdate();
1399
+ /**
1400
+ * @param {string} type
1401
+ * @protected
1402
+ */
1403
+ _flushAnimation(type) {
1404
+ const handler = `__${type}Handler`;
1405
+ if (typeof this[handler] === 'function') {
1406
+ this[handler]();
2080
1407
  }
2081
1408
  }
2082
- }
2083
1409
 
2084
- /**
2085
- * @return {!Element}
2086
- * @protected
2087
- */
2088
- _getActiveElement() {
2089
- // Document.activeElement can be null
2090
- // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
2091
- let active = document.activeElement || document.body;
2092
- while (active.shadowRoot && active.shadowRoot.activeElement) {
2093
- active = active.shadowRoot.activeElement;
2094
- }
2095
- return active;
2096
- }
1410
+ /** @private */
1411
+ _animatedOpening() {
1412
+ if (this.parentNode === document.body && this.hasAttribute('closing')) {
1413
+ this._flushAnimation('closing');
1414
+ }
1415
+ this._attachOverlay();
1416
+ if (!this.modeless) {
1417
+ this._enterModalState();
1418
+ }
1419
+ this.setAttribute('opening', '');
2097
1420
 
2098
- /**
2099
- * @param {!Node} node
2100
- * @return {boolean}
2101
- * @protected
2102
- */
2103
- _deepContains(node) {
2104
- if (this.contains(node)) {
2105
- return true;
1421
+ if (this._shouldAnimate()) {
1422
+ this._enqueueAnimation('opening', () => {
1423
+ this._finishOpening();
1424
+ });
1425
+ } else {
1426
+ this._finishOpening();
1427
+ }
2106
1428
  }
2107
- let n = node;
2108
- const doc = node.ownerDocument;
2109
- // Walk from node to `this` or `document`
2110
- while (n && n !== doc && n !== this) {
2111
- n = n.parentNode || n.host;
1429
+
1430
+ /** @private */
1431
+ _attachOverlay() {
1432
+ this._placeholder = document.createComment('vaadin-overlay-placeholder');
1433
+ this.parentNode.insertBefore(this._placeholder, this);
1434
+ document.body.appendChild(this);
1435
+ this.bringToFront();
2112
1436
  }
2113
- return n === this;
2114
- }
2115
1437
 
2116
- /**
2117
- * Brings the overlay as visually the frontmost one
2118
- */
2119
- bringToFront() {
2120
- let zIndex = '';
2121
- const frontmost = Overlay.__attachedInstances.filter((o) => o !== this).pop();
2122
- if (frontmost) {
2123
- const frontmostZIndex = frontmost.__zIndex;
2124
- zIndex = frontmostZIndex + 1;
2125
- }
2126
- this.style.zIndex = zIndex;
2127
- this.__zIndex = zIndex || parseFloat(getComputedStyle(this).zIndex);
2128
- }
2129
- }
1438
+ /** @private */
1439
+ _finishOpening() {
1440
+ this.removeAttribute('opening');
1441
+ }
2130
1442
 
2131
- customElements.define(Overlay.is, Overlay);
1443
+ /** @private */
1444
+ _finishClosing() {
1445
+ this._detachOverlay();
1446
+ this.$.overlay.style.removeProperty('pointer-events');
1447
+ this.removeAttribute('closing');
1448
+ this.dispatchEvent(new CustomEvent('vaadin-overlay-closed'));
1449
+ }
2132
1450
 
2133
- /**
2134
- * @license
2135
- * Copyright (c) 2017 - 2022 Vaadin Ltd.
2136
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2137
- */
1451
+ /** @private */
1452
+ _animatedClosing() {
1453
+ if (this.hasAttribute('opening')) {
1454
+ this._flushAnimation('opening');
1455
+ }
1456
+ if (this._placeholder) {
1457
+ this._exitModalState();
1458
+ this.setAttribute('closing', '');
1459
+ this.dispatchEvent(new CustomEvent('vaadin-overlay-closing'));
1460
+
1461
+ if (this._shouldAnimate()) {
1462
+ this._enqueueAnimation('closing', () => {
1463
+ this._finishClosing();
1464
+ });
1465
+ } else {
1466
+ this._finishClosing();
1467
+ }
1468
+ }
1469
+ }
2138
1470
 
2139
- const menuOverlayCore = i`
2140
- :host([opening]),
2141
- :host([closing]) {
2142
- animation: 0.14s lumo-overlay-dummy-animation;
2143
- }
1471
+ /** @private */
1472
+ _detachOverlay() {
1473
+ this._placeholder.parentNode.insertBefore(this, this._placeholder);
1474
+ this._placeholder.parentNode.removeChild(this._placeholder);
1475
+ }
2144
1476
 
2145
- [part='overlay'] {
2146
- will-change: opacity, transform;
2147
- }
1477
+ /** @private */
1478
+ _mouseDownListener(event) {
1479
+ this._mouseDownInside = event.composedPath().indexOf(this.$.overlay) >= 0;
1480
+ }
2148
1481
 
2149
- :host([opening]) [part='overlay'] {
2150
- animation: 0.1s lumo-menu-overlay-enter ease-out both;
2151
- }
1482
+ /** @private */
1483
+ _mouseUpListener(event) {
1484
+ this._mouseUpInside = event.composedPath().indexOf(this.$.overlay) >= 0;
1485
+ }
2152
1486
 
2153
- @keyframes lumo-menu-overlay-enter {
2154
- 0% {
2155
- opacity: 0;
2156
- transform: translateY(-4px);
1487
+ /**
1488
+ * Whether to close the overlay on outside click or not.
1489
+ * Override this method to customize the closing logic.
1490
+ *
1491
+ * @param {Event} _event
1492
+ * @return {boolean}
1493
+ * @protected
1494
+ */
1495
+ _shouldCloseOnOutsideClick(_event) {
1496
+ return this._last;
2157
1497
  }
2158
- }
2159
1498
 
2160
- :host([closing]) [part='overlay'] {
2161
- animation: 0.1s lumo-menu-overlay-exit both;
2162
- }
1499
+ /**
1500
+ * Outside click listener used in capture phase to close the overlay before
1501
+ * propagating the event to the listener on the element that triggered it.
1502
+ * Otherwise, calling `open()` would result in closing and re-opening.
1503
+ *
1504
+ * @private
1505
+ */
1506
+ _outsideClickListener(event) {
1507
+ if (event.composedPath().includes(this.$.overlay) || this._mouseDownInside || this._mouseUpInside) {
1508
+ this._mouseDownInside = false;
1509
+ this._mouseUpInside = false;
1510
+ return;
1511
+ }
2163
1512
 
2164
- @keyframes lumo-menu-overlay-exit {
2165
- 100% {
2166
- opacity: 0;
2167
- }
2168
- }
2169
- `;
1513
+ if (!this._shouldCloseOnOutsideClick(event)) {
1514
+ return;
1515
+ }
2170
1516
 
2171
- registerStyles('', menuOverlayCore, { moduleId: 'lumo-menu-overlay-core' });
1517
+ const evt = new CustomEvent('vaadin-overlay-outside-click', {
1518
+ bubbles: true,
1519
+ cancelable: true,
1520
+ detail: { sourceEvent: event },
1521
+ });
1522
+ this.dispatchEvent(evt);
2172
1523
 
2173
- const menuOverlayExt = i`
2174
- /* Small viewport (bottom sheet) styles */
2175
- /* Use direct media queries instead of the state attributes ([phone] and [fullscreen]) provided by the elements */
2176
- @media (max-width: 420px), (max-height: 420px) {
2177
- :host {
2178
- top: 0 !important;
2179
- right: 0 !important;
2180
- bottom: var(--vaadin-overlay-viewport-bottom, 0) !important;
2181
- left: 0 !important;
2182
- align-items: stretch !important;
2183
- justify-content: flex-end !important;
1524
+ if (this.opened && !evt.defaultPrevented) {
1525
+ this.close(event);
1526
+ }
2184
1527
  }
2185
1528
 
2186
- [part='overlay'] {
2187
- max-height: 50vh;
2188
- width: 100vw;
2189
- border-radius: 0;
2190
- box-shadow: var(--lumo-box-shadow-xl);
2191
- }
1529
+ /**
1530
+ * Listener used to close whe overlay on Escape press, if it is the last one.
1531
+ * @private
1532
+ */
1533
+ _keydownListener(event) {
1534
+ if (!this._last) {
1535
+ return;
1536
+ }
2192
1537
 
2193
- /* The content part scrolls instead of the overlay part, because of the gradient fade-out */
2194
- [part='content'] {
2195
- padding: 30px var(--lumo-space-m);
2196
- max-height: inherit;
2197
- box-sizing: border-box;
2198
- -webkit-overflow-scrolling: touch;
2199
- overflow: auto;
2200
- -webkit-mask-image: linear-gradient(transparent, #000 40px, #000 calc(100% - 40px), transparent);
2201
- mask-image: linear-gradient(transparent, #000 40px, #000 calc(100% - 40px), transparent);
2202
- }
1538
+ // Only close modeless overlay on Esc press when it contains focus
1539
+ if (this.modeless && !event.composedPath().includes(this.$.overlay)) {
1540
+ return;
1541
+ }
2203
1542
 
2204
- [part='backdrop'] {
2205
- display: block;
1543
+ if (event.key === 'Escape') {
1544
+ const evt = new CustomEvent('vaadin-overlay-escape-press', {
1545
+ bubbles: true,
1546
+ cancelable: true,
1547
+ detail: { sourceEvent: event },
1548
+ });
1549
+ this.dispatchEvent(evt);
1550
+
1551
+ if (this.opened && !evt.defaultPrevented) {
1552
+ this.close(event);
1553
+ }
1554
+ }
2206
1555
  }
1556
+ };
2207
1557
 
2208
- /* Animations */
1558
+ /**
1559
+ * @license
1560
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
1561
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1562
+ */
2209
1563
 
2210
- :host([opening]) [part='overlay'] {
2211
- animation: 0.2s lumo-mobile-menu-overlay-enter cubic-bezier(0.215, 0.61, 0.355, 1) both;
2212
- }
1564
+ /**
1565
+ * Returns an array of ancestor root nodes for the given node.
1566
+ *
1567
+ * A root node is either a document node or a document fragment node (Shadow Root).
1568
+ * The array is collected by a bottom-up DOM traversing that starts with the given node
1569
+ * and involves both the light DOM and ancestor shadow DOM trees.
1570
+ *
1571
+ * @param {Node} node
1572
+ * @return {Node[]}
1573
+ */
1574
+ function getAncestorRootNodes(node) {
1575
+ const result = [];
2213
1576
 
2214
- :host([closing]),
2215
- :host([closing]) [part='backdrop'] {
2216
- animation-delay: 0.14s;
1577
+ while (node) {
1578
+ if (node.nodeType === Node.DOCUMENT_NODE) {
1579
+ result.push(node);
1580
+ break;
2217
1581
  }
2218
1582
 
2219
- :host([closing]) [part='overlay'] {
2220
- animation: 0.14s 0.14s lumo-mobile-menu-overlay-exit cubic-bezier(0.55, 0.055, 0.675, 0.19) both;
1583
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
1584
+ result.push(node);
1585
+ node = node.host;
1586
+ continue;
2221
1587
  }
2222
- }
2223
1588
 
2224
- @keyframes lumo-mobile-menu-overlay-enter {
2225
- 0% {
2226
- transform: translateY(150%);
1589
+ if (node.assignedSlot) {
1590
+ node = node.assignedSlot;
1591
+ continue;
2227
1592
  }
2228
- }
2229
1593
 
2230
- @keyframes lumo-mobile-menu-overlay-exit {
2231
- 100% {
2232
- transform: translateY(150%);
2233
- }
1594
+ node = node.parentNode;
2234
1595
  }
2235
- `;
2236
-
2237
- const menuOverlay = [overlay, menuOverlayCore, menuOverlayExt];
2238
1596
 
2239
- registerStyles('', menuOverlay, { moduleId: 'lumo-menu-overlay' });
1597
+ return result;
1598
+ }
2240
1599
 
2241
1600
  /**
2242
1601
  * @license
2243
- * Copyright (c) 2017 - 2022 Vaadin Ltd.
1602
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
2244
1603
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2245
1604
  */
2246
1605
 
@@ -2430,10 +1789,6 @@ const PositionMixin = (superClass) =>
2430
1789
  }
2431
1790
  }
2432
1791
 
2433
- get __isRTL() {
2434
- return this.getAttribute('dir') === 'rtl';
2435
- }
2436
-
2437
1792
  __positionSettingsChanged() {
2438
1793
  this._updatePosition();
2439
1794
  }
@@ -2457,9 +1812,9 @@ const PositionMixin = (superClass) =>
2457
1812
  const shouldAlignStartVertically = this.__shouldAlignStartVertically(targetRect);
2458
1813
  this.style.justifyContent = shouldAlignStartVertically ? 'flex-start' : 'flex-end';
2459
1814
 
2460
- const shouldAlignStartHorizontally = this.__shouldAlignStartHorizontally(targetRect, this.__isRTL);
2461
- const flexStart =
2462
- (!this.__isRTL && shouldAlignStartHorizontally) || (this.__isRTL && !shouldAlignStartHorizontally);
1815
+ const isRTL = this.__isRTL;
1816
+ const shouldAlignStartHorizontally = this.__shouldAlignStartHorizontally(targetRect, isRTL);
1817
+ const flexStart = (!isRTL && shouldAlignStartHorizontally) || (isRTL && !shouldAlignStartHorizontally);
2463
1818
  this.style.alignItems = flexStart ? 'flex-start' : 'flex-end';
2464
1819
 
2465
1820
  // Get the overlay rect after possible overlay alignment changes
@@ -2618,7 +1973,73 @@ const PositionMixin = (superClass) =>
2618
1973
 
2619
1974
  /**
2620
1975
  * @license
2621
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
1976
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
1977
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
1978
+ */
1979
+
1980
+ const overlayStyles = i`
1981
+ :host {
1982
+ z-index: 200;
1983
+ position: fixed;
1984
+
1985
+ /* Despite of what the names say, <vaadin-overlay> is just a container
1986
+ for position/sizing/alignment. The actual overlay is the overlay part. */
1987
+
1988
+ /* Default position constraints: the entire viewport. Note: themes can
1989
+ override this to introduce gaps between the overlay and the viewport. */
1990
+ inset: 0;
1991
+ bottom: var(--vaadin-overlay-viewport-bottom);
1992
+
1993
+ /* Use flexbox alignment for the overlay part. */
1994
+ display: flex;
1995
+ flex-direction: column; /* makes dropdowns sizing easier */
1996
+ /* Align to center by default. */
1997
+ align-items: center;
1998
+ justify-content: center;
1999
+
2000
+ /* Allow centering when max-width/max-height applies. */
2001
+ margin: auto;
2002
+
2003
+ /* The host is not clickable, only the overlay part is. */
2004
+ pointer-events: none;
2005
+
2006
+ /* Remove tap highlight on touch devices. */
2007
+ -webkit-tap-highlight-color: transparent;
2008
+
2009
+ /* CSS API for host */
2010
+ --vaadin-overlay-viewport-bottom: 0;
2011
+ }
2012
+
2013
+ :host([hidden]),
2014
+ :host(:not([opened]):not([closing])) {
2015
+ display: none !important;
2016
+ }
2017
+
2018
+ [part='overlay'] {
2019
+ -webkit-overflow-scrolling: touch;
2020
+ overflow: auto;
2021
+ pointer-events: auto;
2022
+
2023
+ /* Prevent overflowing the host */
2024
+ max-width: 100%;
2025
+ box-sizing: border-box;
2026
+
2027
+ -webkit-tap-highlight-color: initial; /* reenable tap highlight inside */
2028
+ }
2029
+
2030
+ [part='backdrop'] {
2031
+ z-index: -1;
2032
+ content: '';
2033
+ background: rgba(0, 0, 0, 0.5);
2034
+ position: fixed;
2035
+ inset: 0;
2036
+ pointer-events: auto;
2037
+ }
2038
+ `;
2039
+
2040
+ /**
2041
+ * @license
2042
+ * Copyright (c) 2021 - 2023 Vaadin Ltd.
2622
2043
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
2623
2044
  */
2624
2045
 
@@ -2655,4 +2076,4 @@ class VirtualKeyboardController {
2655
2076
  }
2656
2077
  }
2657
2078
 
2658
- export { Overlay as O, PositionMixin as P, VirtualKeyboardController as V, OptionalMutableData as a, modelForElement as b, afterNextRender as c, menuOverlayCore as d, menuOverlay as m, overlay as o, templatize as t };
2079
+ export { OverlayMixin as O, PositionMixin as P, VirtualKeyboardController as V, afterNextRender as a, overlay as b, menuOverlayCore as c, hideOthers as h, menuOverlay as m, overlayStyles as o };