Skip to content

feat(tooltip): simplify implementation#3368

Open
jpzwarte wants to merge 57 commits into
mainfrom
feature/3344-tooltip-property-on-button
Open

feat(tooltip): simplify implementation#3368
jpzwarte wants to merge 57 commits into
mainfrom
feature/3344-tooltip-property-on-button

Conversation

@jpzwarte

@jpzwarte jpzwarte commented May 21, 2026

Copy link
Copy Markdown
Member

Closes #3344
Closes #3287
Closes #3275
Closes #3255
Closes #3343
Closes #3274
Closes #3049
Closes #3264

Overview

This PR rewrites the sl-tooltip component and simplifies how tooltips are used across the design system. The new implementation replaces the complex internal machinery with native browser APIs and introduces a first-class tooltip property on several components.


@sl-design-system/tooltip — major rewrite

The tooltip was the component responsible for the most bug reports and had a complex internal implementation. This rewrite significantly simplifies it.

The AnchorController, EventsController, and Tooltip.lazy() static method have all been removed in favour of native browser popover and CSS Anchor Positioning APIs.

Note

CSS Anchor Positioning is not yet supported in all browsers. You may need to include the CSS Anchor Positioning polyfill in your application.

Breaking changes

  • The TooltipOptions interface and Tooltip.lazy() static method have been removed. Use the for attribute to link a tooltip to its anchor instead.
  • The position, offset, maxWidth, arrowPadding, and viewportMargin properties/statics have been removed.
  • hoverShowDelay changed from 500ms to 150ms and hoverHideDelay changed from 200ms to 0ms.

New API

  • for — links the tooltip to an anchor element by ID
  • type — controls the ARIA relationship: 'label' (ariaLabelledByElements, default) or 'description' (ariaDescribedByElements)
  • trigger — space-separated list of triggers: 'focus', 'hover', and/or 'click' (default: 'focus hover')
  • disabled — prevents the tooltip from showing
  • open — reflects the current open state

tooltip property on components — minor

Previously, adding a tooltip to a component required adding a sibling <sl-tooltip> element manually and wiring up the correct aria-describedby or aria-labelledby relationship by hand. The new tooltip property improves the DX by letting you set a tooltip directly on the component, with all accessibility wiring handled automatically:

  • For icon-only buttons the tooltip text acts as the accessible label (aria-labelledby).
  • For text buttons the tooltip text acts as an accessible description (aria-describedby).

Affected components: @sl-design-system/button, @sl-design-system/menu, @sl-design-system/tag, @sl-design-system/toggle-button.


@sl-design-system/toggle-button — minor refactor

Refactored to use an internal <button> element. This improves accessibility and removes the need for manual keyboard and ARIA handling.

Breaking changes

  • The [pressed], [icon-only], [text-only], and [error] attributes have been replaced by CSS custom states (:state(pressed), :state(icon-only), :state(text-only), :state(error)).
  • The shape property type changed from ButtonShape to ToggleButtonShape ('square' | 'pill').
  • The pressed property is no longer reflected as an attribute.
  • The label property has been removed. Use the new tooltip property instead.

New

  • CSS parts button and tooltip for styling the internal elements.
  • Focus delegated to the internal <button> via delegatesFocus: true.
  • ARIA attributes forwarded to the internal <button> via ForwardAriaMixin.

@sl-design-system/shared — minor

ForwardAriaMixin no longer overrides existing ariaDescribedByElements or ariaLabelledByElements values. Components can now maintain their own ARIA relationships without interference.


Other components updated to use the new tooltip implementation — minor

  • @sl-design-system/avatar
  • @sl-design-system/breadcrumbs — tooltips for truncated links now use the for attribute approach, removing the need for manual cleanup functions.
  • @sl-design-system/calendar
  • @sl-design-system/ellipsize-text
  • @sl-design-system/grid
  • @sl-design-system/angular — tooltip directive updated; API unchanged but DOM structure may have changed.

Bug fixes

  • @sl-design-system/menu — Fixed: clicking the menu button a second time did not dismiss the menu. The button's click handler was firing after the popover's toggle event and calling togglePopover() on an already-closed menu. Fixed by tracking a #popoverJustClosed flag via the beforetoggle event.
  • @sl-design-system/button-bar — Fixed: buttons were not given enough time to set their icon-only state.

Copilot AI review requested due to automatic review settings May 21, 2026 09:54
@changeset-bot

changeset-bot Bot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2d1fd1d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 42 packages
Name Type
@sl-design-system/button Minor
@sl-design-system/menu Minor
@sl-design-system/tag Minor
@sl-design-system/toggle-button Minor
@sl-design-system/shared Minor
@sl-design-system/button-bar Patch
@sl-design-system/angular Minor
@sl-design-system/avatar Minor
@sl-design-system/calendar Minor
@sl-design-system/ellipsize-text Minor
@sl-design-system/grid Minor
@sl-design-system/breadcrumbs Minor
@sl-design-system/tooltip Major
@sl-design-system/paginator Patch
@sl-design-system/tabs Patch
@sl-design-system/tool-bar Patch
@sl-design-system/combobox Patch
@sl-design-system/toggle-group Patch
@sl-design-system/accordion Patch
@sl-design-system/announcer Patch
@sl-design-system/callout Patch
@sl-design-system/checkbox Patch
@sl-design-system/data-source Patch
@sl-design-system/date-field Patch
@sl-design-system/dialog Patch
@sl-design-system/editor Patch
@sl-design-system/emoji Patch
@sl-design-system/form Patch
@sl-design-system/format-date Patch
@sl-design-system/format-number Patch
@sl-design-system/inline-message Patch
@sl-design-system/number-field Patch
@sl-design-system/panel Patch
@sl-design-system/popover Patch
@sl-design-system/radio-group Patch
@sl-design-system/search-field Patch
@sl-design-system/select Patch
@sl-design-system/switch Patch
@sl-design-system/text-area Patch
@sl-design-system/text-field Patch
@sl-design-system/tree Patch
@sl-design-system/virtual-list Patch

Not sure what this means? Click here to learn what changesets are.

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

@jpzwarte jpzwarte changed the title Add tooltip property to sl-button feat(button): support tooltip directly on button May 21, 2026
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🕸 Storybook preview

You can view a preview here (commit 61d6159bf7142fa105109895293540a073a5220d).

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.

Pull request overview

This PR adds a tooltip property to @sl-design-system/button (<sl-button>) so consumers can attach an <sl-tooltip> directly to the button and have ARIA relationships applied automatically, improving ergonomics especially for icon-only buttons.

Changes:

  • Added a new tooltip property to sl-button and render an internal <sl-tooltip> via scoped elements.
  • Wired tooltip-based ARIA (aria-describedby for text buttons, aria-labelledby for icon-only buttons) and added logic to merge forwarded aria-labelledby elements.
  • Added Storybook and unit test updates plus a changeset for a minor release.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
yarn.lock Updates workspace dependency metadata for the button package (adds tooltip/scoped-elements/lit entries).
packages/components/button/src/button.ts Implements the tooltip property, internal tooltip rendering, and ARIA wiring/merging logic.
packages/components/button/src/button.stories.ts Updates stories to demonstrate tooltip usage, including icon-only example.
packages/components/button/src/button.spec.ts Adds unit tests for tooltip rendering and ARIA behavior.
packages/components/button/package.json Adds @sl-design-system/tooltip dependency and declares scoped-elements/lit peer/dev dependencies.
.changeset/afraid-parents-nail.md Declares a minor release for the new tooltip API.

Comment thread packages/components/button/src/button.ts
Comment thread packages/components/button/src/button.ts
Comment thread packages/components/button/src/button.ts Outdated
Comment thread packages/components/button/src/button.stories.ts
@jpzwarte jpzwarte marked this pull request as draft May 21, 2026 13:36
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

🕸 Website preview

You can view a preview here (commit 36c87aea2608a2e2aa0e7d66a52e56f8c26fc873).

Copilot AI review requested due to automatic review settings May 21, 2026 13:45

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.

Pull request overview

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

Comment thread packages/components/button/src/button.ts
Comment thread packages/components/button/src/button.spec.ts Outdated
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Copilot AI review requested due to automatic review settings May 21, 2026 15:38

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.

Pull request overview

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

Comment thread packages/components/button/src/button.ts
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Comment thread packages/components/tooltip/src/tooltip2.ts Outdated
Copilot AI review requested due to automatic review settings May 26, 2026 07:23
Copilot AI review requested due to automatic review settings June 3, 2026 11:58

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.

Pull request overview

Copilot reviewed 121 out of 122 changed files in this pull request and generated 4 comments.

Comment thread packages/components/toggle-group/src/toggle-group.ts
Comment thread packages/components/shared/src/mixins/forward-aria-mixin.ts
Comment thread packages/components/breadcrumbs/src/breadcrumbs.ts
Comment thread packages/angular/src/tooltip.directive.ts
Copilot AI review requested due to automatic review settings June 3, 2026 12:42

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.

Pull request overview

Copilot reviewed 122 out of 123 changed files in this pull request and generated 4 comments.

Comment thread packages/components/breadcrumbs/src/breadcrumbs.ts
Comment thread packages/components/shared/src/mixins/forward-aria-mixin.ts
Comment thread packages/components/tag/src/tag.ts
Comment thread packages/components/checkbox/src/checkbox-group.stories.ts

@a11ymiko a11ymiko 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.

For stories in Tooltip2 group (Basic, Hover Bridge, Positions) tooltip is not showed on focus.

Screen.Recording.2026-06-03.at.14.44.36.mov

@a11ymiko a11ymiko marked this pull request as ready for review June 3, 2026 13:21
Copilot AI review requested due to automatic review settings June 3, 2026 13:21

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.

Pull request overview

Copilot reviewed 122 out of 123 changed files in this pull request and generated 5 comments.

Comment thread packages/components/breadcrumbs/src/breadcrumbs.ts
Comment thread packages/components/breadcrumbs/src/breadcrumbs.ts
Comment thread packages/components/breadcrumbs/src/breadcrumbs.ts
Comment thread packages/components/shared/src/mixins/forward-aria-mixin.ts
Comment thread tools/eslint-plugin-slds/lib/rules/button-has-label.js

@michal-sanoma michal-sanoma 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.

Amazing work with this refactor! 🚀

Small compatibility clarification: do we still want to support shared / multi-anchor tooltips?

The old implementation allowed multiple anchors to point to the same <sl-tooltip> via ARIA attributes, and we had coverage for that in the Shared story and tooltip-shared.spec.ts. The new for API appears to model a single tooltip-to-anchor relationship instead

If that change is intentional, I think it would be helpful for consumers if we mention it as a breaking change and suggest the preferred replacement pattern, wdyt?


Refactor toggle button to use an internal `<button>` element. This improves accessibility and removes the need for manual keyboard and ARIA handling.

**Breaking changes**

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.

with this many breaking changes, is it still a minor version update?

});

it('should not move focus when the button is clicked without being focused', async () => {
it('should focus the first menu item when the button is clicked', async () => {

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.

I'm not sure this is correct. When i click the button it doesn't show a focusring on the first menu item:
https://storybook-3368.d3c5h2uor7jlkj.amplifyapp.com/?path=/story/actions-menu-menu-button--submenu

<span>aria-disabled</span>

<span>md</span>
<sl-menu-button aria-label="Label">

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.

these need to be updated as well

expect(el).to.have.attribute('aria-describedby', tooltip?.id);
});

it('should be icon-only even if one icon is missing (Errors variant)', async () => {

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.

why is this removed?

}
}

if (changes.has('disabled')) {

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.

why is this removed?

description: 'This example shows a tool bar with different tooltip techniques on the buttons.',
items: () => html`
<sl-button aria-labelledby="tooltip-bold" fill="outline">
<sl-button aria-labelledby="tooltip-bold" fill="outline" id="bold">

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.

Does it still need the aria-labelledby="tooltip-bold"?

Copilot AI review requested due to automatic review settings June 9, 2026 12:37

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.

Pull request overview

Copilot reviewed 122 out of 123 changed files in this pull request and generated 2 comments.

Comment on lines +43 to 47
// The `tooltip` attribute on sl-button provides the accessible label for icon-only
// buttons at runtime, so it counts as an accessible name at lint time.
const hasTooltipAttribute = ((element.attribs ?? {})['tooltip'] ?? '').trim() !== '';

if (
Comment on lines 224 to +228
if (elementsProp.endsWith('Elements')) {
(targetElement as unknown as Record<string, Element[]>)[elementsProp] = elements;
const elementsPropValue =
(targetElement as unknown as Record<string, Element[]>)[elementsProp] ?? [];

// Make sure we don't override any existing references
Copilot AI review requested due to automatic review settings June 16, 2026 09: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.

Pull request overview

Copilot reviewed 123 out of 124 changed files in this pull request and generated 4 comments.

Comment on lines 38 to 42
this.renderRoot
.querySelector('slot')
?.assignedElements({ flatten: true })
.filter((element): element is ToggleButton => element instanceof ToggleButton) ?? []
.filter(element => element instanceof ToggleButton) ?? []
);
Comment on lines +333 to +337
// Use an existing tooltip, or create a new one
let tooltip = crumb.ariaLabelledByElements?.find(
(el): el is Tooltip => el instanceof Tooltip && el.for === crumb.id
);
this.#tooltipCleanupFunctions.set(link, cleanup);
link.dataset['hasTooltip'] = 'true';
}
} else if (link.hasAttribute('data-has-tooltip') && link.hasAttribute('aria-describedby')) {
const cleanup = this.#tooltipCleanupFunctions.get(link);
if (cleanup) {
cleanup();
}
this.#tooltipCleanupFunctions.delete(link);

const tooltip = tooltipsSlot
.assignedElements()
.find(el => el.id === link.getAttribute('aria-describedby')) as Tooltip | undefined;
tooltip?.remove();
link.removeAttribute('data-has-tooltip');
link.removeAttribute('aria-describedby');
}
if (!tooltip) {
Comment on lines 182 to 190
#onResize(): void {
const slot = this.renderRoot.querySelector('slot');

if (slot && slot.clientWidth < slot.scrollWidth) {
this.#tooltip ||= Tooltip.lazy(
this,
tooltip => {
this.#tooltip = tooltip;
tooltip.textContent = this.label;
},
{ context: this.shadowRoot! }
);
} else if (this.#tooltip instanceof Tooltip) {
this.#tooltip.remove();
this.#tooltip = undefined;
} else if (this.#tooltip) {
this.#tooltip();
this.#tooltip = undefined;
if (typeof this.tooltip === 'string') {
return;
}

// If the contents of the tag overflows, make sure it is keyboard focusable,
// so the user can tab to it.
if (!this.disabled && (this.removable || this.#tooltip)) {
this.setAttribute('tabindex', '0');
} else if (!this.hasAttribute('aria-labelledby')) {
this.removeAttribute('tabindex');
}
const label = this.renderRoot.querySelector('[part="label"]');

this.tooltip = !!label && label.clientWidth < label.scrollWidth;
}
Comment on lines +225 to +232
const elementsPropValue =
(targetElement as unknown as Record<string, Element[]>)[elementsProp] ?? [];

// Make sure we don't override any existing references
(targetElement as unknown as Record<string, Element[]>)[elementsProp] = [
...elementsPropValue,
...elements
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment