Skip to content

Commit 1006cc2

Browse files
crisbetojelbourn
authored andcommitted
fix(tooltip): avoid adding same aria description as trigger's aria-label (#16870)
We automatically add an `aria-describedby` to the triggers of tooltips so that assistive technologies can read out the tooltip text. As a part of this process, the `AriaDescriber` has some functionality that checks whether the element doesn't have an `aria-label` with the same value already, and if it does, it avoids adding the description. The problem is that tooltips add this description very early in their lifecycle which means that if the element has a data-bound `aria-label`, it won't be detected by the `AriaDescriber`, as is the case for the buttons in the `mat-paginator`. These changes work around the issue by deferring the setting of the description. Fixes #16719.
1 parent 425eb7e commit 1006cc2

File tree

2 files changed

+31
-9
lines changed

2 files changed

+31
-9
lines changed

src/material/tooltip/tooltip.spec.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ describe('MatTooltip', () => {
6363
DynamicTooltipsDemo,
6464
TooltipOnTextFields,
6565
TooltipOnDraggableElement,
66+
DataBoundAriaLabelTooltip,
6667
],
6768
providers: [
6869
{provide: Platform, useFactory: () => platform},
@@ -441,20 +442,31 @@ describe('MatTooltip', () => {
441442
expect(overlayContainerElement.textContent).toBe('');
442443
}));
443444

444-
it('should have an aria-described element with the tooltip message', () => {
445+
it('should have an aria-described element with the tooltip message', fakeAsync(() => {
445446
const dynamicTooltipsDemoFixture = TestBed.createComponent(DynamicTooltipsDemo);
446447
const dynamicTooltipsComponent = dynamicTooltipsDemoFixture.componentInstance;
447448

448449
dynamicTooltipsComponent.tooltips = ['Tooltip One', 'Tooltip Two'];
449450
dynamicTooltipsDemoFixture.detectChanges();
451+
tick();
450452

451-
const buttons = dynamicTooltipsComponent.getButtons();
453+
const buttons = dynamicTooltipsDemoFixture.nativeElement.querySelectorAll('button');
452454
const firstButtonAria = buttons[0].getAttribute('aria-describedby');
453455
expect(document.querySelector(`#${firstButtonAria}`)!.textContent).toBe('Tooltip One');
454456

455457
const secondButtonAria = buttons[1].getAttribute('aria-describedby');
456458
expect(document.querySelector(`#${secondButtonAria}`)!.textContent).toBe('Tooltip Two');
457-
});
459+
}));
460+
461+
it('should not add an ARIA description for elements that have the same text as a' +
462+
'data-bound aria-label', fakeAsync(() => {
463+
const ariaLabelFixture = TestBed.createComponent(DataBoundAriaLabelTooltip);
464+
ariaLabelFixture.detectChanges();
465+
tick();
466+
467+
const button = ariaLabelFixture.nativeElement.querySelector('button');
468+
expect(button.getAttribute('aria-describedby')).toBeFalsy();
469+
}));
458470

459471
it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => {
460472
tooltipDirective.show();
@@ -1011,14 +1023,16 @@ class OnPushTooltipDemo {
10111023
})
10121024
class DynamicTooltipsDemo {
10131025
tooltips: Array<string> = [];
1026+
}
10141027

1015-
constructor(private _elementRef: ElementRef<HTMLElement>) {}
1016-
1017-
getButtons() {
1018-
return this._elementRef.nativeElement.querySelectorAll('button');
1019-
}
1028+
@Component({
1029+
template: `<button [matTooltip]="message" [attr.aria-label]="message">Click me</button>`,
1030+
})
1031+
class DataBoundAriaLabelTooltip {
1032+
message = 'Hello there';
10201033
}
10211034

1035+
10221036
@Component({
10231037
template: `
10241038
<input

src/material/tooltip/tooltip.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,15 @@ export class MatTooltip implements OnDestroy, OnInit {
180180
this.hide(0);
181181
} else {
182182
this._updateTooltipMessage();
183-
this._ariaDescriber.describe(this._elementRef.nativeElement, this.message);
183+
this._ngZone.runOutsideAngular(() => {
184+
// The `AriaDescriber` has some functionality that avoids adding a description if it's the
185+
// same as the `aria-label` of an element, however we can't know whether the tooltip trigger
186+
// has a data-bound `aria-label` or when it'll be set for the first time. We can avoid the
187+
// issue by deferring the description by a tick so Angular has time to set the `aria-label`.
188+
Promise.resolve().then(() => {
189+
this._ariaDescriber.describe(this._elementRef.nativeElement, this.message);
190+
});
191+
});
184192
}
185193
}
186194

0 commit comments

Comments
 (0)