Skip to content

feat(core): add HoverController#6358

Open
5t3ph wants to merge 6 commits into
mainfrom
seckles/hover-controller
Open

feat(core): add HoverController#6358
5t3ph wants to merge 6 commits into
mainfrom
seckles/hover-controller

Conversation

@5t3ph
Copy link
Copy Markdown
Contributor

@5t3ph 5t3ph commented Jun 1, 2026

Description

Adds HoverController, a Lit ReactiveController that centralises hover and keyboard-focus event wiring for 2nd-gen components that use the native Popover API (Tooltip, and any future popover-adjacent component).

What it provides

  • Warm-up / cooldown timing — the popover opens only after the pointer has rested on the trigger for delay ms; leaving starts a matching cooldown. delay="0" opens and closes synchronously.
  • Warm-state acceleration — once any instance of a component type warms up, subsequent hovers on the same type open immediately without re-timing.
  • WCAG 1.4.13 pointer bridge — moving the pointer from the trigger into the popover cancels the cooldown, satisfying the "hoverable" requirement so users can interact with popover content.
  • Keyboard focus priorityTab focus opens immediately and suppresses all pointer-driven timers until focusout.
  • Pointer-click exclusion — a pointer click does not open the popover. popover="auto" fires a light dismiss on pointerdown (the trigger is outside the popover surface); opening on the subsequent focusin would cause a visible flash. The controller detects this event sequence and skips the open.
  • disabled / manual guards — re-evaluated live on every host update, no page reload required.

Basic usage

import { LitElement, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import {
  HoverController,
  type HoverControllerHost,
} from '@spectrum-web-components/core/controllers/hover-controller.js';

class SwcTooltip extends LitElement implements HoverControllerHost {
  @property({ type: Number }) delay = 1500;
  @property({ type: Boolean }) manual = false;
  @property({ type: Boolean }) disabled = false;

  private triggerElement: HTMLElement | null = null;

  private readonly hoverController = new HoverController(this, {
    warmStateKey: 'swc-tooltip',
  });

  protected override updated(changes: PropertyValues): void {
    super.updated(changes);
    if (changes.has('triggerElement')) {
      this.hoverController.setTarget(this.triggerElement ?? null);
    }
  }
}

Method

Member Description
setTarget(trigger: HTMLElement | null) Sets the element that receives pointer and focus listeners. Call from updated() whenever the resolved trigger changes. Passing null detaches all listeners.

Constructor options

Option Type Description
warmStateKey string Per-component-type key for shared warm state on document. Use the element tag name (e.g. 'swc-tooltip'). Must be static.

HoverControllerHost interface

Member Type Description
delay number Warm-up and cooldown duration in ms. 0 = immediate.
manual boolean When true, controller skips all event wiring.
disabled boolean When true, controller skips all event wiring.
showPopover() method Called by the controller to open the popover.
hidePopover() method Called by the controller to close the popover.

The controller does not resolve trigger elements, set ARIA attributes, or dispatch lifecycle events — those remain the consuming component's responsibility.

Motivation and context

Tooltip, and any future component that uses popover="auto", requires identical hover/focus logic. Extracting it into a shared controller keeps that logic in one place and tested independently before any consuming component is built.

Related issue(s)

  • fixes SWC-2211

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published. (internal-only controller for now; no changeset)
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers

Manual review test cases

  • Warm-up and cooldown

    1. Open the Hover controller Storybook story
    2. Hover "Trigger A" and hold until the tooltip appears (250 ms demo delay)
    3. Move immediately to "Trigger B"; expect it to open without delay (warm state)
    4. Leave trigger B and wait ~300 ms; expect tooltip to close
    5. Hover "Trigger B" again; expect it to require the full 250 ms warm-up (state has cooled)
  • WCAG 1.4.13 pointer bridge

    1. Open the "WCAG 1.4.13 pointer bridge" story
    2. Hover the trigger until the tooltip appears
    3. Move the pointer from the trigger into the tooltip bubble without pausing outside
    4. Expect the tooltip to stay open indefinitely
  • Keyboard focus

    1. Open the "Keyboard focus opens immediately" story
    2. Tab to the button; expect the tooltip to open at once (no delay)
    3. While focused, move the pointer off the button; expect tooltip to remain open
    4. Tab away; expect tooltip to close immediately
  • Pointer-click focus does not open

    1. Open the Playground story with delay=1500
    2. Click the trigger button without hovering first
    3. Expect no tooltip to appear (click does not trigger focus-open)

Device review

  • Did it pass in Desktop?

Accessibility testing checklist

  • Keyboard (required — document steps below)

    1. Open the "Keyboard focus opens immediately" story
    2. Press Tab to move focus to the trigger button
    3. Expect tooltip to open immediately without warm-up delay
    4. Move pointer away from trigger while focused; expect tooltip to remain open
    5. Press Tab again to move focus away; expect tooltip to close immediately
    6. Confirm focus indicator is visible on the trigger throughout
  • Screen reader (required — document steps below)

    1. HoverController does not set ARIA attributes directly; consuming components (e.g. Tooltip) are responsible for wiring aria-describedby and roles

@5t3ph 5t3ph requested a review from rubencarvalho June 1, 2026 17:37
@5t3ph 5t3ph requested a review from a team as a code owner June 1, 2026 17:37
@5t3ph 5t3ph added Status:Ready for review PR ready for review or re-review. Component:Controllers Includes reactive controllers, element internals controller, etc 2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. labels Jun 1, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 1, 2026

⚠️ No Changeset found

Latest commit: 6694b0a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Comment thread 2nd-gen/packages/swc/.storybook/DocumentTemplate.mdx Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

📚 Branch Preview Links

🔍 First Generation Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6358

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

Copy link
Copy Markdown
Contributor

@Rajdeepc Rajdeepc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how you have created the pointer bridge and keyboard focus. Some feedbacks on optimizations. Overall looks excellent.

Comment thread 2nd-gen/packages/swc/.storybook/DocumentTemplate.mdx Outdated
Comment thread 2nd-gen/packages/core/controllers/hover-controller/src/hover-controller.ts Outdated
Comment thread 2nd-gen/packages/core/controllers/hover-controller/src/hover-controller.ts Outdated
Comment thread 2nd-gen/packages/core/controllers/hover-controller/src/hover-controller.ts Outdated
@caseyisonit caseyisonit added the High priority PR review PR is a high priority and should be reviewed ASAP label Jun 2, 2026
@5t3ph 5t3ph added Status:Ready for re-review PR has had its feedback addressed and is once again ready for review. and removed Status:Ready for review PR ready for review or re-review. labels Jun 3, 2026
@5t3ph 5t3ph requested a review from Rajdeepc June 3, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. Component:Controllers Includes reactive controllers, element internals controller, etc High priority PR review PR is a high priority and should be reviewed ASAP Status:Ready for re-review PR has had its feedback addressed and is once again ready for review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants