@vaadin/overlay 24.1.0 → 24.2.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/overlay",
3
- "version": "24.1.0",
3
+ "version": "24.2.0-alpha1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -36,17 +36,17 @@
36
36
  "dependencies": {
37
37
  "@open-wc/dedupe-mixin": "^1.3.0",
38
38
  "@polymer/polymer": "^3.0.0",
39
- "@vaadin/a11y-base": "~24.1.0",
40
- "@vaadin/component-base": "~24.1.0",
41
- "@vaadin/vaadin-lumo-styles": "~24.1.0",
42
- "@vaadin/vaadin-material-styles": "~24.1.0",
43
- "@vaadin/vaadin-themable-mixin": "~24.1.0"
39
+ "@vaadin/a11y-base": "24.2.0-alpha1",
40
+ "@vaadin/component-base": "24.2.0-alpha1",
41
+ "@vaadin/vaadin-lumo-styles": "24.2.0-alpha1",
42
+ "@vaadin/vaadin-material-styles": "24.2.0-alpha1",
43
+ "@vaadin/vaadin-themable-mixin": "24.2.0-alpha1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@esm-bundle/chai": "^4.3.4",
47
- "@vaadin/testing-helpers": "^0.4.0",
47
+ "@vaadin/testing-helpers": "^0.4.2",
48
48
  "lit": "^2.0.0",
49
49
  "sinon": "^13.0.2"
50
50
  },
51
- "gitHead": "7fdfe7d5ceb4c305a894f8e9dc11e5b7d04cf1f2"
51
+ "gitHead": "0dbb118320203ab6c0c07450a3e718815367589f"
52
52
  }
@@ -36,12 +36,28 @@ export declare class OverlayFocusMixinClass {
36
36
  protected _resetFocus(): void;
37
37
 
38
38
  /**
39
- * Store previously focused node when the overlay starts to open.
39
+ * Save the previously focused node when the overlay starts to open.
40
40
  */
41
- protected _storeFocus(): void;
41
+ protected _saveFocus(): void;
42
42
 
43
43
  /**
44
44
  * Trap focus within the overlay after opening has completed.
45
45
  */
46
46
  protected _trapFocus(): void;
47
+
48
+ /**
49
+ * Returns true if focus is still inside the overlay or on the body element,
50
+ * otherwise false.
51
+ *
52
+ * Focus shouldn't be restored if it's been moved elsewhere by another
53
+ * component or as a result of a user interaction e.g. the user clicked
54
+ * on a button outside the overlay while the overlay was open.
55
+ */
56
+ protected _shouldRestoreFocus(): boolean;
57
+
58
+ /**
59
+ * Returns true if the overlay contains the given node,
60
+ * including those within shadow DOM trees.
61
+ */
62
+ protected _deepContains(node: Node): boolean;
47
63
  }
@@ -4,6 +4,7 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { AriaModalController } from '@vaadin/a11y-base/src/aria-modal-controller.js';
7
+ import { FocusRestorationController } from '@vaadin/a11y-base/src/focus-restoration-controller.js';
7
8
  import { FocusTrapController } from '@vaadin/a11y-base/src/focus-trap-controller.js';
8
9
  import { getDeepActiveElement } from '@vaadin/a11y-base/src/focus-utils.js';
9
10
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
@@ -51,6 +52,7 @@ export const OverlayFocusMixin = (superClass) =>
51
52
 
52
53
  this.__ariaModalController = new AriaModalController(this);
53
54
  this.__focusTrapController = new FocusTrapController(this);
55
+ this.__focusRestorationController = new FocusRestorationController();
54
56
  }
55
57
 
56
58
  /** @protected */
@@ -59,6 +61,7 @@ export const OverlayFocusMixin = (superClass) =>
59
61
 
60
62
  this.addController(this.__ariaModalController);
61
63
  this.addController(this.__focusTrapController);
64
+ this.addController(this.__focusRestorationController);
62
65
  }
63
66
 
64
67
  /**
@@ -72,19 +75,19 @@ export const OverlayFocusMixin = (superClass) =>
72
75
  this.__focusTrapController.releaseFocus();
73
76
  }
74
77
 
75
- if (this.restoreFocusOnClose) {
76
- this.__restoreFocus();
78
+ if (this.restoreFocusOnClose && this._shouldRestoreFocus()) {
79
+ this.__focusRestorationController.restoreFocus();
77
80
  }
78
81
  }
79
82
 
80
83
  /**
81
- * Store previously focused node when the overlay starts to open.
84
+ * Save the previously focused node when the overlay starts to open.
82
85
  *
83
86
  * @protected
84
87
  */
85
- _storeFocus() {
88
+ _saveFocus() {
86
89
  if (this.restoreFocusOnClose) {
87
- this.__storeFocus();
90
+ this.__focusRestorationController.saveFocus(this.restoreFocusNode);
88
91
  }
89
92
  }
90
93
 
@@ -100,49 +103,40 @@ export const OverlayFocusMixin = (superClass) =>
100
103
  }
101
104
  }
102
105
 
103
- /** @private */
104
- __storeFocus() {
105
- // Store the focused node.
106
- this.__restoreFocusNode = getDeepActiveElement();
107
-
108
- // Determine and store the node that has the `focus-ring` attribute
109
- // in order to restore the attribute when the overlay closes.
110
- const restoreFocusNode = this.restoreFocusNode || this.__restoreFocusNode;
111
- if (restoreFocusNode) {
112
- const restoreFocusNodeHost = (restoreFocusNode.assignedSlot || restoreFocusNode).getRootNode().host;
113
- this.__restoreFocusRingNode = [restoreFocusNode, restoreFocusNodeHost].find((node) => {
114
- return node && node.hasAttribute('focus-ring');
115
- });
116
- }
117
- }
118
-
119
- /** @private */
120
- __restoreFocus() {
121
- // If the activeElement is `<body>` or inside the overlay,
122
- // we are allowed to restore the focus. In all the other
123
- // cases focus might have been moved elsewhere by another
124
- // component or by the user interaction (e.g. click on a
125
- // button outside the overlay).
106
+ /**
107
+ * Returns true if focus is still inside the overlay or on the body element,
108
+ * otherwise false.
109
+ *
110
+ * Focus shouldn't be restored if it's been moved elsewhere by another
111
+ * component or as a result of a user interaction e.g. the user clicked
112
+ * on a button outside the overlay while the overlay was open.
113
+ *
114
+ * @protected
115
+ * @return {boolean}
116
+ */
117
+ _shouldRestoreFocus() {
126
118
  const activeElement = getDeepActiveElement();
127
- if (activeElement !== document.body && !this._deepContains(activeElement)) {
128
- return;
129
- }
119
+ return activeElement === document.body || this._deepContains(activeElement);
120
+ }
130
121
 
131
- // Use restoreFocusNode if specified, otherwise fallback to the node
132
- // which was focused before opening the overlay.
133
- const restoreFocusNode = this.restoreFocusNode || this.__restoreFocusNode;
134
- if (restoreFocusNode) {
135
- // Focusing the restoreFocusNode doesn't always work synchronously on Firefox and Safari
136
- // (e.g. combo-box overlay close on outside click).
137
- setTimeout(() => restoreFocusNode.focus());
138
- this.__restoreFocusNode = null;
122
+ /**
123
+ * Returns true if the overlay contains the given node,
124
+ * including those within shadow DOM trees.
125
+ *
126
+ * @param {Node} node
127
+ * @return {boolean}
128
+ * @protected
129
+ */
130
+ _deepContains(node) {
131
+ if (this.contains(node)) {
132
+ return true;
139
133
  }
140
-
141
- // Restore the `focus-ring` attribute if it was present
142
- // when the overlay was opening.
143
- if (this.__restoreFocusRingNode) {
144
- this.__restoreFocusRingNode.setAttribute('focus-ring', '');
145
- this.__restoreFocusRingNode = null;
134
+ let n = node;
135
+ const doc = node.ownerDocument;
136
+ // Walk from node to `this` or `document`
137
+ while (n && n !== doc && n !== this) {
138
+ n = n.parentNode || n.host;
146
139
  }
140
+ return n === this;
147
141
  }
148
142
  };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+
8
+ export declare function OverlayStackMixin<T extends Constructor<HTMLElement>>(
9
+ base: T,
10
+ ): Constructor<OverlayStackMixinClass> & T;
11
+
12
+ export declare class OverlayStackMixinClass {
13
+ /**
14
+ * Returns true if this is the last one in the opened overlays stack.
15
+ */
16
+ protected readonly _last: boolean;
17
+
18
+ /**
19
+ * Brings the overlay as visually the frontmost one.
20
+ */
21
+ bringToFront(): void;
22
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * Returns all attached overlays in visual stacking order.
9
+ * @private
10
+ */
11
+ const getAttachedInstances = () =>
12
+ Array.from(document.body.children)
13
+ .filter((el) => el instanceof HTMLElement && el._hasOverlayStackMixin && !el.hasAttribute('closing'))
14
+ .sort((a, b) => a.__zIndex - b.__zIndex || 0);
15
+
16
+ /**
17
+ * Returns true if the overlay is the last one in the opened overlays stack.
18
+ * @param {HTMLElement} overlay
19
+ * @return {boolean}
20
+ * @protected
21
+ */
22
+ export const isLastOverlay = (overlay) => overlay === getAttachedInstances().pop();
23
+
24
+ /**
25
+ * @polymerMixin
26
+ */
27
+ export const OverlayStackMixin = (superClass) =>
28
+ class OverlayStackMixin extends superClass {
29
+ constructor() {
30
+ super();
31
+
32
+ this._hasOverlayStackMixin = true;
33
+ }
34
+
35
+ /**
36
+ * Returns true if this is the last one in the opened overlays stack.
37
+ *
38
+ * @return {boolean}
39
+ * @protected
40
+ */
41
+ get _last() {
42
+ return isLastOverlay(this);
43
+ }
44
+
45
+ /**
46
+ * Brings the overlay as visually the frontmost one.
47
+ */
48
+ bringToFront() {
49
+ let zIndex = '';
50
+ const frontmost = getAttachedInstances()
51
+ .filter((o) => o !== this)
52
+ .pop();
53
+ if (frontmost) {
54
+ const frontmostZIndex = frontmost.__zIndex;
55
+ zIndex = frontmostZIndex + 1;
56
+ }
57
+ this.style.zIndex = zIndex;
58
+ this.__zIndex = zIndex || parseFloat(getComputedStyle(this).zIndex);
59
+ }
60
+
61
+ /** @protected */
62
+ _enterModalState() {
63
+ if (document.body.style.pointerEvents !== 'none') {
64
+ // Set body pointer-events to 'none' to disable mouse interactions with
65
+ // other document nodes.
66
+ this._previousDocumentPointerEvents = document.body.style.pointerEvents;
67
+ document.body.style.pointerEvents = 'none';
68
+ }
69
+
70
+ // Disable pointer events in other attached overlays
71
+ getAttachedInstances().forEach((el) => {
72
+ if (el !== this) {
73
+ el.$.overlay.style.pointerEvents = 'none';
74
+ }
75
+ });
76
+ }
77
+
78
+ /** @protected */
79
+ _exitModalState() {
80
+ if (this._previousDocumentPointerEvents !== undefined) {
81
+ // Restore body pointer-events
82
+ document.body.style.pointerEvents = this._previousDocumentPointerEvents;
83
+ delete this._previousDocumentPointerEvents;
84
+ }
85
+
86
+ // Restore pointer events in the previous overlay(s)
87
+ const instances = getAttachedInstances();
88
+
89
+ let el;
90
+ // Use instances.pop() to ensure the reverse order
91
+ while ((el = instances.pop())) {
92
+ if (el === this) {
93
+ // Skip the current instance
94
+ continue;
95
+ }
96
+ el.$.overlay.style.removeProperty('pointer-events');
97
+ if (!el.modeless) {
98
+ // Stop after the last modal
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ };
@@ -110,7 +110,7 @@ export type OverlayEventMap = HTMLElementEventMap & OverlayCustomEventMap;
110
110
  * ---|---|---
111
111
  * `--vaadin-overlay-viewport-bottom` | Bottom offset of the visible viewport area | `0` or detected offset
112
112
  *
113
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
113
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
114
114
  *
115
115
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
116
116
  * @fires {CustomEvent} vaadin-overlay-open - Fired after the overlay is opened.
@@ -163,11 +163,6 @@ declare class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(HTMLEleme
163
163
  */
164
164
  hidden: boolean;
165
165
 
166
- /**
167
- * Returns true if this is the last one in the opened overlays stack.
168
- */
169
- protected readonly _last: boolean;
170
-
171
166
  close(sourceEvent?: Event | null): void;
172
167
 
173
168
  /**
@@ -178,11 +173,6 @@ declare class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(HTMLEleme
178
173
  */
179
174
  requestContentUpdate(): void;
180
175
 
181
- /**
182
- * Brings the overlay as visually the frontmost one
183
- */
184
- bringToFront(): void;
185
-
186
176
  addEventListener<K extends keyof OverlayEventMap>(
187
177
  type: K,
188
178
  listener: (this: Overlay, ev: OverlayEventMap[K]) => void,
@@ -10,6 +10,7 @@ import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
10
10
  import { processTemplates } from '@vaadin/component-base/src/templates.js';
11
11
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
12
12
  import { OverlayFocusMixin } from './vaadin-overlay-focus-mixin.js';
13
+ import { OverlayStackMixin } from './vaadin-overlay-stack-mixin.js';
13
14
 
14
15
  /**
15
16
  * `<vaadin-overlay>` is a Web Component for creating overlays. The content of the overlay
@@ -60,7 +61,7 @@ import { OverlayFocusMixin } from './vaadin-overlay-focus-mixin.js';
60
61
  * ---|---|---
61
62
  * `--vaadin-overlay-viewport-bottom` | Bottom offset of the visible viewport area | `0` or detected offset
62
63
  *
63
- * See [Styling Components](https://vaadin.com/docs/latest/styling/custom-theme/styling-components) documentation.
64
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
64
65
  *
65
66
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
66
67
  * @fires {CustomEvent} vaadin-overlay-open - Fired after the overlay is opened.
@@ -75,7 +76,7 @@ import { OverlayFocusMixin } from './vaadin-overlay-focus-mixin.js';
75
76
  * @mixes DirMixin
76
77
  * @mixes OverlayFocusMixin
77
78
  */
78
- class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement))) {
79
+ class Overlay extends OverlayStackMixin(OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))) {
79
80
  static get template() {
80
81
  return html`
81
82
  <style>
@@ -251,16 +252,6 @@ class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))
251
252
  return ['_rendererOrDataChanged(renderer, owner, model, opened)'];
252
253
  }
253
254
 
254
- /**
255
- * Returns all attached overlays in visual stacking order.
256
- * @private
257
- */
258
- static get __attachedInstances() {
259
- return Array.from(document.body.children)
260
- .filter((el) => el instanceof Overlay && !el.hasAttribute('closing'))
261
- .sort((a, b) => a.__zIndex - b.__zIndex || 0);
262
- }
263
-
264
255
  constructor() {
265
256
  super();
266
257
  this._boundMouseDownListener = this._mouseDownListener.bind(this);
@@ -274,15 +265,6 @@ class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))
274
265
  }
275
266
  }
276
267
 
277
- /**
278
- * Returns true if this is the last one in the opened overlays stack
279
- * @return {boolean}
280
- * @protected
281
- */
282
- get _last() {
283
- return this === Overlay.__attachedInstances.pop();
284
- }
285
-
286
268
  /** @protected */
287
269
  ready() {
288
270
  super.ready();
@@ -449,7 +431,7 @@ class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))
449
431
  /** @private */
450
432
  _openedChanged(opened, wasOpened) {
451
433
  if (opened) {
452
- this._storeFocus();
434
+ this._saveFocus();
453
435
 
454
436
  this._animatedOpening();
455
437
 
@@ -615,23 +597,6 @@ class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))
615
597
  document.documentElement.addEventListener('click', this._boundOutsideClickListener, true);
616
598
  }
617
599
 
618
- /** @private */
619
- _enterModalState() {
620
- if (document.body.style.pointerEvents !== 'none') {
621
- // Set body pointer-events to 'none' to disable mouse interactions with
622
- // other document nodes.
623
- this._previousDocumentPointerEvents = document.body.style.pointerEvents;
624
- document.body.style.pointerEvents = 'none';
625
- }
626
-
627
- // Disable pointer events in other attached overlays
628
- Overlay.__attachedInstances.forEach((el) => {
629
- if (el !== this) {
630
- el.shadowRoot.querySelector('[part="overlay"]').style.pointerEvents = 'none';
631
- }
632
- });
633
- }
634
-
635
600
  /** @private */
636
601
  _removeGlobalListeners() {
637
602
  document.removeEventListener('mousedown', this._boundMouseDownListener);
@@ -639,31 +604,6 @@ class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))
639
604
  document.documentElement.removeEventListener('click', this._boundOutsideClickListener, true);
640
605
  }
641
606
 
642
- /** @private */
643
- _exitModalState() {
644
- if (this._previousDocumentPointerEvents !== undefined) {
645
- // Restore body pointer-events
646
- document.body.style.pointerEvents = this._previousDocumentPointerEvents;
647
- delete this._previousDocumentPointerEvents;
648
- }
649
-
650
- // Restore pointer events in the previous overlay(s)
651
- const instances = Overlay.__attachedInstances;
652
- let el;
653
- // Use instances.pop() to ensure the reverse order
654
- while ((el = instances.pop())) {
655
- if (el === this) {
656
- // Skip the current instance
657
- continue;
658
- }
659
- el.shadowRoot.querySelector('[part="overlay"]').style.removeProperty('pointer-events');
660
- if (!el.modeless) {
661
- // Stop after the last modal
662
- break;
663
- }
664
- }
665
- }
666
-
667
607
  /** @private */
668
608
  _rendererOrDataChanged(renderer, owner, model, opened) {
669
609
  const ownerOrModelChanged = this._oldOwner !== owner || this._oldModel !== model;
@@ -689,38 +629,6 @@ class Overlay extends OverlayFocusMixin(ThemableMixin(DirMixin(PolymerElement)))
689
629
  }
690
630
  }
691
631
 
692
- /**
693
- * @param {!Node} node
694
- * @return {boolean}
695
- * @private
696
- */
697
- _deepContains(node) {
698
- if (this.contains(node)) {
699
- return true;
700
- }
701
- let n = node;
702
- const doc = node.ownerDocument;
703
- // Walk from node to `this` or `document`
704
- while (n && n !== doc && n !== this) {
705
- n = n.parentNode || n.host;
706
- }
707
- return n === this;
708
- }
709
-
710
- /**
711
- * Brings the overlay as visually the frontmost one
712
- */
713
- bringToFront() {
714
- let zIndex = '';
715
- const frontmost = Overlay.__attachedInstances.filter((o) => o !== this).pop();
716
- if (frontmost) {
717
- const frontmostZIndex = frontmost.__zIndex;
718
- zIndex = frontmostZIndex + 1;
719
- }
720
- this.style.zIndex = zIndex;
721
- this.__zIndex = zIndex || parseFloat(getComputedStyle(this).zIndex);
722
- }
723
-
724
632
  /**
725
633
  * @event vaadin-overlay-open
726
634
  * Fired after the overlay is opened.