feat(button): support tooltip directly on button#3368
Conversation
🦋 Changeset detectedLatest commit: 7979d82 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
tooltip property to sl-buttontooltip directly on button
🕸 Storybook previewYou can view a preview here (commit |
There was a problem hiding this comment.
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
tooltipproperty tosl-buttonand render an internal<sl-tooltip>via scoped elements. - Wired tooltip-based ARIA (
aria-describedbyfor text buttons,aria-labelledbyfor icon-only buttons) and added logic to merge forwardedaria-labelledbyelements. - 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. |
| // If the button is icon only, the tooltip functions as the label, otherwise it functions as the description. | ||
| let ariaLabelledBy: string | undefined, ariaDescribedBy: string | undefined; | ||
| if (this.tooltip) { | ||
| if (this.internals.states.has('icon-only')) { | ||
| ariaLabelledBy = 'tooltip'; | ||
| } else { | ||
| ariaDescribedBy = 'tooltip'; | ||
| } | ||
| } |
| // Capture any aria-labelledby elements the mixin just forwarded to the inner button. | ||
| this.#forwardedLabelElements = [ | ||
| ...((this.button as unknown as { ariaLabelledByElements: Element[] | null }) | ||
| .ariaLabelledByElements ?? []) | ||
| ]; |
| <p> | ||
| This example shows an icon-only button. When using an icon-only button, it's important to | ||
| provide an accessible name using the <code>aria-label</code> attribute so that assistive | ||
| technologies can convey the purpose of the button to users. | ||
| </p> |
🕸 Website previewYou can view a preview here (commit |
| // If the button is icon only, the tooltip functions as the label, otherwise it functions as the description. | ||
| let ariaLabelledBy: string | undefined, ariaDescribedBy: string | undefined; | ||
| if (this.tooltip) { | ||
| if (this.internals.states.has('icon-only')) { | ||
| ariaLabelledBy = 'tooltip'; | ||
| } else { | ||
| ariaDescribedBy = 'tooltip'; | ||
| } | ||
| } | ||
|
|
||
| return html` | ||
| <button | ||
| @click=${this.#onClick} | ||
| aria-describedby=${ifDefined(ariaDescribedBy)} | ||
| aria-labelledby=${ifDefined(ariaLabelledBy)} | ||
| command=${ifDefined(this.command)} | ||
| .commandForElement=${target} | ||
| ?disabled=${this.disabled} | ||
| part="button" | ||
| type="button"> | ||
| <slot></slot> | ||
| </button> | ||
| ${this.tooltip ? html`<sl-tooltip id="tooltip">${this.tooltip}</sl-tooltip>` : nothing} | ||
| `; |
| it('should include both the tooltip and aria-labelledby element in ariaLabelledByElements', async () => { | ||
| const wrapper = await fixture(html` | ||
| <div> | ||
| <span id="icon-btn-label">Favorite star</span> | ||
| <sl-button aria-labelledby="icon-btn-label" tooltip="Mark as favorite"> | ||
| Hello world | ||
| </sl-button> | ||
| </div> |
| super.willUpdate(changes); | ||
|
|
||
| if (changes.has('anchor') || changes.has('for')) { | ||
| this.#updateAnchor(); |
| if (this.anchor) { | ||
| this.#removeAriaRelation(this.anchor); | ||
| } | ||
|
|
| import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html } from 'lit'; | ||
| import { property, state } from 'lit/decorators.js'; | ||
| import styles from './tooltip2.scss.js'; | ||
|
|
||
| let nextUniqueId = 0; | ||
|
|
||
| const SHOW_DELAY = 150, | ||
| HIDE_DELAY = 0; | ||
|
|
||
| /** | ||
| * A tooltip component that can be used to display additional information about an element when the | ||
| * user hovers over it, focuses it, or clicks it. The tooltip is positioned relative to an anchor | ||
| * element, which can be specified using the `for` attribute. | ||
| * | ||
| * The tooltip will automatically determine the appropriate ARIA relation to use based on the `type` | ||
| * property. By default, it will use `ariaLabelledByElements`, but if `type` is set to | ||
| * `description`, it will use `ariaDescribedByElements` instead. | ||
| */ | ||
| export class Tooltip2 extends LitElement { | ||
| /** @internal */ |
Closes #3344
Summary
Adds a
tooltipproperty tosl-buttonthat renders an<sl-tooltip>inside the button's shadow DOM and wires up the correct ARIA relationships automatically.Previously, adding a tooltip to a button required adding a sibling
<sl-tooltip>and settingaria-describedby/aria-labelledbyby hand — especially cumbersome for icon-only buttons where the tooltip doubles as the accessible label.Behaviour
aria-labelledby).aria-describedby).aria-labelledbyis also set on the host, both the external label element and the tooltip are preserved inariaLabelledByElementson the inner<button>.Changes
packages/components/button/src/button.ts— newtooltipproperty, scopedsl-tooltipin the render template,#forwardedLabelElementstracking, and#syncAriaLabelledBy()to merge forwarded label elements with the tooltip element after each render.packages/components/button/src/button.spec.ts— unit tests covering render, text content, aria wiring for text and icon-only variants, removal, and the combinedaria-labelledby+ tooltip case..changeset/afraid-parents-nail.md— minor changeset for@sl-design-system/button.