@vaadin/popover 24.5.0-alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +53 -0
- package/package.json +56 -0
- package/src/vaadin-popover-overlay-mixin.js +93 -0
- package/src/vaadin-popover-overlay.js +118 -0
- package/src/vaadin-popover-position-mixin.d.ts +37 -0
- package/src/vaadin-popover-position-mixin.js +67 -0
- package/src/vaadin-popover-target-mixin.d.ts +35 -0
- package/src/vaadin-popover-target-mixin.js +92 -0
- package/src/vaadin-popover.d.ts +191 -0
- package/src/vaadin-popover.js +739 -0
- package/theme/lumo/vaadin-popover-styles.d.ts +3 -0
- package/theme/lumo/vaadin-popover-styles.js +20 -0
- package/theme/lumo/vaadin-popover.d.ts +2 -0
- package/theme/lumo/vaadin-popover.js +2 -0
- package/theme/material/vaadin-popover-styles.d.ts +1 -0
- package/theme/material/vaadin-popover-styles.js +17 -0
- package/theme/material/vaadin-popover.d.ts +2 -0
- package/theme/material/vaadin-popover.js +2 -0
- package/vaadin-popover.d.ts +1 -0
- package/vaadin-popover.js +2 -0
- package/web-types.json +402 -0
- package/web-types.lit.json +160 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2024 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import './vaadin-popover-overlay.js';
|
|
7
|
+
import { html, LitElement } from 'lit';
|
|
8
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
9
|
+
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
10
|
+
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
11
|
+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
12
|
+
import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
|
|
13
|
+
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
14
|
+
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
|
|
15
|
+
import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
|
|
16
|
+
import { PopoverPositionMixin } from './vaadin-popover-position-mixin.js';
|
|
17
|
+
import { PopoverTargetMixin } from './vaadin-popover-target-mixin.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Controller for handling popover opened state.
|
|
21
|
+
*/
|
|
22
|
+
class PopoverOpenedStateController {
|
|
23
|
+
constructor(host) {
|
|
24
|
+
this.host = host;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether closing is currently in progress.
|
|
29
|
+
* @return {boolean}
|
|
30
|
+
*/
|
|
31
|
+
get isClosing() {
|
|
32
|
+
return this.__closeTimeout != null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @private */
|
|
36
|
+
get __focusDelay() {
|
|
37
|
+
return this.host.focusDelay || 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @private */
|
|
41
|
+
get __hoverDelay() {
|
|
42
|
+
return this.host.hoverDelay || 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @private */
|
|
46
|
+
get __hideDelay() {
|
|
47
|
+
return this.host.hideDelay || 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Schedule opening the popover.
|
|
52
|
+
* @param {Object} options
|
|
53
|
+
*/
|
|
54
|
+
open(options = { immediate: false }) {
|
|
55
|
+
const { immediate, trigger } = options;
|
|
56
|
+
const shouldDelayHover = trigger === 'hover' && this.__hoverDelay > 0;
|
|
57
|
+
const shouldDelayFocus = trigger === 'focus' && this.__focusDelay > 0;
|
|
58
|
+
|
|
59
|
+
if (!immediate && (shouldDelayHover || shouldDelayFocus) && !this.__closeTimeout) {
|
|
60
|
+
this.__scheduleOpen(trigger);
|
|
61
|
+
} else {
|
|
62
|
+
this.__showPopover();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Schedule closing the popover.
|
|
68
|
+
* @param {boolean} immediate
|
|
69
|
+
*/
|
|
70
|
+
close(immediate) {
|
|
71
|
+
if (!immediate && this.__hideDelay > 0) {
|
|
72
|
+
this.__scheduleClose();
|
|
73
|
+
} else {
|
|
74
|
+
this.__abortClose();
|
|
75
|
+
this.__setOpened(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @private */
|
|
80
|
+
__setOpened(opened) {
|
|
81
|
+
this.host.opened = opened;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @private */
|
|
85
|
+
__showPopover() {
|
|
86
|
+
this.__abortClose();
|
|
87
|
+
this.__setOpened(true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @private */
|
|
91
|
+
__abortClose() {
|
|
92
|
+
if (this.__closeTimeout) {
|
|
93
|
+
clearTimeout(this.__closeTimeout);
|
|
94
|
+
this.__closeTimeout = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @private */
|
|
99
|
+
__abortOpen() {
|
|
100
|
+
if (this.__openTimeout) {
|
|
101
|
+
clearTimeout(this.__openTimeout);
|
|
102
|
+
this.__openTimeout = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @private */
|
|
107
|
+
__scheduleClose() {
|
|
108
|
+
this.__closeTimeout = setTimeout(() => {
|
|
109
|
+
this.__closeTimeout = null;
|
|
110
|
+
this.__setOpened(false);
|
|
111
|
+
}, this.__hideDelay);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @private */
|
|
115
|
+
__scheduleOpen(trigger) {
|
|
116
|
+
this.__abortOpen();
|
|
117
|
+
|
|
118
|
+
const delay = trigger === 'focus' ? this.__focusDelay : this.__hoverDelay;
|
|
119
|
+
this.__openTimeout = setTimeout(() => {
|
|
120
|
+
this.__openTimeout = null;
|
|
121
|
+
this.__showPopover();
|
|
122
|
+
}, delay);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `<vaadin-popover>` is a Web Component for creating overlays
|
|
128
|
+
* that are positioned next to specified DOM element (target).
|
|
129
|
+
*
|
|
130
|
+
* Unlike `<vaadin-tooltip>`, the popover supports rich content
|
|
131
|
+
* that can be provided by using `renderer` function.
|
|
132
|
+
*
|
|
133
|
+
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
|
|
134
|
+
*
|
|
135
|
+
* @customElement
|
|
136
|
+
* @extends HTMLElement
|
|
137
|
+
* @mixes ElementMixin
|
|
138
|
+
* @mixes ElementMixin
|
|
139
|
+
* @mixes PopoverPositionMixin
|
|
140
|
+
* @mixes PopoverTargetMixin
|
|
141
|
+
* @mixes ThemePropertyMixin
|
|
142
|
+
*/
|
|
143
|
+
class Popover extends PopoverPositionMixin(
|
|
144
|
+
PopoverTargetMixin(OverlayClassMixin(ThemePropertyMixin(ElementMixin(PolylitMixin(LitElement))))),
|
|
145
|
+
) {
|
|
146
|
+
static get is() {
|
|
147
|
+
return 'vaadin-popover';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static get properties() {
|
|
151
|
+
return {
|
|
152
|
+
/**
|
|
153
|
+
* String used to label the overlay to screen reader users.
|
|
154
|
+
*
|
|
155
|
+
* @attr {string} accessible-name
|
|
156
|
+
*/
|
|
157
|
+
accessibleName: {
|
|
158
|
+
type: String,
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Id of the element used as label of the overlay to screen reader users.
|
|
163
|
+
*
|
|
164
|
+
* @attr {string} accessible-name-ref
|
|
165
|
+
*/
|
|
166
|
+
accessibleNameRef: {
|
|
167
|
+
type: String,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Height to be set on the overlay content.
|
|
172
|
+
*
|
|
173
|
+
* @attr {string} content-height
|
|
174
|
+
*/
|
|
175
|
+
contentHeight: {
|
|
176
|
+
type: String,
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Width to be set on the overlay content.
|
|
181
|
+
*
|
|
182
|
+
* @attr {string} content-width
|
|
183
|
+
*/
|
|
184
|
+
contentWidth: {
|
|
185
|
+
type: String,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The delay in milliseconds before the popover is opened
|
|
190
|
+
* on focus when the corresponding trigger is used.
|
|
191
|
+
* @attr {number} focus-delay
|
|
192
|
+
*/
|
|
193
|
+
focusDelay: {
|
|
194
|
+
type: Number,
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The delay in milliseconds before the popover is closed
|
|
199
|
+
* on losing hover, when the corresponding trigger is used.
|
|
200
|
+
* On blur, the popover is closed immediately.
|
|
201
|
+
* @attr {number} hide-delay
|
|
202
|
+
*/
|
|
203
|
+
hideDelay: {
|
|
204
|
+
type: Number,
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The delay in milliseconds before the popover is opened
|
|
209
|
+
* on hover when the corresponding trigger is used.
|
|
210
|
+
* @attr {number} hover-delay
|
|
211
|
+
*/
|
|
212
|
+
hoverDelay: {
|
|
213
|
+
type: Number,
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* True if the popover overlay is opened, false otherwise.
|
|
218
|
+
*/
|
|
219
|
+
opened: {
|
|
220
|
+
type: Boolean,
|
|
221
|
+
value: false,
|
|
222
|
+
notify: true,
|
|
223
|
+
observer: '__openedChanged',
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The `role` attribute value to be set on the overlay.
|
|
228
|
+
*
|
|
229
|
+
* @attr {string} overlay-role
|
|
230
|
+
*/
|
|
231
|
+
overlayRole: {
|
|
232
|
+
type: String,
|
|
233
|
+
value: 'dialog',
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Custom function for rendering the content of the overlay.
|
|
238
|
+
* Receives two arguments:
|
|
239
|
+
*
|
|
240
|
+
* - `root` The root container DOM element. Append your content to it.
|
|
241
|
+
* - `popover` The reference to the `vaadin-popover` element (overlay host).
|
|
242
|
+
*/
|
|
243
|
+
renderer: {
|
|
244
|
+
type: Object,
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* When true, the popover prevents interacting with background elements
|
|
249
|
+
* by setting `pointer-events` style on the document body to `none`.
|
|
250
|
+
* This also enables trapping focus inside the overlay.
|
|
251
|
+
*/
|
|
252
|
+
modal: {
|
|
253
|
+
type: Boolean,
|
|
254
|
+
value: false,
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Set to true to disable closing popover overlay on outside click.
|
|
259
|
+
*
|
|
260
|
+
* @attr {boolean} no-close-on-outside-click
|
|
261
|
+
*/
|
|
262
|
+
noCloseOnOutsideClick: {
|
|
263
|
+
type: Boolean,
|
|
264
|
+
value: false,
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Set to true to disable closing popover overlay on Escape press.
|
|
269
|
+
* When the popover is modal, pressing Escape anywhere in the
|
|
270
|
+
* document closes the overlay. Otherwise, only Escape press
|
|
271
|
+
* from the popover itself or its target closes the overlay.
|
|
272
|
+
*
|
|
273
|
+
* @attr {boolean} no-close-on-esc
|
|
274
|
+
*/
|
|
275
|
+
noCloseOnEsc: {
|
|
276
|
+
type: Boolean,
|
|
277
|
+
value: false,
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Popover trigger mode, used to configure how the overlay is opened or closed.
|
|
282
|
+
* Could be set to multiple by providing an array, e.g. `trigger = ['hover', 'focus']`.
|
|
283
|
+
*
|
|
284
|
+
* Supported values:
|
|
285
|
+
* - `click` (default) - opens and closes on target click.
|
|
286
|
+
* - `hover` - opens on target mouseenter, closes on target mouseleave. Moving mouse
|
|
287
|
+
* to the popover overlay content keeps the overlay opened.
|
|
288
|
+
* - `focus` - opens on target focus, closes on target blur. Moving focus to the
|
|
289
|
+
* popover overlay content keeps the overlay opened.
|
|
290
|
+
*
|
|
291
|
+
* In addition to the behavior specified by `trigger`, the popover can be closed by:
|
|
292
|
+
* - pressing Escape key (unless `noCloseOnEsc` property is true)
|
|
293
|
+
* - outside click (unless `noCloseOnOutsideClick` property is true)
|
|
294
|
+
*
|
|
295
|
+
* When setting `trigger` property to `null`, `undefined` or empty array, the popover
|
|
296
|
+
* can be only opened or closed programmatically by changing `opened` property.
|
|
297
|
+
*/
|
|
298
|
+
trigger: {
|
|
299
|
+
type: Array,
|
|
300
|
+
value: () => ['click'],
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* When true, the overlay has a backdrop (modality curtain) on top of the
|
|
305
|
+
* underlying page content, covering the whole viewport.
|
|
306
|
+
*
|
|
307
|
+
* @attr {boolean} with-backdrop
|
|
308
|
+
*/
|
|
309
|
+
withBackdrop: {
|
|
310
|
+
type: Boolean,
|
|
311
|
+
value: false,
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/** @private */
|
|
315
|
+
__shouldRestoreFocus: {
|
|
316
|
+
type: Boolean,
|
|
317
|
+
value: false,
|
|
318
|
+
sync: true,
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/** @private */
|
|
322
|
+
__overlayId: {
|
|
323
|
+
type: String,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static get observers() {
|
|
329
|
+
return [
|
|
330
|
+
'__updateContentHeight(contentHeight, _overlayElement)',
|
|
331
|
+
'__updateContentWidth(contentWidth, _overlayElement)',
|
|
332
|
+
'__openedOrTargetChanged(opened, target)',
|
|
333
|
+
'__overlayRoleOrTargetChanged(overlayRole, target)',
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
constructor() {
|
|
338
|
+
super();
|
|
339
|
+
|
|
340
|
+
this.__overlayId = `vaadin-popover-${generateUniqueId()}`;
|
|
341
|
+
|
|
342
|
+
this.__onGlobalClick = this.__onGlobalClick.bind(this);
|
|
343
|
+
this.__onGlobalKeyDown = this.__onGlobalKeyDown.bind(this);
|
|
344
|
+
this.__onTargetClick = this.__onTargetClick.bind(this);
|
|
345
|
+
this.__onTargetKeydown = this.__onTargetKeydown.bind(this);
|
|
346
|
+
this.__onTargetFocusIn = this.__onTargetFocusIn.bind(this);
|
|
347
|
+
this.__onTargetFocusOut = this.__onTargetFocusOut.bind(this);
|
|
348
|
+
this.__onTargetMouseEnter = this.__onTargetMouseEnter.bind(this);
|
|
349
|
+
this.__onTargetMouseLeave = this.__onTargetMouseLeave.bind(this);
|
|
350
|
+
|
|
351
|
+
this._openedStateController = new PopoverOpenedStateController(this);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @protected */
|
|
355
|
+
render() {
|
|
356
|
+
const effectivePosition = this.__effectivePosition;
|
|
357
|
+
|
|
358
|
+
return html`
|
|
359
|
+
<vaadin-popover-overlay
|
|
360
|
+
id="${this.__overlayId}"
|
|
361
|
+
role="${this.overlayRole}"
|
|
362
|
+
aria-label="${ifDefined(this.accessibleName)}"
|
|
363
|
+
aria-labelledby="${ifDefined(this.accessibleNameRef)}"
|
|
364
|
+
.renderer="${this.renderer}"
|
|
365
|
+
.owner="${this}"
|
|
366
|
+
theme="${ifDefined(this._theme)}"
|
|
367
|
+
.positionTarget="${this.target}"
|
|
368
|
+
.position="${effectivePosition}"
|
|
369
|
+
.opened="${this.opened}"
|
|
370
|
+
.modeless="${!this.modal}"
|
|
371
|
+
.focusTrap="${this.modal}"
|
|
372
|
+
.withBackdrop="${this.withBackdrop}"
|
|
373
|
+
?no-horizontal-overlap="${this.__computeNoHorizontalOverlap(effectivePosition)}"
|
|
374
|
+
?no-vertical-overlap="${this.__computeNoVerticalOverlap(effectivePosition)}"
|
|
375
|
+
.horizontalAlign="${this.__computeHorizontalAlign(effectivePosition)}"
|
|
376
|
+
.verticalAlign="${this.__computeVerticalAlign(effectivePosition)}"
|
|
377
|
+
@mouseenter="${this.__onOverlayMouseEnter}"
|
|
378
|
+
@mouseleave="${this.__onOverlayMouseLeave}"
|
|
379
|
+
@focusin="${this.__onOverlayFocusIn}"
|
|
380
|
+
@focusout="${this.__onOverlayFocusOut}"
|
|
381
|
+
@opened-changed="${this.__onOpenedChanged}"
|
|
382
|
+
.restoreFocusOnClose="${this.__shouldRestoreFocus}"
|
|
383
|
+
.restoreFocusNode="${this.target}"
|
|
384
|
+
@vaadin-overlay-escape-press="${this.__onEscapePress}"
|
|
385
|
+
@vaadin-overlay-outside-click="${this.__onOutsideClick}"
|
|
386
|
+
@vaadin-overlay-closed="${this.__onOverlayClosed}"
|
|
387
|
+
></vaadin-popover-overlay>
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Requests an update for the content of the popover.
|
|
393
|
+
* While performing the update, it invokes the renderer passed in the `renderer` property.
|
|
394
|
+
*
|
|
395
|
+
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
|
|
396
|
+
*/
|
|
397
|
+
requestContentUpdate() {
|
|
398
|
+
if (!this.renderer || !this._overlayElement) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this._overlayElement.requestContentUpdate();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** @protected */
|
|
406
|
+
ready() {
|
|
407
|
+
super.ready();
|
|
408
|
+
|
|
409
|
+
this._overlayElement = this.shadowRoot.querySelector('vaadin-popover-overlay');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** @protected */
|
|
413
|
+
connectedCallback() {
|
|
414
|
+
super.connectedCallback();
|
|
415
|
+
|
|
416
|
+
document.addEventListener('click', this.__onGlobalClick, true);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** @protected */
|
|
420
|
+
disconnectedCallback() {
|
|
421
|
+
super.disconnectedCallback();
|
|
422
|
+
|
|
423
|
+
document.removeEventListener('click', this.__onGlobalClick, true);
|
|
424
|
+
|
|
425
|
+
this._openedStateController.close(true);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @param {HTMLElement} target
|
|
430
|
+
* @protected
|
|
431
|
+
* @override
|
|
432
|
+
*/
|
|
433
|
+
_addTargetListeners(target) {
|
|
434
|
+
target.addEventListener('click', this.__onTargetClick);
|
|
435
|
+
target.addEventListener('keydown', this.__onTargetKeydown);
|
|
436
|
+
target.addEventListener('mouseenter', this.__onTargetMouseEnter);
|
|
437
|
+
target.addEventListener('mouseleave', this.__onTargetMouseLeave);
|
|
438
|
+
target.addEventListener('focusin', this.__onTargetFocusIn);
|
|
439
|
+
target.addEventListener('focusout', this.__onTargetFocusOut);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {HTMLElement} target
|
|
444
|
+
* @protected
|
|
445
|
+
* @override
|
|
446
|
+
*/
|
|
447
|
+
_removeTargetListeners(target) {
|
|
448
|
+
target.removeEventListener('click', this.__onTargetClick);
|
|
449
|
+
target.removeEventListener('keydown', this.__onTargetKeydown);
|
|
450
|
+
target.removeEventListener('mouseenter', this.__onTargetMouseEnter);
|
|
451
|
+
target.removeEventListener('mouseleave', this.__onTargetMouseLeave);
|
|
452
|
+
target.removeEventListener('focusin', this.__onTargetFocusIn);
|
|
453
|
+
target.removeEventListener('focusout', this.__onTargetFocusOut);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @private */
|
|
457
|
+
__openedChanged(opened, oldOpened) {
|
|
458
|
+
if (opened) {
|
|
459
|
+
document.addEventListener('keydown', this.__onGlobalKeyDown, true);
|
|
460
|
+
} else if (oldOpened) {
|
|
461
|
+
document.removeEventListener('keydown', this.__onGlobalKeyDown, true);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** @private */
|
|
466
|
+
__openedOrTargetChanged(opened, target) {
|
|
467
|
+
if (target) {
|
|
468
|
+
target.setAttribute('aria-expanded', opened ? 'true' : 'false');
|
|
469
|
+
|
|
470
|
+
if (opened) {
|
|
471
|
+
target.setAttribute('aria-controls', this.__overlayId);
|
|
472
|
+
} else {
|
|
473
|
+
target.removeAttribute('aria-controls');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @private */
|
|
479
|
+
__overlayRoleOrTargetChanged(overlayRole, target) {
|
|
480
|
+
if (this.__oldTarget) {
|
|
481
|
+
this.__oldTarget.removeAttribute('aria-haspopup');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (target) {
|
|
485
|
+
const isDialog = overlayRole === 'dialog' || overlayRole === 'alertdialog';
|
|
486
|
+
target.setAttribute('aria-haspopup', isDialog ? 'dialog' : 'true');
|
|
487
|
+
|
|
488
|
+
this.__oldTarget = target;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Overlay's global outside click listener doesn't work when
|
|
494
|
+
* the overlay is modeless, so we use a separate listener.
|
|
495
|
+
* @private
|
|
496
|
+
*/
|
|
497
|
+
__onGlobalClick(event) {
|
|
498
|
+
if (
|
|
499
|
+
this.opened &&
|
|
500
|
+
!this.__isManual &&
|
|
501
|
+
!this.modal &&
|
|
502
|
+
!event.composedPath().some((el) => el === this._overlayElement || el === this.target) &&
|
|
503
|
+
!this.noCloseOnOutsideClick
|
|
504
|
+
) {
|
|
505
|
+
this._openedStateController.close(true);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** @private */
|
|
510
|
+
__onTargetClick() {
|
|
511
|
+
if (this.__hasTrigger('click')) {
|
|
512
|
+
if (!this.opened) {
|
|
513
|
+
this.__shouldRestoreFocus = true;
|
|
514
|
+
}
|
|
515
|
+
if (this.opened) {
|
|
516
|
+
this._openedStateController.close(true);
|
|
517
|
+
} else {
|
|
518
|
+
this._openedStateController.open({ immediate: true });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Overlay's global Escape press listener doesn't work when
|
|
525
|
+
* the overlay is modeless, so we use a separate listener.
|
|
526
|
+
* @private
|
|
527
|
+
*/
|
|
528
|
+
__onGlobalKeyDown(event) {
|
|
529
|
+
if (event.key === 'Escape' && !this.modal && !this.noCloseOnEsc && this.opened && !this.__isManual) {
|
|
530
|
+
// Prevent closing parent overlay (e.g. dialog)
|
|
531
|
+
event.stopPropagation();
|
|
532
|
+
this._openedStateController.close(true);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** @private */
|
|
537
|
+
__onTargetKeydown(event) {
|
|
538
|
+
// Prevent restoring focus after target blur on Tab key
|
|
539
|
+
if (event.key === 'Tab' && this.__shouldRestoreFocus) {
|
|
540
|
+
this.__shouldRestoreFocus = false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** @private */
|
|
545
|
+
__onTargetFocusIn() {
|
|
546
|
+
this.__focusInside = true;
|
|
547
|
+
|
|
548
|
+
if (this.__hasTrigger('focus')) {
|
|
549
|
+
// When trigger is set to both focus and click, only open on
|
|
550
|
+
// keyboard focus, to prevent issue when immediately closing
|
|
551
|
+
// on click which occurs after the focus caused by mousedown.
|
|
552
|
+
if (this.__hasTrigger('click') && !isKeyboardActive()) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Prevent overlay re-opening when restoring focus on close.
|
|
557
|
+
if (!this.__shouldRestoreFocus) {
|
|
558
|
+
this.__shouldRestoreFocus = true;
|
|
559
|
+
this._openedStateController.open({ trigger: 'focus' });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** @private */
|
|
565
|
+
__onTargetFocusOut(event) {
|
|
566
|
+
if (this._overlayElement.contains(event.relatedTarget)) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.__handleFocusout();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** @private */
|
|
574
|
+
__onTargetMouseEnter() {
|
|
575
|
+
this.__hoverInside = true;
|
|
576
|
+
|
|
577
|
+
if (this.__hasTrigger('hover') && !this.opened) {
|
|
578
|
+
// Prevent closing due to `pointer-events: none` set on body.
|
|
579
|
+
if (this.modal) {
|
|
580
|
+
this.target.style.pointerEvents = 'auto';
|
|
581
|
+
}
|
|
582
|
+
this._openedStateController.open({ trigger: 'hover' });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** @private */
|
|
587
|
+
__onTargetMouseLeave(event) {
|
|
588
|
+
if (this._overlayElement.contains(event.relatedTarget)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
this.__handleMouseLeave();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** @private */
|
|
596
|
+
__onOverlayFocusIn() {
|
|
597
|
+
this.__focusInside = true;
|
|
598
|
+
|
|
599
|
+
// When using Tab to move focus, restoring focus is reset. However, if pressing Tab
|
|
600
|
+
// causes focus to be moved inside the overlay, we should restore focus on close.
|
|
601
|
+
if (this.__hasTrigger('focus') || this.__hasTrigger('click')) {
|
|
602
|
+
this.__shouldRestoreFocus = true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** @private */
|
|
607
|
+
__onOverlayFocusOut(event) {
|
|
608
|
+
if (event.relatedTarget === this.target || this._overlayElement.contains(event.relatedTarget)) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
this.__handleFocusout();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** @private */
|
|
616
|
+
__onOverlayMouseEnter() {
|
|
617
|
+
this.__hoverInside = true;
|
|
618
|
+
|
|
619
|
+
// Prevent closing if cursor moves to the overlay during hide delay.
|
|
620
|
+
if (this.__hasTrigger('hover') && this._openedStateController.isClosing) {
|
|
621
|
+
this._openedStateController.open({ immediate: true });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** @private */
|
|
626
|
+
__onOverlayMouseLeave(event) {
|
|
627
|
+
if (event.relatedTarget === this.target) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this.__handleMouseLeave();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** @private */
|
|
635
|
+
__handleFocusout() {
|
|
636
|
+
this.__focusInside = false;
|
|
637
|
+
|
|
638
|
+
if (this.__hasTrigger('hover') && this.__hoverInside) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (this.__hasTrigger('focus')) {
|
|
643
|
+
this._openedStateController.close(true);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** @private */
|
|
648
|
+
__handleMouseLeave() {
|
|
649
|
+
this.__hoverInside = false;
|
|
650
|
+
|
|
651
|
+
if (this.__hasTrigger('focus') && this.__focusInside) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (this.__hasTrigger('hover')) {
|
|
656
|
+
this._openedStateController.close();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** @private */
|
|
661
|
+
__onOpenedChanged(event) {
|
|
662
|
+
this.opened = event.detail.value;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** @private */
|
|
666
|
+
__onOverlayClosed() {
|
|
667
|
+
// Reset restoring focus state after a timeout to make sure focus was restored
|
|
668
|
+
// and then allow re-opening overlay on re-focusing target with focus trigger.
|
|
669
|
+
if (this.__shouldRestoreFocus) {
|
|
670
|
+
setTimeout(() => {
|
|
671
|
+
this.__shouldRestoreFocus = false;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Restore pointer-events set when opening on hover.
|
|
676
|
+
if (this.modal && this.target.style.pointerEvents) {
|
|
677
|
+
this.target.style.pointerEvents = '';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Close the popover if `noCloseOnEsc` isn't set to true.
|
|
683
|
+
* @private
|
|
684
|
+
*/
|
|
685
|
+
__onEscapePress(e) {
|
|
686
|
+
if (this.noCloseOnEsc || this.__isManual) {
|
|
687
|
+
e.preventDefault();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Close the popover if `noCloseOnOutsideClick` isn't set to true.
|
|
693
|
+
* @private
|
|
694
|
+
*/
|
|
695
|
+
__onOutsideClick(e) {
|
|
696
|
+
if (this.noCloseOnOutsideClick || this.__isManual) {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** @private */
|
|
702
|
+
__hasTrigger(trigger) {
|
|
703
|
+
return Array.isArray(this.trigger) && this.trigger.includes(trigger);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** @private */
|
|
707
|
+
get __isManual() {
|
|
708
|
+
return this.trigger == null || (Array.isArray(this.trigger) && this.trigger.length === 0);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** @private */
|
|
712
|
+
__updateDimension(overlay, dimension, value) {
|
|
713
|
+
const prop = `--_vaadin-popover-content-${dimension}`;
|
|
714
|
+
|
|
715
|
+
if (value) {
|
|
716
|
+
overlay.style.setProperty(prop, value);
|
|
717
|
+
} else {
|
|
718
|
+
overlay.style.removeProperty(prop);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** @private */
|
|
723
|
+
__updateContentHeight(height, overlay) {
|
|
724
|
+
if (overlay) {
|
|
725
|
+
this.__updateDimension(overlay, 'height', height);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** @private */
|
|
730
|
+
__updateContentWidth(width, overlay) {
|
|
731
|
+
if (overlay) {
|
|
732
|
+
this.__updateDimension(overlay, 'width', width);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
defineCustomElement(Popover);
|
|
738
|
+
|
|
739
|
+
export { Popover };
|