Skip to content

feat(tooltip): show at a point - #2561

Open
rkaraivanov wants to merge 8 commits into
masterfrom
rkaraivanov/tooltip-enhancements
Open

rkaraivanov wants to merge 8 commits into
masterfrom
rkaraivanov/tooltip-enhancements

Conversation

@rkaraivanov

Copy link
Copy Markdown
Member

Description

Adds showAt(rect: TooltipRect) to igc-tooltip. It shows the tooltip at a point or against a rectangle in viewport coordinates, with no anchor element. The main use is a tooltip that follows the pointer over a canvas, a chart or an SVG drawing.

canvas.addEventListener('pointermove', (e) =>
  tooltip.showAt({ x: e.clientX, y: e.clientY })
);
canvas.addEventListener('pointerleave', () => tooltip.hide());

API

  • showAt({ x, y, width?, height? }). TooltipRect is a plain object type (jsonAPIPlainObject), so a DOMRect fits it and Blazor can pass a struct. show() keeps its signature, because Blazor cannot easily express a union overload.
  • A call while the tooltip shows at a point moves it, with no animation and no events.
  • placement, offset, withArrow and the flip at the viewport edges apply.
  • At a point, the tooltip binds no triggers and no aria-describedby, and it ignores the pointer on itself. The app shows and hides it. Escape still closes it.
  • After the tooltip closes, the configured anchor applies again, also an anchor that changed while it showed at the point.

Fixes found on the way

  • Fallback strategy, for all popover-based components:
    • A popup inside an ancestor with CSS zoom is now at its anchor, with a centered arrow.
    • A popup whose anchor is in a modal dialog no longer lags one frame behind on scroll.
  • Arrow: both strategies now center it from the client rectangles. With a -start or -end placement on a small anchor, it stays inside the tooltip.
  • Tooltip:
    • A trigger that is in both show-triggers and hide-triggers is handled once.
    • The anchor triggers are no longer bound twice.
    • show(target) after an anchor change in the same task shows at the target.
    • Escape and the pointer work after the tooltip moves in the DOM.
    • A tooltip that is not connected no longer takes Escape.
    • Removing placement no longer throws.

Known limits

  • The point is in viewport coordinates and does not follow a page scroll. On a scroll, call showAt() again or hide the tooltip.
  • With CSS anchor positioning, a flipped side stays flipped while the tooltip shows (the browser remembers the last position). The next show starts from placement again.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)

Related Issues

Closes #2538

Checklist

  • My code follows the project's coding standards
  • I have tested my changes locally
  • I have updated documentation if needed
  • If I changed the public API on purpose, I have run npm run public-api:update and committed public-api.json
  • I have added a CHANGELOG.md entry under [Unreleased]

🤖 Generated with Claude Code

- Tooltip: `showAt(rect: TooltipRect)` places the tooltip in viewport
  coordinates. Repeated calls move it with no animation. A point has no
  triggers and no ARIA relation; the anchor applies again after it closes.
- Popover: `anchorRect` anchors the content to a hidden top-layer proxy,
  correct inside `transform` and `zoom` ancestors, with no new floating-ui
  dependency.
- Fixes: triggers bind once, `anchor` resolves before `show(target)`, a moved
  tooltip keeps Escape and the pointer, a removed `placement` no longer
  throws, the arrow stays inside small anchors, the fallback handles `zoom`,
  a new anchor starts from `placement`.
- ChartPointer story with rich slotted content, specs, spec.md, changelog.

Refs #2538
- Register only a connected tooltip for Escape; a same-value `open`
  write is a no-op.
- `showAt()` with no rectangle shows at 0,0 instead of throwing.
- Fallback: use `fixed` for an anchor inside a top-layer element, keep
  layout-shift tracking for anchors other than the rectangle, and round
  to device pixels before the zoom division.
- Skip rebinding when the same anchor resolves again; spec cleanups.

Refs #2538
@rkaraivanov rkaraivanov added the enhancement New feature or request label Nov 2, 2026
Copilot AI balanced review requested due to automatic review settings November 2, 2026 07:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The anchor accessor introduces a separate property-change mechanism that conflicts with coding guidelines requiring @coercedProperty and onChange for per-set side effects.

1 open issue
What changed in this PR

Adds showAt(rect: TooltipRect) so tooltips can follow pointer coordinates or computed rectangles without an anchor element, addressing #2538 for canvas, chart, and SVG use cases.

Changes:

  • Adds rectangle anchoring to the shared popover implementation and exposes it through the tooltip API.
  • Fixes zoom-aware positioning, arrow placement, trigger binding, and tooltip lifecycle behavior.
  • Adds positioning tests, a chart example, specifications, and public API documentation.
File Description
stories/​tooltip.stories.ts Adds a chart demo with pointer-following and bar-anchored tooltips.
src/​internals/​utils/​dom.ts Adds a shared CSS zoom helper.
src/​components/​types.ts Defines the public TooltipRect type.
src/​components/​tooltip/​tooltip.ts Adds showAt() and synchronizes anchor changes.
src/​components/​tooltip/​tooltip.spec.ts Tests rectangle positioning and tooltip regressions.
src/​components/​tooltip/​themes/​tooltip.base.scss Removes a redundant popover background override.
src/​components/​tooltip/​spec.md Documents rectangle anchoring and its behavioral contract.
src/​components/​tooltip/​service.ts Makes the Escape callback collection readonly.
src/​components/​tooltip/​controller.ts Manages rectangle state, triggers, and listener restoration.
src/​components/​tabs/​tab-dom.ts Reuses the shared zoom helper.
src/​components/​popover/​themes/​light/​popover.base.scss Adds proxy-anchor styling and scopes container styles.
src/​components/​popover/​spec.md Documents rectangle anchors and positioning limits.
src/​components/​popover/​position/​types.ts Defines rectangle inputs for positioning strategies.
src/​components/​popover/​position/​strategy.ts Adds a dedicated repositioning operation.
src/​components/​popover/​position/​native.ts Updates native repositioning and shared arrow handling.
src/​components/​popover/​position/​floating.ts Accounts for zoom and top-layer anchors in fallback positioning.
src/​components/​popover/​position/​arrow.ts Centralizes zoom-aware arrow centering and bounds.
src/​components/​popover/​popover.ts Implements rectangle proxy anchors and their lifecycle.
src/​components/​popover/​popover.spec.ts Tests rectangle anchors and top-layer positioning.
public-api.json Records the new type, method, and popover property.
CHANGELOG.md Describes the feature and related fixes.

Review effort: Balanced

Comment thread src/components/tooltip/tooltip.ts Resolved
@simeonoff
simeonoff self-requested a review November 3, 2026 09:46
@simeonoff simeonoff self-assigned this Nov 3, 2026

@simeonoff simeonoff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ran the full suite in Chromium, and the tooltip/popover/select/combo/dropdown specs in WebKit and Firefox 157 against master. No new failures, and the new showAt() specs pass in all three. Existing behavior stays the same unless showAt() is called, so this looks good to me.

A few non-blocking things:

  • TooltipRect and PopoverRect describe the same thing. PopoverRect also ends up in public-api.json through the anchorRect property of igc-popover, but the type itself isn't exported. I'd either mark anchorRect as @internal or export the type. PopoverRect = Required<TooltipRect> would make the relation explicit.
  • _resolveAnchor() in tooltip.ts and resolveAnchor() in the controller share a name but do different things. The tooltip one only applies a pending anchor change, so in show() it reads like a forced re-resolve. Something like _applyPendingAnchor() would be clearer.
  • The "only the rect moved" check in update() of popover.ts relies on properties.size === 1. It works today, but if the tooltip changes any other property in the same update, it silently falls back to the slower update() path. Checking the relevant properties by name would be more explicit.
  • The new _placement getter does the same as resolvePlacement() from popover/position/types.ts. Reusing it keeps the default placement in one place.

- PopoverRect is now Required<TooltipRect>.
- Tooltip: _resolveAnchor is renamed _applyPendingAnchor. The pending
  anchor is applied in update(), not in willUpdate(), because it binds
  listeners and writes aria-describedby.
- Tooltip: the show and hide animations are module constants. The
  queued-transition check in _applyTooltipState is simpler. The Escape
  service handles the key synchronously.
- Popover: an instanceof check replaces the position mode field. The
  fallback strategy checks a stale module load against _pendingLoad,
  not a separate attach counter. Two SCSS if() expressions are plain
  values, and the built CSS is the same.
- Comments that only repeat the code are removed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Tooltip: show at a point (pointer coordinates or a virtual anchor)

3 participants