@vaadin/popover 24.9.11 → 24.10.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/popover",
3
- "version": "24.9.11",
3
+ "version": "24.10.0-alpha2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -37,18 +37,18 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@open-wc/dedupe-mixin": "^1.3.0",
40
- "@vaadin/a11y-base": "~24.9.11",
41
- "@vaadin/component-base": "~24.9.11",
42
- "@vaadin/lit-renderer": "~24.9.11",
43
- "@vaadin/overlay": "~24.9.11",
44
- "@vaadin/vaadin-lumo-styles": "~24.9.11",
45
- "@vaadin/vaadin-material-styles": "~24.9.11",
46
- "@vaadin/vaadin-themable-mixin": "~24.9.11",
40
+ "@vaadin/a11y-base": "24.10.0-alpha2",
41
+ "@vaadin/component-base": "24.10.0-alpha2",
42
+ "@vaadin/lit-renderer": "24.10.0-alpha2",
43
+ "@vaadin/overlay": "24.10.0-alpha2",
44
+ "@vaadin/vaadin-lumo-styles": "24.10.0-alpha2",
45
+ "@vaadin/vaadin-material-styles": "24.10.0-alpha2",
46
+ "@vaadin/vaadin-themable-mixin": "24.10.0-alpha2",
47
47
  "lit": "^3.0.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@vaadin/chai-plugins": "~24.9.11",
51
- "@vaadin/test-runner-commands": "~24.9.11",
50
+ "@vaadin/chai-plugins": "24.10.0-alpha2",
51
+ "@vaadin/test-runner-commands": "24.10.0-alpha2",
52
52
  "@vaadin/testing-helpers": "^1.1.0",
53
53
  "sinon": "^18.0.0"
54
54
  },
@@ -56,5 +56,5 @@
56
56
  "web-types.json",
57
57
  "web-types.lit.json"
58
58
  ],
59
- "gitHead": "235991ba683038cc6556b3551c7763d8b538120f"
59
+ "gitHead": "43abf4293381464fe47f5339b0ee64caf8867cfa"
60
60
  }
@@ -6,6 +6,7 @@
6
6
  import './vaadin-popover-overlay.js';
7
7
  import { css, html, LitElement } from 'lit';
8
8
  import { ifDefined } from 'lit/directives/if-defined.js';
9
+ import { getActiveTrappingNode } from '@vaadin/a11y-base/src/focus-trap-controller.js';
9
10
  import {
10
11
  getDeepActiveElement,
11
12
  getFocusableElements,
@@ -662,24 +663,76 @@ class Popover extends PopoverPositionMixin(
662
663
  return;
663
664
  }
664
665
 
665
- // Move focus to the next element after target on content Tab
666
- const lastFocusable = this.__getLastFocusable(overlayPart);
667
- if (lastFocusable && isElementFocused(lastFocusable)) {
668
- const focusable = this.__getNextBodyFocusable(this.__getTargetFocusable());
669
- if (focusable && focusable !== overlayPart) {
666
+ // Handle Tab within the overlay content explicitly. The overlay is
667
+ // teleported to the body and is outside the dialog's focus trap, so the
668
+ // FocusTrapController would otherwise intercept the Tab event.
669
+ if (isElementFocused(overlayPart)) {
670
+ const contentFocusables = getFocusableElements(this._overlayElement.$.content);
671
+ if (contentFocusables.length > 0) {
672
+ event.preventDefault();
673
+ contentFocusables[0].focus();
674
+ return;
675
+ }
676
+ // No focusable content - fall through to isFocusOut handling below
677
+ } else if (this._overlayElement.contains(getDeepActiveElement())) {
678
+ const contentFocusables = getFocusableElements(this._overlayElement.$.content);
679
+ const activeEl = getDeepActiveElement();
680
+ const idx = contentFocusables.indexOf(activeEl);
681
+ if (idx >= 0 && idx < contentFocusables.length - 1) {
682
+ event.preventDefault();
683
+ contentFocusables[idx + 1].focus();
684
+ return;
685
+ }
686
+ // Last content focusable - fall through to isFocusOut handling below
687
+ }
688
+
689
+ // Cache filtered focusable list for this keystroke to avoid redundant DOM traversals
690
+ const focusables = this.__getScopeFocusables();
691
+
692
+ // Move focus to the next element after target on last content Tab,
693
+ // or when overlay part itself is focused and has no focusable content
694
+ const lastFocusable = this.__getLastFocusable();
695
+ const isFocusOut = lastFocusable ? isElementFocused(lastFocusable) : isElementFocused(overlayPart);
696
+ if (isFocusOut) {
697
+ let focusable = this.__getNextScopeFocusable(this.__getTargetFocusable(), focusables);
698
+ // If the next element after the target is the overlay part (DOM position
699
+ // differs from logical position), skip past it to the actual next element.
700
+ if (focusable === overlayPart) {
701
+ focusable = this.__getNextScopeFocusable(overlayPart, focusables);
702
+ }
703
+ if (focusable) {
670
704
  event.preventDefault();
671
705
  focusable.focus();
672
706
  return;
673
707
  }
708
+ // No next element after the target in the scope. When inside a focus trap,
709
+ // wrap explicitly to the first focusable. Don't fall through - the
710
+ // FocusTrapController uses DOM order which may differ from the popover's
711
+ // logical tab position.
712
+ if (getActiveTrappingNode(this) && focusables[0]) {
713
+ event.preventDefault();
714
+ focusables[0].focus();
715
+ return;
716
+ }
674
717
  }
675
718
 
676
- // Prevent focusing the popover content on previous element Tab
719
+ // Handle cases where Tab from the current element would land on the overlay
677
720
  const activeElement = getDeepActiveElement();
678
- const nextFocusable = this.__getNextBodyFocusable(activeElement);
679
- if (nextFocusable === overlayPart && lastFocusable) {
680
- // Move focus to the last overlay focusable and do NOT prevent keydown
681
- // to move focus outside the popover content (e.g. to the URL bar).
682
- lastFocusable.focus();
721
+ const nextFocusable = this.__getNextScopeFocusable(activeElement, focusables);
722
+ if (nextFocusable === overlayPart) {
723
+ // The overlay should only be Tab-reachable from its target (handled above).
724
+ // Skip the overlay when Tab from any other element would land on it
725
+ // due to its DOM position.
726
+ const focusableAfterOverlay = this.__getNextScopeFocusable(overlayPart, focusables);
727
+ if (focusableAfterOverlay) {
728
+ event.preventDefault();
729
+ focusableAfterOverlay.focus();
730
+ } else if (getActiveTrappingNode(this) && focusables[0]) {
731
+ // Overlay is last in DOM scope but shouldn't be Tab-reachable from
732
+ // non-target elements. Wrap to first focusable in focus trap.
733
+ event.preventDefault();
734
+ focusables[0].focus();
735
+ }
683
736
  }
684
737
  }
685
738
 
@@ -700,30 +753,136 @@ class Popover extends PopoverPositionMixin(
700
753
  return;
701
754
  }
702
755
 
703
- // Move focus back to the popover on next element Shift + Tab
704
- const nextFocusable = this.__getNextBodyFocusable(this.__getTargetFocusable());
705
- if (nextFocusable && isElementFocused(nextFocusable)) {
706
- const lastFocusable = this.__getLastFocusable(overlayPart);
707
- if (lastFocusable) {
756
+ // Handle Shift+Tab within the overlay content explicitly. The overlay is
757
+ // teleported to the body and is outside the dialog's focus trap, so the
758
+ // FocusTrapController would otherwise intercept the Shift+Tab event.
759
+ const activeElement = getDeepActiveElement();
760
+ if (this._overlayElement.contains(activeElement)) {
761
+ const contentFocusables = getFocusableElements(this._overlayElement.$.content);
762
+ const idx = contentFocusables.indexOf(activeElement);
763
+ if (idx > 0) {
764
+ event.preventDefault();
765
+ contentFocusables[idx - 1].focus();
766
+ return;
767
+ }
768
+ // First content focusable or not found - move to overlay part
769
+ event.preventDefault();
770
+ overlayPart.focus();
771
+ return;
772
+ }
773
+
774
+ // Cache filtered focusable list for this keystroke to avoid redundant DOM traversals
775
+ const focusables = this.__getScopeFocusables();
776
+
777
+ // Get previous focusable element excluding the overlay
778
+ const prevFocusable = this.__getPrevScopeFocusable(activeElement, focusables);
779
+ const targetFocusable = this.__getTargetFocusable();
780
+
781
+ // Intercept Shift+Tab when the previous focusable (excluding the overlay)
782
+ // is the target. Instead of moving to the target, redirect focus into
783
+ // the overlay's last focusable content (or the overlay part itself).
784
+ if (prevFocusable === targetFocusable) {
785
+ event.preventDefault();
786
+ this.__focusLastOrSelf();
787
+ return;
788
+ }
789
+
790
+ // Move focus into the overlay when:
791
+ // 1. There is no previous focusable element in the focus trap (at the
792
+ // beginning, would wrap around), and
793
+ // 2. The target is the last focusable in the focus trap (making the
794
+ // overlay logically last).
795
+ // Don't fall through - the FocusTrapController uses DOM order which
796
+ // may differ from the popover's logical tab position.
797
+ if (!prevFocusable && getActiveTrappingNode(this)) {
798
+ const list = focusables.filter((el) => el !== overlayPart);
799
+ if (list.at(-1) === targetFocusable) {
800
+ event.preventDefault();
801
+ this.__focusLastOrSelf();
802
+ return;
803
+ }
804
+ // Overlay is last in DOM but target is not the last focusable.
805
+ // Wrap to last non-overlay focusable to prevent FocusTrapController
806
+ // from landing on the overlay.
807
+ const last = list.at(-1);
808
+ if (last) {
809
+ event.preventDefault();
810
+ last.focus();
811
+ return;
812
+ }
813
+ }
814
+
815
+ // Get previous focusable element including the overlay (simulates native Tab order)
816
+ const prevFocusableNative = this.__getPrevScopeFocusable(activeElement, focusables, true);
817
+ // Skip the overlay when native Shift+Tab would land on it
818
+ // and redirect to the actual previous element
819
+ if (prevFocusableNative === overlayPart) {
820
+ if (prevFocusable) {
708
821
  event.preventDefault();
709
- lastFocusable.focus();
822
+ prevFocusable.focus();
823
+ } else if (getActiveTrappingNode(this)) {
824
+ // Overlay is first in DOM scope but shouldn't be Shift+Tab-reachable
825
+ // from non-target elements. Wrap to last non-overlay focusable.
826
+ const list = focusables.filter((el) => el !== overlayPart);
827
+ const last = list.at(-1);
828
+ if (last) {
829
+ event.preventDefault();
830
+ last.focus();
831
+ }
710
832
  }
711
833
  }
712
834
  }
713
835
 
836
+ /**
837
+ * Returns whether the element is an overlay content child of this popover
838
+ * (i.e. content rendered inside the overlay, excluding the overlay part itself).
839
+ * @param {Element} el
840
+ * @return {boolean}
841
+ * @private
842
+ */
843
+ __isPopoverContent(el) {
844
+ return this._overlayElement && el !== this._overlayElement && this._overlayElement.contains(el);
845
+ }
846
+
847
+ /**
848
+ * Returns focusable elements within the current scope (active focus trap or
849
+ * document body) with popover overlay content children filtered out.
850
+ * @return {Element[]}
851
+ * @private
852
+ */
853
+ __getScopeFocusables() {
854
+ const scope = getActiveTrappingNode(this) || document.body;
855
+ return getFocusableElements(scope).filter((el) => !this.__isPopoverContent(el));
856
+ }
857
+
714
858
  /** @private */
715
- __getNextBodyFocusable(target) {
716
- const focusables = getFocusableElements(document.body);
859
+ __getNextScopeFocusable(target, focusables = this.__getScopeFocusables()) {
717
860
  const idx = focusables.findIndex((el) => el === target);
718
- return focusables[idx + 1];
861
+ return idx >= 0 ? focusables[idx + 1] : undefined;
719
862
  }
720
863
 
721
864
  /** @private */
722
- __getLastFocusable(container) {
723
- const focusables = getFocusableElements(container);
865
+ __getPrevScopeFocusable(target, focusables = this.__getScopeFocusables(), includeOverlay = false) {
866
+ const overlayPart = this._overlayElement.$.overlay;
867
+ const list = includeOverlay ? focusables : focusables.filter((el) => el !== overlayPart);
868
+ const idx = list.findIndex((el) => el === target);
869
+ // Returns null both when target is the first element (idx === 0)
870
+ // and when target is not found in the list (idx === -1)
871
+ return idx > 0 ? list[idx - 1] : null;
872
+ }
873
+
874
+ /** @private */
875
+ __getLastFocusable() {
876
+ // Search within the overlay's content area to avoid returning the overlay part itself
877
+ const focusables = getFocusableElements(this._overlayElement.$.content);
724
878
  return focusables.pop();
725
879
  }
726
880
 
881
+ /** @private */
882
+ __focusLastOrSelf() {
883
+ (this.__getLastFocusable() || this._overlayElement.$.overlay).focus();
884
+ }
885
+
727
886
  /** @private */
728
887
  __getTargetFocusable() {
729
888
  if (!this.target) {
package/web-types.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/popover",
4
- "version": "24.9.11",
4
+ "version": "24.10.0-alpha2",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
8
8
  "elements": [
9
9
  {
10
10
  "name": "vaadin-popover",
11
- "description": "`<vaadin-popover>` is a Web Component for creating overlays\nthat are positioned next to specified DOM element (target).\n\nUnlike `<vaadin-tooltip>`, the popover supports rich content\nthat can be provided by using `renderer` function.\n\n### Styling\n\n`<vaadin-popover>` uses `<vaadin-popover-overlay>` internal\nthemable component as the actual visible overlay.\n\nSee [`<vaadin-overlay>`](https://cdn.vaadin.com/vaadin-web-components/24.9.11/#/elements/vaadin-overlay) documentation\nfor `<vaadin-popover-overlay>` parts.\n\nIn addition to `<vaadin-overlay>` parts, the following parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`arrow` | Optional arrow pointing to the target when using `theme=\"arrow\"`\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n-----------------|----------------------------------------\n`position` | Reflects the `position` property value.\n\nNote: the `theme` attribute value set on `<vaadin-popover>` is\npropagated to the internal `<vaadin-popover-overlay>` component.\n\n### Custom CSS Properties\n\nThe following custom CSS properties are available on the `<vaadin-popover>` element:\n\nCustom CSS property | Description\n---------------------------------|-------------\n`--vaadin-popover-offset-top` | Used as an offset when the popover is aligned vertically below the target\n`--vaadin-popover-offset-bottom` | Used as an offset when the popover is aligned vertically above the target\n`--vaadin-popover-offset-start` | Used as an offset when the popover is aligned horizontally after the target\n`--vaadin-popover-offset-end` | Used as an offset when the popover is aligned horizontally before the target\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
11
+ "description": "`<vaadin-popover>` is a Web Component for creating overlays\nthat are positioned next to specified DOM element (target).\n\nUnlike `<vaadin-tooltip>`, the popover supports rich content\nthat can be provided by using `renderer` function.\n\n### Styling\n\n`<vaadin-popover>` uses `<vaadin-popover-overlay>` internal\nthemable component as the actual visible overlay.\n\nSee [`<vaadin-overlay>`](https://cdn.vaadin.com/vaadin-web-components/24.10.0-alpha2/#/elements/vaadin-overlay) documentation\nfor `<vaadin-popover-overlay>` parts.\n\nIn addition to `<vaadin-overlay>` parts, the following parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`arrow` | Optional arrow pointing to the target when using `theme=\"arrow\"`\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n-----------------|----------------------------------------\n`position` | Reflects the `position` property value.\n\nNote: the `theme` attribute value set on `<vaadin-popover>` is\npropagated to the internal `<vaadin-popover-overlay>` component.\n\n### Custom CSS Properties\n\nThe following custom CSS properties are available on the `<vaadin-popover>` element:\n\nCustom CSS property | Description\n---------------------------------|-------------\n`--vaadin-popover-offset-top` | Used as an offset when the popover is aligned vertically below the target\n`--vaadin-popover-offset-bottom` | Used as an offset when the popover is aligned vertically above the target\n`--vaadin-popover-offset-start` | Used as an offset when the popover is aligned horizontally after the target\n`--vaadin-popover-offset-end` | Used as an offset when the popover is aligned horizontally before the target\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
12
12
  "attributes": [
13
13
  {
14
14
  "name": "overlay-class",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/popover",
4
- "version": "24.9.11",
4
+ "version": "24.10.0-alpha2",
5
5
  "description-markup": "markdown",
6
6
  "framework": "lit",
7
7
  "framework-config": {
@@ -16,7 +16,7 @@
16
16
  "elements": [
17
17
  {
18
18
  "name": "vaadin-popover",
19
- "description": "`<vaadin-popover>` is a Web Component for creating overlays\nthat are positioned next to specified DOM element (target).\n\nUnlike `<vaadin-tooltip>`, the popover supports rich content\nthat can be provided by using `renderer` function.\n\n### Styling\n\n`<vaadin-popover>` uses `<vaadin-popover-overlay>` internal\nthemable component as the actual visible overlay.\n\nSee [`<vaadin-overlay>`](https://cdn.vaadin.com/vaadin-web-components/24.9.11/#/elements/vaadin-overlay) documentation\nfor `<vaadin-popover-overlay>` parts.\n\nIn addition to `<vaadin-overlay>` parts, the following parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`arrow` | Optional arrow pointing to the target when using `theme=\"arrow\"`\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n-----------------|----------------------------------------\n`position` | Reflects the `position` property value.\n\nNote: the `theme` attribute value set on `<vaadin-popover>` is\npropagated to the internal `<vaadin-popover-overlay>` component.\n\n### Custom CSS Properties\n\nThe following custom CSS properties are available on the `<vaadin-popover>` element:\n\nCustom CSS property | Description\n---------------------------------|-------------\n`--vaadin-popover-offset-top` | Used as an offset when the popover is aligned vertically below the target\n`--vaadin-popover-offset-bottom` | Used as an offset when the popover is aligned vertically above the target\n`--vaadin-popover-offset-start` | Used as an offset when the popover is aligned horizontally after the target\n`--vaadin-popover-offset-end` | Used as an offset when the popover is aligned horizontally before the target\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
19
+ "description": "`<vaadin-popover>` is a Web Component for creating overlays\nthat are positioned next to specified DOM element (target).\n\nUnlike `<vaadin-tooltip>`, the popover supports rich content\nthat can be provided by using `renderer` function.\n\n### Styling\n\n`<vaadin-popover>` uses `<vaadin-popover-overlay>` internal\nthemable component as the actual visible overlay.\n\nSee [`<vaadin-overlay>`](https://cdn.vaadin.com/vaadin-web-components/24.10.0-alpha2/#/elements/vaadin-overlay) documentation\nfor `<vaadin-popover-overlay>` parts.\n\nIn addition to `<vaadin-overlay>` parts, the following parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`arrow` | Optional arrow pointing to the target when using `theme=\"arrow\"`\n\nThe following state attributes are available for styling:\n\nAttribute | Description\n-----------------|----------------------------------------\n`position` | Reflects the `position` property value.\n\nNote: the `theme` attribute value set on `<vaadin-popover>` is\npropagated to the internal `<vaadin-popover-overlay>` component.\n\n### Custom CSS Properties\n\nThe following custom CSS properties are available on the `<vaadin-popover>` element:\n\nCustom CSS property | Description\n---------------------------------|-------------\n`--vaadin-popover-offset-top` | Used as an offset when the popover is aligned vertically below the target\n`--vaadin-popover-offset-bottom` | Used as an offset when the popover is aligned vertically above the target\n`--vaadin-popover-offset-start` | Used as an offset when the popover is aligned horizontally after the target\n`--vaadin-popover-offset-end` | Used as an offset when the popover is aligned horizontally before the target\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.",
20
20
  "extension": true,
21
21
  "attributes": [
22
22
  {