` so they persist when hovering over the tooltip and dismiss when pressing [[Esc]] [#1734]
## 2.12.0
diff --git a/src/components/menu-item/menu-item.styles.ts b/src/components/menu-item/menu-item.styles.ts
index 476add68..14fe5aa1 100644
--- a/src/components/menu-item/menu-item.styles.ts
+++ b/src/components/menu-item/menu-item.styles.ts
@@ -7,14 +7,6 @@ export default css`
:host {
--submenu-offset: -2px;
- /* Private */
- --safe-triangle-cursor-x: 0;
- --safe-triangle-cursor-y: 0;
- --safe-triangle-submenu-start-x: 0;
- --safe-triangle-submenu-start-y: 0;
- --safe-triangle-submenu-end-x: 0;
- --safe-triangle-submenu-end-y: 0;
-
display: block;
}
@@ -82,10 +74,11 @@ export default css`
right: 0;
bottom: 0;
left: 0;
+ background: tomato;
clip-path: polygon(
- var(--safe-triangle-cursor-x) var(--safe-triangle-cursor-y),
- var(--safe-triangle-submenu-start-x) var(--safe-triangle-submenu-start-y),
- var(--safe-triangle-submenu-end-x) var(--safe-triangle-submenu-end-y)
+ var(--safe-triangle-cursor-x, 0) var(--safe-triangle-cursor-y, 0),
+ var(--safe-triangle-submenu-start-x, 0) var(--safe-triangle-submenu-start-y, 0),
+ var(--safe-triangle-submenu-end-x, 0) var(--safe-triangle-submenu-end-y, 0)
);
}
diff --git a/src/components/popup/popup.component.ts b/src/components/popup/popup.component.ts
index 93bda74c..53ff039b 100644
--- a/src/components/popup/popup.component.ts
+++ b/src/components/popup/popup.component.ts
@@ -32,6 +32,7 @@ function isVirtualElement(e: unknown): e is VirtualElement {
* assigned dynamically as the popup moves. This is most useful for applying a background color to match the popup, and
* maybe a border or box shadow.
* @csspart popup - The popup's container. Useful for setting a background color, box shadow, etc.
+ * @csspart hover-bridge - The hover bridge element. Only available when the `hover-bridge` option is enabled.
*
* @cssproperty [--arrow-size=6px] - The size of the arrow. Note that an arrow won't be shown unless the `arrow`
* attribute is used.
@@ -189,6 +190,14 @@ export default class SlPopup extends ShoelaceElement {
/** The amount of padding, in pixels, to exceed before the auto-size behavior will occur. */
@property({ attribute: 'auto-size-padding', type: Number }) autoSizePadding = 0;
+ /**
+ * When a gap exists between the anchor and the popup element, this option will add a "hover bridge" that fills the
+ * gap using an invisible element. This makes listening for events such as `mouseenter` and `mouseleave` more sane
+ * because the pointer never technically leaves the element. The hover bridge will only be drawn when the popover is
+ * active.
+ */
+ @property({ attribute: 'hover-bridge', type: Boolean }) hoverBridge = false;
+
async connectedCallback() {
super.connectedCallback();
@@ -447,13 +456,99 @@ export default class SlPopup extends ShoelaceElement {
}
});
+ // Wait until the new position is drawn before updating the hover bridge, otherwise it can get out of sync
+ requestAnimationFrame(() => this.updateHoverBridge());
+
this.emit('sl-reposition');
}
+ private updateHoverBridge = () => {
+ if (this.hoverBridge && this.anchorEl) {
+ const anchorRect = this.anchorEl.getBoundingClientRect();
+ const popupRect = this.popup.getBoundingClientRect();
+ const isVertical = this.placement.includes('top') || this.placement.includes('bottom');
+ let topLeftX = 0;
+ let topLeftY = 0;
+ let topRightX = 0;
+ let topRightY = 0;
+ let bottomLeftX = 0;
+ let bottomLeftY = 0;
+ let bottomRightX = 0;
+ let bottomRightY = 0;
+
+ if (isVertical) {
+ if (anchorRect.top < popupRect.top) {
+ // Anchor is above
+ topLeftX = anchorRect.left;
+ topLeftY = anchorRect.bottom;
+ topRightX = anchorRect.right;
+ topRightY = anchorRect.bottom;
+
+ bottomLeftX = popupRect.left;
+ bottomLeftY = popupRect.top;
+ bottomRightX = popupRect.right;
+ bottomRightY = popupRect.top;
+ } else {
+ // Anchor is below
+ topLeftX = popupRect.left;
+ topLeftY = popupRect.bottom;
+ topRightX = popupRect.right;
+ topRightY = popupRect.bottom;
+
+ bottomLeftX = anchorRect.left;
+ bottomLeftY = anchorRect.top;
+ bottomRightX = anchorRect.right;
+ bottomRightY = anchorRect.top;
+ }
+ } else {
+ if (anchorRect.left < popupRect.left) {
+ // Anchor is on the left
+ topLeftX = anchorRect.right;
+ topLeftY = anchorRect.top;
+ topRightX = popupRect.left;
+ topRightY = popupRect.top;
+
+ bottomLeftX = anchorRect.right;
+ bottomLeftY = anchorRect.bottom;
+ bottomRightX = popupRect.left;
+ bottomRightY = popupRect.bottom;
+ } else {
+ // Anchor is on the right
+ topLeftX = popupRect.right;
+ topLeftY = popupRect.top;
+ topRightX = anchorRect.left;
+ topRightY = anchorRect.top;
+
+ bottomLeftX = popupRect.right;
+ bottomLeftY = popupRect.bottom;
+ bottomRightX = anchorRect.left;
+ bottomRightY = anchorRect.bottom;
+ }
+ }
+
+ this.style.setProperty('--hover-bridge-top-left-x', `${topLeftX}px`);
+ this.style.setProperty('--hover-bridge-top-left-y', `${topLeftY}px`);
+ this.style.setProperty('--hover-bridge-top-right-x', `${topRightX}px`);
+ this.style.setProperty('--hover-bridge-top-right-y', `${topRightY}px`);
+ this.style.setProperty('--hover-bridge-bottom-left-x', `${bottomLeftX}px`);
+ this.style.setProperty('--hover-bridge-bottom-left-y', `${bottomLeftY}px`);
+ this.style.setProperty('--hover-bridge-bottom-right-x', `${bottomRightX}px`);
+ this.style.setProperty('--hover-bridge-bottom-right-y', `${bottomRightY}px`);
+ }
+ };
+
render() {
return html`
+
+
{
- // Pressing escape when the target element has focus should dismiss the tooltip
- if (this.open && !this.disabled && event.key === 'Escape') {
+ private handleDocumentKeyDown = (event: KeyboardEvent) => {
+ // Pressing escape when a tooltip is open should dismiss it
+ if (event.key === 'Escape') {
event.stopPropagation();
this.hide();
}
@@ -181,17 +181,20 @@ export default class SlTooltip extends ShoelaceElement {
// Show
this.emit('sl-show');
+ document.addEventListener('keydown', this.handleDocumentKeyDown);
await stopAnimations(this.body);
this.body.hidden = false;
this.popup.active = true;
const { keyframes, options } = getAnimation(this, 'tooltip.show', { dir: this.localize.dir() });
await animateTo(this.popup.popup, keyframes, options);
+ this.popup.reposition();
this.emit('sl-after-show');
} else {
// Hide
this.emit('sl-hide');
+ document.removeEventListener('keydown', this.handleDocumentKeyDown);
await stopAnimations(this.body);
const { keyframes, options } = getAnimation(this, 'tooltip.hide', { dir: this.localize.dir() });
@@ -263,6 +266,7 @@ export default class SlTooltip extends ShoelaceElement {
flip
shift
arrow
+ hover-bridge
>
${'' /* eslint-disable-next-line lit-a11y/no-aria-slot */}
diff --git a/src/components/tooltip/tooltip.styles.ts b/src/components/tooltip/tooltip.styles.ts
index 4250f284..218893a9 100644
--- a/src/components/tooltip/tooltip.styles.ts
+++ b/src/components/tooltip/tooltip.styles.ts
@@ -18,7 +18,7 @@ export default css`
}
.tooltip::part(popup) {
- pointer-events: none;
+ user-select: none;
z-index: var(--sl-z-index-tooltip);
}
diff --git a/src/components/tooltip/tooltip.test.ts b/src/components/tooltip/tooltip.test.ts
index a14f8855..457cf0a2 100644
--- a/src/components/tooltip/tooltip.test.ts
+++ b/src/components/tooltip/tooltip.test.ts
@@ -1,7 +1,6 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
-import type SlPopup from '../popup/popup';
import type SlTooltip from './tooltip';
describe('', () => {
@@ -149,14 +148,15 @@ describe('', () => {
expect(body.hidden).to.be.false;
});
- it('should not accept pointer events on the tooltip', async () => {
+ it('should not accept user selection on the tooltip', async () => {
const el = await fixture(html`
Hover Me
`);
- const popup = el.shadowRoot!.querySelector('sl-popup')!;
+ const tooltipBody = el.shadowRoot!.querySelector('.tooltip__body')!;
+ const userSelect = getComputedStyle(tooltipBody).userSelect || getComputedStyle(tooltipBody).webkitUserSelect;
- expect(getComputedStyle(popup.popup).pointerEvents).to.equal('none');
+ expect(userSelect).to.equal('none');
});
});