@vaadin/vaadin-overlay 23.0.0-alpha1 → 23.0.0-alpha2
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/vaadin-overlay",
|
|
3
|
-
"version": "23.0.0-
|
|
3
|
+
"version": "23.0.0-alpha2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@polymer/polymer": "^3.0.0",
|
|
37
|
-
"@vaadin/component-base": "23.0.0-
|
|
38
|
-
"@vaadin/vaadin-lumo-styles": "23.0.0-
|
|
39
|
-
"@vaadin/vaadin-material-styles": "23.0.0-
|
|
40
|
-
"@vaadin/vaadin-themable-mixin": "23.0.0-
|
|
37
|
+
"@vaadin/component-base": "23.0.0-alpha2",
|
|
38
|
+
"@vaadin/vaadin-lumo-styles": "23.0.0-alpha2",
|
|
39
|
+
"@vaadin/vaadin-material-styles": "23.0.0-alpha2",
|
|
40
|
+
"@vaadin/vaadin-themable-mixin": "23.0.0-alpha2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@esm-bundle/chai": "^4.3.4",
|
|
44
44
|
"@polymer/iron-overlay-behavior": "^3.0.0",
|
|
45
|
-
"@vaadin/button": "23.0.0-
|
|
46
|
-
"@vaadin/radio-group": "23.0.0-
|
|
45
|
+
"@vaadin/button": "23.0.0-alpha2",
|
|
46
|
+
"@vaadin/radio-group": "23.0.0-alpha2",
|
|
47
47
|
"@vaadin/testing-helpers": "^0.3.2",
|
|
48
|
-
"@vaadin/text-field": "23.0.0-
|
|
48
|
+
"@vaadin/text-field": "23.0.0-alpha2",
|
|
49
49
|
"lit": "^2.0.0",
|
|
50
50
|
"sinon": "^9.2.1"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "070f586dead02ca41b66717820c647f48bf1665f"
|
|
53
53
|
}
|
|
@@ -76,6 +76,7 @@ export const PositionMixin = (superClass) =>
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
+
|
|
79
80
|
static get observers() {
|
|
80
81
|
return [
|
|
81
82
|
'__positionSettingsChanged(positionTarget, horizontalAlign, verticalAlign, noHorizontalOverlap, noVerticalOverlap)',
|
|
@@ -212,6 +213,7 @@ export const PositionMixin = (superClass) =>
|
|
|
212
213
|
);
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
// eslint-disable-next-line max-params
|
|
215
217
|
__shouldAlignStart(targetRect, contentSize, viewportSize, margins, defaultAlignStart, noOverlap, propNames) {
|
|
216
218
|
const spaceForStartAlignment =
|
|
217
219
|
viewportSize - targetRect[noOverlap ? propNames.end : propNames.start] - margins[propNames.end];
|
|
@@ -230,6 +232,7 @@ export const PositionMixin = (superClass) =>
|
|
|
230
232
|
* Returns an object with CSS position properties to set,
|
|
231
233
|
* e.g. { top: "100px", bottom: "" }
|
|
232
234
|
*/
|
|
235
|
+
// eslint-disable-next-line max-params
|
|
233
236
|
__calculatePositionInOneDimension(targetRect, overlayRect, noOverlap, propNames, overlay, shouldAlignStart) {
|
|
234
237
|
const cssPropNameToSet = shouldAlignStart ? propNames.start : propNames.end;
|
|
235
238
|
const cssPropNameToClear = shouldAlignStart ? propNames.end : propNames.start;
|
package/src/vaadin-overlay.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
+
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
|
|
6
7
|
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
|
|
7
8
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
8
9
|
|
|
@@ -13,8 +14,41 @@ export type OverlayRenderer = (root: HTMLElement, owner: HTMLElement, model?: ob
|
|
|
13
14
|
*/
|
|
14
15
|
export type OverlayOpenedChangedEvent = CustomEvent<{ value: boolean }>;
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Fired after the overlay is opened.
|
|
19
|
+
*/
|
|
20
|
+
export type OverlayOpenEvent = CustomEvent;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fired before the overlay will be closed.
|
|
24
|
+
* If canceled the closing of the overlay is canceled as well.
|
|
25
|
+
*/
|
|
26
|
+
export type OverlayCloseEvent = CustomEvent;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fired when the overlay will be closed.
|
|
30
|
+
*/
|
|
31
|
+
export type OverlayClosingEvent = CustomEvent;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fired before the overlay will be closed on outside click.
|
|
35
|
+
* If canceled the closing of the overlay is canceled as well.
|
|
36
|
+
*/
|
|
37
|
+
export type OverlayOutsideClickEvent = CustomEvent<{ sourceEvent: MouseEvent }>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fired before the overlay will be closed on ESC button press.
|
|
41
|
+
* If canceled the closing of the overlay is canceled as well.
|
|
42
|
+
*/
|
|
43
|
+
export type OverlayEscapePressEvent = CustomEvent<{ sourceEvent: KeyboardEvent }>;
|
|
44
|
+
|
|
16
45
|
export interface OverlayElementEventMap {
|
|
17
46
|
'opened-changed': OverlayOpenedChangedEvent;
|
|
47
|
+
'vaadin-overlay-open': OverlayOpenEvent;
|
|
48
|
+
'vaadin-overlay-close': OverlayCloseEvent;
|
|
49
|
+
'vaadin-overlay-closing': OverlayClosingEvent;
|
|
50
|
+
'vaadin-overlay-outside-click': OverlayOutsideClickEvent;
|
|
51
|
+
'vaadin-overlay-escape-press': OverlayEscapePressEvent;
|
|
18
52
|
}
|
|
19
53
|
|
|
20
54
|
export type OverlayEventMap = HTMLElementEventMap & OverlayElementEventMap;
|
|
@@ -104,8 +138,13 @@ export type OverlayEventMap = HTMLElementEventMap & OverlayElementEventMap;
|
|
|
104
138
|
* See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
|
|
105
139
|
*
|
|
106
140
|
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
|
|
141
|
+
* @fires {CustomEvent} vaadin-overlay-open - Fired after the overlay is opened.
|
|
142
|
+
* @fires {CustomEvent} vaadin-overlay-close - Fired before the overlay will be closed. If canceled the closing of the overlay is canceled as well.
|
|
143
|
+
* @fires {CustomEvent} vaadin-overlay-closing - Fired when the overlay will be closed.
|
|
144
|
+
* @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.
|
|
145
|
+
* @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.
|
|
107
146
|
*/
|
|
108
|
-
declare class OverlayElement extends ThemableMixin(DirMixin(HTMLElement)) {
|
|
147
|
+
declare class OverlayElement extends ThemableMixin(DirMixin(ControllerMixin(HTMLElement))) {
|
|
109
148
|
/**
|
|
110
149
|
* When true, the overlay is visible and attached to body.
|
|
111
150
|
*/
|
|
@@ -169,6 +208,12 @@ declare class OverlayElement extends ThemableMixin(DirMixin(HTMLElement)) {
|
|
|
169
208
|
*/
|
|
170
209
|
restoreFocusOnClose: boolean;
|
|
171
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Set to specify the element which should be focused on overlay close,
|
|
213
|
+
* if `restoreFocusOnClose` is set to true.
|
|
214
|
+
*/
|
|
215
|
+
restoreFocusNode?: HTMLElement;
|
|
216
|
+
|
|
172
217
|
close(sourceEvent?: Event | null): void;
|
|
173
218
|
|
|
174
219
|
/**
|
package/src/vaadin-overlay.js
CHANGED
|
@@ -8,9 +8,10 @@ import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
|
|
|
8
8
|
import { templatize } from '@polymer/polymer/lib/utils/templatize.js';
|
|
9
9
|
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
|
|
10
10
|
import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
|
|
11
|
+
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
|
|
11
12
|
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
|
|
13
|
+
import { FocusTrapController } from '@vaadin/component-base/src/focus-trap-controller.js';
|
|
12
14
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
13
|
-
import { FocusablesHelper } from './vaadin-focusables-helper.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
*
|
|
@@ -92,12 +93,18 @@ import { FocusablesHelper } from './vaadin-focusables-helper.js';
|
|
|
92
93
|
* See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
|
|
93
94
|
*
|
|
94
95
|
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
|
|
96
|
+
* @fires {CustomEvent} vaadin-overlay-open - Fired after the overlay is opened.
|
|
97
|
+
* @fires {CustomEvent} vaadin-overlay-close - Fired before the overlay will be closed. If canceled the closing of the overlay is canceled as well.
|
|
98
|
+
* @fires {CustomEvent} vaadin-overlay-closing - Fired when the overlay will be closed.
|
|
99
|
+
* @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.
|
|
100
|
+
* @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.
|
|
95
101
|
*
|
|
96
102
|
* @extends HTMLElement
|
|
97
103
|
* @mixes ThemableMixin
|
|
98
104
|
* @mixes DirMixin
|
|
105
|
+
* @mixes ControllerMixin
|
|
99
106
|
*/
|
|
100
|
-
class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
107
|
+
class OverlayElement extends ThemableMixin(DirMixin(ControllerMixin(PolymerElement))) {
|
|
101
108
|
static get template() {
|
|
102
109
|
return html`
|
|
103
110
|
<style>
|
|
@@ -282,6 +289,15 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
282
289
|
value: false
|
|
283
290
|
},
|
|
284
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Set to specify the element which should be focused on overlay close,
|
|
294
|
+
* if `restoreFocusOnClose` is set to true.
|
|
295
|
+
* @type {HTMLElement}
|
|
296
|
+
*/
|
|
297
|
+
restoreFocusNode: {
|
|
298
|
+
type: HTMLElement
|
|
299
|
+
},
|
|
300
|
+
|
|
285
301
|
/** @private */
|
|
286
302
|
_mouseDownInside: {
|
|
287
303
|
type: Boolean
|
|
@@ -342,6 +358,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
342
358
|
if (isIOS) {
|
|
343
359
|
this._boundIosResizeListener = () => this._detectIosNavbar();
|
|
344
360
|
}
|
|
361
|
+
|
|
362
|
+
this.__focusTrapController = new FocusTrapController(this);
|
|
345
363
|
}
|
|
346
364
|
|
|
347
365
|
/** @protected */
|
|
@@ -356,6 +374,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
356
374
|
// and <vaadin-context-menu>).
|
|
357
375
|
this.addEventListener('click', () => {});
|
|
358
376
|
this.$.backdrop.addEventListener('click', () => {});
|
|
377
|
+
|
|
378
|
+
this.addController(this.__focusTrapController);
|
|
359
379
|
}
|
|
360
380
|
|
|
361
381
|
/** @private */
|
|
@@ -495,15 +515,7 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
495
515
|
return;
|
|
496
516
|
}
|
|
497
517
|
|
|
498
|
-
|
|
499
|
-
if (event.key === 'Tab' && this.focusTrap && !event.defaultPrevented) {
|
|
500
|
-
// if only tab key is pressed, cycle forward, else cycle backwards.
|
|
501
|
-
this._cycleTab(event.shiftKey ? -1 : 1);
|
|
502
|
-
|
|
503
|
-
event.preventDefault();
|
|
504
|
-
|
|
505
|
-
// ESC
|
|
506
|
-
} else if (event.key === 'Escape' || event.key === 'Esc') {
|
|
518
|
+
if (event.key === 'Escape') {
|
|
507
519
|
const evt = new CustomEvent('vaadin-overlay-escape-press', {
|
|
508
520
|
bubbles: true,
|
|
509
521
|
cancelable: true,
|
|
@@ -539,8 +551,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
539
551
|
this._animatedOpening();
|
|
540
552
|
|
|
541
553
|
afterNextRender(this, () => {
|
|
542
|
-
if (this.focusTrap
|
|
543
|
-
this.
|
|
554
|
+
if (this.focusTrap) {
|
|
555
|
+
this.__focusTrapController.trapFocus(this.$.overlay);
|
|
544
556
|
}
|
|
545
557
|
|
|
546
558
|
const evt = new CustomEvent('vaadin-overlay-open', { bubbles: true });
|
|
@@ -551,6 +563,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
551
563
|
this._addGlobalListeners();
|
|
552
564
|
}
|
|
553
565
|
} else if (wasOpened) {
|
|
566
|
+
this.__focusTrapController.releaseFocus();
|
|
567
|
+
|
|
554
568
|
this._animatedClosing();
|
|
555
569
|
|
|
556
570
|
if (!this.modeless) {
|
|
@@ -617,16 +631,12 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
617
631
|
}
|
|
618
632
|
this.setAttribute('opening', '');
|
|
619
633
|
|
|
620
|
-
const finishOpening = () => {
|
|
621
|
-
document.addEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
|
|
622
|
-
|
|
623
|
-
this.removeAttribute('opening');
|
|
624
|
-
};
|
|
625
|
-
|
|
626
634
|
if (this._shouldAnimate()) {
|
|
627
|
-
this._enqueueAnimation('opening',
|
|
635
|
+
this._enqueueAnimation('opening', () => {
|
|
636
|
+
this._finishOpening();
|
|
637
|
+
});
|
|
628
638
|
} else {
|
|
629
|
-
|
|
639
|
+
this._finishOpening();
|
|
630
640
|
}
|
|
631
641
|
}
|
|
632
642
|
|
|
@@ -639,6 +649,25 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
639
649
|
}
|
|
640
650
|
|
|
641
651
|
/** @protected */
|
|
652
|
+
_finishOpening() {
|
|
653
|
+
document.addEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
|
|
654
|
+
this.removeAttribute('opening');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** @protected */
|
|
658
|
+
_finishClosing() {
|
|
659
|
+
document.removeEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
|
|
660
|
+
this._detachOverlay();
|
|
661
|
+
this.$.overlay.style.removeProperty('pointer-events');
|
|
662
|
+
this.removeAttribute('closing');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* @event vaadin-overlay-closing
|
|
667
|
+
* Fired when the overlay will be closed.
|
|
668
|
+
*
|
|
669
|
+
* @protected
|
|
670
|
+
*/
|
|
642
671
|
_animatedClosing() {
|
|
643
672
|
if (this.hasAttribute('opening')) {
|
|
644
673
|
this._flushAnimation('opening');
|
|
@@ -646,7 +675,11 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
646
675
|
if (this._placeholder) {
|
|
647
676
|
this._exitModalState();
|
|
648
677
|
|
|
649
|
-
|
|
678
|
+
// Use this.restoreFocusNode if specified, othwerwise fallback to the node
|
|
679
|
+
// which was focused before opening the overlay.
|
|
680
|
+
const restoreFocusNode = this.restoreFocusNode || this.__restoreFocusNode;
|
|
681
|
+
|
|
682
|
+
if (this.restoreFocusOnClose && restoreFocusNode) {
|
|
650
683
|
// If the activeElement is `<body>` or inside the overlay,
|
|
651
684
|
// we are allowed to restore the focus. In all the other
|
|
652
685
|
// cases focus might have been moved elsewhere by another
|
|
@@ -655,24 +688,22 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
655
688
|
const activeElement = this._getActiveElement();
|
|
656
689
|
|
|
657
690
|
if (activeElement === document.body || this._deepContains(activeElement)) {
|
|
658
|
-
|
|
691
|
+
// Focusing the restoreFocusNode doesn't always work synchronously on Firefox and Safari
|
|
692
|
+
// (e.g. combo-box overlay close on outside click).
|
|
693
|
+
setTimeout(() => restoreFocusNode.focus());
|
|
659
694
|
}
|
|
660
695
|
this.__restoreFocusNode = null;
|
|
661
696
|
}
|
|
662
697
|
|
|
663
698
|
this.setAttribute('closing', '');
|
|
664
|
-
|
|
665
|
-
const finishClosing = () => {
|
|
666
|
-
document.removeEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
|
|
667
|
-
this._detachOverlay();
|
|
668
|
-
this.shadowRoot.querySelector('[part="overlay"]').style.removeProperty('pointer-events');
|
|
669
|
-
this.removeAttribute('closing');
|
|
670
|
-
};
|
|
699
|
+
this.dispatchEvent(new CustomEvent('vaadin-overlay-closing'));
|
|
671
700
|
|
|
672
701
|
if (this._shouldAnimate()) {
|
|
673
|
-
this._enqueueAnimation('closing',
|
|
702
|
+
this._enqueueAnimation('closing', () => {
|
|
703
|
+
this._finishClosing();
|
|
704
|
+
});
|
|
674
705
|
} else {
|
|
675
|
-
|
|
706
|
+
this._finishClosing();
|
|
676
707
|
}
|
|
677
708
|
}
|
|
678
709
|
}
|
|
@@ -863,6 +894,7 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
863
894
|
}
|
|
864
895
|
|
|
865
896
|
/** @private */
|
|
897
|
+
// eslint-disable-next-line max-params
|
|
866
898
|
_templateOrRendererChanged(template, renderer, owner, model, opened) {
|
|
867
899
|
if (template && renderer) {
|
|
868
900
|
this._removeNewRendererOrTemplate(template, this._oldTemplate, renderer, this._oldRenderer);
|
|
@@ -900,59 +932,6 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
|
|
|
900
932
|
}
|
|
901
933
|
}
|
|
902
934
|
|
|
903
|
-
/**
|
|
904
|
-
* @param {Element} element
|
|
905
|
-
* @return {boolean}
|
|
906
|
-
* @protected
|
|
907
|
-
*/
|
|
908
|
-
_isFocused(element) {
|
|
909
|
-
return element && element.getRootNode().activeElement === element;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* @param {Element[]} elements
|
|
914
|
-
* @return {number}
|
|
915
|
-
* @protected
|
|
916
|
-
*/
|
|
917
|
-
_focusedIndex(elements) {
|
|
918
|
-
elements = elements || this._getFocusableElements();
|
|
919
|
-
return elements.indexOf(elements.filter(this._isFocused).pop());
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* @param {number} increment
|
|
924
|
-
* @param {number | undefined} index
|
|
925
|
-
* @protected
|
|
926
|
-
*/
|
|
927
|
-
_cycleTab(increment, index) {
|
|
928
|
-
const focusableElements = this._getFocusableElements();
|
|
929
|
-
|
|
930
|
-
if (index === undefined) {
|
|
931
|
-
index = this._focusedIndex(focusableElements);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
index += increment;
|
|
935
|
-
|
|
936
|
-
// rollover to first item
|
|
937
|
-
if (index >= focusableElements.length) {
|
|
938
|
-
index = 0;
|
|
939
|
-
// go to last item
|
|
940
|
-
} else if (index < 0) {
|
|
941
|
-
index = focusableElements.length - 1;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
focusableElements[index].focus();
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* @return {!Array<!HTMLElement>}
|
|
949
|
-
* @protected
|
|
950
|
-
*/
|
|
951
|
-
_getFocusableElements() {
|
|
952
|
-
// collect all focusable elements
|
|
953
|
-
return FocusablesHelper.getTabbableNodes(this.$.overlay);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
935
|
/**
|
|
957
936
|
* @return {!Element}
|
|
958
937
|
* @protected
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `Polymer.IronFocusablesHelper` relies on some Polymer-specific legacy API,
|
|
3
|
-
* especially the `root` property which does not exist for native shadow DOM.
|
|
4
|
-
* That's why we have this helper here.
|
|
5
|
-
* See https://github.com/PolymerElements/iron-overlay-behavior/issues/282
|
|
6
|
-
*/
|
|
7
|
-
declare class FocusablesHelper {
|
|
8
|
-
/**
|
|
9
|
-
* Returns a sorted array of tabbable nodes, including the root node.
|
|
10
|
-
* It searches the tabbable nodes in the light and shadow dom of the children,
|
|
11
|
-
* sorting the result by tabindex.
|
|
12
|
-
*/
|
|
13
|
-
static getTabbableNodes(node: Node): HTMLElement[];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Returns if a element is focusable.
|
|
17
|
-
*/
|
|
18
|
-
static isFocusable(element: HTMLElement): boolean;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Returns if a element is tabbable. To be tabbable, a element must be
|
|
22
|
-
* focusable, visible, and with a tabindex !== -1.
|
|
23
|
-
*/
|
|
24
|
-
static isTabbable(element: HTMLElement): boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export { FocusablesHelper };
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `Polymer.IronFocusablesHelper` relies on some Polymer-specific legacy API,
|
|
9
|
-
* especially the `root` property which does not exist for native shadow DOM.
|
|
10
|
-
* That's why we have this helper here.
|
|
11
|
-
* See https://github.com/PolymerElements/iron-overlay-behavior/issues/282
|
|
12
|
-
*/
|
|
13
|
-
class FocusablesHelper {
|
|
14
|
-
/**
|
|
15
|
-
* Returns a sorted array of tabbable nodes, including the root node.
|
|
16
|
-
* It searches the tabbable nodes in the light and shadow dom of the children,
|
|
17
|
-
* sorting the result by tabindex.
|
|
18
|
-
* @param {!Node} node
|
|
19
|
-
* @return {!Array<!HTMLElement>}
|
|
20
|
-
*/
|
|
21
|
-
static getTabbableNodes(node) {
|
|
22
|
-
const result = [];
|
|
23
|
-
// If there is at least one element with tabindex > 0, we need to sort
|
|
24
|
-
// the final array by tabindex.
|
|
25
|
-
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
|
26
|
-
if (needsSortByTabIndex) {
|
|
27
|
-
return this._sortByTabIndex(result);
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Returns if a element is focusable.
|
|
34
|
-
* @param {!HTMLElement} element
|
|
35
|
-
* @return {boolean}
|
|
36
|
-
*/
|
|
37
|
-
static isFocusable(element) {
|
|
38
|
-
// From http://stackoverflow.com/a/1600194/4228703:
|
|
39
|
-
// There isn't a definite list, it's up to the browser. The only
|
|
40
|
-
// standard we have is DOM Level 2 HTML
|
|
41
|
-
// https://www.w3.org/TR/DOM-Level-2-HTML/html.html, according to which the
|
|
42
|
-
// only elements that have a focus() method are HTMLInputElement,
|
|
43
|
-
// HTMLSelectElement, HTMLTextAreaElement and HTMLAnchorElement. This
|
|
44
|
-
// notably omits HTMLButtonElement and HTMLAreaElement. Referring to these
|
|
45
|
-
// tests with tabbables in different browsers
|
|
46
|
-
// http://allyjs.io/data-tables/focusable.html
|
|
47
|
-
|
|
48
|
-
// Elements that cannot be focused if they have [disabled] attribute.
|
|
49
|
-
if (element.matches('input, select, textarea, button, object')) {
|
|
50
|
-
return element.matches(':not([disabled])');
|
|
51
|
-
}
|
|
52
|
-
// Elements that can be focused even if they have [disabled] attribute.
|
|
53
|
-
return element.matches('a[href], area[href], iframe, [tabindex], [contentEditable]');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Returns if a element is tabbable. To be tabbable, a element must be
|
|
58
|
-
* focusable, visible, and with a tabindex !== -1.
|
|
59
|
-
* @param {!HTMLElement} element
|
|
60
|
-
* @return {boolean}
|
|
61
|
-
*/
|
|
62
|
-
static isTabbable(element) {
|
|
63
|
-
return this.isFocusable(element) && element.matches(':not([tabindex="-1"])') && this._isVisible(element);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Returns the normalized element tabindex. If not focusable, returns -1.
|
|
68
|
-
* It checks for the attribute "tabindex" instead of the element property
|
|
69
|
-
* `tabIndex` since browsers assign different values to it.
|
|
70
|
-
* e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
|
|
71
|
-
* @param {!HTMLElement} element
|
|
72
|
-
* @return {!number}
|
|
73
|
-
* @private
|
|
74
|
-
*/
|
|
75
|
-
static _normalizedTabIndex(element) {
|
|
76
|
-
if (this.isFocusable(element)) {
|
|
77
|
-
const tabIndex = element.getAttribute('tabindex') || 0;
|
|
78
|
-
return Number(tabIndex);
|
|
79
|
-
}
|
|
80
|
-
return -1;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Searches for nodes that are tabbable and adds them to the `result` array.
|
|
85
|
-
* Returns if the `result` array needs to be sorted by tabindex.
|
|
86
|
-
* @param {!Node} node The starting point for the search; added to `result` if tabbable.
|
|
87
|
-
* @param {!Array<!HTMLElement>} result
|
|
88
|
-
* @return {boolean}
|
|
89
|
-
* @private
|
|
90
|
-
*/
|
|
91
|
-
static _collectTabbableNodes(node, result) {
|
|
92
|
-
// If not an element or not visible, no need to explore children.
|
|
93
|
-
if (node.nodeType !== Node.ELEMENT_NODE || !this._isVisible(node)) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
const element = /** @type {!HTMLElement} */ (node);
|
|
97
|
-
const tabIndex = this._normalizedTabIndex(element);
|
|
98
|
-
let needsSort = tabIndex > 0;
|
|
99
|
-
if (tabIndex >= 0) {
|
|
100
|
-
result.push(element);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// In ShadowDOM v1, tab order is affected by the order of distribution.
|
|
104
|
-
// E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B];
|
|
105
|
-
// in ShadowDOM v0 tab order is not affected by the distribution order,
|
|
106
|
-
// in fact getTabbableNodes(#root) returns [#B, #A].
|
|
107
|
-
// <div id="root">
|
|
108
|
-
// <!-- shadow -->
|
|
109
|
-
// <slot name="a">
|
|
110
|
-
// <slot name="b">
|
|
111
|
-
// <!-- /shadow -->
|
|
112
|
-
// <input id="A" slot="a">
|
|
113
|
-
// <input id="B" slot="b" tabindex="1">
|
|
114
|
-
// </div>
|
|
115
|
-
let children;
|
|
116
|
-
if (element.localName === 'slot') {
|
|
117
|
-
children = element.assignedNodes({ flatten: true });
|
|
118
|
-
} else {
|
|
119
|
-
// Use shadow root if possible, will check for distributed nodes.
|
|
120
|
-
children = (element.shadowRoot || element).children;
|
|
121
|
-
}
|
|
122
|
-
if (children) {
|
|
123
|
-
for (let i = 0; i < children.length; i++) {
|
|
124
|
-
// Ensure method is always invoked to collect tabbable children.
|
|
125
|
-
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return needsSort;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Returns false if the element has `visibility: hidden` or `display: none`
|
|
133
|
-
* @param {!HTMLElement} element
|
|
134
|
-
* @return {boolean}
|
|
135
|
-
* @private
|
|
136
|
-
*/
|
|
137
|
-
static _isVisible(element) {
|
|
138
|
-
// Check inline style first to save a re-flow. If looks good, check also
|
|
139
|
-
// computed style.
|
|
140
|
-
let style = element.style;
|
|
141
|
-
if (style.visibility !== 'hidden' && style.display !== 'none') {
|
|
142
|
-
style = window.getComputedStyle(element);
|
|
143
|
-
return style.visibility !== 'hidden' && style.display !== 'none';
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Sorts an array of tabbable elements by tabindex. Returns a new array.
|
|
150
|
-
* @param {!Array<!HTMLElement>} tabbables
|
|
151
|
-
* @return {!Array<!HTMLElement>}
|
|
152
|
-
* @private
|
|
153
|
-
*/
|
|
154
|
-
static _sortByTabIndex(tabbables) {
|
|
155
|
-
// Implement a merge sort as Array.prototype.sort does a non-stable sort
|
|
156
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
|
|
157
|
-
const len = tabbables.length;
|
|
158
|
-
if (len < 2) {
|
|
159
|
-
return tabbables;
|
|
160
|
-
}
|
|
161
|
-
const pivot = Math.ceil(len / 2);
|
|
162
|
-
const left = this._sortByTabIndex(tabbables.slice(0, pivot));
|
|
163
|
-
const right = this._sortByTabIndex(tabbables.slice(pivot));
|
|
164
|
-
return this._mergeSortByTabIndex(left, right);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Merge sort iterator, merges the two arrays into one, sorted by tab index.
|
|
169
|
-
* @param {!Array<!HTMLElement>} left
|
|
170
|
-
* @param {!Array<!HTMLElement>} right
|
|
171
|
-
* @return {!Array<!HTMLElement>}
|
|
172
|
-
* @private
|
|
173
|
-
*/
|
|
174
|
-
static _mergeSortByTabIndex(left, right) {
|
|
175
|
-
const result = [];
|
|
176
|
-
while (left.length > 0 && right.length > 0) {
|
|
177
|
-
if (this._hasLowerTabOrder(left[0], right[0])) {
|
|
178
|
-
result.push(right.shift());
|
|
179
|
-
} else {
|
|
180
|
-
result.push(left.shift());
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return result.concat(left, right);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Returns if element `a` has lower tab order compared to element `b`
|
|
189
|
-
* (both elements are assumed to be focusable and tabbable).
|
|
190
|
-
* Elements with tabindex = 0 have lower tab order compared to elements
|
|
191
|
-
* with tabindex > 0.
|
|
192
|
-
* If both have same tabindex, it returns false.
|
|
193
|
-
* @param {!HTMLElement} a
|
|
194
|
-
* @param {!HTMLElement} b
|
|
195
|
-
* @return {boolean}
|
|
196
|
-
* @private
|
|
197
|
-
*/
|
|
198
|
-
static _hasLowerTabOrder(a, b) {
|
|
199
|
-
// Normalize tabIndexes
|
|
200
|
-
// e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
|
|
201
|
-
const ati = Math.max(a.tabIndex, 0);
|
|
202
|
-
const bti = Math.max(b.tabIndex, 0);
|
|
203
|
-
return ati === 0 || bti === 0 ? bti > ati : ati > bti;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export { FocusablesHelper };
|