@vaadin/vaadin-overlay 22.0.1 → 23.0.0-alpha3

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": "22.0.1",
3
+ "version": "23.0.0-alpha3",
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": "^22.0.1",
38
- "@vaadin/vaadin-lumo-styles": "^22.0.1",
39
- "@vaadin/vaadin-material-styles": "^22.0.1",
40
- "@vaadin/vaadin-themable-mixin": "^22.0.1"
37
+ "@vaadin/component-base": "23.0.0-alpha3",
38
+ "@vaadin/vaadin-lumo-styles": "23.0.0-alpha3",
39
+ "@vaadin/vaadin-material-styles": "23.0.0-alpha3",
40
+ "@vaadin/vaadin-themable-mixin": "23.0.0-alpha3"
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": "^22.0.1",
46
- "@vaadin/radio-group": "^22.0.1",
45
+ "@vaadin/button": "23.0.0-alpha3",
46
+ "@vaadin/radio-group": "23.0.0-alpha3",
47
47
  "@vaadin/testing-helpers": "^0.3.2",
48
- "@vaadin/text-field": "^22.0.1",
48
+ "@vaadin/text-field": "23.0.0-alpha3",
49
49
  "lit": "^2.0.0",
50
50
  "sinon": "^9.2.1"
51
51
  },
52
- "gitHead": "2b0a2bff0369d6020f7cc33ad35506aa2d1f6f68"
52
+ "gitHead": "490037919a9e054cc002c1b3be0c94a1603e1a44"
53
53
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
 
@@ -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;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2022 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;
@@ -61,8 +95,7 @@ export type OverlayEventMap = HTMLElementEventMap & OverlayElementEventMap;
61
95
  *
62
96
  * The overlay provides `forwardHostProp` when calling
63
97
  * `Polymer.Templatize.templatize` for the template, so that the bindings
64
- * from the parent scope propagate to the content. You can also pass
65
- * custom `instanceProps` object using the `instanceProps` property.
98
+ * from the parent scope propagate to the content.
66
99
  *
67
100
  * ```html
68
101
  * <vaadin-overlay>
@@ -70,10 +103,6 @@ export type OverlayEventMap = HTMLElementEventMap & OverlayElementEventMap;
70
103
  * </vaadin-overlay>
71
104
  * ```
72
105
  *
73
- * **NOTE:** when using `instanceProps`: because of the Polymer limitation,
74
- * every template can only be templatized once, so it is important
75
- * to set `instanceProps` before the `template` is assigned to the overlay.
76
- *
77
106
  * ### Styling
78
107
  *
79
108
  * To style the overlay content, use styles in the parent scope:
@@ -109,8 +138,13 @@ export type OverlayEventMap = HTMLElementEventMap & OverlayElementEventMap;
109
138
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
110
139
  *
111
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.
112
146
  */
113
- declare class OverlayElement extends ThemableMixin(DirMixin(HTMLElement)) {
147
+ declare class OverlayElement extends ThemableMixin(DirMixin(ControllerMixin(HTMLElement))) {
114
148
  /**
115
149
  * When true, the overlay is visible and attached to body.
116
150
  */
@@ -136,11 +170,6 @@ declare class OverlayElement extends ThemableMixin(DirMixin(HTMLElement)) {
136
170
  */
137
171
  template: HTMLTemplateElement | null | undefined;
138
172
 
139
- /**
140
- * Optional argument for `Polymer.Templatize.templatize`.
141
- */
142
- instanceProps: object | null | undefined;
143
-
144
173
  /**
145
174
  * References the content container after the template is stamped.
146
175
  */
@@ -179,6 +208,12 @@ declare class OverlayElement extends ThemableMixin(DirMixin(HTMLElement)) {
179
208
  */
180
209
  restoreFocusOnClose: boolean;
181
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
+
182
217
  close(sourceEvent?: Event | null): void;
183
218
 
184
219
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
@@ -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
  *
@@ -55,18 +56,7 @@ import { FocusablesHelper } from './vaadin-focusables-helper.js';
55
56
  *
56
57
  * The overlay provides `forwardHostProp` when calling
57
58
  * `Polymer.Templatize.templatize` for the template, so that the bindings
58
- * from the parent scope propagate to the content. You can also pass
59
- * custom `instanceProps` object using the `instanceProps` property.
60
- *
61
- * ```html
62
- * <vaadin-overlay>
63
- * <template>Overlay content</template>
64
- * </vaadin-overlay>
65
- * ```
66
- *
67
- * **NOTE:** when using `instanceProps`: because of the Polymer limitation,
68
- * every template can only be templatized once, so it is important
69
- * to set `instanceProps` before the `template` is assigned to the overlay.
59
+ * from the parent scope propagate to the content.
70
60
  *
71
61
  * ### Styling
72
62
  *
@@ -103,12 +93,18 @@ import { FocusablesHelper } from './vaadin-focusables-helper.js';
103
93
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
104
94
  *
105
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.
106
101
  *
107
102
  * @extends HTMLElement
108
103
  * @mixes ThemableMixin
109
104
  * @mixes DirMixin
105
+ * @mixes ControllerMixin
110
106
  */
111
- class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
107
+ class OverlayElement extends ThemableMixin(DirMixin(ControllerMixin(PolymerElement))) {
112
108
  static get template() {
113
109
  return html`
114
110
  <style>
@@ -227,13 +223,6 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
227
223
  notify: true
228
224
  },
229
225
 
230
- /**
231
- * Optional argument for `Polymer.Templatize.templatize`.
232
- */
233
- instanceProps: {
234
- type: Object
235
- },
236
-
237
226
  /**
238
227
  * References the content container after the template is stamped.
239
228
  * @type {!HTMLElement | undefined}
@@ -300,6 +289,15 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
300
289
  value: false
301
290
  },
302
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
+
303
301
  /** @private */
304
302
  _mouseDownInside: {
305
303
  type: Boolean
@@ -330,9 +328,6 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
330
328
  /** @private */
331
329
  _oldTemplate: Object,
332
330
 
333
- /** @private */
334
- _oldInstanceProps: Object,
335
-
336
331
  /** @private */
337
332
  _oldRenderer: Object,
338
333
 
@@ -342,7 +337,7 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
342
337
  }
343
338
 
344
339
  static get observers() {
345
- return ['_templateOrRendererChanged(template, renderer, owner, model, instanceProps, opened)'];
340
+ return ['_templateOrRendererChanged(template, renderer, owner, model, opened)'];
346
341
  }
347
342
 
348
343
  constructor() {
@@ -363,6 +358,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
363
358
  if (isIOS) {
364
359
  this._boundIosResizeListener = () => this._detectIosNavbar();
365
360
  }
361
+
362
+ this.__focusTrapController = new FocusTrapController(this);
366
363
  }
367
364
 
368
365
  /** @protected */
@@ -377,6 +374,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
377
374
  // and <vaadin-context-menu>).
378
375
  this.addEventListener('click', () => {});
379
376
  this.$.backdrop.addEventListener('click', () => {});
377
+
378
+ this.addController(this.__focusTrapController);
380
379
  }
381
380
 
382
381
  /** @private */
@@ -516,15 +515,7 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
516
515
  return;
517
516
  }
518
517
 
519
- // TAB
520
- if (event.key === 'Tab' && this.focusTrap && !event.defaultPrevented) {
521
- // if only tab key is pressed, cycle forward, else cycle backwards.
522
- this._cycleTab(event.shiftKey ? -1 : 1);
523
-
524
- event.preventDefault();
525
-
526
- // ESC
527
- } else if (event.key === 'Escape' || event.key === 'Esc') {
518
+ if (event.key === 'Escape') {
528
519
  const evt = new CustomEvent('vaadin-overlay-escape-press', {
529
520
  bubbles: true,
530
521
  cancelable: true,
@@ -560,8 +551,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
560
551
  this._animatedOpening();
561
552
 
562
553
  afterNextRender(this, () => {
563
- if (this.focusTrap && !this.contains(document.activeElement)) {
564
- this._cycleTab(0, 0);
554
+ if (this.focusTrap) {
555
+ this.__focusTrapController.trapFocus(this.$.overlay);
565
556
  }
566
557
 
567
558
  const evt = new CustomEvent('vaadin-overlay-open', { bubbles: true });
@@ -572,6 +563,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
572
563
  this._addGlobalListeners();
573
564
  }
574
565
  } else if (wasOpened) {
566
+ this.__focusTrapController.releaseFocus();
567
+
575
568
  this._animatedClosing();
576
569
 
577
570
  if (!this.modeless) {
@@ -638,16 +631,12 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
638
631
  }
639
632
  this.setAttribute('opening', '');
640
633
 
641
- const finishOpening = () => {
642
- document.addEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
643
-
644
- this.removeAttribute('opening');
645
- };
646
-
647
634
  if (this._shouldAnimate()) {
648
- this._enqueueAnimation('opening', finishOpening);
635
+ this._enqueueAnimation('opening', () => {
636
+ this._finishOpening();
637
+ });
649
638
  } else {
650
- finishOpening();
639
+ this._finishOpening();
651
640
  }
652
641
  }
653
642
 
@@ -660,6 +649,25 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
660
649
  }
661
650
 
662
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
+ */
663
671
  _animatedClosing() {
664
672
  if (this.hasAttribute('opening')) {
665
673
  this._flushAnimation('opening');
@@ -667,7 +675,11 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
667
675
  if (this._placeholder) {
668
676
  this._exitModalState();
669
677
 
670
- if (this.restoreFocusOnClose && this.__restoreFocusNode) {
678
+ // Use this.restoreFocusNode if specified, otherwise 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) {
671
683
  // If the activeElement is `<body>` or inside the overlay,
672
684
  // we are allowed to restore the focus. In all the other
673
685
  // cases focus might have been moved elsewhere by another
@@ -676,24 +688,22 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
676
688
  const activeElement = this._getActiveElement();
677
689
 
678
690
  if (activeElement === document.body || this._deepContains(activeElement)) {
679
- 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());
680
694
  }
681
695
  this.__restoreFocusNode = null;
682
696
  }
683
697
 
684
698
  this.setAttribute('closing', '');
685
-
686
- const finishClosing = () => {
687
- document.removeEventListener('iron-overlay-canceled', this._boundIronOverlayCanceledListener);
688
- this._detachOverlay();
689
- this.shadowRoot.querySelector('[part="overlay"]').style.removeProperty('pointer-events');
690
- this.removeAttribute('closing');
691
- };
699
+ this.dispatchEvent(new CustomEvent('vaadin-overlay-closing'));
692
700
 
693
701
  if (this._shouldAnimate()) {
694
- this._enqueueAnimation('closing', finishClosing);
702
+ this._enqueueAnimation('closing', () => {
703
+ this._finishClosing();
704
+ });
695
705
  } else {
696
- finishClosing();
706
+ this._finishClosing();
697
707
  }
698
708
  }
699
709
  }
@@ -825,15 +835,13 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
825
835
 
826
836
  /**
827
837
  * @param {!HTMLTemplateElement} template
828
- * @param {object} instanceProps
829
838
  * @protected
830
839
  */
831
- _stampOverlayTemplate(template, instanceProps) {
840
+ _stampOverlayTemplate(template) {
832
841
  this._removeOldContent();
833
842
 
834
843
  if (!template._Templatizer) {
835
844
  template._Templatizer = templatize(template, this, {
836
- instanceProps: instanceProps,
837
845
  forwardHostProp: function (prop, value) {
838
846
  if (this._instance) {
839
847
  this._instance.forwardHostProp(prop, value);
@@ -886,7 +894,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
886
894
  }
887
895
 
888
896
  /** @private */
889
- _templateOrRendererChanged(template, renderer, owner, model, instanceProps, opened) {
897
+ // eslint-disable-next-line max-params
898
+ _templateOrRendererChanged(template, renderer, owner, model, opened) {
890
899
  if (template && renderer) {
891
900
  this._removeNewRendererOrTemplate(template, this._oldTemplate, renderer, this._oldRenderer);
892
901
  throw new Error('You should only use either a renderer or a template for overlay content');
@@ -896,8 +905,7 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
896
905
  this._oldModel = model;
897
906
  this._oldOwner = owner;
898
907
 
899
- const templateOrInstancePropsChanged = this._oldInstanceProps !== instanceProps || this._oldTemplate !== template;
900
- this._oldInstanceProps = instanceProps;
908
+ const templateChanged = this._oldTemplate !== template;
901
909
  this._oldTemplate = template;
902
910
 
903
911
  const rendererChanged = this._oldRenderer !== renderer;
@@ -915,8 +923,8 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
915
923
  delete this.content._$litPart$;
916
924
  }
917
925
 
918
- if (template && templateOrInstancePropsChanged) {
919
- this._stampOverlayTemplate(template, instanceProps);
926
+ if (template && templateChanged) {
927
+ this._stampOverlayTemplate(template);
920
928
  } else if (renderer && (rendererChanged || openedChanged || ownerOrModelChanged)) {
921
929
  if (opened) {
922
930
  this.requestContentUpdate();
@@ -924,59 +932,6 @@ class OverlayElement extends ThemableMixin(DirMixin(PolymerElement)) {
924
932
  }
925
933
  }
926
934
 
927
- /**
928
- * @param {Element} element
929
- * @return {boolean}
930
- * @protected
931
- */
932
- _isFocused(element) {
933
- return element && element.getRootNode().activeElement === element;
934
- }
935
-
936
- /**
937
- * @param {Element[]} elements
938
- * @return {number}
939
- * @protected
940
- */
941
- _focusedIndex(elements) {
942
- elements = elements || this._getFocusableElements();
943
- return elements.indexOf(elements.filter(this._isFocused).pop());
944
- }
945
-
946
- /**
947
- * @param {number} increment
948
- * @param {number | undefined} index
949
- * @protected
950
- */
951
- _cycleTab(increment, index) {
952
- const focusableElements = this._getFocusableElements();
953
-
954
- if (index === undefined) {
955
- index = this._focusedIndex(focusableElements);
956
- }
957
-
958
- index += increment;
959
-
960
- // rollover to first item
961
- if (index >= focusableElements.length) {
962
- index = 0;
963
- // go to last item
964
- } else if (index < 0) {
965
- index = focusableElements.length - 1;
966
- }
967
-
968
- focusableElements[index].focus();
969
- }
970
-
971
- /**
972
- * @return {!Array<!HTMLElement>}
973
- * @protected
974
- */
975
- _getFocusableElements() {
976
- // collect all focusable elements
977
- return FocusablesHelper.getTabbableNodes(this.$.overlay);
978
- }
979
-
980
935
  /**
981
936
  * @return {!Element}
982
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 };