From 79337ff0c8df69203877732ce0541d9f1d49f33d Mon Sep 17 00:00:00 2001 From: Westbrook Johnson Date: Mon, 18 Mar 2024 15:02:21 -0400 Subject: [PATCH] fix(overlay): prevent focus based hover interaction without :focus-visible --- packages/overlay/src/HoverController.ts | 9 +++++++- .../overlay/test/overlay-lifecycle.test.ts | 17 ++++++++++---- .../test/overlay-trigger-hover-click.test.ts | 23 +++++++++++++++---- .../test/overlay-trigger-longpress.test.ts | 9 +++++++- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/overlay/src/HoverController.ts b/packages/overlay/src/HoverController.ts index aa23c23348..7d55989cdf 100644 --- a/packages/overlay/src/HoverController.ts +++ b/packages/overlay/src/HoverController.ts @@ -13,7 +13,10 @@ governing permissions and limitations under the License. import { conditionAttributeWithId } from '@spectrum-web-components/base/src/condition-attribute-with-id.js'; import { randomID } from '@spectrum-web-components/shared/src/random-id.js'; -import { InteractionController, InteractionTypes } from './InteractionController.js'; +import { + InteractionController, + InteractionTypes, +} from './InteractionController.js'; import { noop } from './AbstractOverlay.js'; const HOVER_DELAY = 300; @@ -30,6 +33,10 @@ export class HoverController extends InteractionController { pointerentered = false; handleTargetFocusin(): void { + // eslint-disable-next-line @spectrum-web-components/document-active-element + if (!document.activeElement?.matches(':focus-visible')) { + return; + } this.host.open = true; this.focusedin = true; } diff --git a/packages/overlay/test/overlay-lifecycle.test.ts b/packages/overlay/test/overlay-lifecycle.test.ts index db37f3c8b6..63b1e69974 100644 --- a/packages/overlay/test/overlay-lifecycle.test.ts +++ b/packages/overlay/test/overlay-lifecycle.test.ts @@ -22,7 +22,11 @@ import '@spectrum-web-components/tooltip/sp-tooltip.js'; import '@spectrum-web-components/action-button/sp-action-button.js'; import { OverlayTrigger } from '@spectrum-web-components/overlay'; import '@spectrum-web-components/overlay/overlay-trigger.js'; -import { a11ySnapshot, findAccessibilityNode } from '@web/test-runner-commands'; +import { + a11ySnapshot, + findAccessibilityNode, + sendKeys, +} from '@web/test-runner-commands'; import { Tooltip } from '@spectrum-web-components/tooltip'; describe('Overlay Trigger - accessible hover content management', () => { @@ -158,10 +162,15 @@ describe('Overlay Trigger - accessible hover content management', () => { expect(trigger.getAttribute('aria-describedby')).to.equal(tooltip.id); expect(el.open).to.be.undefined; + // For `:focus-visible` heuristic. + const input = document.createElement('input'); + el.insertAdjacentElement('afterbegin', input); + input.focus(); + const opened = oneEvent(el, 'sp-opened'); - trigger.dispatchEvent( - new FocusEvent('focusin', { bubbles: true, composed: true }) - ); + await sendKeys({ + press: 'Tab', + }); await opened; expect(trigger.getAttribute('aria-describedby')).to.equal(tooltip.id); diff --git a/packages/overlay/test/overlay-trigger-hover-click.test.ts b/packages/overlay/test/overlay-trigger-hover-click.test.ts index 47b799d42c..889a7d2029 100644 --- a/packages/overlay/test/overlay-trigger-hover-click.test.ts +++ b/packages/overlay/test/overlay-trigger-hover-click.test.ts @@ -31,6 +31,8 @@ import { sendMouse } from '../../../test/plugins/browser.js'; import { clickAndHoverTargets, deep } from '../stories/overlay.stories.js'; import { ignoreResizeObserverLoopError } from '../../../test/testing-helpers.js'; import { Tooltip } from '@spectrum-web-components/tooltip/src/Tooltip.js'; +import { sendKeys } from '@web/test-runner-commands'; +import { Button } from '@spectrum-web-components/button'; ignoreResizeObserverLoopError(before, after); @@ -209,6 +211,7 @@ describe('Overlay Trigger - Hover and Click', () => {
${deep()}
`); const el = test.querySelector('overlay-trigger') as OverlayTrigger; + const trigger = test.querySelector('sp-button') as Button; const button = el.querySelector('sp-action-button') as ActionButton; const button2 = el.querySelector( 'sp-action-button:nth-of-type(2)' @@ -219,10 +222,18 @@ describe('Overlay Trigger - Hover and Click', () => { expect(tooltip.open).to.be.false; const opened = oneEvent(el, 'sp-opened'); - const tooltipOpen = oneEvent(button, 'sp-opened'); - el.open = 'click'; + trigger.focus(); + // For `:focus-visible` heuristic. + await sendKeys({ + press: 'Tab', + }); + await sendKeys({ + press: 'Shift+Tab', + }); + await sendKeys({ + press: 'Space', + }); await opened; - await tooltipOpen; expect(el.open).to.equal('click'); expect(tooltip.open).to.be.true; @@ -235,7 +246,11 @@ describe('Overlay Trigger - Hover and Click', () => { expect(tooltip.open).to.be.true; let closed = oneEvent(button, 'sp-closed'); - button2.focus(); + expect(document.activeElement === button, `button focused`).to.be.true; + await sendKeys({ + press: 'Tab', + }); + expect(document.activeElement === button2, `button focused`).to.be.true; await closed; expect(el.open).to.equal('click'); diff --git a/packages/overlay/test/overlay-trigger-longpress.test.ts b/packages/overlay/test/overlay-trigger-longpress.test.ts index 84b89ee753..8578037842 100644 --- a/packages/overlay/test/overlay-trigger-longpress.test.ts +++ b/packages/overlay/test/overlay-trigger-longpress.test.ts @@ -56,8 +56,15 @@ describe('Overlay Trigger - Longpress', () => { expect(this.content).to.not.be.null; expect(this.content.open).to.be.false; + // For `:focus-visible` heuristic. + const input = document.createElement('input'); + this.el.insertAdjacentElement('beforebegin', input); + input.focus(); + const open = oneEvent(this.el, 'sp-opened'); - this.trigger.focus(); + await sendKeys({ + press: 'Tab', + }); await open; }); it('opens/closes for `Space`', async function () {