Skip to content

Commit

Permalink
fix(material/tooltip): remove aria-describedby when disabled (#29520)
Browse files Browse the repository at this point in the history
Fixes that we were setting an `aria-describedby` even if the tooltip won't show up because it's disabled.

Fixes #29501.
  • Loading branch information
crisbeto committed Aug 1, 2024
1 parent 799766e commit fd416a3
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 35 deletions.
53 changes: 34 additions & 19 deletions src/material/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ describe('MDC-based MatTooltip', () => {
let buttonElement: HTMLButtonElement;
let tooltipDirective: MatTooltip;

beforeEach(() => {
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(BasicTooltipDemo);
fixture.detectChanges();
tick();
buttonDebugElement = fixture.debugElement.query(By.css('button'))!;
buttonElement = <HTMLButtonElement>buttonDebugElement.nativeElement;
buttonElement = buttonDebugElement.nativeElement;
tooltipDirective = buttonDebugElement.injector.get<MatTooltip>(MatTooltip);
});
}));

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

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

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

it(
'should not add an ARIA description for elements that have the same text as a' +
'data-bound aria-label',
fakeAsync(() => {
const ariaLabelFixture = TestBed.createComponent(DataBoundAriaLabelTooltip);
ariaLabelFixture.detectChanges();
tick();
it('should not add an ARIA description for elements that have the same text as a data-bound aria-label', fakeAsync(() => {
const ariaLabelFixture = TestBed.createComponent(DataBoundAriaLabelTooltip);
ariaLabelFixture.detectChanges();
tick();

const button = ariaLabelFixture.nativeElement.querySelector('button');
expect(button.getAttribute('aria-describedby')).toBeFalsy();
}));

it('should toggle aria-describedby depending on whether the tooltip is disabled', fakeAsync(() => {
expect(buttonElement.getAttribute('aria-describedby')).toBeTruthy();

const button = ariaLabelFixture.nativeElement.querySelector('button');
expect(button.getAttribute('aria-describedby')).toBeFalsy();
}),
);
fixture.componentInstance.tooltipDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(buttonElement.hasAttribute('aria-describedby')).toBe(false);

fixture.componentInstance.tooltipDisabled = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(buttonElement.getAttribute('aria-describedby')).toBeTruthy();
}));

it('should not try to dispose the tooltip when destroyed and done hiding', fakeAsync(() => {
tooltipDirective.show();
Expand Down Expand Up @@ -1585,17 +1598,19 @@ describe('MDC-based MatTooltip', () => {
<button #button
[matTooltip]="message"
[matTooltipPosition]="position"
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass }"
[matTooltipTouchGestures]="touchGestures">Button</button>
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass}"
[matTooltipTouchGestures]="touchGestures"
[matTooltipDisabled]="tooltipDisabled">Button</button>
}`,
standalone: true,
imports: [MatTooltipModule, OverlayModule],
})
class BasicTooltipDemo {
position: string = 'below';
position = 'below';
message: any = initialTooltipMessage;
showButton: boolean = true;
showButton = true;
showTooltipClass = false;
tooltipDisabled = false;
touchGestures: TooltipTouchGestures = 'auto';
@ViewChild(MatTooltip) tooltip: MatTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
Expand Down
56 changes: 40 additions & 16 deletions src/material/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
private _viewportMargin = 8;
private _currentPosition: TooltipPosition;
private readonly _cssClassPrefix: string = 'mat-mdc';
private _ariaDescriptionPending: boolean;

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

set disabled(value: BooleanInput) {
this._disabled = coerceBooleanProperty(value);
const isDisabled = coerceBooleanProperty(value);

// If tooltip is disabled, hide immediately.
if (this._disabled) {
this.hide(0);
} else {
this._setupPointerEnterEventsIfNeeded();
if (this._disabled !== isDisabled) {
this._disabled = isDisabled;

// If tooltip is disabled, hide immediately.
if (isDisabled) {
this.hide(0);
} else {
this._setupPointerEnterEventsIfNeeded();
}

this._syncAriaDescription(this.message);
}
}

Expand Down Expand Up @@ -307,7 +314,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
}

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

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

this._syncAriaDescription(oldMessage);
}

private _message = '';
Expand Down Expand Up @@ -904,6 +904,30 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
(style as any).webkitTapHighlightColor = 'transparent';
}
}

/** Updates the tooltip's ARIA description based on it current state. */
private _syncAriaDescription(oldMessage: string): void {
if (this._ariaDescriptionPending) {
return;
}

this._ariaDescriptionPending = true;
this._ariaDescriber.removeDescription(this._elementRef.nativeElement, oldMessage, 'tooltip');

this._ngZone.runOutsideAngular(() => {
// The `AriaDescriber` has some functionality that avoids adding a description if it's the
// same as the `aria-label` of an element, however we can't know whether the tooltip trigger
// has a data-bound `aria-label` or when it'll be set for the first time. We can avoid the
// issue by deferring the description by a tick so Angular has time to set the `aria-label`.
Promise.resolve().then(() => {
this._ariaDescriptionPending = false;

if (this.message && !this.disabled) {
this._ariaDescriber.describe(this._elementRef.nativeElement, this.message, 'tooltip');
}
});
});
}
}

/**
Expand Down

0 comments on commit fd416a3

Please sign in to comment.