@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 +11 -11
- package/src/vaadin-popover.js +181 -22
- package/web-types.json +2 -2
- package/web-types.lit.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/popover",
|
|
3
|
-
"version": "24.
|
|
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": "
|
|
41
|
-
"@vaadin/component-base": "
|
|
42
|
-
"@vaadin/lit-renderer": "
|
|
43
|
-
"@vaadin/overlay": "
|
|
44
|
-
"@vaadin/vaadin-lumo-styles": "
|
|
45
|
-
"@vaadin/vaadin-material-styles": "
|
|
46
|
-
"@vaadin/vaadin-themable-mixin": "
|
|
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": "
|
|
51
|
-
"@vaadin/test-runner-commands": "
|
|
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": "
|
|
59
|
+
"gitHead": "43abf4293381464fe47f5339b0ee64caf8867cfa"
|
|
60
60
|
}
|
package/src/vaadin-popover.js
CHANGED
|
@@ -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
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
//
|
|
719
|
+
// Handle cases where Tab from the current element would land on the overlay
|
|
677
720
|
const activeElement = getDeepActiveElement();
|
|
678
|
-
const nextFocusable = this.
|
|
679
|
-
if (nextFocusable === overlayPart
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
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
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
723
|
-
const
|
|
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.
|
|
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.
|
|
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",
|
package/web-types.lit.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/web-types",
|
|
3
3
|
"name": "@vaadin/popover",
|
|
4
|
-
"version": "24.
|
|
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.
|
|
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
|
{
|