Skip to content

feat(widgets): custom tooltip system for widget buttons#10435

Open
chrisgervang wants to merge 18 commits into
masterfrom
chr/widget-tooltip-system
Open

feat(widgets): custom tooltip system for widget buttons#10435
chrisgervang wants to merge 18 commits into
masterfrom
chr/widget-tooltip-system

Conversation

@chrisgervang

@chrisgervang chrisgervang commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Background

Built-in widgets use native HTML title attributes which are slow (~2s delay), unstyled, and cannot contain rich content like keyboard shortcuts. Prior PRs (#10414, #10427) required adding lifecycle hooks to core — this approach avoids any core changes.

Default, tooltip per action Text override "(F)" HTML override (keyboard shortcut)
Screenshot 2026-07-09 at 4 29 06 PM Screenshot 2026-07-09 at 4 29 03 PM Screenshot 2026-07-09 at 4 29 11 PM

Change List

  • Add a Preact <Tooltip> component using @floating-ui/dom for positioning (flip/shift/offset)
  • Integrate into IconButton so all button widgets get styled tooltips automatically
  • Add per-widget *Tooltip props (string | HTMLElement) for user overrides (e.g. keyboard shortcuts)
  • Update CompassWidget and GimbalWidget (custom buttons) to use <Tooltip> directly
  • Add .deck-widget-tooltip CSS class with theme-aware variables (--tooltip-max-width, --tooltip-z-index)
  • Export _Tooltip component for Preact-based custom widget authors
  • Add docs/api-reference/widgets/tooltips.md covering built-in customization, Preact usage, and vanilla JS/CSS-only guidance
  • Zero changes to @deck.gl/core

Note

Low Risk
UI-only widget layer changes with no @deck.gl/core modifications; behavior is covered by updated widget tests and accessibility shifts from title to aria-label plus role=tooltip.

Overview
Replaces slow native title tooltips on built-in button widgets with themed, immediate tooltips powered by a new Preact Tooltip component (@floating-ui/dom positioning).

IconButton wraps buttons when tooltip content exists; label stays as aria-label while optional tooltip / per-widget *Tooltip props accept string | HTMLElement | false for overrides or disabling. Compass, Gimbal, Zoom, Fullscreen, Theme, Timeline, Toggle, and other button widgets are wired through; _Tooltip is exported for custom Preact widgets. Adds .deck-widget-tooltip theme CSS variables, tooltips.md, and widget API doc updates; tests assert aria-label and tooltip behavior instead of title.

Reviewed by Cursor Bugbot for commit 88f2ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

chrisgervang and others added 2 commits July 9, 2026 15:24
Replace native HTML title tooltips with themed, positioned tooltips using
a Preact Tooltip component and @floating-ui/dom. Zero changes to core.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
display: contents elements have no bounding box, so computePosition
must reference the actual child button element. Also switch to
pointerover/pointerout which bubble through pointer-events: none parents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ant show

- Fix vertical centering by using inline critical styles (whiteSpace/fontSize/padding)
  so computePosition gets correct dimensions regardless of stylesheet load timing
- Fix button group border-radius by using descendant selectors for button
- Show tooltips instantly with opacity transition to prevent flash
- Use ref callback instead of useEffect for immediate positioning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a styled, theme-aware tooltip system to @deck.gl/widgets using a new Preact <Tooltip> component (positioned via @floating-ui/dom), and wires it into bundled button widgets so consumers can override tooltip content with string | HTMLElement (e.g. for shortcut hints) without touching @deck.gl/core.

Changes:

  • Introduces a new Preact Tooltip component and exports it as _Tooltip for custom widget authors.
  • Updates IconButton to use the custom tooltip system (replacing native title) and propagates new per-widget tooltip override props across bundled button widgets.
  • Adds tooltip theming CSS variables and documentation for customization and usage.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
modules/widgets/src/lib/components/tooltip.tsx New tooltip component using Floating UI for positioning.
modules/widgets/src/lib/components/icon-button.tsx Integrates tooltips into IconButton and replaces native title.
modules/widgets/src/zoom-widget.tsx Adds zoomInTooltip/zoomOutTooltip and passes through to IconButton.
modules/widgets/src/fullscreen-widget.tsx Adds enterTooltip/exitTooltip and passes through to IconButton.
modules/widgets/src/toggle-widget.tsx Adds tooltip/onTooltip overrides and conditionally selects tooltip per state.
modules/widgets/src/theme-widget.tsx Adds lightModeTooltip/darkModeTooltip and passes through to IconButton.
modules/widgets/src/screenshot-widget.tsx Adds tooltip override and passes through to IconButton.
modules/widgets/src/reset-view-widget.tsx Adds tooltip override and passes through to IconButton.
modules/widgets/src/icon-widget.tsx Adds tooltip override and passes through to IconButton.
modules/widgets/src/compass-widget.tsx Switches custom button markup to use <Tooltip> directly.
modules/widgets/src/gimbal-widget.tsx Switches custom button markup to use <Tooltip> directly.
modules/widgets/src/stylesheet.css Adds tooltip trigger + tooltip styling and theme variable hooks.
modules/widgets/src/index.ts Exports _Tooltip and its props for external Preact widget usage.
docs/api-reference/widgets/tooltips.md New docs page describing tooltip overrides, styling, and custom usage patterns.
docs/api-reference/widgets/styling.md Documents --tooltip-* CSS variables.
docs/api-reference/widgets/overview.md Links to the new tooltip documentation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread modules/widgets/src/lib/components/tooltip.tsx
Comment thread modules/widgets/src/lib/components/tooltip.tsx
Comment thread modules/widgets/src/lib/components/tooltip.tsx
Comment thread modules/widgets/src/lib/components/tooltip.tsx
Comment thread modules/widgets/src/lib/components/tooltip.tsx Outdated
Comment thread modules/widgets/src/lib/components/icon-button.tsx
Comment thread docs/api-reference/widgets/tooltips.md
Comment thread docs/api-reference/widgets/tooltips.md
Comment thread modules/widgets/src/theme-widget.tsx
chrisgervang and others added 3 commits July 9, 2026 16:53
Tooltip props now accept `string | HTMLElement | false`:
- `false` explicitly disables the tooltip (no fallback to label)
- `undefined` (default) falls back to the widget's label
- Update docs to document the `false` option

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch from pointerOver/Out to pointerEnter/Leave to prevent spurious
tooltip flicker when moving between child elements. Remove unimplemented
aria-describedby and :focus-visible claims from docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests were checking button.title which no longer exists after tooltip
system replaced native titles with aria-label + custom tooltips. Also
update selectors for compass/gimbal widgets where tooltip wrapper sits
between .deck-widget-button and button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coveralls

coveralls commented Jul 10, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 83.181% (+0.02%) from 83.165% — chr/widget-tooltip-system into master

@chrisgervang
chrisgervang marked this pull request as ready for review July 10, 2026 17:21
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread modules/widgets/src/theme-widget.tsx Outdated
Comment thread modules/widgets/src/lib/components/tooltip.tsx

ibgreen commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Thanks Chris. I compared this against #10414 / #10427, and I like this direction. In particular, avoiding a new core Widget lifecycle hook is a nice simplification, and the explicit *Tooltip override props give users a better customization story than the delegated data-deck-widget-tooltip approach.

A few things I think we should tighten before merging:

  • ThemeWidget tooltip selection looks inconsistent with the button label selection. In dark mode the label is currently darkModeLabel, but the tooltip prop passed to IconButton is lightModeTooltip (and vice versa). That means custom tooltip overrides can describe the opposite mode from the visible/accessibility label. I think the tooltip should be selected from the same branch as label, or the prop semantics need to be renamed/documented very explicitly with tests.
  • For HTMLElement content, the tooltip mount code only appends a clone if the container has no first child. If content changes while the tooltip is visible, it looks like the old clone can remain mounted. Using replaceChildren(content.cloneNode(true)) would make this deterministic.
  • Since the new *Tooltip props are public API, it would be good to add them to the individual widget API pages too, not only the new overview page. The overview should also mention inherited IconButton behavior for widgets like Selector/Timeline/Loading if those are intended to get styled tooltips automatically, or disable them where they are not intended.
  • I would like to see focused coverage for string override, false disables, HTMLElement replacement, and the ThemeWidget mode-specific labels/tooltips.

Overall this is promising. If those edges are addressed, I would prefer converging on this PR over the stacked helper/lifecycle approach.

chrisgervang and others added 2 commits July 10, 2026 14:27
- Fix HTMLElement tooltip never refreshing (use replaceChildren)
- Fix ThemeWidget tooltip using wrong mode branch
- Add tooltip tests: string override, false disables, HTMLElement
  content, HTMLElement replacement, ThemeWidget mode alignment
- Document tooltip override props on all individual widget API pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ba2eb05. Configure here.

Comment thread modules/widgets/src/lib/components/tooltip.tsx Outdated
chrisgervang and others added 9 commits July 10, 2026 14:34
- Fix HTMLElement tooltip never refreshing (use replaceChildren)
- Fix ThemeWidget tooltip using wrong mode branch
- Add tooltip tests: string override, false disables, HTMLElement
  content, HTMLElement replacement
- Add ThemeWidget tooltip tests (mode alignment, label fallback)
- Document tooltip override props on all individual widget API pages
- Document automatic IconButton tooltip behavior for Selector, Loading,
  Timeline, Stats, and Popup widgets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The per-widget tooltip props are already documented on each widget's
own API page — maintaining a duplicate list invites staleness.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…gets

Describe that `label` drives a styled tooltip (not a native title),
and link to the tooltips page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow users to customize or disable the Play/Pause button tooltips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These widgets previously only showed tooltips from their label with no
way to override or disable them independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Widgets like CompassWidget re-render on every viewport change. The
inline ref was calling replaceChildren on every render even when the
tooltip content hadn't changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@ibgreen-openai ibgreen-openai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Very nice indeed, big thanks for doing this!!!

Some inline thoughts on the docs, really just some food for thought based on my reaction.

High level comments

  • In an ideal world, I would be able to add something to the tooltips (key bindings) rather than replace the tooltips wholesale which means that I also have to duplicate the labels.
    But I am not convinced it is worth extra props or callback tooltip props etc.
  • Also in my apps, I will be changing the tooltips sometimes during widget lifetime especially custom widget buttons as the keyboard binding changes with changing application state, so if the tests cover that it would be nice.

btn.setAttribute('aria-label', 'My Action');
btn.setAttribute('aria-describedby', 'my-tooltip');

const tooltip = document.createElement('div');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a nit: This example is rather confusing to me and makes the tooltip system read as more complex than it is.

My knee jerk reaction, is why would we suggest users to do all this? It makes it seems very complicated.

For the folks who don't want to import preact and configure JSX in their project for just one custom widget, can't we just export the a plain JS function that renders the internal Preact into an HTML element and this also handles the positioning so we don't have to ask the user to do all this and install the external library etc.

Separately, since we clearly care about Accessibility, for me it would make more sense to have a section that describes Accessibility first (perhaps in every widget page).

Then the example we are giving here would seem less random, as it is clearly implementing the explicit accessibility spec above, rather than forcing the user to try to understand what those HTML element stanzas are for - are they standard DOM fields or some deck convention etc.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Idea: since we clearly care about Accessibility, for me it would make more sense to have a section that describes Accessibility support/considerations first (perhaps even in every widget page).

There are clearly a number of things that need to be done, setting roles and aria-... attributes etc. and not every developer is up to date on that.

Then the non-Preact example we are giving would seem less random to, as it would clearly be implementing the explicit accessibility spec above, rather than forcing the user to try to understand what those HTML element stanzas are for - are they standard DOM fields or some deck.gl/widgets convention etc.

btn.addEventListener('pointerenter', () => { tooltip.hidden = false; });
btn.addEventListener('pointerleave', () => { tooltip.hidden = true; });

rootElement.replaceChildren(btn, tooltip);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Replacing all children seems a bit heavy handed?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants