Repository: solid-design-system/solid
Pull request #2138: fix: sd-tooltip a11y re-audit
Author: login: auroraVasconcelos
Created: 2025-04-14T14:02:22Z | Merged: 2025-04-22T16:50:48Z

<!-- ## Title: Please consider adding the [skip chromatic] flag to the PR title in case you dont need chromatic testing your changes. -->
## Description:
Closes https://github.com/solid-design-system/solid/issues/2064

## Definition of Reviewable:
- [x] E2E tests (features, a11y, bug fixes) are created/updated
<!-- *If this PR includes a bug fix, an E2E test is necessary to verify the change. If the fix is purely visual, ensuring it is captured within our chromatic screenshot tests is sufficient.* -->
- [x] Stories (features, a11y) are created/updated
- [x] relevant tickets are linked



Diff:
diff --git a/.changeset/moody-owls-brake.md b/.changeset/moody-owls-brake.md
new file mode 100644
index 0000000000..25a66cc8b6
--- /dev/null
+++ b/.changeset/moody-owls-brake.md
@@ -0,0 +1,7 @@
+---
+'@solid-design-system/docs': patch
+---
+
+Improved `sd-tooltip` accessibility:
+
+- Custom trigger announcement.
diff --git a/.changeset/public-parks-help.md b/.changeset/public-parks-help.md
new file mode 100644
index 0000000000..88363ac0fc
--- /dev/null
+++ b/.changeset/public-parks-help.md
@@ -0,0 +1,10 @@
+---
+'@solid-design-system/components': patch
+---
+
+Improved `sd-tooltip` accessibility:
+
+- The button indicates whether it’s open;
+- Keep focus on the button if trigger is click;
+- Buttons are now accessible with VoiceOVer in Safari;
+- Screen readers announce the tooltip content when it becomes visible;
diff --git a/packages/components/src/components/tooltip/tooltip.test.ts b/packages/components/src/components/tooltip/tooltip.test.ts
index d99710fafa..9dc0044187 100644
--- a/packages/components/src/components/tooltip/tooltip.test.ts
+++ b/packages/components/src/components/tooltip/tooltip.test.ts
@@ -232,7 +232,7 @@ describe('<sd-tooltip>', () => {
     expect(document.activeElement).to.not.equal(el);
   });
 
-  it('should close tooltip when clicking trigger if it was opened by focus', async () => {
+  it('should close tooltip when clicking trigger if it was opened by focus, while keeping the button focused', async () => {
     const el = await fixture<SdTooltip>(html`
       <sd-tooltip content="This is a tooltip" trigger="click focus">
         <sd-button>Click Me</sd-button>
@@ -257,6 +257,7 @@ describe('<sd-tooltip>', () => {
 
     // tooltip is hidden again after clicking the button a second time
     expect(body.hidden).to.be.true;
+    expect(button).to.have.focus;
     expect(document.activeElement).to.not.equal(el);
   });
 });
diff --git a/packages/components/src/components/tooltip/tooltip.ts b/packages/components/src/components/tooltip/tooltip.ts
index cc675dd6a4..29cbc6ad0a 100644
--- a/packages/components/src/components/tooltip/tooltip.ts
+++ b/packages/components/src/components/tooltip/tooltip.ts
@@ -136,8 +136,15 @@ export default class SdTooltip extends SolidElement {
     document.removeEventListener('keydown', this.handleKeyboardInteraction);
   }
 
+  private get hasCustomTrigger() {
+    return !!this.querySelector('*:not([slot])');
+  }
+
   // removes empty text nodes from the default slot
-  private formatDefaultSlot(slot: HTMLSlotElement) {
+  private formatDefaultSlot(slot: HTMLSlotElement | null) {
+    if (!slot) {
+      return;
+    }
     const nodes = slot.assignedNodes({ flatten: true });
     nodes.forEach(node => {
       if (node.nodeType === Node.TEXT_NODE && !node?.textContent?.trim()) {
@@ -266,8 +273,15 @@ export default class SdTooltip extends SolidElement {
     if (!this.open) {
       return undefined;
     }
+
     this.open = false;
-    this.blur();
+
+    if (this.trigger === 'click' || this.trigger === 'click focus') {
+      this.focus();
+    } else {
+      this.blur();
+    }
+
     return waitForEvent(this, 'sd-after-hide');
   }
 
@@ -294,23 +308,31 @@ export default class SdTooltip extends SolidElement {
         auto-size="vertical"
         arrow-padding="0"
       >
-        <slot slot="anchor" class=${cx(this.size === 'lg' ? 'text-xl' : 'text-base')}>
-          <button aria-describedby="tooltip" class="flex sd-interactive rounded-full" ?disabled=${this.disabled}>
-            <sd-icon
-              library="system"
-              name="info-circle"
-              label="Tooltip"
-              class=${cx(this.disabled && 'sd-interactive--disabled')}
-            ></sd-icon>
-          </button>
-        </slot>
+        <!-- Dev note: This condition addresses a Safari limitation where VoiceOver ignores the tooltip if its trigger is placed inside a slot. -->
+        ${this.hasCustomTrigger
+          ? html` <slot slot="anchor"></slot>`
+          : html`<button
+              slot="anchor"
+              aria-describedby="tooltip"
+              class=${cx('flex sd-interactive rounded-full', this.size === 'lg' ? 'text-xl' : 'text-base')}
+              ?disabled=${this.disabled}
+              aria-expanded=${this.open}
+              aria-controls="tooltip"
+            >
+              <sd-icon
+                library="system"
+                name="info-circle"
+                label="Tooltip"
+                class=${cx(this.disabled && 'sd-interactive--disabled')}
+              ></sd-icon>
+            </button>`}
         <slot
           name="content"
           part="body"
           id="tooltip"
           class=" bg-primary text-white py-3 px-4 block rounded-none text-sm text-left"
           role="tooltip"
-          aria-label="Tooltip"
+          aria-label=${this.content}
           aria-live=${this.open ? 'polite' : 'off'}
         >
           ${this.content}
diff --git a/packages/docs/src/stories/components/tooltip.stories.ts b/packages/docs/src/stories/components/tooltip.stories.ts
index dbf6eea750..db8933d288 100644
--- a/packages/docs/src/stories/components/tooltip.stories.ts
+++ b/packages/docs/src/stories/components/tooltip.stories.ts
@@ -198,11 +198,8 @@ export const CustomTrigger = {
       const tooltipLabel = tooltipContent;
 
       tooltip.addEventListener('sd-after-show', event => {
-        liveRegion.textContent = tooltipLabel;
-      });
-
-      tooltip.addEventListener('sd-after-hide', event => {
-        liveRegion.textContent = '';
+        liveRegion.textContent =
+          liveRegion.textContent === tooltipContent ? tooltipContent + '\\u200B' : tooltipContent;
       });
     </script>
   `


Reviews:
user: login: paulovareiro29
body: @auroraVasconcelos  I have found a couple of issues:
- When closing the tooltip using keyboard, focus is not being kept on the button which means that focus is being lost.
- On Safari, with default trigger button, it is reading as "Tooltip tooltip" instead of "Tooltip Lorem Ipsum". I believe this is due to the extra `aria-label` on the `slot[name="content"]`.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T11:36:26Z
user: login: paulovareiro29
body: In the `Custom Trigger` story it states:
> In this example we provide a solution to this problem by using a live region to announce the tooltip content when the trigger element is clicked.

I've tested it and it is does not announce anything. Could you have a look into it, since we are already taking care of improving the tooltip a11y.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:28:41Z
user: login: paulovareiro29
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:21:07Z
user: login: paulovareiro29
body: Change changeset to patch, since it is improvements, not new features
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:21:51Z
user: login: paulovareiro29
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:22:59Z
user: login: paulovareiro29
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:42:11Z
user: login: smfonseca
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-04-22T16:01:38Z
user: login: paulovareiro29
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:28:33Z
user: login: MartaPintoTeixeira
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:32:00Z

Review comments:
diff_hunk: @@ -0,0 +1,10 @@
+---
+'@solid-design-system/components': minor
path: .changeset/public-parks-help.md
user: login: paulovareiro29
body: ```suggestion
'@solid-design-system/components': patch
```
created_at: 2025-04-22T15:21:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+---
+'@solid-design-system/components': minor
path: .changeset/public-parks-help.md
user: login: paulovareiro29
body: Also, create a `docs` changeset to inform the tooltip story was improved regarding a11y
created_at: 2025-04-22T15:22:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-14T14:02:26Z
body: ###  🦋  Changeset detected

Latest commit: 5b58e3c2b0d686e7593e7950af50c0dc102ab1e7

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



Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/solid-design-system/solid/new/fix/sd-tooltip-a11y-re-audit?filename=.changeset/spotty-frogs-speak.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0Afix%3A%20sd-tooltip%20a11y%20re-audit%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-14T14:05:43Z
body: 🚀 Storybook has been deployed for branch `fix_sd-tooltip-a11y-re-audit`
- [View Storybook](https://solid-design-system.github.io/solid/fix_sd-tooltip-a11y-re-audit)
author_association: CONTRIBUTOR
user: login: auroraVasconcelos
created_at: 2025-04-16T07:30:47Z
body: > @auroraVasconcelos I have found a couple of issues:
> 
> * When closing the tooltip using keyboard, focus is not being kept on the button which means that focus is being lost.

@paulovareiro29, on the Default we have a 'Click Focus' trigger. If the focus stays in the button after closing it, then you can't open the button with focus. Therefore I have only implemented the focus staying if, and only if, the trigger is 'Click'.



author_association: CONTRIBUTOR
user: login: paulovareiro29
created_at: 2025-04-17T08:41:50Z
body: > > @auroraVasconcelos I have found a couple of issues:
> > 
> > * When closing the tooltip using keyboard, focus is not being kept on the button which means that focus is being lost.
> 
> @paulovareiro29, on the Default we have a 'Click Focus' trigger. If the focus stays in the button after closing it, then you can't open the button with focus. Therefore I have only implemented the focus staying if, and only if, the trigger is 'Click'.

Aligned with Aurora that when using `Click Focus`, when using the keyboard after the tooltip is focused (and open), it should override the "opened by focus" and close the tooltip while keeping the trigger focused.
author_association: CONTRIBUTOR