@vaadin/overlay 25.0.0-alpha7 → 25.0.0-alpha8

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": "25.0.0-alpha7",
3
+ "version": "25.0.0-alpha8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -36,17 +36,17 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@open-wc/dedupe-mixin": "^1.3.0",
39
- "@vaadin/a11y-base": "25.0.0-alpha7",
40
- "@vaadin/component-base": "25.0.0-alpha7",
41
- "@vaadin/vaadin-lumo-styles": "25.0.0-alpha7",
42
- "@vaadin/vaadin-themable-mixin": "25.0.0-alpha7",
39
+ "@vaadin/a11y-base": "25.0.0-alpha8",
40
+ "@vaadin/component-base": "25.0.0-alpha8",
41
+ "@vaadin/vaadin-lumo-styles": "25.0.0-alpha8",
42
+ "@vaadin/vaadin-themable-mixin": "25.0.0-alpha8",
43
43
  "lit": "^3.0.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@vaadin/chai-plugins": "25.0.0-alpha7",
47
- "@vaadin/test-runner-commands": "25.0.0-alpha7",
46
+ "@vaadin/chai-plugins": "25.0.0-alpha8",
47
+ "@vaadin/test-runner-commands": "25.0.0-alpha8",
48
48
  "@vaadin/testing-helpers": "^2.0.0",
49
49
  "sinon": "^18.0.0"
50
50
  },
51
- "gitHead": "87f72707ce6866892f8be5782fa0da008e87dcbc"
51
+ "gitHead": "ebf53673d5f639d2b1b6f2b31f640f530643ee2f"
52
52
  }
@@ -7,76 +7,81 @@ import '@vaadin/component-base/src/style-props.js';
7
7
  import { css } from 'lit';
8
8
 
9
9
  export const overlayStyles = css`
10
- @layer base {
11
- :host {
12
- z-index: 200;
13
- position: fixed;
10
+ :host {
11
+ z-index: 200;
12
+ position: fixed;
14
13
 
15
- /* Despite of what the names say, <vaadin-overlay> is just a container
14
+ /* Despite of what the names say, <vaadin-overlay> is just a container
16
15
  for position/sizing/alignment. The actual overlay is the overlay part. */
17
16
 
18
- /* Default position constraints. Themes can
17
+ /* Default position constraints. Themes can
19
18
  override this to adjust the gap between the overlay and the viewport. */
20
- inset: 8px;
21
- bottom: var(--vaadin-overlay-viewport-bottom);
19
+ inset: 8px;
20
+ bottom: var(--vaadin-overlay-viewport-bottom);
22
21
 
23
- /* Use flexbox alignment for the overlay part. */
24
- display: flex;
25
- flex-direction: column; /* makes dropdowns sizing easier */
26
- /* Align to center by default. */
27
- align-items: center;
28
- justify-content: center;
22
+ /* Override native [popover] user agent styles */
23
+ width: auto;
24
+ height: auto;
25
+ border: none;
26
+ padding: 0;
27
+ background-color: transparent;
28
+ overflow: visible;
29
29
 
30
- /* Allow centering when max-width/max-height applies. */
31
- margin: auto;
30
+ /* Use flexbox alignment for the overlay part. */
31
+ display: flex;
32
+ flex-direction: column; /* makes dropdowns sizing easier */
33
+ /* Align to center by default. */
34
+ align-items: center;
35
+ justify-content: center;
32
36
 
33
- /* The host is not clickable, only the overlay part is. */
34
- pointer-events: none;
37
+ /* Allow centering when max-width/max-height applies. */
38
+ margin: auto;
35
39
 
36
- /* Remove tap highlight on touch devices. */
37
- -webkit-tap-highlight-color: transparent;
40
+ /* The host is not clickable, only the overlay part is. */
41
+ pointer-events: none;
38
42
 
39
- /* CSS API for host */
40
- --vaadin-overlay-viewport-bottom: 8px;
41
- }
43
+ /* Remove tap highlight on touch devices. */
44
+ -webkit-tap-highlight-color: transparent;
42
45
 
43
- :host([hidden]),
44
- :host(:not([opened]):not([closing])),
45
- :host(:not([opened]):not([closing])) [part='overlay'] {
46
- display: none !important;
47
- }
46
+ /* CSS API for host */
47
+ --vaadin-overlay-viewport-bottom: 8px;
48
+ }
48
49
 
49
- [part='overlay'] {
50
- background: var(--vaadin-overlay-background, var(--vaadin-background-color));
51
- border: var(--vaadin-overlay-border, 1px solid var(--vaadin-border-color));
52
- border-radius: var(--vaadin-overlay-border-radius, var(--vaadin-radius-m));
53
- box-shadow: var(--vaadin-overlay-box-shadow, 0 8px 24px -4px rgba(0, 0, 0, 0.3));
54
- box-sizing: border-box;
55
- max-width: 100%;
56
- overflow: auto;
57
- overscroll-behavior: contain;
58
- pointer-events: auto;
59
- -webkit-tap-highlight-color: initial;
60
- }
50
+ :host([hidden]),
51
+ :host(:not([opened]):not([closing])),
52
+ :host(:not([opened]):not([closing])) [part='overlay'] {
53
+ display: none !important;
54
+ }
61
55
 
62
- [part='backdrop'] {
63
- background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.5));
64
- content: '';
65
- inset: 0;
66
- pointer-events: auto;
67
- position: fixed;
68
- z-index: -1;
69
- }
56
+ [part='overlay'] {
57
+ background: var(--vaadin-overlay-background, var(--vaadin-background-color));
58
+ border: var(--vaadin-overlay-border-width, 1px) solid var(--vaadin-overlay-border-color, var(--vaadin-border-color));
59
+ border-radius: var(--vaadin-overlay-border-radius, var(--vaadin-radius-m));
60
+ box-shadow: var(--vaadin-overlay-box-shadow, 0 8px 24px -4px rgba(0, 0, 0, 0.3));
61
+ box-sizing: border-box;
62
+ max-width: 100%;
63
+ overflow: auto;
64
+ overscroll-behavior: contain;
65
+ pointer-events: auto;
66
+ -webkit-tap-highlight-color: initial;
67
+ }
68
+
69
+ [part='backdrop'] {
70
+ background: var(--vaadin-overlay-backdrop-background, rgba(0, 0, 0, 0.5));
71
+ content: '';
72
+ inset: 0;
73
+ pointer-events: auto;
74
+ position: fixed;
75
+ z-index: -1;
76
+ }
70
77
 
71
- @media (forced-colors: active) {
72
- [part='overlay'] {
73
- border: 3px solid;
74
- }
78
+ [part='overlay']:focus-visible {
79
+ outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
80
+ }
75
81
 
76
- [part='overlay']:focus-visible {
77
- outline: var(--vaadin-focus-ring-width) solid;
78
- outline-offset: 1px;
79
- }
82
+ @media (forced-colors: active) {
83
+ [part='overlay'] {
84
+ border: 3px solid;
80
85
  }
81
86
  }
82
87
  `;
@@ -18,6 +18,14 @@ export const overlayStyles = css`
18
18
  inset: 0;
19
19
  bottom: var(--vaadin-overlay-viewport-bottom);
20
20
 
21
+ /* Override native [popover] user agent styles */
22
+ width: auto;
23
+ height: auto;
24
+ border: none;
25
+ padding: 0;
26
+ background-color: transparent;
27
+ overflow: visible;
28
+
21
29
  /* Use flexbox alignment for the overlay part. */
22
30
  display: flex;
23
31
  flex-direction: column; /* makes dropdowns sizing easier */
@@ -48,11 +48,20 @@ export const OverlayFocusMixin = (superClass) =>
48
48
  constructor() {
49
49
  super();
50
50
 
51
- this.__ariaModalController = new AriaModalController(this);
51
+ this.__ariaModalController = new AriaModalController(this, () => this._modalRoot);
52
52
  this.__focusTrapController = new FocusTrapController(this);
53
53
  this.__focusRestorationController = new FocusRestorationController();
54
54
  }
55
55
 
56
+ /**
57
+ * Override to specify another element used as a content root,
58
+ * e.g. slotted into the overlay, rather than overlay itself.
59
+ * @protected
60
+ */
61
+ get _contentRoot() {
62
+ return this;
63
+ }
64
+
56
65
  /** @protected */
57
66
  ready() {
58
67
  super.ready();
@@ -62,6 +71,15 @@ export const OverlayFocusMixin = (superClass) =>
62
71
  this.addController(this.__focusRestorationController);
63
72
  }
64
73
 
74
+ /**
75
+ * Override to specify another element used as a modality root,
76
+ * e.g. the overlay's owner element, rather than overlay itself.
77
+ * @protected
78
+ */
79
+ get _modalRoot() {
80
+ return this;
81
+ }
82
+
65
83
  /**
66
84
  * Release focus and restore focus after the overlay is closed.
67
85
  *
@@ -127,15 +145,15 @@ export const OverlayFocusMixin = (superClass) =>
127
145
  * @protected
128
146
  */
129
147
  _deepContains(node) {
130
- if (this.contains(node)) {
148
+ if (this._contentRoot.contains(node)) {
131
149
  return true;
132
150
  }
133
151
  let n = node;
134
152
  const doc = node.ownerDocument;
135
- // Walk from node to `this` or `document`
136
- while (n && n !== doc && n !== this) {
153
+ // Walk from node to content root or `document`
154
+ while (n && n !== doc && n !== this._contentRoot) {
137
155
  n = n.parentNode || n.host;
138
156
  }
139
- return n === this;
157
+ return n === this._contentRoot;
140
158
  }
141
159
  };
@@ -6,6 +6,7 @@
6
6
  import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
7
7
  import { OverlayFocusMixin } from './vaadin-overlay-focus-mixin.js';
8
8
  import { OverlayStackMixin } from './vaadin-overlay-stack-mixin.js';
9
+ import { setOverlayStateAttribute } from './vaadin-overlay-utils.js';
9
10
 
10
11
  /**
11
12
  * @polymerMixin
@@ -91,6 +92,7 @@ export const OverlayMixin = (superClass) =>
91
92
  type: Boolean,
92
93
  value: false,
93
94
  reflectToAttribute: true,
95
+ observer: '_withBackdropChanged',
94
96
  sync: true,
95
97
  },
96
98
  };
@@ -123,7 +125,9 @@ export const OverlayMixin = (superClass) =>
123
125
  // get invoked on iOS Safari (reproducible in <vaadin-dialog>
124
126
  // and <vaadin-context-menu>).
125
127
  this.addEventListener('click', () => {});
126
- this.$.backdrop.addEventListener('click', () => {});
128
+ if (this.$.backdrop) {
129
+ this.$.backdrop.addEventListener('click', () => {});
130
+ }
127
131
 
128
132
  this.addEventListener('mouseup', () => {
129
133
  // In Chrome, focus moves to body on overlay content mousedown
@@ -163,7 +167,7 @@ export const OverlayMixin = (superClass) =>
163
167
  */
164
168
  requestContentUpdate() {
165
169
  if (this.renderer) {
166
- this.renderer.call(this.owner, this, this.owner, this.model);
170
+ this.renderer.call(this.owner, this._contentRoot, this.owner, this.model);
167
171
  }
168
172
  }
169
173
 
@@ -256,11 +260,11 @@ export const OverlayMixin = (superClass) =>
256
260
  this._oldOpened = opened;
257
261
 
258
262
  if (rendererChanged && hasOldRenderer) {
259
- this.innerHTML = '';
263
+ this._contentRoot.innerHTML = '';
260
264
  // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
261
265
  // When clearing the rendered content, this part needs to be manually disposed of.
262
266
  // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
263
- delete this._$litPart$;
267
+ delete this._contentRoot._$litPart$;
264
268
  }
265
269
 
266
270
  if (opened && renderer && (rendererChanged || openedChanged || ownerOrModelChanged)) {
@@ -279,11 +283,23 @@ export const OverlayMixin = (superClass) =>
279
283
  this._removeGlobalListeners();
280
284
  this._exitModalState();
281
285
  }
286
+ setOverlayStateAttribute(this, 'modeless', modeless);
287
+ }
288
+
289
+ /** @private */
290
+ _withBackdropChanged(withBackdrop) {
291
+ setOverlayStateAttribute(this, 'with-backdrop', withBackdrop);
282
292
  }
283
293
 
284
294
  /** @private */
285
295
  _openedChanged(opened, wasOpened) {
286
296
  if (opened) {
297
+ // Prevent possible errors on setting `opened` to `true` while disconnected
298
+ if (!this.isConnected) {
299
+ this.opened = false;
300
+ return;
301
+ }
302
+
287
303
  this._saveFocus();
288
304
 
289
305
  this._animatedOpening();
@@ -292,7 +308,7 @@ export const OverlayMixin = (superClass) =>
292
308
  setTimeout(() => {
293
309
  this._trapFocus();
294
310
 
295
- this.dispatchEvent(new CustomEvent('vaadin-overlay-open', { bubbles: true }));
311
+ this.dispatchEvent(new CustomEvent('vaadin-overlay-open', { bubbles: true, composed: true }));
296
312
  });
297
313
  });
298
314
 
@@ -369,14 +385,16 @@ export const OverlayMixin = (superClass) =>
369
385
 
370
386
  /** @private */
371
387
  _animatedOpening() {
372
- if (this.parentNode === document.body && this.hasAttribute('closing')) {
388
+ if (this._isAttached && this.hasAttribute('closing')) {
373
389
  this._flushAnimation('closing');
374
390
  }
375
391
  this._attachOverlay();
392
+ this._appendAttachedInstance();
393
+ this.bringToFront();
376
394
  if (!this.modeless) {
377
395
  this._enterModalState();
378
396
  }
379
- this.setAttribute('opening', '');
397
+ setOverlayStateAttribute(this, 'opening', true);
380
398
 
381
399
  if (this._shouldAnimate()) {
382
400
  this._enqueueAnimation('opening', () => {
@@ -392,19 +410,19 @@ export const OverlayMixin = (superClass) =>
392
410
  this._placeholder = document.createComment('vaadin-overlay-placeholder');
393
411
  this.parentNode.insertBefore(this._placeholder, this);
394
412
  document.body.appendChild(this);
395
- this.bringToFront();
396
413
  }
397
414
 
398
415
  /** @private */
399
416
  _finishOpening() {
400
- this.removeAttribute('opening');
417
+ setOverlayStateAttribute(this, 'opening', false);
401
418
  }
402
419
 
403
420
  /** @private */
404
421
  _finishClosing() {
405
422
  this._detachOverlay();
423
+ this._removeAttachedInstance();
406
424
  this.$.overlay.style.removeProperty('pointer-events');
407
- this.removeAttribute('closing');
425
+ setOverlayStateAttribute(this, 'closing', false);
408
426
  this.dispatchEvent(new CustomEvent('vaadin-overlay-closed'));
409
427
  }
410
428
 
@@ -413,9 +431,9 @@ export const OverlayMixin = (superClass) =>
413
431
  if (this.hasAttribute('opening')) {
414
432
  this._flushAnimation('opening');
415
433
  }
416
- if (this._placeholder) {
434
+ if (this._isAttached) {
417
435
  this._exitModalState();
418
- this.setAttribute('closing', '');
436
+ setOverlayStateAttribute(this, 'closing', true);
419
437
  this.dispatchEvent(new CustomEvent('vaadin-overlay-closing'));
420
438
 
421
439
  if (this._shouldAnimate()) {
@@ -4,7 +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 { getAncestorRootNodes } from '@vaadin/component-base/src/dom-utils.js';
7
- import { observeMove } from './vaadin-overlay-utils.js';
7
+ import { observeMove, setOverlayStateAttribute } from './vaadin-overlay-utils.js';
8
8
 
9
9
  const PROP_NAMES_VERTICAL = {
10
10
  start: 'top',
@@ -271,11 +271,11 @@ export const PositionMixin = (superClass) =>
271
271
  // Apply the positioning properties to the overlay
272
272
  Object.assign(this.style, verticalProps, horizontalProps);
273
273
 
274
- this.toggleAttribute('bottom-aligned', !shouldAlignStartVertically);
275
- this.toggleAttribute('top-aligned', shouldAlignStartVertically);
274
+ setOverlayStateAttribute(this, 'bottom-aligned', !shouldAlignStartVertically);
275
+ setOverlayStateAttribute(this, 'top-aligned', shouldAlignStartVertically);
276
276
 
277
- this.toggleAttribute('end-aligned', !flexStart);
278
- this.toggleAttribute('start-aligned', flexStart);
277
+ setOverlayStateAttribute(this, 'end-aligned', !flexStart);
278
+ setOverlayStateAttribute(this, 'start-aligned', flexStart);
279
279
  }
280
280
 
281
281
  __shouldAlignStartHorizontally(targetRect, rtl) {
@@ -4,14 +4,17 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
 
7
+ /** @type {Set<HTMLElement>} */
8
+ const attachedInstances = new Set();
9
+
7
10
  /**
8
11
  * Returns all attached overlays in visual stacking order.
9
12
  * @private
10
13
  */
11
14
  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
+ [...attachedInstances].filter(
16
+ (el) => el instanceof HTMLElement && el._hasOverlayStackMixin && !el.hasAttribute('closing'),
17
+ );
15
18
 
16
19
  /**
17
20
  * Returns all attached overlay instances excluding notification container,
@@ -20,6 +23,24 @@ const getAttachedInstances = () =>
20
23
  */
21
24
  const getOverlayInstances = () => getAttachedInstances().filter((el) => el.$.overlay);
22
25
 
26
+ /**
27
+ * Returns true if all the instances on top of the overlay are nested overlays.
28
+ * @private
29
+ */
30
+ const hasOnlyNestedOverlays = (overlay) => {
31
+ const instances = getAttachedInstances();
32
+ const next = instances[instances.indexOf(overlay) + 1];
33
+ if (!next) {
34
+ return true;
35
+ }
36
+
37
+ if (!overlay._deepContains(next)) {
38
+ return false;
39
+ }
40
+
41
+ return hasOnlyNestedOverlays(next);
42
+ };
43
+
23
44
  /**
24
45
  * Returns true if the overlay is the last one in the opened overlays stack.
25
46
  * @param {HTMLElement} overlay
@@ -70,20 +91,48 @@ export const OverlayStackMixin = (superClass) =>
70
91
  return isLastOverlay(this);
71
92
  }
72
93
 
94
+ /**
95
+ * Returns true if this is overlay is attached.
96
+ *
97
+ * @return {boolean}
98
+ * @protected
99
+ */
100
+ get _isAttached() {
101
+ return attachedInstances.has(this);
102
+ }
103
+
73
104
  /**
74
105
  * Brings the overlay as visually the frontmost one.
75
106
  */
76
107
  bringToFront() {
108
+ // Update z-index to be the highest among all attached overlays
109
+ // TODO: Can be removed after switching all overlays to be based on native popover
77
110
  let zIndex = '';
78
111
  const frontmost = getAttachedInstances()
79
112
  .filter((o) => o !== this)
80
113
  .pop();
81
114
  if (frontmost) {
82
- const frontmostZIndex = frontmost.__zIndex;
115
+ const frontmostZIndex = parseFloat(getComputedStyle(frontmost).zIndex);
83
116
  zIndex = frontmostZIndex + 1;
84
117
  }
85
118
  this.style.zIndex = zIndex;
86
- this.__zIndex = zIndex || parseFloat(getComputedStyle(this).zIndex);
119
+
120
+ // If the overlay is the last one, or if all other overlays shown above
121
+ // are nested overlays (e.g. date-picker inside a dialog), do not call
122
+ // `showPopover()` unnecessarily to avoid scroll position being reset.
123
+ if (isLastOverlay(this) || hasOnlyNestedOverlays(this)) {
124
+ return;
125
+ }
126
+
127
+ // Update stacking order of native popover-based overlays
128
+ if (this.matches(':popover-open')) {
129
+ this.hidePopover();
130
+ this.showPopover();
131
+ }
132
+
133
+ // Update order of attached instances
134
+ this._removeAttachedInstance();
135
+ this._appendAttachedInstance();
87
136
 
88
137
  // If there is a nested overlay, call `bringToFront()` for it as well.
89
138
  if (overlayMap.has(this)) {
@@ -133,4 +182,16 @@ export const OverlayStackMixin = (superClass) =>
133
182
  }
134
183
  }
135
184
  }
185
+
186
+ /** @protected */
187
+ _appendAttachedInstance() {
188
+ attachedInstances.add(this);
189
+ }
190
+
191
+ /** @protected */
192
+ _removeAttachedInstance() {
193
+ if (this._isAttached) {
194
+ attachedInstances.delete(this);
195
+ }
196
+ }
136
197
  };
@@ -11,3 +11,9 @@
11
11
  * https://github.com/floating-ui/floating-ui/blob/58ed169/packages/dom/src/autoUpdate.ts#L45
12
12
  */
13
13
  export function observeMove(element: HTMLElement, callback: () => void): () => void;
14
+
15
+ /**
16
+ * Toggle the state attribute on the overlay element and also its owner element. This allows targeting state attributes
17
+ * in the light DOM in case the overlay is in the shadow DOM of its owner.
18
+ */
19
+ export function setOverlayStateAttribute(overlay: HTMLElement, name: string, value: string | boolean): void;
@@ -87,3 +87,32 @@ export function observeMove(element, callback) {
87
87
 
88
88
  return cleanup;
89
89
  }
90
+
91
+ /**
92
+ * Toggle the state attribute on the overlay element and also its owner element. This allows targeting state attributes
93
+ * in the light DOM in case the overlay is in the shadow DOM of its owner.
94
+ * @param {HTMLElement} overlay The overlay element on which to toggle the attribute.
95
+ * @param {string} name The name of the attribute to toggle.
96
+ * @param {string|boolean} value The value of the attribute. If a string is provided, it will be set as the attribute
97
+ * value. Otherwise, the attribute will be added or removed depending on whether `value` is truthy or falsy.
98
+ */
99
+ export function setOverlayStateAttribute(overlay, name, value) {
100
+ const elements = [overlay];
101
+ if (overlay.owner) {
102
+ elements.push(overlay.owner);
103
+ }
104
+
105
+ if (typeof value === 'string') {
106
+ elements.forEach((element) => {
107
+ element.setAttribute(name, value);
108
+ });
109
+ } else if (value) {
110
+ elements.forEach((element) => {
111
+ element.setAttribute(name, '');
112
+ });
113
+ } else {
114
+ elements.forEach((element) => {
115
+ element.removeAttribute(name);
116
+ });
117
+ }
118
+ }
@@ -77,7 +77,7 @@ import { OverlayMixin } from './vaadin-overlay-mixin.js';
77
77
  * @mixes DirMixin
78
78
  * @mixes OverlayMixin
79
79
  */
80
- class Overlay extends OverlayMixin(DirMixin(ThemableMixin(LumoInjectionMixin(PolylitMixin(LitElement))))) {
80
+ class Overlay extends OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))) {
81
81
  static get is() {
82
82
  return 'vaadin-overlay';
83
83
  }