diff --git a/karma.conf.js b/karma.conf.js index 4440c37d15..f4989c2d84 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -54,10 +54,10 @@ module.exports = (config) => { coverageIstanbulReporter: { thresholds: { global: { - statements: 97, - branches: 93, - functions: 97, - lines: 97, + statements: 98, + branches: 94, + functions: 98, + lines: 98, }, }, }, diff --git a/packages/overlay/src/active-overlay.css b/packages/overlay/src/active-overlay.css index 4208a615de..263db508d3 100644 --- a/packages/overlay/src/active-overlay.css +++ b/packages/overlay/src/active-overlay.css @@ -49,8 +49,11 @@ sp-theme, #contents { display: inline-block; pointer-events: none; - animation-duration: var(--spectrum-global-animation-duration-200); - animation-timing-function: var(--spectrum-global-animation-ease-out); + animation-duration: var(--spectrum-global-animation-duration-200, 160ms); + animation-timing-function: var( + --spectrum-global-animation-ease-out, + ease-out + ); opacity: 1; visibility: visible; } diff --git a/packages/overlay/src/overlay-stack.ts b/packages/overlay/src/overlay-stack.ts index e2c9f4b541..299a0e23e0 100644 --- a/packages/overlay/src/overlay-stack.ts +++ b/packages/overlay/src/overlay-stack.ts @@ -40,6 +40,7 @@ export class OverlayStack { private overlayHolder!: HTMLElement; private initTabTrapping(): void { + /* istanbul ignore if */ if (this.document.body.shadowRoot) { this.canTabTrap = false; return; @@ -87,6 +88,7 @@ export class OverlayStack { } private startTabTrapping(): void { + /* istanbul ignore if */ if (!this.canTabTrap) { return; } @@ -95,6 +97,7 @@ export class OverlayStack { } private stopTabTrapping(): void { + /* istanbul ignore if */ if (!this.canTabTrap) { return; } diff --git a/packages/overlay/test/overlay-trigger.test.ts b/packages/overlay/test/overlay-trigger.test.ts index 12c3981de5..88c2932d03 100644 --- a/packages/overlay/test/overlay-trigger.test.ts +++ b/packages/overlay/test/overlay-trigger.test.ts @@ -601,6 +601,7 @@ describe('Overlay Trigger', () => { const mouseEnter = new MouseEvent('mouseenter'); const mouseLeave = new MouseEvent('mouseleave'); triggerShadowDiv.dispatchEvent(mouseEnter); + await nextFrame(); triggerShadowDiv.dispatchEvent(mouseLeave); await waitUntil( diff --git a/packages/overlay/test/overlay.test.ts b/packages/overlay/test/overlay.test.ts index 2167b57a58..5bda58a280 100644 --- a/packages/overlay/test/overlay.test.ts +++ b/packages/overlay/test/overlay.test.ts @@ -24,6 +24,17 @@ import { waitUntil, } from '@open-wc/testing'; +const keyboardEvent = (code: string, shiftKey = false): KeyboardEvent => + new KeyboardEvent('keydown', { + bubbles: true, + composed: true, + cancelable: true, + code, + shiftKey, + }); +const tabEvent = keyboardEvent('Tab'); +const shiftTabEvent = keyboardEvent('Tab', true); + describe('Overlays', () => { let testDiv!: HTMLDivElement; let openOverlays: (() => void)[] = []; @@ -124,11 +135,13 @@ describe('Overlays', () => { expect(button).to.exist; - Overlay.open(button, 'click', outerPopover, { - delayed: false, - placement, - offset: 10, - }); + openOverlays.push( + await Overlay.open(button, 'click', outerPopover, { + delayed: false, + placement, + offset: 10, + }) + ); // Wait for the DOM node to be stolen and reparented into the overlay await waitForPredicate( @@ -198,10 +211,12 @@ describe('Overlays', () => { expect(button).to.exist; - await Overlay.open(button, 'click', outerPopover, { - delayed: true, - offset: 10, - }); + openOverlays.push( + await Overlay.open(button, 'click', outerPopover, { + delayed: true, + offset: 10, + }) + ); // Wait for the DOM node to be stolen and reparented into the overlay await waitUntil( @@ -357,11 +372,13 @@ describe('Overlays', () => { const dialog = el.querySelector('sp-dialog') as Dialog; - Overlay.open(el, 'click', dialog, { - delayed: false, - placement: 'bottom', - offset: 10, - }); + openOverlays.push( + await Overlay.open(el, 'click', dialog, { + delayed: false, + placement: 'bottom', + offset: 10, + }) + ); await waitUntil( () => @@ -379,4 +396,92 @@ describe('Overlays', () => { 'content is returned' ); }); + + it('closes an inline overlay when tabbing past the content', async () => { + const el = await fixture(html` +
+ +
+ +
+
+ `); + + const trigger = el.querySelector('.trigger') as HTMLElement; + const content = el.querySelector('.content') as HTMLElement; + + openOverlays.push(await Overlay.open(trigger, 'inline', content, {})); + + await waitUntil( + () => !!el.querySelector('span[tabindex="-1"]'), + 'returnFocusElement available' + ); + + const overlays = document.querySelectorAll('active-overlay'); + const overlay = overlays[0]; + + expect(overlay).to.not.be.undefined; + + trigger.dispatchEvent(tabEvent); + + await waitUntil( + () => !!el.querySelector('span[tabindex="-1"]'), + 'returnFocusElement persists on forward tab' + ); + + content.dispatchEvent(shiftTabEvent); + + expect(document.activeElement === overlay.returnFocusElement).to.be + .true; + + content.dispatchEvent(tabEvent); + + await waitUntil( + () => el.querySelector('span[tabindex="-1"]') === null, + 'returnFocusElement no longer available' + ); + }); + + it('closes an inline overlay when tabbing before the trigger', async () => { + const el = await fixture(html` +
+ +
+ +
+
+ `); + + const trigger = el.querySelector('.trigger') as HTMLElement; + const content = el.querySelector('.content') as HTMLElement; + + openOverlays.push(await Overlay.open(trigger, 'inline', content, {})); + + await waitUntil( + () => !!el.querySelector('span[tabindex="-1"]'), + 'returnFocusElement available' + ); + + const overlays = document.querySelectorAll('active-overlay'); + const overlay = overlays[0]; + + await elementUpdated(overlay); + + trigger.dispatchEvent(tabEvent); + + await waitUntil( + () => !!el.querySelector('span[tabindex="-1"]'), + 'returnFocusElement persists on forward tab' + ); + + trigger.dispatchEvent(shiftTabEvent); + + await waitUntil( + () => el.querySelector('span[tabindex="-1"]') === null, + 'returnFocusElement no longer available' + ); + }); });