diff --git a/.changeset/funny-eggs-sell.md b/.changeset/funny-eggs-sell.md new file mode 100644 index 00000000000..456094822b7 --- /dev/null +++ b/.changeset/funny-eggs-sell.md @@ -0,0 +1,6 @@ +--- +'@spectrum-web-components/overlay': patch +'@spectrum-web-components/core': patch +--- + +hover overlays should close with the Esc key when trigger is not focused diff --git a/1st-gen/packages/overlay/src/OverlayStack.ts b/1st-gen/packages/overlay/src/OverlayStack.ts index cc5c8ffd99b..a4f55ec8fc0 100644 --- a/1st-gen/packages/overlay/src/OverlayStack.ts +++ b/1st-gen/packages/overlay/src/OverlayStack.ts @@ -176,6 +176,13 @@ class OverlayStack { if (event.code !== 'Escape') return; if (!this.stack.length) return; const last = this.stack[this.stack.length - 1]; + if (last?.type === 'hint') { + // Close hint/tooltip overlays on "Escape" key and prevent further handling of the event. + event.preventDefault(); + event.stopPropagation(); + this.closeOverlay(last); + return; + } if (last?.type === 'page') { event.preventDefault(); return; diff --git a/1st-gen/packages/overlay/test/index.ts b/1st-gen/packages/overlay/test/index.ts index 8f2d39c82b3..5f719361b90 100644 --- a/1st-gen/packages/overlay/test/index.ts +++ b/1st-gen/packages/overlay/test/index.ts @@ -624,6 +624,32 @@ export const runOverlayTriggerTests = (type: string): void => { ).to.be.false; }); + it('Escape key closes a hover popover', async function () { + expect(await isOnTopLayer(this.hoverContent)).to.be.false; + + const rect = this.outerTrigger.getBoundingClientRect(); + const open = oneEvent(this.outerTrigger, 'sp-opened'); + await sendMouse({ + type: 'move', + position: [ + rect.left + rect.width / 2, + rect.top + rect.height / 2, + ], + }); + await open; + const close = oneEvent(this.outerTrigger, 'sp-closed'); + expect( + await isOnTopLayer(this.hoverContent), + 'hover content is available at point' + ).to.be.true; + await sendKeys({ press: 'Escape' }); + await close; + expect( + await isOnTopLayer(this.hoverContent), + 'hover content is not available at point' + ).to.be.false; + }); + it('dispatches events on open/close', async function () { const opened = oneEvent(this.outerButton, 'sp-opened'); this.outerButton.click(); diff --git a/1st-gen/packages/overlay/test/overlay-trigger-hover.test.ts b/1st-gen/packages/overlay/test/overlay-trigger-hover.test.ts index 02c6d4e2961..a926f5eef5b 100644 --- a/1st-gen/packages/overlay/test/overlay-trigger-hover.test.ts +++ b/1st-gen/packages/overlay/test/overlay-trigger-hover.test.ts @@ -140,13 +140,14 @@ describe('Overlay Trigger - Hover', () => { ); await elementUpdated(tooltip); - button.dispatchEvent( + tooltip.dispatchEvent( new MouseEvent('pointerenter', { bubbles: true, composed: true, }) ); await elementUpdated(tooltip); + expect(tooltip.open).to.be.true; tooltip.dispatchEvent( new MouseEvent('pointerleave', { @@ -210,6 +211,36 @@ describe('Overlay Trigger - Hover', () => { expect(el.open).to.be.undefined; }); + it('closes the "tooltip" on "escape" keydown', async () => { + // Open the tooltip + const opened = oneEvent(button, 'sp-opened'); + button.dispatchEvent( + new MouseEvent('pointerenter', { + bubbles: true, + composed: true, + }) + ); + await waitUntil( + () => tooltip.open === true, + 'tooltip should open', + { timeout: 500 } + ); + await opened; + expect(el.open).to.equal('hover'); + + // Test escape key closes tooltip when focus is not on trigger + const body = el.ownerDocument.body; + body.focus(); + const closed = oneEvent(button, 'sp-closed'); + const escapeKeydown = new KeyboardEvent('keydown', { + code: 'Escape', + bubbles: true, + composed: true, + }); + body.dispatchEvent(escapeKeydown); + await closed; + expect(el.open).to.be.undefined; + }); }); it('persists hover content', async () => { const el = await fixture( diff --git a/2nd-gen/packages/core/shared/base/version.ts b/2nd-gen/packages/core/shared/base/version.ts index 622cc87119d..cb9e28ff658 100644 --- a/2nd-gen/packages/core/shared/base/version.ts +++ b/2nd-gen/packages/core/shared/base/version.ts @@ -1,2 +1,14 @@ +/** + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + // Generated by genversion. export const version = '1.10.0';