diff --git a/src/material/badge/badge.spec.ts b/src/material/badge/badge.spec.ts index 7c5a6ca9da22..3429ebcf2055 100644 --- a/src/material/badge/badge.spec.ts +++ b/src/material/badge/badge.spec.ts @@ -243,16 +243,16 @@ describe('MatBadge', () => { badgeHostNativeElement = badgeHostDebugElement.nativeElement; }); - it('should insert the description inline after the host', () => { + it('should insert description as next sibling for host', () => { testComponent.description.set('Extra info'); fixture.detectChanges(); - const inlineDescription = badgeHostNativeElement.querySelector('.cdk-visually-hidden')!; + const inlineDescription = badgeHostNativeElement.nextSibling!; expect(inlineDescription) .withContext('A visually hidden description element should exist') .toBeDefined(); expect(inlineDescription.textContent) - .withContext('The badge host next sibling should contain its description') + .withContext('The badge next sibling should contain its description') .toBe('Extra info'); testComponent.description.set('Different info'); @@ -263,7 +263,7 @@ describe('MatBadge', () => { .toBe('Different info'); }); - it('should not apply aria-describedby for non-interactive hosts', () => { + it('should not apply aria-describedby for non-interactive host', () => { testComponent.description.set('Extra info'); fixture.detectChanges(); @@ -271,6 +271,26 @@ describe('MatBadge', () => { .withContext('Non-interactive hosts should not have aria-describedby') .toBeFalse(); }); + + it('should not insert description as next sibling if description is not provided', () => { + fixture.detectChanges(); + + expect(badgeHostNativeElement.nextSibling).toBeFalsy(); + }); + + it('should not create multiple description elements if description changes', () => { + testComponent.description.set('one'); + fixture.detectChanges(); + + let siblings = fixture.nativeElement.querySelectorAll('.cdk-visually-hidden'); + expect(siblings.length).toBe(1); + + testComponent.description.set('two'); + fixture.detectChanges(); + + siblings = fixture.nativeElement.querySelectorAll('.cdk-visually-hidden'); + expect(siblings.length).toBe(1); + }); }); }); diff --git a/src/material/badge/badge.ts b/src/material/badge/badge.ts index 806f2146d192..89c55c0575cd 100644 --- a/src/material/badge/badge.ts +++ b/src/material/badge/badge.ts @@ -164,20 +164,6 @@ export class MatBadge implements OnInit, OnDestroy { if (nativeElement.nodeType !== nativeElement.ELEMENT_NODE) { throw Error('matBadge must be attached to an element node.'); } - - // Heads-up for developers to avoid putting matBadge on - // as it is aria-hidden by default docs mention this at: - // https://material.angular.io/components/badge/overview#accessibility - if ( - nativeElement.tagName.toLowerCase() === 'mat-icon' && - nativeElement.getAttribute('aria-hidden') === 'true' - ) { - console.warn( - `Detected a matBadge on an "aria-hidden" "". ` + - `Consider setting aria-hidden="false" in order to surface the information assistive technology.` + - `\n${nativeElement.outerHTML}`, - ); - } } } @@ -310,6 +296,9 @@ export class MatBadge implements OnInit, OnDestroy { } private _updateInlineDescription() { + // We do not need to need empty description element for non interactive elements. + if (!this.description) return; + // Create the inline description element if it doesn't exist if (!this._inlineBadgeDescription) { this._inlineBadgeDescription = this._document.createElement('span'); @@ -317,7 +306,13 @@ export class MatBadge implements OnInit, OnDestroy { } this._inlineBadgeDescription.textContent = this.description; - this._badgeElement?.appendChild(this._inlineBadgeDescription); + // We want to add the inline description element right after the not interactive element that + // badge is on therefore we can't use appendChild here as they will keep getting stacked in + // last. + this._elementRef.nativeElement.parentNode?.insertBefore( + this._inlineBadgeDescription, + this._elementRef.nativeElement.nextSibling, + ); } private _removeInlineDescription() {