From 9e90bdb5a6ca8ac80565e9296ebb27f1b71e49f3 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Fri, 14 Jun 2024 13:40:42 -0700 Subject: [PATCH] fix(multiple): include APP_ID in element IDs Many components generate element IDs, which are used primarily for accessible labeling. The IDs all use an incrementing number concatenated with some stirng specific to that component. This change add Angular's `APP_ID` into these IDs in order to avoid ID collisions in cases where there are multiple instances of Angular running on the same page. --- src/cdk-experimental/combobox/combobox-popup.ts | 7 +++++-- .../table-scroll-container.ts | 17 +++++++++++++++-- src/cdk/a11y/live-announcer/live-announcer.ts | 7 ++++++- src/cdk/accordion/accordion-item.ts | 6 +++++- src/cdk/accordion/accordion.ts | 7 ++++++- src/cdk/dialog/dialog.ts | 7 ++++++- src/cdk/drag-drop/directives/drop-list.ts | 7 ++++++- src/cdk/listbox/listbox.spec.ts | 4 ++-- src/cdk/listbox/listbox.ts | 11 +++++++++-- src/cdk/menu/menu-base.ts | 6 +++++- src/cdk/overlay/overlay.ts | 7 ++++++- src/cdk/stepper/stepper.ts | 7 ++++++- src/material/autocomplete/autocomplete.ts | 7 ++++++- src/material/badge/badge.ts | 6 +++++- src/material/button-toggle/button-toggle.ts | 7 ++++++- src/material/checkbox/checkbox.spec.ts | 6 +++--- src/material/checkbox/checkbox.ts | 7 ++++++- src/material/chips/chip-input.ts | 7 ++++++- src/material/chips/chip.ts | 6 +++++- src/material/core/option/optgroup.ts | 7 ++++++- src/material/core/option/option.ts | 7 ++++++- src/material/datepicker/calendar-body.ts | 6 +++++- src/material/datepicker/calendar-header.spec.ts | 2 +- src/material/datepicker/calendar.ts | 7 ++++++- src/material/datepicker/date-range-input.ts | 7 ++++++- src/material/datepicker/datepicker-base.ts | 6 +++++- .../dialog/dialog-content-directives.ts | 7 ++++++- src/material/dialog/dialog.ts | 6 +++++- .../dialog/testing/dialog-harness.spec.ts | 2 +- src/material/expansion/expansion-panel.ts | 8 ++++---- src/material/form-field/directives/error.ts | 15 +++++++++++++-- src/material/form-field/directives/hint.ts | 7 +++++-- src/material/form-field/form-field.ts | 8 ++++++-- src/material/input/input.spec.ts | 2 +- src/material/input/input.ts | 7 ++++++- .../input/testing/input-harness.spec.ts | 6 +++--- src/material/menu/menu.ts | 6 +++++- src/material/paginator/paginator.ts | 7 ++++++- src/material/radio/radio.ts | 11 +++++++++-- src/material/select/select.spec.ts | 2 +- src/material/select/select.ts | 8 ++++++-- src/material/slide-toggle/slide-toggle.spec.ts | 4 ++-- src/material/slide-toggle/slide-toggle.ts | 7 ++++++- src/material/snack-bar/snack-bar-container.ts | 6 +++++- src/material/tabs/tab-group.ts | 6 +++++- src/material/tabs/tab-nav-bar/tab-nav-bar.ts | 12 ++++++++++-- 46 files changed, 254 insertions(+), 64 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-popup.ts b/src/cdk-experimental/combobox/combobox-popup.ts index 90aa3e99d069..f477ac98495b 100644 --- a/src/cdk-experimental/combobox/combobox-popup.ts +++ b/src/cdk-experimental/combobox/combobox-popup.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Inject, Input, OnInit} from '@angular/core'; +import {APP_ID, Directive, ElementRef, inject, Inject, Input, OnInit} from '@angular/core'; import {AriaHasPopupValue, CDK_COMBOBOX, CdkCombobox} from './combobox'; let nextId = 0; @@ -24,6 +24,9 @@ let nextId = 0; standalone: true, }) export class CdkComboboxPopup implements OnInit { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + @Input() get role(): AriaHasPopupValue { return this._role; @@ -42,7 +45,7 @@ export class CdkComboboxPopup implements OnInit { } private _firstFocusElement: HTMLElement; - @Input() id = `cdk-combobox-popup-${nextId++}`; + @Input() id = `cdk-combobox-popup-${this._appId}${nextId++}`; constructor( private readonly _elementRef: ElementRef, diff --git a/src/cdk-experimental/table-scroll-container/table-scroll-container.ts b/src/cdk-experimental/table-scroll-container/table-scroll-container.ts index 89d164f57956..ce5ac0f999ef 100644 --- a/src/cdk-experimental/table-scroll-container/table-scroll-container.ts +++ b/src/cdk-experimental/table-scroll-container/table-scroll-container.ts @@ -6,7 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {CSP_NONCE, Directive, ElementRef, Inject, OnDestroy, OnInit, Optional} from '@angular/core'; +import { + APP_ID, + CSP_NONCE, + Directive, + ElementRef, + inject, + Inject, + OnDestroy, + OnInit, + Optional, +} from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {Directionality} from '@angular/cdk/bidi'; import {_getShadowRoot} from '@angular/cdk/platform'; @@ -39,6 +49,9 @@ let nextId = 0; standalone: true, }) export class CdkTableScrollContainer implements StickyPositioningListener, OnDestroy, OnInit { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private readonly _uniqueClassName: string; private _styleRoot!: Node; private _styleElement?: HTMLStyleElement; @@ -55,7 +68,7 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes @Optional() private readonly _directionality?: Directionality, @Optional() @Inject(CSP_NONCE) private readonly _nonce?: string | null, ) { - this._uniqueClassName = `cdk-table-scroll-container-${++nextId}`; + this._uniqueClassName = `cdk-table-scroll-container-${this._appId}${++nextId}`; _elementRef.nativeElement.classList.add(this._uniqueClassName); } diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index b9ec9e6fe652..0c45acfe7eee 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -9,8 +9,10 @@ import {ContentObserver} from '@angular/cdk/observers'; import {DOCUMENT} from '@angular/common'; import { + APP_ID, Directive, ElementRef, + inject, Inject, Injectable, Input, @@ -30,6 +32,9 @@ let uniqueIds = 0; @Injectable({providedIn: 'root'}) export class LiveAnnouncer implements OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _liveElement: HTMLElement; private _document: Document; private _previousTimeout: number; @@ -179,7 +184,7 @@ export class LiveAnnouncer implements OnDestroy { liveEl.setAttribute('aria-atomic', 'true'); liveEl.setAttribute('aria-live', 'polite'); - liveEl.id = `cdk-live-announcer-${uniqueIds++}`; + liveEl.id = `cdk-live-announcer-${this._appId}${uniqueIds++}`; this._document.body.appendChild(liveEl); diff --git a/src/cdk/accordion/accordion-item.ts b/src/cdk/accordion/accordion-item.ts index 64bb79bfd60b..9e89628bb15e 100644 --- a/src/cdk/accordion/accordion-item.ts +++ b/src/cdk/accordion/accordion-item.ts @@ -17,6 +17,8 @@ import { SkipSelf, Inject, booleanAttribute, + inject, + APP_ID, } from '@angular/core'; import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import {CDK_ACCORDION, CdkAccordion} from './accordion'; @@ -40,6 +42,8 @@ let nextId = 0; standalone: true, }) export class CdkAccordionItem implements OnDestroy { + protected _appId = inject(APP_ID); + /** Subscription to openAll/closeAll events. */ private _openCloseAllSubscription = Subscription.EMPTY; /** Event emitted every time the AccordionItem is closed. */ @@ -57,7 +61,7 @@ export class CdkAccordionItem implements OnDestroy { @Output() readonly expandedChange: EventEmitter = new EventEmitter(); /** The unique AccordionItem id. */ - readonly id: string = `cdk-accordion-child-${nextId++}`; + readonly id: string = `cdk-accordion-child-${this._appId}${nextId++}`; /** Whether the AccordionItem is expanded. */ @Input({transform: booleanAttribute}) diff --git a/src/cdk/accordion/accordion.ts b/src/cdk/accordion/accordion.ts index 294e7ba6fdfc..ec7ed87be767 100644 --- a/src/cdk/accordion/accordion.ts +++ b/src/cdk/accordion/accordion.ts @@ -14,6 +14,8 @@ import { OnDestroy, SimpleChanges, booleanAttribute, + inject, + APP_ID, } from '@angular/core'; import {Subject} from 'rxjs'; @@ -37,6 +39,9 @@ export const CDK_ACCORDION = new InjectionToken('CdkAccordion'); standalone: true, }) export class CdkAccordion implements OnDestroy, OnChanges { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Emits when the state of the accordion changes */ readonly _stateChanges = new Subject(); @@ -44,7 +49,7 @@ export class CdkAccordion implements OnDestroy, OnChanges { readonly _openCloseAllActions: Subject = new Subject(); /** A readonly id value to use for unique selection coordination. */ - readonly id: string = `cdk-accordion-${nextId++}`; + readonly id: string = `cdk-accordion-${this._appId}${nextId++}`; /** Whether the accordion should allow multiple expanded accordion items simultaneously. */ @Input({transform: booleanAttribute}) multi: boolean = false; diff --git a/src/cdk/dialog/dialog.ts b/src/cdk/dialog/dialog.ts index 25c1aa51a825..8cf64fdef439 100644 --- a/src/cdk/dialog/dialog.ts +++ b/src/cdk/dialog/dialog.ts @@ -17,6 +17,8 @@ import { Optional, SkipSelf, ComponentRef, + APP_ID, + inject, } from '@angular/core'; import {BasePortalOutlet, ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; import {of as observableOf, Observable, Subject, defer} from 'rxjs'; @@ -41,6 +43,9 @@ let uniqueId = 0; @Injectable({providedIn: 'root'}) export class Dialog implements OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _openDialogsAtThisLevel: DialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject(); @@ -114,7 +119,7 @@ export class Dialog implements OnDestroy { DialogRef >; config = {...defaults, ...config}; - config.id = config.id || `cdk-dialog-${uniqueId++}`; + config.id = config.id || `cdk-dialog-${this._appId}${uniqueId++}`; if ( config.id && diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index 41e0d16fb1c4..441168c31ed9 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -19,6 +19,8 @@ import { SkipSelf, Inject, booleanAttribute, + inject, + APP_ID, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; @@ -55,6 +57,9 @@ let _uniqueIdCounter = 0; }, }) export class CdkDropList implements OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Emits when the list has been destroyed. */ private readonly _destroyed = new Subject(); @@ -85,7 +90,7 @@ export class CdkDropList implements OnDestroy { * Unique ID for the drop zone. Can be used as a reference * in the `connectedTo` of another `CdkDropList`. */ - @Input() id: string = `cdk-drop-list-${_uniqueIdCounter++}`; + @Input() id: string = `cdk-drop-list-${this._appId}${_uniqueIdCounter++}`; /** Locks the position of the draggable elements inside the container along the specified axis. */ @Input('cdkDropListLockAxis') lockAxis: DragAxis; diff --git a/src/cdk/listbox/listbox.spec.ts b/src/cdk/listbox/listbox.spec.ts index e49c7951d14a..9cfba4985b27 100644 --- a/src/cdk/listbox/listbox.spec.ts +++ b/src/cdk/listbox/listbox.spec.ts @@ -46,10 +46,10 @@ describe('CdkOption and CdkListbox', () => { expect(optionIds.size).toBe(options.length); for (let i = 0; i < options.length; i++) { expect(options[i].id).toBe(optionEls[i].id); - expect(options[i].id).toMatch(/cdk-option-\d+/); + expect(options[i].id).toMatch(/cdk-option-\w+/); } expect(listbox.id).toEqual(listboxEl.id); - expect(listbox.id).toMatch(/cdk-listbox-\d+/); + expect(listbox.id).toMatch(/cdk-listbox-\w+/); }); it('should not overwrite user given ids', () => { diff --git a/src/cdk/listbox/listbox.ts b/src/cdk/listbox/listbox.ts index 213d1f31adef..f44081484f1b 100644 --- a/src/cdk/listbox/listbox.ts +++ b/src/cdk/listbox/listbox.ts @@ -25,6 +25,7 @@ import { import {Platform} from '@angular/cdk/platform'; import { AfterContentInit, + APP_ID, booleanAttribute, ChangeDetectorRef, ContentChildren, @@ -96,6 +97,9 @@ class ListboxSelectionModel extends SelectionModel { }, }) export class CdkOption implements ListKeyManagerOption, Highlightable, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** The id of the option's host element. */ @Input() get id() { @@ -105,7 +109,7 @@ export class CdkOption implements ListKeyManagerOption, Highlightab this._id = value; } private _id: string; - private _generatedId = `cdk-option-${nextId++}`; + private _generatedId = `cdk-option-${this._appId}${nextId++}`; /** The value of this option. */ @Input('cdkOption') value: T; @@ -249,6 +253,9 @@ export class CdkOption implements ListKeyManagerOption, Highlightab ], }) export class CdkListbox implements AfterContentInit, OnDestroy, ControlValueAccessor { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** The id of the option's host element. */ @Input() get id() { @@ -258,7 +265,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, Con this._id = value; } private _id: string; - private _generatedId = `cdk-listbox-${nextId++}`; + private _generatedId = `cdk-listbox-${this._appId}${nextId++}`; /** The tabindex to use when the listbox is enabled. */ @Input('tabindex') diff --git a/src/cdk/menu/menu-base.ts b/src/cdk/menu/menu-base.ts index 031e323abec3..6e2468a1781c 100644 --- a/src/cdk/menu/menu-base.ts +++ b/src/cdk/menu/menu-base.ts @@ -20,6 +20,7 @@ import { computed, inject, signal, + APP_ID, } from '@angular/core'; import {Subject, merge} from 'rxjs'; import {mapTo, mergeAll, mergeMap, startWith, switchMap, takeUntil} from 'rxjs/operators'; @@ -55,6 +56,9 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterContentInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** The menu's native DOM host element. */ readonly nativeElement: HTMLElement = inject(ElementRef).nativeElement; @@ -71,7 +75,7 @@ export abstract class CdkMenuBase protected readonly dir = inject(Directionality, {optional: true}); /** The id of the menu's host element. */ - @Input() id = `cdk-menu-${nextId++}`; + @Input() id = `cdk-menu-${this._appId}${nextId++}`; /** All child MenuItem elements nested in this Menu. */ @ContentChildren(CdkMenuItem, {descendants: true}) diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index be660242b437..bc2f82ffe4fb 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -19,6 +19,8 @@ import { ANIMATION_MODULE_TYPE, Optional, EnvironmentInjector, + APP_ID, + inject, } from '@angular/core'; import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher'; import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher'; @@ -44,6 +46,9 @@ let nextUniqueId = 0; */ @Injectable({providedIn: 'root'}) export class Overlay { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _appRef: ApplicationRef; constructor( @@ -106,7 +111,7 @@ export class Overlay { private _createPaneElement(host: HTMLElement): HTMLElement { const pane = this._document.createElement('div'); - pane.id = `cdk-overlay-${nextUniqueId++}`; + pane.id = `cdk-overlay-${this._appId}${nextUniqueId++}`; pane.classList.add('cdk-overlay-pane'); host.appendChild(pane); diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 1a7d7851965e..83f50012b1d7 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -34,6 +34,8 @@ import { AfterContentInit, booleanAttribute, numberAttribute, + APP_ID, + inject, } from '@angular/core'; import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {Observable, of as observableOf, Subject} from 'rxjs'; @@ -236,6 +238,9 @@ export class CdkStep implements OnChanges { standalone: true, }) export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Emits when the component is destroyed. */ protected readonly _destroyed = new Subject(); @@ -420,7 +425,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { /** Returns unique id for each step content element. */ _getStepContentId(i: number): string { - return `cdk-step-content-${this._groupId}-${i}`; + return `cdk-step-content-${this._appId}${this._groupId}-${i}`; } /** Marks the component to be change detected. */ diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts index bfa8227258f5..66c49974f4f0 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -24,6 +24,8 @@ import { ViewChild, ViewEncapsulation, booleanAttribute, + inject, + APP_ID, } from '@angular/core'; import {AnimationEvent} from '@angular/animations'; import { @@ -119,6 +121,9 @@ export function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): MatAutocompleteDefau standalone: true, }) export class MatAutocomplete implements AfterContentInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _activeOptionChanges = Subscription.EMPTY; /** Emits when the panel animation is done. Null if the panel doesn't animate. */ @@ -244,7 +249,7 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy { } /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */ - id: string = `mat-autocomplete-${_uniqueAutocompleteIdCounter++}`; + id: string = `mat-autocomplete-${this._appId}${_uniqueAutocompleteIdCounter++}`; /** * Tells any descendant `mat-optgroup` to use the inert a11y pattern. diff --git a/src/material/badge/badge.ts b/src/material/badge/badge.ts index 4b8676981409..57de70e443d6 100644 --- a/src/material/badge/badge.ts +++ b/src/material/badge/badge.ts @@ -27,6 +27,7 @@ import { Renderer2, ViewEncapsulation, ANIMATION_MODULE_TYPE, + APP_ID, } from '@angular/core'; import {ThemePalette} from '@angular/material/core'; @@ -83,6 +84,9 @@ export class _MatBadgeStyleLoader {} standalone: true, }) export class MatBadge implements OnInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** * The color of the badge. Can be `primary`, `accent`, or `warn`. * Not recommended in M3, for more information see https://material.angular.io/guide/material-2-theming#optional-add-backwards-compatibility-styles-for-color-variants. @@ -256,7 +260,7 @@ export class MatBadge implements OnInit, OnDestroy { const badgeElement = this._renderer.createElement('span'); const activeClass = 'mat-badge-active'; - badgeElement.setAttribute('id', `mat-badge-content-${this._id}`); + badgeElement.setAttribute('id', `mat-badge-content-${this._appId}${this._id}`); // The badge is aria-hidden because we don't want it to appear in the page's navigation // flow. Instead, we use the badge to describe the decorated element with aria-describedby. diff --git a/src/material/button-toggle/button-toggle.ts b/src/material/button-toggle/button-toggle.ts index 42c0d7175264..989fcf0b7fbb 100644 --- a/src/material/button-toggle/button-toggle.ts +++ b/src/material/button-toggle/button-toggle.ts @@ -32,6 +32,8 @@ import { Inject, AfterViewInit, booleanAttribute, + inject, + APP_ID, } from '@angular/core'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; @@ -134,6 +136,9 @@ export class MatButtonToggleChange { standalone: true, }) export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, AfterContentInit { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _multiple = false; private _disabled = false; private _selectionModel: SelectionModel; @@ -175,7 +180,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After this._name = value; this._markButtonsForCheck(); } - private _name = `mat-button-toggle-group-${uniqueIdCounter++}`; + private _name = `mat-button-toggle-group-${this._appId}${uniqueIdCounter++}`; /** Whether the toggle group is vertical. */ @Input({transform: booleanAttribute}) vertical: boolean; diff --git a/src/material/checkbox/checkbox.spec.ts b/src/material/checkbox/checkbox.spec.ts index d7efa1223b32..386bc3e12d28 100644 --- a/src/material/checkbox/checkbox.spec.ts +++ b/src/material/checkbox/checkbox.spec.ts @@ -302,7 +302,7 @@ describe('MDC-based MatCheckbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(checkboxInstance.inputId).toMatch(/mat-mdc-checkbox-\d+/); + expect(checkboxInstance.inputId).toMatch(/mat-mdc-checkbox-\w+/); expect(inputElement.id).toBe(checkboxInstance.inputId); })); @@ -846,8 +846,8 @@ describe('MDC-based MatCheckbox', () => { .queryAll(By.directive(MatCheckbox)) .map(debugElement => debugElement.nativeElement.querySelector('input').id); - expect(firstId).toMatch(/mat-mdc-checkbox-\d+-input/); - expect(secondId).toMatch(/mat-mdc-checkbox-\d+-input/); + expect(firstId).toMatch(/mat-mdc-checkbox-\w+-input/); + expect(secondId).toMatch(/mat-mdc-checkbox-\w+-input/); expect(firstId).not.toEqual(secondId); })); }); diff --git a/src/material/checkbox/checkbox.ts b/src/material/checkbox/checkbox.ts index 58a58877252c..a2036d073d3c 100644 --- a/src/material/checkbox/checkbox.ts +++ b/src/material/checkbox/checkbox.ts @@ -28,6 +28,8 @@ import { booleanAttribute, forwardRef, numberAttribute, + APP_ID, + inject, } from '@angular/core'; import { AbstractControl, @@ -117,6 +119,9 @@ const defaults = MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY(); export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccessor, Validator, FocusableOption { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Focuses the checkbox. */ focus() { this._inputElement.nativeElement.focus(); @@ -240,7 +245,7 @@ export class MatCheckbox this._options = this._options || defaults; this.color = this._options.color || defaults.color; this.tabIndex = parseInt(tabIndex) || 0; - this.id = this._uniqueId = `mat-mdc-checkbox-${++nextUniqueId}`; + this.id = this._uniqueId = `mat-mdc-checkbox-${this._appId}${++nextUniqueId}`; } ngOnChanges(changes: SimpleChanges) { diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index 6f39cbdb906c..7745363410a1 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -18,6 +18,8 @@ import { Optional, Output, booleanAttribute, + APP_ID, + inject, } from '@angular/core'; import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field'; import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; @@ -69,6 +71,9 @@ let nextUniqueId = 0; standalone: true, }) export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Whether the control is focused. */ focused: boolean = false; @@ -107,7 +112,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { @Input() placeholder: string = ''; /** Unique id for the input. */ - @Input() id: string = `mat-mdc-chip-list-input-${nextUniqueId++}`; + @Input() id: string = `mat-mdc-chip-list-input-${this._appId}${nextUniqueId++}`; /** Whether the input is disabled. */ @Input({transform: booleanAttribute}) diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index 21b7308b375e..65f2871ec6c1 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -37,6 +37,7 @@ import { booleanAttribute, inject, numberAttribute, + APP_ID, } from '@angular/core'; import { MAT_RIPPLE_GLOBAL_OPTIONS, @@ -96,6 +97,9 @@ export interface MatChipEvent { imports: [MatChipAction], }) export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + protected _document: Document; /** Emits when the chip is focused. */ @@ -139,7 +143,7 @@ export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck } /** A unique id for the chip. If none is supplied, it will be auto-generated. */ - @Input() id: string = `mat-mdc-chip-${uid++}`; + @Input() id: string = `mat-mdc-chip-${this._appId}${uid++}`; // TODO(#26104): Consider deprecating and using `_computeAriaAccessibleName` instead. // `ariaLabel` may be unnecessary, and `_computeAriaAccessibleName` only supports diff --git a/src/material/core/option/optgroup.ts b/src/material/core/option/optgroup.ts index dc5e75eeed6f..511169b69e3f 100644 --- a/src/material/core/option/optgroup.ts +++ b/src/material/core/option/optgroup.ts @@ -15,6 +15,8 @@ import { Optional, InjectionToken, booleanAttribute, + APP_ID, + inject, } from '@angular/core'; import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent'; @@ -68,6 +70,9 @@ export const MAT_OPTGROUP = new InjectionToken('MatOptgroup'); standalone: true, }) export class MatOptgroup { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Label for the option group. */ @Input() label: string; @@ -75,7 +80,7 @@ export class MatOptgroup { @Input({transform: booleanAttribute}) disabled: boolean = false; /** Unique id for the underlying label. */ - _labelId: string = `mat-optgroup-label-${_uniqueOptgroupIdCounter++}`; + _labelId: string = `mat-optgroup-label-${this._appId}${_uniqueOptgroupIdCounter++}`; /** Whether the group is in inert a11y mode. */ _inert: boolean; diff --git a/src/material/core/option/option.ts b/src/material/core/option/option.ts index 9c28ddd1cf7c..5ecf975162c6 100644 --- a/src/material/core/option/option.ts +++ b/src/material/core/option/option.ts @@ -24,6 +24,8 @@ import { QueryList, ViewChild, booleanAttribute, + inject, + APP_ID, } from '@angular/core'; import {Subject} from 'rxjs'; import {MAT_OPTGROUP, MatOptgroup} from './optgroup'; @@ -83,6 +85,9 @@ export class MatOptionSelectionChange { imports: [MatPseudoCheckbox, MatRipple], }) export class MatOption implements FocusableOption, AfterViewChecked, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _selected = false; private _active = false; private _disabled = false; @@ -102,7 +107,7 @@ export class MatOption implements FocusableOption, AfterViewChecked, On @Input() value: T; /** The unique ID of the option. */ - @Input() id: string = `mat-option-${_uniqueIdCounter++}`; + @Input() id: string = `mat-option-${this._appId}${_uniqueIdCounter++}`; /** Whether the option is disabled. */ @Input({transform: booleanAttribute}) diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts index 1e4cdadc220f..bec33a472e74 100644 --- a/src/material/datepicker/calendar-body.ts +++ b/src/material/datepicker/calendar-body.ts @@ -23,6 +23,7 @@ import { inject, afterNextRender, Injector, + APP_ID, } from '@angular/core'; import {NgClass} from '@angular/common'; @@ -96,6 +97,9 @@ const passiveEventOptions = normalizePassiveListenerOptions({passive: true}); imports: [NgClass], }) export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _platform = inject(Platform); /** @@ -595,7 +599,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView return null; } - private _id = `mat-calendar-body-${calendarBodyId++}`; + private _id = `mat-calendar-body-${this._appId}${calendarBodyId++}`; _startDateLabelId = `${this._id}-start-date`; diff --git a/src/material/datepicker/calendar-header.spec.ts b/src/material/datepicker/calendar-header.spec.ts index 6c25947d4835..03da9ff5dc1b 100644 --- a/src/material/datepicker/calendar-header.spec.ts +++ b/src/material/datepicker/calendar-header.spec.ts @@ -201,7 +201,7 @@ describe('MatCalendarHeader', () => { expect(periodButton.hasAttribute('aria-label')).toBe(true); expect(periodButton.getAttribute('aria-label')).toMatch(/^[a-z0-9\s]+$/i); expect(periodButton.hasAttribute('aria-describedby')).toBe(true); - expect(periodButton.getAttribute('aria-describedby')).toMatch(/mat-calendar-header-[0-9]+/i); + expect(periodButton.getAttribute('aria-describedby')).toMatch(/mat-calendar-header-\w+/i); }); }); diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts index ad9501c3ae0a..d5a014b638db 100644 --- a/src/material/datepicker/calendar.ts +++ b/src/material/datepicker/calendar.ts @@ -10,11 +10,13 @@ import {CdkPortalOutlet, ComponentPortal, ComponentType, Portal} from '@angular/ import { AfterContentInit, AfterViewChecked, + APP_ID, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, forwardRef, + inject, Inject, Input, OnChanges, @@ -62,6 +64,9 @@ export type MatCalendarView = 'month' | 'year' | 'multi-year'; imports: [MatButton, MatIconButton], }) export class MatCalendarHeader { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + constructor( private _intl: MatDatepickerIntl, @Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar, @@ -221,7 +226,7 @@ export class MatCalendarHeader { return [minYearLabel, maxYearLabel]; } - private _id = `mat-calendar-header-${calendarHeaderId++}`; + private _id = `mat-calendar-header-${this._appId}${calendarHeaderId++}`; _periodButtonLabelId = `${this._id}-period-label`; } diff --git a/src/material/datepicker/date-range-input.ts b/src/material/datepicker/date-range-input.ts index a48fdc877739..5f44dbb651a9 100644 --- a/src/material/datepicker/date-range-input.ts +++ b/src/material/datepicker/date-range-input.ts @@ -24,6 +24,8 @@ import { ViewEncapsulation, booleanAttribute, signal, + inject, + APP_ID, } from '@angular/core'; import {ControlContainer, NgControl, Validators} from '@angular/forms'; import {DateAdapter, ThemePalette} from '@angular/material/core'; @@ -79,6 +81,9 @@ export class MatDateRangeInput OnChanges, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _closedSubscription = Subscription.EMPTY; private _openedSubscription = Subscription.EMPTY; @@ -88,7 +93,7 @@ export class MatDateRangeInput } /** Unique ID for the group. */ - id = `mat-date-range-input-${nextUniqueId++}`; + id = `mat-date-range-input-${this._appId}${nextUniqueId++}`; /** Whether the control is focused. */ focused = false; diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index b22a52399580..00283fce9992 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -34,6 +34,7 @@ import {DOCUMENT} from '@angular/common'; import { afterNextRender, AfterViewInit, + APP_ID, booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, @@ -356,6 +357,9 @@ export abstract class MatDatepickerBase< > implements MatDatepickerPanel, OnDestroy, OnChanges { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _scrollStrategy: () => ScrollStrategy; private _inputStateChanges = Subscription.EMPTY; private _document = inject(DOCUMENT); @@ -485,7 +489,7 @@ export abstract class MatDatepickerBase< private _opened = false; /** The id for the datepicker calendar. */ - id: string = `mat-datepicker-${datepickerUid++}`; + id: string = `mat-datepicker-${this._appId}${datepickerUid++}`; /** The minimum selectable date. */ _getMinDate(): D | null { diff --git a/src/material/dialog/dialog-content-directives.ts b/src/material/dialog/dialog-content-directives.ts index dac9c74ccf66..e5fc19a26e8b 100644 --- a/src/material/dialog/dialog-content-directives.ts +++ b/src/material/dialog/dialog-content-directives.ts @@ -7,8 +7,10 @@ */ import { + APP_ID, Directive, ElementRef, + inject, Input, OnChanges, OnDestroy, @@ -140,7 +142,10 @@ export abstract class MatDialogLayoutSection implements OnInit, OnDestroy { }, }) export class MatDialogTitle extends MatDialogLayoutSection { - @Input() id: string = `mat-mdc-dialog-title-${dialogElementUid++}`; + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + + @Input() id: string = `mat-mdc-dialog-title-${this._appId}${dialogElementUid++}`; protected _onAdd() { // Note: we null check the queue, because there are some internal diff --git a/src/material/dialog/dialog.ts b/src/material/dialog/dialog.ts index f85a26a40055..961c28e5c30d 100644 --- a/src/material/dialog/dialog.ts +++ b/src/material/dialog/dialog.ts @@ -21,6 +21,7 @@ import { TemplateRef, Type, inject, + APP_ID, } from '@angular/core'; import {MatDialogConfig} from './dialog-config'; import {MatDialogContainer} from './dialog-container'; @@ -79,6 +80,9 @@ let uniqueId = 0; */ @Injectable({providedIn: 'root'}) export class MatDialog implements OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private readonly _openDialogsAtThisLevel: MatDialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject>(); @@ -178,7 +182,7 @@ export class MatDialog implements OnDestroy { ): MatDialogRef { let dialogRef: MatDialogRef; config = {...(this._defaultOptions || new MatDialogConfig()), ...config}; - config.id = config.id || `mat-mdc-dialog-${uniqueId++}`; + config.id = config.id || `mat-mdc-dialog-${this._appId}${uniqueId++}`; config.scrollStrategy = config.scrollStrategy || this._scrollStrategy(); const cdkRef = this._dialog.open(componentOrTemplateRef, { diff --git a/src/material/dialog/testing/dialog-harness.spec.ts b/src/material/dialog/testing/dialog-harness.spec.ts index ddd3de3dad79..b63201c6610e 100644 --- a/src/material/dialog/testing/dialog-harness.spec.ts +++ b/src/material/dialog/testing/dialog-harness.spec.ts @@ -72,7 +72,7 @@ describe('MatDialogHarness', () => { fixture.componentInstance.open(); fixture.componentInstance.open({ariaLabelledBy: 'dialog-label'}); const dialogs = await loader.getAllHarnesses(MatDialogHarness); - expect(await dialogs[0].getAriaLabelledby()).toMatch(/-dialog-title-\d+/); + expect(await dialogs[0].getAriaLabelledby()).toMatch(/-dialog-title-\w+/); expect(await dialogs[1].getAriaLabelledby()).toBe('dialog-label'); }); diff --git a/src/material/expansion/expansion-panel.ts b/src/material/expansion/expansion-panel.ts index d2a26cdbec4a..a2714892bcd4 100644 --- a/src/material/expansion/expansion-panel.ts +++ b/src/material/expansion/expansion-panel.ts @@ -13,6 +13,8 @@ import {CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal'; import {DOCUMENT} from '@angular/common'; import { AfterContentInit, + ANIMATION_MODULE_TYPE, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -32,12 +34,10 @@ import { ViewChild, ViewContainerRef, ViewEncapsulation, - booleanAttribute, - ANIMATION_MODULE_TYPE, } from '@angular/core'; import {Subject} from 'rxjs'; import {filter, startWith, take} from 'rxjs/operators'; -import {MatAccordionBase, MatAccordionTogglePosition, MAT_ACCORDION} from './accordion-base'; +import {MAT_ACCORDION, MatAccordionBase, MatAccordionTogglePosition} from './accordion-base'; import {matExpansionAnimations} from './expansion-animations'; import {MAT_EXPANSION_PANEL} from './expansion-panel-base'; import {MatExpansionPanelContent} from './expansion-panel-content'; @@ -146,7 +146,7 @@ export class MatExpansionPanel _portal: TemplatePortal; /** ID for the associated header element. Used for a11y labelling. */ - _headerId = `mat-expansion-panel-header-${uniqueId++}`; + _headerId = `mat-expansion-panel-header-${this._appId}${uniqueId++}`; constructor( @Optional() @SkipSelf() @Inject(MAT_ACCORDION) accordion: MatAccordionBase, diff --git a/src/material/form-field/directives/error.ts b/src/material/form-field/directives/error.ts index 59800fd68392..a0b646f2e45c 100644 --- a/src/material/form-field/directives/error.ts +++ b/src/material/form-field/directives/error.ts @@ -6,7 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {Attribute, Directive, ElementRef, InjectionToken, Input} from '@angular/core'; +import { + APP_ID, + Attribute, + Directive, + ElementRef, + inject, + InjectionToken, + Input, +} from '@angular/core'; let nextUniqueId = 0; @@ -29,7 +37,10 @@ export const MAT_ERROR = new InjectionToken('MatError'); standalone: true, }) export class MatError { - @Input() id: string = `mat-mdc-error-${nextUniqueId++}`; + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + + @Input() id: string = `mat-mdc-error-${this._appId}${nextUniqueId++}`; constructor(@Attribute('aria-live') ariaLive: string, elementRef: ElementRef) { // If no aria-live value is set add 'polite' as a default. This is preferred over setting diff --git a/src/material/form-field/directives/hint.ts b/src/material/form-field/directives/hint.ts index be63b37f5990..a446760ff826 100644 --- a/src/material/form-field/directives/hint.ts +++ b/src/material/form-field/directives/hint.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Input} from '@angular/core'; +import {APP_ID, Directive, inject, Input} from '@angular/core'; let nextUniqueId = 0; @@ -23,9 +23,12 @@ let nextUniqueId = 0; standalone: true, }) export class MatHint { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Whether to align the hint label at the start or end of the line. */ @Input() align: 'start' | 'end' = 'start'; /** Unique ID for the hint. Used for the aria-describedby on the form field control. */ - @Input() id: string = `mat-mdc-hint-${nextUniqueId++}`; + @Input() id: string = `mat-mdc-hint-${this._appId}${nextUniqueId++}`; } diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 2706d2260522..8e527875d8b3 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -32,6 +32,7 @@ import { ViewEncapsulation, afterRender, inject, + APP_ID, } from '@angular/core'; import {AbstractControlDirective} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; @@ -187,6 +188,9 @@ interface MatFormFieldControl extends _MatFormFieldControl {} export class MatFormField implements FloatingLabelParent, AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + @ViewChild('textField') _textField: ElementRef; @ViewChild('iconPrefixContainer') _iconPrefixContainer: ElementRef; @ViewChild('textPrefixContainer') _textPrefixContainer: ElementRef; @@ -294,10 +298,10 @@ export class MatFormField _hasTextSuffix = false; // Unique id for the internal form field label. - readonly _labelId = `mat-mdc-form-field-label-${nextUniqueId++}`; + readonly _labelId = `mat-mdc-form-field-label-${this._appId}${nextUniqueId++}`; // Unique id for the hint label. - readonly _hintLabelId = `mat-mdc-hint-${nextUniqueId++}`; + readonly _hintLabelId = `mat-mdc-hint-${this._appId}${nextUniqueId++}`; /** State of the mat-hint and mat-error animations. */ _subscriptAnimationState = ''; diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 7760f24eb703..481284f80c92 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -529,7 +529,7 @@ describe('MatMdcInput without forms', () => { fixture.componentInstance.formControl.markAsTouched(); fixture.componentInstance.formControl.setErrors({invalid: true}); fixture.detectChanges(); - expect(input.getAttribute('aria-describedby')).toMatch(/^custom-error mat-mdc-error-\d+$/); + expect(input.getAttribute('aria-describedby')).toMatch(/^custom-error mat-mdc-error-\w+$/); fixture.componentInstance.label = ''; fixture.componentInstance.userDescribedByValue = ''; diff --git a/src/material/input/input.ts b/src/material/input/input.ts index b97ef537734f..e2f9be9ac99f 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -11,9 +11,11 @@ import {getSupportedInputTypes, Platform} from '@angular/cdk/platform'; import {AutofillMonitor} from '@angular/cdk/text-field'; import { AfterViewInit, + APP_ID, Directive, DoCheck, ElementRef, + inject, Inject, Input, NgZone, @@ -82,7 +84,10 @@ let nextUniqueId = 0; export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, AfterViewInit, DoCheck { - protected _uid = `mat-input-${nextUniqueId++}`; + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + + protected _uid = `mat-input-${this._appId}${nextUniqueId++}`; protected _previousNativeValue: any; private _inputValueAccessor: {value: any}; private _previousPlaceholder: string | null; diff --git a/src/material/input/testing/input-harness.spec.ts b/src/material/input/testing/input-harness.spec.ts index c455d6310284..cc93d04d28bd 100644 --- a/src/material/input/testing/input-harness.spec.ts +++ b/src/material/input/testing/input-harness.spec.ts @@ -66,11 +66,11 @@ describe('MatInputHarness', () => { it('should be able to get id of input', async () => { const inputs = await loader.getAllHarnesses(MatInputHarness); expect(inputs.length).toBe(7); - expect(await inputs[0].getId()).toMatch(/mat-input-\d+/); - expect(await inputs[1].getId()).toMatch(/mat-input-\d+/); + expect(await inputs[0].getId()).toMatch(/mat-input-\w+/); + expect(await inputs[1].getId()).toMatch(/mat-input-\w+/); expect(await inputs[2].getId()).toBe('myTextarea'); expect(await inputs[3].getId()).toBe('nativeControl'); - expect(await inputs[4].getId()).toMatch(/mat-input-\d+/); + expect(await inputs[4].getId()).toMatch(/mat-input-\w+/); expect(await inputs[5].getId()).toBe('has-ng-model'); }); diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index b625f64727e2..4b6a6f0bf2e0 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -31,6 +31,7 @@ import { AfterRenderRef, inject, Injector, + APP_ID, } from '@angular/core'; import {AnimationEvent} from '@angular/animations'; import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; @@ -114,6 +115,9 @@ export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions { standalone: true, }) export class MatMenu implements AfterContentInit, MatMenuPanel, OnInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _keyManager: FocusKeyManager; private _xPosition: MenuPositionX; private _yPosition: MenuPositionY; @@ -270,7 +274,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnI */ @Output() readonly close: EventEmitter = this.closed; - readonly panelId = `mat-menu-panel-${menuPanelUid++}`; + readonly panelId = `mat-menu-panel-${this._appId}${menuPanelUid++}`; private _injector = inject(Injector); diff --git a/src/material/paginator/paginator.ts b/src/material/paginator/paginator.ts index 0e589fdb4b5d..c22eb4257abc 100644 --- a/src/material/paginator/paginator.ts +++ b/src/material/paginator/paginator.ts @@ -21,6 +21,8 @@ import { ViewEncapsulation, booleanAttribute, numberAttribute, + APP_ID, + inject, } from '@angular/core'; import {MatOption, ThemePalette} from '@angular/material/core'; import {MatSelect} from '@angular/material/select'; @@ -112,11 +114,14 @@ let nextUniqueId = 0; imports: [MatFormField, MatSelect, MatOption, MatIconButton, MatTooltip], }) export class MatPaginator implements OnInit, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** If set, styles the "page size" form field with the designated style. */ _formFieldAppearance?: MatFormFieldAppearance; /** ID for the DOM node containing the paginator's items per page label. */ - readonly _pageSizeLabelId = `mat-paginator-page-size-label-${nextUniqueId++}`; + readonly _pageSizeLabelId = `mat-paginator-page-size-label-${this._appId}${nextUniqueId++}`; private _intlChanges: Subscription; private _isInitialized = false; diff --git a/src/material/radio/radio.ts b/src/material/radio/radio.ts index e163988bfdbf..f778c81350b5 100644 --- a/src/material/radio/radio.ts +++ b/src/material/radio/radio.ts @@ -37,6 +37,7 @@ import { forwardRef, inject, numberAttribute, + APP_ID, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {MatRipple, ThemePalette, _MatInternalFormField} from '@angular/material/core'; @@ -108,11 +109,14 @@ export function MAT_RADIO_DEFAULT_OPTIONS_FACTORY(): MatRadioDefaultOptions { standalone: true, }) export class MatRadioGroup implements AfterContentInit, OnDestroy, ControlValueAccessor { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Selected value for the radio group. */ private _value: any = null; /** The HTML name attribute applied to radio buttons in this group. */ - private _name: string = `mat-radio-group-${nextUniqueId++}`; + private _name: string = `mat-radio-group-${this._appId}${nextUniqueId++}`; /** The currently selected radio button. Should match value. */ private _selected: MatRadioButton | null = null; @@ -383,7 +387,10 @@ export class MatRadioGroup implements AfterContentInit, OnDestroy, ControlValueA imports: [MatRipple, _MatInternalFormField], }) export class MatRadioButton implements OnInit, AfterViewInit, DoCheck, OnDestroy { - private _uniqueId: string = `mat-radio-${++nextUniqueId}`; + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + + private _uniqueId: string = `mat-radio-${this._appId}${++nextUniqueId}`; /** The unique ID for the radio button. */ @Input() id: string = this._uniqueId; diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts index 1b8caf91b3c8..087679d6a613 100644 --- a/src/material/select/select.spec.ts +++ b/src/material/select/select.spec.ts @@ -222,7 +222,7 @@ describe('MDC-based MatSelect', () => { fixture.detectChanges(); const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement; expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id')); - expect(select.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\d+$/); + expect(select.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\w+$/); })); it('should support user binding to `aria-describedby`', fakeAsync(() => { diff --git a/src/material/select/select.ts b/src/material/select/select.ts index 05ac5fa6896d..35a85172e1c0 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -34,6 +34,7 @@ import { import {ViewportRuler} from '@angular/cdk/scrolling'; import { AfterContentInit, + APP_ID, Attribute, booleanAttribute, ChangeDetectionStrategy, @@ -217,6 +218,9 @@ export class MatSelect ControlValueAccessor, MatFormFieldControl { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** All of the defined select options. */ @ContentChildren(MatOption, {descendants: true}) options: QueryList; @@ -308,7 +312,7 @@ export class MatSelect private _compareWith = (o1: any, o2: any) => o1 === o2; /** Unique id for this input. */ - private _uid = `mat-select-${nextUniqueId++}`; + private _uid = `mat-select-${this._appId}${nextUniqueId++}`; /** Current `aria-labelledby` value for the select trigger. */ private _triggerAriaLabelledBy: string | null = null; @@ -363,7 +367,7 @@ export class MatSelect _onTouched = () => {}; /** ID for the DOM node containing the select's value. */ - _valueId = `mat-select-value-${nextUniqueId++}`; + _valueId = `mat-select-value-${this._appId}${nextUniqueId++}`; /** Emits when the panel element is finished transforming in. */ readonly _panelDoneAnimatingStream = new Subject(); diff --git a/src/material/slide-toggle/slide-toggle.spec.ts b/src/material/slide-toggle/slide-toggle.spec.ts index 2b4a26718723..17f0d5ab923e 100644 --- a/src/material/slide-toggle/slide-toggle.spec.ts +++ b/src/material/slide-toggle/slide-toggle.spec.ts @@ -172,7 +172,7 @@ describe('MDC-based MatSlideToggle without forms', () => { fixture.detectChanges(); // Once the id binding is set to null, the id property should auto-generate a unique id. - expect(buttonElement.id).toMatch(/mat-mdc-slide-toggle-\d+-button/); + expect(buttonElement.id).toMatch(/mat-mdc-slide-toggle-\w+-button/); })); it('should forward the tabIndex to the underlying element', fakeAsync(() => { @@ -226,7 +226,7 @@ describe('MDC-based MatSlideToggle without forms', () => { // We fall back to pointing to the label if a value isn't provided. expect(buttonElement.getAttribute('aria-labelledby')).toMatch( - /mat-mdc-slide-toggle-\d+-label/, + /mat-mdc-slide-toggle-\w+-label/, ); })); diff --git a/src/material/slide-toggle/slide-toggle.ts b/src/material/slide-toggle/slide-toggle.ts index 2e4501a8b3b6..0556f7d85ebf 100644 --- a/src/material/slide-toggle/slide-toggle.ts +++ b/src/material/slide-toggle/slide-toggle.ts @@ -27,6 +27,8 @@ import { ViewChild, ViewEncapsulation, ANIMATION_MODULE_TYPE, + APP_ID, + inject, } from '@angular/core'; import { AbstractControl, @@ -100,6 +102,9 @@ let nextUniqueId = 0; export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _onChange = (_: any) => {}; private _onTouched = () => {}; private _validatorOnChange = () => {}; @@ -213,7 +218,7 @@ export class MatSlideToggle this.tabIndex = parseInt(tabIndex) || 0; this.color = defaults.color || 'accent'; this._noopAnimations = animationMode === 'NoopAnimations'; - this.id = this._uniqueId = `mat-mdc-slide-toggle-${++nextUniqueId}`; + this.id = this._uniqueId = `mat-mdc-slide-toggle-${this._appId}${++nextUniqueId}`; this.hideIcon = defaults.hideIcon ?? false; this._labelId = this._uniqueId + '-label'; } diff --git a/src/material/snack-bar/snack-bar-container.ts b/src/material/snack-bar/snack-bar-container.ts index 163b2969f4c2..f60cf08e3fce 100644 --- a/src/material/snack-bar/snack-bar-container.ts +++ b/src/material/snack-bar/snack-bar-container.ts @@ -7,6 +7,7 @@ */ import { + APP_ID, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -60,6 +61,9 @@ let uniqueId = 0; }, }) export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private _document = inject(DOCUMENT); private _trackedModals = new Set(); @@ -104,7 +108,7 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy _role?: 'status' | 'alert'; /** Unique ID of the aria-live element. */ - readonly _liveElementId = `mat-snack-bar-container-live-${uniqueId++}`; + readonly _liveElementId = `mat-snack-bar-container-live-${this._appId}${uniqueId++}`; constructor( private _ngZone: NgZone, diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts index 1f2f5fa0a69a..015cb165060e 100644 --- a/src/material/tabs/tab-group.ts +++ b/src/material/tabs/tab-group.ts @@ -27,6 +27,7 @@ import { inject, numberAttribute, ANIMATION_MODULE_TYPE, + APP_ID, } from '@angular/core'; import {MAT_TAB_GROUP, MatTab} from './tab'; import {MatTabHeader} from './tab-header'; @@ -94,6 +95,9 @@ const ENABLE_BACKGROUND_INPUT = true; ], }) export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDestroy { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** * All tabs inside the tab group. This includes tabs that belong to groups that are nested * inside the current one. We filter out only the tabs that belong to this group in `_tabs`. @@ -468,7 +472,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes /** Returns a unique id for each tab label element */ _getTabLabelId(i: number): string { - return `mat-tab-label-${this._groupId}-${i}`; + return `mat-tab-label-${this._appId}${this._groupId}-${i}`; } /** Returns a unique id for each tab content element */ diff --git a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts index 614b41a66d84..696e3d796bdb 100644 --- a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts @@ -27,6 +27,8 @@ import { ViewChild, ViewEncapsulation, ANIMATION_MODULE_TYPE, + inject, + APP_ID, } from '@angular/core'; import { MAT_RIPPLE_GLOBAL_OPTIONS, @@ -265,6 +267,9 @@ export class MatTabLink extends InkBarItem implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + private readonly _destroyed = new Subject(); /** Whether the tab link is active or not. */ @@ -318,7 +323,7 @@ export class MatTabLink } /** Unique id for the tab. */ - @Input() id = `mat-tab-link-${nextUniqueId++}`; + @Input() id = `mat-tab-link-${this._appId}${nextUniqueId++}`; constructor( private _tabNavBar: MatTabNav, @@ -431,8 +436,11 @@ export class MatTabLink standalone: true, }) export class MatTabNavPanel { + /** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */ + private _appId = inject(APP_ID); + /** Unique id for the tab panel. */ - @Input() id = `mat-tab-nav-panel-${nextUniqueId++}`; + @Input() id = `mat-tab-nav-panel-${this._appId}${nextUniqueId++}`; /** Id of the active tab in the nav bar. */ _activeTabId?: string;