Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/tooltip): remove aria-describedby when disabled #29520

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading