@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-alpha1",
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-alpha1",
38
- "@vaadin/vaadin-lumo-styles": "23.0.0-alpha1",
39
- "@vaadin/vaadin-material-styles": "23.0.0-alpha1",
40
- "@vaadin/vaadin-themable-mixin": "23.0.0-alpha1"
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-alpha1",
46
- "@vaadin/radio-group": "23.0.0-alpha1",
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-alpha1",
48
+ "@vaadin/text-field": "23.0.0-alpha2",
49
49
  "lit": "^2.0.0",
50
50
  "sinon": "^9.2.1"
51
51
  },
52
- "gitHead": "fbcb07328fdf88260e3b461088d207426b21c710"
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;
@@ -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
  /**
@@ -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
- // TAB
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 && !this.contains(document.activeElement)) {
543
- this._cycleTab(0, 0);
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', finishOpening);
635
+ this._enqueueAnimation('opening', () => {
636
+ this._finishOpening();
637
+ });
628
638
  } else {
629
- finishOpening();
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
- if (this.restoreFocusOnClose && this.__restoreFocusNode) {
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
- this.__restoreFocusNode.focus();
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', finishClosing);
702
+ this._enqueueAnimation('closing', () => {
703
+ this._finishClosing();
704
+ });
674
705
  } else {
675
- finishClosing();
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 };