Skip to content

Commit fd416a3

Browse files
authoredAug 1, 2024··
fix(material/tooltip): remove aria-describedby when disabled (#29520)
Fixes that we were setting an `aria-describedby` even if the tooltip won't show up because it's disabled. Fixes #29501.
1 parent 799766e commit fd416a3

File tree

2 files changed

+74
-35
lines changed

2 files changed

+74
-35
lines changed
 

‎src/material/tooltip/tooltip.spec.ts

+34-19
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,14 @@ describe('MDC-based MatTooltip', () => {
9090
let buttonElement: HTMLButtonElement;
9191
let tooltipDirective: MatTooltip;
9292

93-
beforeEach(() => {
93+
beforeEach(fakeAsync(() => {
9494
fixture = TestBed.createComponent(BasicTooltipDemo);
9595
fixture.detectChanges();
96+
tick();
9697
buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
97-
buttonElement = <HTMLButtonElement>buttonDebugElement.nativeElement;
98+
buttonElement = buttonDebugElement.nativeElement;
9899
tooltipDirective = buttonDebugElement.injector.get<MatTooltip>(MatTooltip);
99-
});
100+
}));
100101

101102
it('should show and hide the tooltip', fakeAsync(() => {
102103
assertTooltipInstance(tooltipDirective, false);
@@ -616,7 +617,7 @@ describe('MDC-based MatTooltip', () => {
616617
expect(overlayContainerElement.textContent).toBe('');
617618
}));
618619

619-
it('should have an aria-described element with the tooltip message', fakeAsync(() => {
620+
it('should have an aria-describedby element with the tooltip message', fakeAsync(() => {
620621
const dynamicTooltipsDemoFixture = TestBed.createComponent(DynamicTooltipsDemo);
621622
const dynamicTooltipsComponent = dynamicTooltipsDemoFixture.componentInstance;
622623

@@ -632,18 +633,30 @@ describe('MDC-based MatTooltip', () => {
632633
expect(document.querySelector(`#${secondButtonAria}`)!.textContent).toBe('Tooltip Two');
633634
}));
634635

635-
it(
636-
'should not add an ARIA description for elements that have the same text as a' +
637-
'data-bound aria-label',
638-
fakeAsync(() => {
639-
const ariaLabelFixture = TestBed.createComponent(DataBoundAriaLabelTooltip);
640-
ariaLabelFixture.detectChanges();
641-
tick();
636+
it('should not add an ARIA description for elements that have the same text as a data-bound aria-label', fakeAsync(() => {
637+
const ariaLabelFixture = TestBed.createComponent(DataBoundAriaLabelTooltip);
638+
ariaLabelFixture.detectChanges();
639+
tick();
640+
641+
const button = ariaLabelFixture.nativeElement.querySelector('button');
642+
expect(button.getAttribute('aria-describedby')).toBeFalsy();
643+
}));
644+
645+
it('should toggle aria-describedby depending on whether the tooltip is disabled', fakeAsync(() => {
646+
expect(buttonElement.getAttribute('aria-describedby')).toBeTruthy();
642647

643-
const button = ariaLabelFixture.nativeElement.querySelector('button');
644-
expect(button.getAttribute('aria-describedby')).toBeFalsy();
645-
}),
646-
);
648+
fixture.componentInstance.tooltipDisabled = true;
649+
fixture.changeDetectorRef.markForCheck();
650+
fixture.detectChanges();
651+
tick();
652+
expect(buttonElement.hasAttribute('aria-describedby')).toBe(false);
653+
654+
fixture.componentInstance.tooltipDisabled = false;
655+
fixture.changeDetectorRef.markForCheck();
656+
fixture.detectChanges();
657+
tick();
658+
expect(buttonElement.getAttribute('aria-describedby')).toBeTruthy();
659+
}));
647660

648661
it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => {
649662
tooltipDirective.show();
@@ -1585,17 +1598,19 @@ describe('MDC-based MatTooltip', () => {
15851598
<button #button
15861599
[matTooltip]="message"
15871600
[matTooltipPosition]="position"
1588-
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass }"
1589-
[matTooltipTouchGestures]="touchGestures">Button</button>
1601+
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass}"
1602+
[matTooltipTouchGestures]="touchGestures"
1603+
[matTooltipDisabled]="tooltipDisabled">Button</button>
15901604
}`,
15911605
standalone: true,
15921606
imports: [MatTooltipModule, OverlayModule],
15931607
})
15941608
class BasicTooltipDemo {
1595-
position: string = 'below';
1609+
position = 'below';
15961610
message: any = initialTooltipMessage;
1597-
showButton: boolean = true;
1611+
showButton = true;
15981612
showTooltipClass = false;
1613+
tooltipDisabled = false;
15991614
touchGestures: TooltipTouchGestures = 'auto';
16001615
@ViewChild(MatTooltip) tooltip: MatTooltip;
16011616
@ViewChild('button') button: ElementRef<HTMLButtonElement>;

‎src/material/tooltip/tooltip.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
205205
private _viewportMargin = 8;
206206
private _currentPosition: TooltipPosition;
207207
private readonly _cssClassPrefix: string = 'mat-mdc';
208+
private _ariaDescriptionPending: boolean;
208209

209210
/** Allows the user to define the position of the tooltip relative to the parent element */
210211
@Input('matTooltipPosition')
@@ -246,13 +247,19 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
246247
}
247248

248249
set disabled(value: BooleanInput) {
249-
this._disabled = coerceBooleanProperty(value);
250+
const isDisabled = coerceBooleanProperty(value);
250251

251-
// If tooltip is disabled, hide immediately.
252-
if (this._disabled) {
253-
this.hide(0);
254-
} else {
255-
this._setupPointerEnterEventsIfNeeded();
252+
if (this._disabled !== isDisabled) {
253+
this._disabled = isDisabled;
254+
255+
// If tooltip is disabled, hide immediately.
256+
if (isDisabled) {
257+
this.hide(0);
258+
} else {
259+
this._setupPointerEnterEventsIfNeeded();
260+
}
261+
262+
this._syncAriaDescription(this.message);
256263
}
257264
}
258265

@@ -307,7 +314,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
307314
}
308315

309316
set message(value: string | null | undefined) {
310-
this._ariaDescriber.removeDescription(this._elementRef.nativeElement, this._message, 'tooltip');
317+
const oldMessage = this._message;
311318

312319
// If the message is not a string (e.g. number), convert it to a string and trim it.
313320
// Must convert with `String(value)`, not `${value}`, otherwise Closure Compiler optimises
@@ -319,16 +326,9 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
319326
} else {
320327
this._setupPointerEnterEventsIfNeeded();
321328
this._updateTooltipMessage();
322-
this._ngZone.runOutsideAngular(() => {
323-
// The `AriaDescriber` has some functionality that avoids adding a description if it's the
324-
// same as the `aria-label` of an element, however we can't know whether the tooltip trigger
325-
// has a data-bound `aria-label` or when it'll be set for the first time. We can avoid the
326-
// issue by deferring the description by a tick so Angular has time to set the `aria-label`.
327-
Promise.resolve().then(() => {
328-
this._ariaDescriber.describe(this._elementRef.nativeElement, this.message, 'tooltip');
329-
});
330-
});
331329
}
330+
331+
this._syncAriaDescription(oldMessage);
332332
}
333333

334334
private _message = '';
@@ -904,6 +904,30 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
904904
(style as any).webkitTapHighlightColor = 'transparent';
905905
}
906906
}
907+
908+
/** Updates the tooltip's ARIA description based on it current state. */
909+
private _syncAriaDescription(oldMessage: string): void {
910+
if (this._ariaDescriptionPending) {
911+
return;
912+
}
913+
914+
this._ariaDescriptionPending = true;
915+
this._ariaDescriber.removeDescription(this._elementRef.nativeElement, oldMessage, 'tooltip');
916+
917+
this._ngZone.runOutsideAngular(() => {
918+
// The `AriaDescriber` has some functionality that avoids adding a description if it's the
919+
// same as the `aria-label` of an element, however we can't know whether the tooltip trigger
920+
// has a data-bound `aria-label` or when it'll be set for the first time. We can avoid the
921+
// issue by deferring the description by a tick so Angular has time to set the `aria-label`.
922+
Promise.resolve().then(() => {
923+
this._ariaDescriptionPending = false;
924+
925+
if (this.message && !this.disabled) {
926+
this._ariaDescriber.describe(this._elementRef.nativeElement, this.message, 'tooltip');
927+
}
928+
});
929+
});
930+
}
907931
}
908932

909933
/**

0 commit comments

Comments
 (0)
Please sign in to comment.