From 3c90aa297a86d53062af13b8e990cfe811db6de7 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. --- .../combobox/combobox-popup.ts | 10 ++-- .../table-scroll-container/BUILD.bazel | 1 + .../table-scroll-container.ts | 21 ++++++-- src/cdk/a11y/live-announcer/live-announcer.ts | 13 +++-- src/cdk/accordion/BUILD.bazel | 1 + src/cdk/accordion/accordion-item.ts | 21 ++++---- src/cdk/accordion/accordion.ts | 12 +++-- src/cdk/dialog/dialog.ts | 48 +++++++++-------- src/cdk/drag-drop/directives/drop-list.ts | 40 +++++++------- src/cdk/listbox/listbox.spec.ts | 4 +- src/cdk/listbox/listbox.ts | 20 ++++--- src/cdk/menu/menu-base.ts | 16 +++--- src/cdk/overlay/BUILD.bazel | 1 + src/cdk/overlay/overlay.ts | 14 ++--- src/cdk/stepper/stepper.ts | 15 ++++-- src/material/autocomplete/autocomplete.ts | 22 ++++---- src/material/badge/badge.ts | 9 ++-- src/material/button-toggle/button-toggle.ts | 22 ++++---- src/material/checkbox/checkbox.spec.ts | 6 +-- src/material/checkbox/checkbox.ts | 21 ++++---- src/material/chips/BUILD.bazel | 1 + src/material/chips/chip-input.ts | 14 +++-- src/material/chips/chip.ts | 21 ++++---- src/material/core/option/optgroup.ts | 20 ++++--- src/material/core/option/option.ts | 34 ++++++------ src/material/datepicker/calendar-body.ts | 26 ++++----- .../datepicker/calendar-header.spec.ts | 2 +- src/material/datepicker/calendar.ts | 16 +++--- src/material/datepicker/date-range-input.ts | 18 ++++--- src/material/datepicker/datepicker-base.ts | 10 ++-- .../dialog/dialog-content-directives.ts | 12 +++-- src/material/dialog/dialog.ts | 17 +++--- .../dialog/testing/dialog-harness.spec.ts | 2 +- src/material/expansion/expansion-panel.ts | 11 ++-- src/material/form-field/directives/error.ts | 10 ++-- src/material/form-field/directives/hint.ts | 10 ++-- src/material/form-field/form-field.ts | 18 ++++--- src/material/input/input.spec.ts | 2 +- src/material/input/input.ts | 13 +++-- .../input/testing/input-harness.spec.ts | 6 +-- src/material/menu/menu.ts | 53 ++++++++++--------- src/material/paginator/paginator.ts | 17 +++--- src/material/radio/radio.ts | 27 ++++++---- src/material/select/select.spec.ts | 2 +- src/material/select/select.ts | 12 +++-- .../slide-toggle/slide-toggle.spec.ts | 4 +- src/material/slide-toggle/slide-toggle.ts | 13 +++-- src/material/snack-bar/snack-bar-container.ts | 31 +++++------ src/material/tabs/tab-group.ts | 28 +++++----- src/material/tabs/tab-nav-bar/tab-nav-bar.ts | 32 ++++++----- tools/public_api_guard/cdk/accordion.md | 3 ++ 51 files changed, 449 insertions(+), 353 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-popup.ts b/src/cdk-experimental/combobox/combobox-popup.ts index 90aa3e99d069..99b390ae1000 100644 --- a/src/cdk-experimental/combobox/combobox-popup.ts +++ b/src/cdk-experimental/combobox/combobox-popup.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Inject, Input, OnInit} from '@angular/core'; +import {IdGenerator} from '@angular/cdk/a11y'; +import {Directive, ElementRef, inject, Inject, Input, OnInit} from '@angular/core'; import {AriaHasPopupValue, CDK_COMBOBOX, CdkCombobox} from './combobox'; -let nextId = 0; - @Directive({ selector: '[cdkComboboxPopup]', exportAs: 'cdkComboboxPopup', @@ -24,6 +23,9 @@ let nextId = 0; standalone: true, }) export class CdkComboboxPopup implements OnInit { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + @Input() get role(): AriaHasPopupValue { return this._role; @@ -42,7 +44,7 @@ export class CdkComboboxPopup implements OnInit { } private _firstFocusElement: HTMLElement; - @Input() id = `cdk-combobox-popup-${nextId++}`; + @Input() id = this._idGenerator.getId('cdk-combobox-popup-'); constructor( private readonly _elementRef: ElementRef, diff --git a/src/cdk-experimental/table-scroll-container/BUILD.bazel b/src/cdk-experimental/table-scroll-container/BUILD.bazel index 9db3cec310c4..e624e99e819f 100644 --- a/src/cdk-experimental/table-scroll-container/BUILD.bazel +++ b/src/cdk-experimental/table-scroll-container/BUILD.bazel @@ -14,6 +14,7 @@ ng_module( exclude = ["**/*.spec.ts"], ), deps = [ + "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/platform", "//src/cdk/table", 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..937635ed72c9 100644 --- a/src/cdk-experimental/table-scroll-container/table-scroll-container.ts +++ b/src/cdk-experimental/table-scroll-container/table-scroll-container.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CSP_NONCE, Directive, ElementRef, Inject, OnDestroy, OnInit, Optional} from '@angular/core'; -import {DOCUMENT} from '@angular/common'; +import {IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {_getShadowRoot} from '@angular/cdk/platform'; import { @@ -16,8 +15,17 @@ import { StickySize, StickyUpdate, } from '@angular/cdk/table'; - -let nextId = 0; +import {DOCUMENT} from '@angular/common'; +import { + CSP_NONCE, + Directive, + ElementRef, + inject, + Inject, + OnDestroy, + OnInit, + Optional, +} from '@angular/core'; /** * Applies styles to the host element that make its scrollbars match up with @@ -39,6 +47,9 @@ let nextId = 0; standalone: true, }) export class CdkTableScrollContainer implements StickyPositioningListener, OnDestroy, OnInit { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private readonly _uniqueClassName: string; private _styleRoot!: Node; private _styleElement?: HTMLStyleElement; @@ -55,7 +66,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 = this._idGenerator.getId('cdk-table-scroll-container-'); _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..835c8ccd781a 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import {ContentObserver} from '@angular/cdk/observers'; import {DOCUMENT} from '@angular/common'; import { Directive, ElementRef, + inject, Inject, Injectable, Input, @@ -21,15 +23,16 @@ import { import {Subscription} from 'rxjs'; import { AriaLivePoliteness, - LiveAnnouncerDefaultOptions, - LIVE_ANNOUNCER_ELEMENT_TOKEN, LIVE_ANNOUNCER_DEFAULT_OPTIONS, + LIVE_ANNOUNCER_ELEMENT_TOKEN, + LiveAnnouncerDefaultOptions, } from './live-announcer-tokens'; -let uniqueIds = 0; - @Injectable({providedIn: 'root'}) export class LiveAnnouncer implements OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _liveElement: HTMLElement; private _document: Document; private _previousTimeout: number; @@ -179,7 +182,7 @@ export class LiveAnnouncer implements OnDestroy { liveEl.setAttribute('aria-atomic', 'true'); liveEl.setAttribute('aria-live', 'polite'); - liveEl.id = `cdk-live-announcer-${uniqueIds++}`; + liveEl.id = this._idGenerator.getId('cdk-live-announcer-'); this._document.body.appendChild(liveEl); diff --git a/src/cdk/accordion/BUILD.bazel b/src/cdk/accordion/BUILD.bazel index 919a0b06faab..7b1065d33951 100644 --- a/src/cdk/accordion/BUILD.bazel +++ b/src/cdk/accordion/BUILD.bazel @@ -15,6 +15,7 @@ ng_module( exclude = ["**/*.spec.ts"], ), deps = [ + "//src/cdk/a11y", "//src/cdk/collections", "@npm//@angular/core", "@npm//rxjs", diff --git a/src/cdk/accordion/accordion-item.ts b/src/cdk/accordion/accordion-item.ts index 64bb79bfd60b..529cbe0d5da2 100644 --- a/src/cdk/accordion/accordion-item.ts +++ b/src/cdk/accordion/accordion-item.ts @@ -6,24 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; +import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import { - Output, + booleanAttribute, + ChangeDetectorRef, Directive, EventEmitter, + Inject, + inject, Input, OnDestroy, Optional, - ChangeDetectorRef, + Output, SkipSelf, - Inject, - booleanAttribute, } from '@angular/core'; -import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; -import {CDK_ACCORDION, CdkAccordion} from './accordion'; import {Subscription} from 'rxjs'; - -/** Used to generate unique ID for each accordion item. */ -let nextId = 0; +import {CDK_ACCORDION, CdkAccordion} from './accordion'; /** * An basic directive expected to be extended and decorated as a component. Sets up all @@ -40,6 +39,8 @@ let nextId = 0; standalone: true, }) export class CdkAccordionItem implements OnDestroy { + protected _idGenerator = inject(IdGenerator); + /** Subscription to openAll/closeAll events. */ private _openCloseAllSubscription = Subscription.EMPTY; /** Event emitted every time the AccordionItem is closed. */ @@ -57,7 +58,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 = this._idGenerator.getId('cdk-accordion-child-'); /** Whether the AccordionItem is expanded. */ @Input({transform: booleanAttribute}) diff --git a/src/cdk/accordion/accordion.ts b/src/cdk/accordion/accordion.ts index 294e7ba6fdfc..eea2bb6cf033 100644 --- a/src/cdk/accordion/accordion.ts +++ b/src/cdk/accordion/accordion.ts @@ -6,20 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import { + booleanAttribute, Directive, + inject, InjectionToken, Input, OnChanges, OnDestroy, SimpleChanges, - booleanAttribute, } from '@angular/core'; import {Subject} from 'rxjs'; -/** Used to generate unique ID for each accordion. */ -let nextId = 0; - /** * Injection token that can be used to reference instances of `CdkAccordion`. It serves * as alternative token to the actual `CdkAccordion` class which could cause unnecessary @@ -37,6 +36,9 @@ export const CDK_ACCORDION = new InjectionToken('CdkAccordion'); standalone: true, }) export class CdkAccordion implements OnDestroy, OnChanges { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** Emits when the state of the accordion changes */ readonly _stateChanges = new Subject(); @@ -44,7 +46,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 = this._idGenerator.getId('cdk-accordion-'); /** 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..a1685a6c21c4 100644 --- a/src/cdk/dialog/dialog.ts +++ b/src/cdk/dialog/dialog.ts @@ -6,41 +6,43 @@ * found in the LICENSE file at https://angular.io/license */ -import { - TemplateRef, - Injectable, - Injector, - OnDestroy, - Type, - StaticProvider, - Inject, - Optional, - SkipSelf, - ComponentRef, -} from '@angular/core'; -import {BasePortalOutlet, ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; -import {of as observableOf, Observable, Subject, defer} from 'rxjs'; -import {DialogRef} from './dialog-ref'; -import {DialogConfig} from './dialog-config'; +import {IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { ComponentType, Overlay, - OverlayRef, OverlayConfig, - ScrollStrategy, OverlayContainer, + OverlayRef, + ScrollStrategy, } from '@angular/cdk/overlay'; +import {BasePortalOutlet, ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; +import { + ComponentRef, + Inject, + inject, + Injectable, + Injector, + OnDestroy, + Optional, + SkipSelf, + StaticProvider, + TemplateRef, + Type, +} from '@angular/core'; +import {defer, Observable, of as observableOf, Subject} from 'rxjs'; import {startWith} from 'rxjs/operators'; - -import {DEFAULT_DIALOG_CONFIG, DIALOG_DATA, DIALOG_SCROLL_STRATEGY} from './dialog-injectors'; +import {DialogConfig} from './dialog-config'; import {CdkDialogContainer} from './dialog-container'; -/** Unique id for the created dialog. */ -let uniqueId = 0; +import {DEFAULT_DIALOG_CONFIG, DIALOG_DATA, DIALOG_SCROLL_STRATEGY} from './dialog-injectors'; +import {DialogRef} from './dialog-ref'; @Injectable({providedIn: 'root'}) export class Dialog implements OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _openDialogsAtThisLevel: DialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject(); @@ -114,7 +116,7 @@ export class Dialog implements OnDestroy { DialogRef >; config = {...defaults, ...config}; - config.id = config.id || `cdk-dialog-${uniqueId++}`; + config.id = config.id || this._idGenerator.getId('cdk-dialog-'); 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..0ab49b54a737 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -6,35 +6,34 @@ * found in the LICENSE file at https://angular.io/license */ -import {NumberInput, coerceArray, coerceNumberProperty} from '@angular/cdk/coercion'; +import {IdGenerator} from '@angular/cdk/a11y'; +import {Directionality} from '@angular/cdk/bidi'; +import {coerceArray, coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; +import {ScrollDispatcher} from '@angular/cdk/scrolling'; import { + booleanAttribute, + ChangeDetectorRef, + Directive, ElementRef, EventEmitter, + Inject, + inject, Input, OnDestroy, - Output, Optional, - Directive, - ChangeDetectorRef, + Output, SkipSelf, - Inject, - booleanAttribute, } from '@angular/core'; -import {Directionality} from '@angular/cdk/bidi'; -import {ScrollDispatcher} from '@angular/cdk/scrolling'; -import {CDK_DROP_LIST, CdkDrag} from './drag'; -import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from '../drag-events'; -import {CDK_DROP_LIST_GROUP, CdkDropListGroup} from './drop-list-group'; -import {DropListRef} from '../drop-list-ref'; -import {DragRef} from '../drag-ref'; -import {DragDrop} from '../drag-drop'; -import {DropListOrientation, DragAxis, DragDropConfig, CDK_DRAG_CONFIG} from './config'; import {merge, Subject} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; +import {DragDrop} from '../drag-drop'; +import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from '../drag-events'; +import {DragRef} from '../drag-ref'; +import {DropListRef} from '../drop-list-ref'; import {assertElementNode} from './assertions'; - -/** Counter used to generate unique ids for drop zones. */ -let _uniqueIdCounter = 0; +import {CDK_DRAG_CONFIG, DragAxis, DragDropConfig, DropListOrientation} from './config'; +import {CDK_DROP_LIST, CdkDrag} from './drag'; +import {CDK_DROP_LIST_GROUP, CdkDropListGroup} from './drop-list-group'; /** Container that wraps a set of draggable items. */ @Directive({ @@ -55,6 +54,9 @@ let _uniqueIdCounter = 0; }, }) export class CdkDropList implements OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** Emits when the list has been destroyed. */ private readonly _destroyed = new Subject(); @@ -85,7 +87,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 = this._idGenerator.getId('cdk-drop-list-'); /** 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..0e2b2e4efa13 100644 --- a/src/cdk/listbox/listbox.ts +++ b/src/cdk/listbox/listbox.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y'; +import { + ActiveDescendantKeyManager, + Highlightable, + IdGenerator, + ListKeyManagerOption, +} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceArray} from '@angular/cdk/coercion'; import {SelectionModel} from '@angular/cdk/collections'; @@ -42,9 +47,6 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {defer, fromEvent, merge, Observable, Subject} from 'rxjs'; import {filter, map, startWith, switchMap, takeUntil} from 'rxjs/operators'; -/** The next id to use for creating unique DOM IDs. */ -let nextId = 0; - /** * An implementation of SelectionModel that internally always represents the selection as a * multi-selection. This is necessary so that we can recover the full selection if the user @@ -96,6 +98,9 @@ class ListboxSelectionModel extends SelectionModel { }, }) export class CdkOption implements ListKeyManagerOption, Highlightable, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** The id of the option's host element. */ @Input() get id() { @@ -105,7 +110,7 @@ export class CdkOption implements ListKeyManagerOption, Highlightab this._id = value; } private _id: string; - private _generatedId = `cdk-option-${nextId++}`; + private _generatedId = this._idGenerator.getId('cdk-option-'); /** The value of this option. */ @Input('cdkOption') value: T; @@ -249,6 +254,9 @@ export class CdkOption implements ListKeyManagerOption, Highlightab ], }) export class CdkListbox implements AfterContentInit, OnDestroy, ControlValueAccessor { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** The id of the option's host element. */ @Input() get id() { @@ -258,7 +266,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, Con this._id = value; } private _id: string; - private _generatedId = `cdk-listbox-${nextId++}`; + private _generatedId = this._idGenerator.getId('cdk-listbox-'); /** 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..17a7b1dff56a 100644 --- a/src/cdk/menu/menu-base.ts +++ b/src/cdk/menu/menu-base.ts @@ -6,22 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; +import {FocusKeyManager, FocusOrigin, IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { AfterContentInit, + computed, ContentChildren, Directive, ElementRef, + inject, Input, NgZone, OnDestroy, QueryList, - computed, - inject, signal, } from '@angular/core'; -import {Subject, merge} from 'rxjs'; +import {merge, Subject} from 'rxjs'; import {mapTo, mergeAll, mergeMap, startWith, switchMap, takeUntil} from 'rxjs/operators'; import {MENU_AIM} from './menu-aim'; import {CdkMenuGroup} from './menu-group'; @@ -30,9 +30,6 @@ import {CdkMenuItem} from './menu-item'; import {MENU_STACK, MenuStack, MenuStackItem} from './menu-stack'; import {PointerFocusTracker} from './pointer-focus-tracker'; -/** Counter used to create unique IDs for menus. */ -let nextId = 0; - /** * Abstract directive that implements shared logic common to all menus. * This class can be extended to create custom menu types. @@ -55,6 +52,9 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterContentInit, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** The menu's native DOM host element. */ readonly nativeElement: HTMLElement = inject(ElementRef).nativeElement; @@ -71,7 +71,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 = this._idGenerator.getId('cdk-menu-'); /** All child MenuItem elements nested in this Menu. */ @ContentChildren(CdkMenuItem, {descendants: true}) diff --git a/src/cdk/overlay/BUILD.bazel b/src/cdk/overlay/BUILD.bazel index 55713dc87df7..1a48335bcd86 100644 --- a/src/cdk/overlay/BUILD.bazel +++ b/src/cdk/overlay/BUILD.bazel @@ -20,6 +20,7 @@ ng_module( ), deps = [ "//src:dev_mode_types", + "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/coercion", "//src/cdk/keycodes", diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index be660242b437..ee7e2f05d639 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -6,19 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {DomPortalOutlet} from '@angular/cdk/portal'; import {DOCUMENT, Location} from '@angular/common'; import { + ANIMATION_MODULE_TYPE, ApplicationRef, ComponentFactoryResolver, + EnvironmentInjector, Inject, + inject, Injectable, Injector, NgZone, - ANIMATION_MODULE_TYPE, Optional, - EnvironmentInjector, } from '@angular/core'; import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher'; import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher'; @@ -28,9 +30,6 @@ import {OverlayRef} from './overlay-ref'; import {OverlayPositionBuilder} from './position/overlay-position-builder'; import {ScrollStrategyOptions} from './scroll/index'; -/** Next overlay unique ID. */ -let nextUniqueId = 0; - // Note that Overlay is *not* scoped to the app root because of the ComponentFactoryResolver // which needs to be different depending on where OverlayModule is imported. @@ -44,6 +43,9 @@ let nextUniqueId = 0; */ @Injectable({providedIn: 'root'}) export class Overlay { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _appRef: ApplicationRef; constructor( @@ -106,7 +108,7 @@ export class Overlay { private _createPaneElement(host: HTMLElement): HTMLElement { const pane = this._document.createElement('div'); - pane.id = `cdk-overlay-${nextUniqueId++}`; + pane.id = this._idGenerator.getId('cdk-overlay-'); 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..94001b9a889c 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -9,8 +9,12 @@ import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes'; +import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import { + AfterContentInit, AfterViewInit, + APP_ID, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -21,8 +25,10 @@ import { EventEmitter, forwardRef, Inject, + inject, InjectionToken, Input, + numberAttribute, OnChanges, OnDestroy, Optional, @@ -31,11 +37,7 @@ import { TemplateRef, ViewChild, ViewEncapsulation, - AfterContentInit, - booleanAttribute, - numberAttribute, } from '@angular/core'; -import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {Observable, of as observableOf, Subject} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; @@ -236,6 +238,9 @@ export class CdkStep implements OnChanges { standalone: true, }) export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + 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..53940978c189 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -6,8 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {AnimationEvent} from '@angular/animations'; +import {ActiveDescendantKeyManager, IdGenerator} from '@angular/cdk/a11y'; +import {Platform} from '@angular/cdk/platform'; import { AfterContentInit, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -15,6 +19,7 @@ import { ElementRef, EventEmitter, Inject, + inject, InjectionToken, Input, OnDestroy, @@ -23,9 +28,7 @@ import { TemplateRef, ViewChild, ViewEncapsulation, - booleanAttribute, } from '@angular/core'; -import {AnimationEvent} from '@angular/animations'; import { MAT_OPTGROUP, MAT_OPTION_PARENT_COMPONENT, @@ -33,16 +36,8 @@ import { MatOption, ThemePalette, } from '@angular/material/core'; -import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; -import {Platform} from '@angular/cdk/platform'; -import {panelAnimation} from './animations'; import {Subscription} from 'rxjs'; - -/** - * Autocomplete IDs need to be unique across components, so this counter exists outside of - * the component definition. - */ -let _uniqueAutocompleteIdCounter = 0; +import {panelAnimation} from './animations'; /** Event object that is emitted when an autocomplete option is selected. */ export class MatAutocompleteSelectedEvent { @@ -119,6 +114,9 @@ export function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): MatAutocompleteDefau standalone: true, }) export class MatAutocomplete implements AfterContentInit, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _activeOptionChanges = Subscription.EMPTY; /** Emits when the panel animation is done. Null if the panel doesn't animate. */ @@ -244,7 +242,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 = this._idGenerator.getId('mat-autocomplete-'); /** * 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..22868e9ad0ed 100644 --- a/src/material/badge/badge.ts +++ b/src/material/badge/badge.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {AriaDescriber, InteractivityChecker} from '@angular/cdk/a11y'; +import {AriaDescriber, IdGenerator, InteractivityChecker} from '@angular/cdk/a11y'; import {DOCUMENT} from '@angular/common'; import { + ANIMATION_MODULE_TYPE, ApplicationRef, booleanAttribute, ChangeDetectionStrategy, @@ -26,7 +27,6 @@ import { Optional, Renderer2, ViewEncapsulation, - ANIMATION_MODULE_TYPE, } from '@angular/core'; import {ThemePalette} from '@angular/material/core'; @@ -83,6 +83,9 @@ export class _MatBadgeStyleLoader {} standalone: true, }) export class MatBadge implements OnInit, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** * 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 +259,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', this._idGenerator.getId('mat-badge-content-')); // 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..62351a9ba53a 100644 --- a/src/material/button-toggle/button-toggle.ts +++ b/src/material/button-toggle/button-toggle.ts @@ -6,12 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {FocusMonitor} from '@angular/cdk/a11y'; +import {FocusMonitor, IdGenerator} from '@angular/cdk/a11y'; +import {Direction, Directionality} from '@angular/cdk/bidi'; import {SelectionModel} from '@angular/cdk/collections'; -import {DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, SPACE, ENTER} from '@angular/cdk/keycodes'; +import {DOWN_ARROW, ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, UP_ARROW} from '@angular/cdk/keycodes'; import { AfterContentInit, + AfterViewInit, Attribute, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -20,6 +23,9 @@ import { ElementRef, EventEmitter, forwardRef, + Inject, + inject, + InjectionToken, Input, OnDestroy, OnInit, @@ -28,14 +34,9 @@ import { QueryList, ViewChild, ViewEncapsulation, - InjectionToken, - Inject, - AfterViewInit, - booleanAttribute, } from '@angular/core'; -import {Direction, Directionality} from '@angular/cdk/bidi'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {MatRipple, MatPseudoCheckbox} from '@angular/material/core'; +import {MatPseudoCheckbox, MatRipple} from '@angular/material/core'; /** * @deprecated No longer used. @@ -134,6 +135,9 @@ export class MatButtonToggleChange { standalone: true, }) export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, AfterContentInit { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _multiple = false; private _disabled = false; private _selectionModel: SelectionModel; @@ -175,7 +179,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After this._name = value; this._markButtonsForCheck(); } - private _name = `mat-button-toggle-group-${uniqueIdCounter++}`; + private _name = this._idGenerator.getId('mat-button-toggle-group-'); /** 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..e73706072dbc 100644 --- a/src/material/checkbox/checkbox.ts +++ b/src/material/checkbox/checkbox.ts @@ -6,28 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ -import {FocusableOption} from '@angular/cdk/a11y'; +import {FocusableOption, IdGenerator} from '@angular/cdk/a11y'; import { - ANIMATION_MODULE_TYPE, AfterViewInit, + ANIMATION_MODULE_TYPE, Attribute, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, + forwardRef, Inject, + inject, Input, NgZone, + numberAttribute, OnChanges, Optional, Output, SimpleChanges, ViewChild, ViewEncapsulation, - booleanAttribute, - forwardRef, - numberAttribute, } from '@angular/core'; import { AbstractControl, @@ -37,7 +38,7 @@ import { ValidationErrors, Validator, } from '@angular/forms'; -import {MatRipple, _MatInternalFormField} from '@angular/material/core'; +import {_MatInternalFormField, MatRipple} from '@angular/material/core'; import { MAT_CHECKBOX_DEFAULT_OPTIONS, MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY, @@ -77,9 +78,6 @@ export class MatCheckboxChange { checked: boolean; } -// Increasing integer for generating unique ids for checkbox components. -let nextUniqueId = 0; - // Default checkbox configuration. const defaults = MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY(); @@ -117,6 +115,9 @@ const defaults = MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY(); export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccessor, Validator, FocusableOption { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** Focuses the checkbox. */ focus() { this._inputElement.nativeElement.focus(); @@ -240,7 +241,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 = this._idGenerator.getId('mat-mdc-checkbox-'); } ngOnChanges(changes: SimpleChanges) { diff --git a/src/material/chips/BUILD.bazel b/src/material/chips/BUILD.bazel index 3633d55b234a..6e8ffec38f55 100644 --- a/src/material/chips/BUILD.bazel +++ b/src/material/chips/BUILD.bazel @@ -24,6 +24,7 @@ ng_module( ] + glob(["**/*.html"]), deps = [ "//src:dev_mode_types", + "//src/cdk/a11y", "//src/material/core", "//src/material/form-field", "@npm//@angular/animations", diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index 6f39cbdb906c..4af7492783b5 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -6,23 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import {BACKSPACE, hasModifierKey} from '@angular/cdk/keycodes'; import { + booleanAttribute, Directive, ElementRef, EventEmitter, Inject, + inject, Input, OnChanges, OnDestroy, Optional, Output, - booleanAttribute, } from '@angular/core'; -import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field'; -import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; +import {MAT_FORM_FIELD, MatFormField} from '@angular/material/form-field'; import {MatChipGrid} from './chip-grid'; import {MatChipTextControl} from './chip-text-control'; +import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './tokens'; /** Represents an input event on a `matChipInput`. */ export interface MatChipInputEvent { @@ -41,7 +43,6 @@ export interface MatChipInputEvent { } // Increasing integer for generating unique ids. -let nextUniqueId = 0; /** * Directive that adds chip-specific behaviors to an input element inside ``. @@ -69,6 +70,9 @@ let nextUniqueId = 0; standalone: true, }) export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** Whether the control is focused. */ focused: boolean = false; @@ -107,7 +111,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 = this._idGenerator.getId('mat-mdc-chip-list-input-'); /** Whether the input is disabled. */ @Input({transform: booleanAttribute}) diff --git a/src/material/chips/chip.ts b/src/material/chips/chip.ts index 21b7308b375e..ea79c7d79ab9 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -6,14 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {FocusMonitor} from '@angular/cdk/a11y'; +import {FocusMonitor, IdGenerator} from '@angular/cdk/a11y'; import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; import {DOCUMENT} from '@angular/common'; import { - ANIMATION_MODULE_TYPE, AfterContentInit, + afterNextRender, AfterViewInit, + ANIMATION_MODULE_TYPE, Attribute, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -23,9 +25,11 @@ import { ElementRef, EventEmitter, Inject, + inject, Injector, Input, NgZone, + numberAttribute, OnDestroy, OnInit, Optional, @@ -33,10 +37,6 @@ import { QueryList, ViewChild, ViewEncapsulation, - afterNextRender, - booleanAttribute, - inject, - numberAttribute, } from '@angular/core'; import { MAT_RIPPLE_GLOBAL_OPTIONS, @@ -44,13 +44,11 @@ import { MatRippleLoader, RippleGlobalOptions, } from '@angular/material/core'; -import {Subject, Subscription, merge} from 'rxjs'; +import {merge, Subject, Subscription} from 'rxjs'; import {MatChipAction} from './chip-action'; import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons'; import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; -let uid = 0; - /** Represents an event fired on an individual `mat-chip`. */ export interface MatChipEvent { /** The chip the event was fired on. */ @@ -96,6 +94,9 @@ export interface MatChipEvent { imports: [MatChipAction], }) export class MatChip implements OnInit, AfterViewInit, AfterContentInit, DoCheck, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + protected _document: Document; /** Emits when the chip is focused. */ @@ -139,7 +140,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 = this._idGenerator.getId('mat-mdc-chip-'); // 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..bbfcc139f180 100644 --- a/src/material/core/option/optgroup.ts +++ b/src/material/core/option/optgroup.ts @@ -6,17 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import { - Component, - ViewEncapsulation, + booleanAttribute, ChangeDetectionStrategy, - Input, + Component, Inject, - Optional, + inject, InjectionToken, - booleanAttribute, + Input, + Optional, + ViewEncapsulation, } from '@angular/core'; -import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent'; +import {MAT_OPTION_PARENT_COMPONENT, MatOptionParentComponent} from './option-parent'; // Notes on the accessibility pattern used for `mat-optgroup`. // The option group has two different "modes": regular and inert. The regular mode uses the @@ -39,7 +41,6 @@ import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-pa // doesn't read out the text at all. Furthermore, on // Counter for unique group ids. -let _uniqueOptgroupIdCounter = 0; /** * Injection token that can be used to reference instances of `MatOptgroup`. It serves as @@ -68,6 +69,9 @@ export const MAT_OPTGROUP = new InjectionToken('MatOptgroup'); standalone: true, }) export class MatOptgroup { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** Label for the option group. */ @Input() label: string; @@ -75,7 +79,7 @@ export class MatOptgroup { @Input({transform: booleanAttribute}) disabled: boolean = false; /** Unique id for the underlying label. */ - _labelId: string = `mat-optgroup-label-${_uniqueOptgroupIdCounter++}`; + _labelId: string = this._idGenerator.getId('mat-optgroup-label-'); /** 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..dfc9e75a49c8 100644 --- a/src/material/core/option/option.ts +++ b/src/material/core/option/option.ts @@ -6,36 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ -import {FocusableOption, FocusOrigin} from '@angular/cdk/a11y'; +import {FocusableOption, FocusOrigin, IdGenerator} from '@angular/cdk/a11y'; import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes'; import { - Component, - ViewEncapsulation, + AfterViewChecked, + booleanAttribute, ChangeDetectionStrategy, - ElementRef, ChangeDetectorRef, - Optional, + Component, + ElementRef, + EventEmitter, Inject, - AfterViewChecked, - OnDestroy, + inject, Input, + OnDestroy, + Optional, Output, - EventEmitter, QueryList, ViewChild, - booleanAttribute, + ViewEncapsulation, } from '@angular/core'; import {Subject} from 'rxjs'; -import {MAT_OPTGROUP, MatOptgroup} from './optgroup'; -import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent'; import {MatRipple} from '../ripple/ripple'; import {MatPseudoCheckbox} from '../selection/pseudo-checkbox/pseudo-checkbox'; - -/** - * Option IDs need to be unique across components, so this counter exists outside of - * the component definition. - */ -let _uniqueIdCounter = 0; +import {MAT_OPTGROUP, MatOptgroup} from './optgroup'; +import {MAT_OPTION_PARENT_COMPONENT, MatOptionParentComponent} from './option-parent'; /** Event object emitted by MatOption when selected or deselected. */ export class MatOptionSelectionChange { @@ -83,6 +78,9 @@ export class MatOptionSelectionChange { imports: [MatPseudoCheckbox, MatRipple], }) export class MatOption implements FocusableOption, AfterViewChecked, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _selected = false; private _active = false; private _disabled = false; @@ -102,7 +100,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 = this._idGenerator.getId('mat-option-'); /** 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..31c26a3e8798 100644 --- a/src/material/datepicker/calendar-body.ts +++ b/src/material/datepicker/calendar-body.ts @@ -6,25 +6,26 @@ * found in the LICENSE file at https://angular.io/license */ -import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform'; +import {IdGenerator} from '@angular/cdk/a11y'; +import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform'; +import {NgClass} from '@angular/common'; import { + afterNextRender, + AfterViewChecked, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, + inject, + Injector, Input, - Output, - ViewEncapsulation, NgZone, OnChanges, - SimpleChanges, OnDestroy, - AfterViewChecked, - inject, - afterNextRender, - Injector, + Output, + SimpleChanges, + ViewEncapsulation, } from '@angular/core'; -import {NgClass} from '@angular/common'; /** Extra CSS classes that can be associated with a calendar cell. */ export type MatCalendarCellCssClasses = string | string[] | Set | {[key: string]: any}; @@ -61,8 +62,6 @@ export interface MatCalendarUserEvent { event: Event; } -let calendarBodyId = 1; - /** Event options that can be used to bind an active, capturing event. */ const activeCapturingEventOptions = normalizePassiveListenerOptions({ passive: false, @@ -96,6 +95,9 @@ const passiveEventOptions = normalizePassiveListenerOptions({passive: true}); imports: [NgClass], }) export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _platform = inject(Platform); /** @@ -595,7 +597,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView return null; } - private _id = `mat-calendar-body-${calendarBodyId++}`; + private _id = this._idGenerator.getId('mat-calendar-body-'); _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..f387749c43ed 100644 --- a/src/material/datepicker/calendar.ts +++ b/src/material/datepicker/calendar.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {CdkMonitorFocus, IdGenerator} from '@angular/cdk/a11y'; import {CdkPortalOutlet, ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal'; import { AfterContentInit, @@ -15,6 +16,7 @@ import { Component, EventEmitter, forwardRef, + inject, Inject, Input, OnChanges, @@ -26,9 +28,11 @@ import { ViewChild, ViewEncapsulation, } from '@angular/core'; +import {MatButton, MatIconButton} from '@angular/material/button'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {Subject, Subscription} from 'rxjs'; -import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body'; +import {MatCalendarCellClassFunction, MatCalendarUserEvent} from './calendar-body'; +import {DateRange, MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER} from './date-selection-model'; import {createMissingDateImplError} from './datepicker-errors'; import {MatDatepickerIntl} from './datepicker-intl'; import {MatMonthView} from './month-view'; @@ -39,11 +43,6 @@ import { yearsPerPage, } from './multi-year-view'; import {MatYearView} from './year-view'; -import {MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER, DateRange} from './date-selection-model'; -import {MatIconButton, MatButton} from '@angular/material/button'; -import {CdkMonitorFocus} from '@angular/cdk/a11y'; - -let calendarHeaderId = 1; /** * Possible views for the calendar. @@ -62,6 +61,9 @@ export type MatCalendarView = 'month' | 'year' | 'multi-year'; imports: [MatButton, MatIconButton], }) export class MatCalendarHeader { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + constructor( private _intl: MatDatepickerIntl, @Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar, @@ -221,7 +223,7 @@ export class MatCalendarHeader { return [minYearLabel, maxYearLabel]; } - private _id = `mat-calendar-header-${calendarHeaderId++}`; + private _id = this._idGenerator.getId('mat-calendar-header-'); _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..80827c62034d 100644 --- a/src/material/datepicker/date-range-input.ts +++ b/src/material/datepicker/date-range-input.ts @@ -6,29 +6,30 @@ * found in the LICENSE file at https://angular.io/license */ -import {CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y'; +import {CdkMonitorFocus, FocusOrigin, IdGenerator} from '@angular/cdk/a11y'; import { AfterContentInit, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, Inject, + inject, Input, OnChanges, OnDestroy, Optional, Self, + signal, SimpleChanges, ViewEncapsulation, - booleanAttribute, - signal, } from '@angular/core'; import {ControlContainer, NgControl, Validators} from '@angular/forms'; import {DateAdapter, ThemePalette} from '@angular/material/core'; import {MAT_FORM_FIELD, MatFormFieldControl} from '@angular/material/form-field'; -import {Subject, Subscription, merge} from 'rxjs'; +import {merge, Subject, Subscription} from 'rxjs'; import { MAT_DATE_RANGE_INPUT_PARENT, MatDateRangeInputParent, @@ -39,9 +40,7 @@ import {MatDateRangePickerInput} from './date-range-picker'; import {DateRange, MatDateSelectionModel} from './date-selection-model'; import {MatDatepickerControl, MatDatepickerPanel} from './datepicker-base'; import {createMissingDateImplError} from './datepicker-errors'; -import {DateFilterFn, _MatFormFieldPartial, dateInputsHaveChanged} from './datepicker-input-base'; - -let nextUniqueId = 0; +import {_MatFormFieldPartial, DateFilterFn, dateInputsHaveChanged} from './datepicker-input-base'; @Component({ selector: 'mat-date-range-input', @@ -79,6 +78,9 @@ export class MatDateRangeInput OnChanges, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _closedSubscription = Subscription.EMPTY; private _openedSubscription = Subscription.EMPTY; @@ -88,7 +90,7 @@ export class MatDateRangeInput } /** Unique ID for the group. */ - id = `mat-date-range-input-${nextUniqueId++}`; + id = this._idGenerator.getId('mat-date-range-input-'); /** 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..0a0220ee33d2 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -7,7 +7,7 @@ */ import {AnimationEvent} from '@angular/animations'; -import {CdkTrapFocus} from '@angular/cdk/a11y'; +import {CdkTrapFocus, IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceStringArray} from '@angular/cdk/coercion'; import { @@ -78,9 +78,6 @@ import {createMissingDateImplError} from './datepicker-errors'; import {DateFilterFn} from './datepicker-input-base'; import {MatDatepickerIntl} from './datepicker-intl'; -/** Used to generate a unique ID for each datepicker instance. */ -let datepickerUid = 0; - /** Injection token that determines the scroll handling while the calendar is open. */ export const MAT_DATEPICKER_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( 'mat-datepicker-scroll-strategy', @@ -356,6 +353,9 @@ export abstract class MatDatepickerBase< > implements MatDatepickerPanel, OnDestroy, OnChanges { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _scrollStrategy: () => ScrollStrategy; private _inputStateChanges = Subscription.EMPTY; private _document = inject(DOCUMENT); @@ -485,7 +485,7 @@ export abstract class MatDatepickerBase< private _opened = false; /** The id for the datepicker calendar. */ - id: string = `mat-datepicker-${datepickerUid++}`; + id: string = this._idGenerator.getId('mat-datepicker-'); /** 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..f73633bddacb 100644 --- a/src/material/dialog/dialog-content-directives.ts +++ b/src/material/dialog/dialog-content-directives.ts @@ -6,9 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; +import {CdkScrollable} from '@angular/cdk/scrolling'; import { Directive, ElementRef, + inject, Input, OnChanges, OnDestroy, @@ -16,14 +19,10 @@ import { Optional, SimpleChanges, } from '@angular/core'; -import {CdkScrollable} from '@angular/cdk/scrolling'; import {MatDialog} from './dialog'; import {_closeDialogVia, MatDialogRef} from './dialog-ref'; -/** Counter used to generate unique IDs for dialog elements. */ -let dialogElementUid = 0; - /** * Button that will close the current dialog. */ @@ -140,7 +139,10 @@ export abstract class MatDialogLayoutSection implements OnInit, OnDestroy { }, }) export class MatDialogTitle extends MatDialogLayoutSection { - @Input() id: string = `mat-mdc-dialog-title-${dialogElementUid++}`; + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + + @Input() id: string = this._idGenerator.getId('mat-mdc-dialog-title-'); 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..74a8dc6962e6 100644 --- a/src/material/dialog/dialog.ts +++ b/src/material/dialog/dialog.ts @@ -6,12 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; +import {Dialog, DialogConfig} from '@angular/cdk/dialog'; import {ComponentType, Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay'; import {Location} from '@angular/common'; import { ANIMATION_MODULE_TYPE, ComponentRef, Inject, + inject, Injectable, InjectionToken, Injector, @@ -20,14 +23,12 @@ import { SkipSelf, TemplateRef, Type, - inject, } from '@angular/core'; +import {defer, Observable, Subject} from 'rxjs'; +import {startWith} from 'rxjs/operators'; import {MatDialogConfig} from './dialog-config'; import {MatDialogContainer} from './dialog-container'; import {MatDialogRef} from './dialog-ref'; -import {defer, Observable, Subject} from 'rxjs'; -import {Dialog, DialogConfig} from '@angular/cdk/dialog'; -import {startWith} from 'rxjs/operators'; /** Injection token that can be used to access the data that was passed in to a dialog. */ export const MAT_DIALOG_DATA = new InjectionToken('MatMdcDialogData'); @@ -71,14 +72,14 @@ export const MAT_DIALOG_SCROLL_STRATEGY_PROVIDER = { useFactory: MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY, }; -// Counter for unique dialog ids. -let uniqueId = 0; - /** * Service to open Material Design modal dialogs. */ @Injectable({providedIn: 'root'}) export class MatDialog implements OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private readonly _openDialogsAtThisLevel: MatDialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject>(); @@ -178,7 +179,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 || this._idGenerator.getId('mat-mdc-dialog-'); 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..4760f3ffcfaa 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'; @@ -45,9 +45,6 @@ import {MatExpansionPanelContent} from './expansion-panel-content'; /** MatExpansionPanel's states. */ export type MatExpansionPanelState = 'expanded' | 'collapsed'; -/** Counter for generating unique element ids. */ -let uniqueId = 0; - /** * Object that can be used to override the default options * for all of the expansion panels in a module. @@ -146,7 +143,7 @@ export class MatExpansionPanel _portal: TemplatePortal; /** ID for the associated header element. Used for a11y labelling. */ - _headerId = `mat-expansion-panel-header-${uniqueId++}`; + _headerId = this._idGenerator.getId('mat-expansion-panel-header-'); 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..ff875cad45bc 100644 --- a/src/material/form-field/directives/error.ts +++ b/src/material/form-field/directives/error.ts @@ -6,9 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Attribute, Directive, ElementRef, InjectionToken, Input} from '@angular/core'; - -let nextUniqueId = 0; +import {IdGenerator} from '@angular/cdk/a11y'; +import {Attribute, Directive, ElementRef, inject, InjectionToken, Input} from '@angular/core'; /** * Injection token that can be used to reference instances of `MatError`. It serves as @@ -29,7 +28,10 @@ export const MAT_ERROR = new InjectionToken('MatError'); standalone: true, }) export class MatError { - @Input() id: string = `mat-mdc-error-${nextUniqueId++}`; + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + + @Input() id: string = this._idGenerator.getId('mat-mdc-error-'); 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..66ea3641f4e4 100644 --- a/src/material/form-field/directives/hint.ts +++ b/src/material/form-field/directives/hint.ts @@ -6,9 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Input} from '@angular/core'; - -let nextUniqueId = 0; +import {IdGenerator} from '@angular/cdk/a11y'; +import {Directive, inject, Input} from '@angular/core'; /** Hint text to be shown underneath the form field control. */ @Directive({ @@ -23,9 +22,12 @@ let nextUniqueId = 0; standalone: true, }) export class MatHint { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** 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 = this._idGenerator.getId('mat-mdc-hint-'); } diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 24a839919df8..962af492a6ea 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -5,15 +5,17 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {DOCUMENT, NgTemplateOutlet} from '@angular/common'; import {Platform, _getShadowRoot} from '@angular/cdk/platform'; import { - ANIMATION_MODULE_TYPE, AfterContentChecked, AfterContentInit, + afterRender, AfterViewInit, + ANIMATION_MODULE_TYPE, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -21,6 +23,7 @@ import { ContentChildren, ElementRef, Inject, + inject, InjectionToken, Injector, Input, @@ -30,12 +33,10 @@ import { QueryList, ViewChild, ViewEncapsulation, - afterRender, - inject, } from '@angular/core'; import {AbstractControlDirective} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; -import {Subject, merge} from 'rxjs'; +import {merge, Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {MAT_ERROR, MatError} from './directives/error'; import { @@ -104,8 +105,6 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken extends _MatFormFieldControl {} export class MatFormField implements FloatingLabelParent, AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + @ViewChild('textField') _textField: ElementRef; @ViewChild('iconPrefixContainer') _iconPrefixContainer: ElementRef; @ViewChild('textPrefixContainer') _textPrefixContainer: ElementRef; @@ -294,10 +296,10 @@ export class MatFormField _hasTextSuffix = false; // Unique id for the internal form field label. - readonly _labelId = `mat-mdc-form-field-label-${nextUniqueId++}`; + readonly _labelId = this._idGenerator.getId('mat-mdc-form-field-label-'); // Unique id for the hint label. - readonly _hintLabelId = `mat-mdc-hint-${nextUniqueId++}`; + readonly _hintLabelId = this._idGenerator.getId('mat-mdc-hint-'); /** 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..6c4e0e807622 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {getSupportedInputTypes, Platform} from '@angular/cdk/platform'; import {AutofillMonitor} from '@angular/cdk/text-field'; @@ -14,6 +15,7 @@ import { Directive, DoCheck, ElementRef, + inject, Inject, Input, NgZone, @@ -23,8 +25,8 @@ import { Self, } from '@angular/core'; import {FormGroupDirective, NgControl, NgForm, Validators} from '@angular/forms'; -import {ErrorStateMatcher, _ErrorStateTracker} from '@angular/material/core'; -import {MatFormFieldControl, MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field'; +import {_ErrorStateTracker, ErrorStateMatcher} from '@angular/material/core'; +import {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '@angular/material/form-field'; import {Subject} from 'rxjs'; import {getMatInputUnsupportedTypeError} from './input-errors'; import {MAT_INPUT_VALUE_ACCESSOR} from './input-value-accessor'; @@ -42,8 +44,6 @@ const MAT_INPUT_INVALID_TYPES = [ 'submit', ]; -let nextUniqueId = 0; - @Directive({ selector: `input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]`, @@ -82,7 +82,10 @@ let nextUniqueId = 0; export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, AfterViewInit, DoCheck { - protected _uid = `mat-input-${nextUniqueId++}`; + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + + protected _uid = this._idGenerator.getId('mat-input-'); 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..72df820fe77d 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -6,53 +6,51 @@ * found in the LICENSE file at https://angular.io/license */ +import {AnimationEvent} from '@angular/animations'; +import {FocusKeyManager, FocusOrigin, IdGenerator} from '@angular/cdk/a11y'; +import {Direction} from '@angular/cdk/bidi'; +import { + DOWN_ARROW, + ESCAPE, + hasModifierKey, + LEFT_ARROW, + RIGHT_ARROW, + UP_ARROW, +} from '@angular/cdk/keycodes'; import { AfterContentInit, + afterNextRender, + AfterRenderRef, + booleanAttribute, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, EventEmitter, Inject, + inject, InjectionToken, + Injector, Input, NgZone, OnDestroy, + OnInit, Output, - TemplateRef, QueryList, + TemplateRef, ViewChild, ViewEncapsulation, - OnInit, - ChangeDetectorRef, - booleanAttribute, - afterNextRender, - AfterRenderRef, - inject, - Injector, } from '@angular/core'; -import {AnimationEvent} from '@angular/animations'; -import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; -import {Direction} from '@angular/cdk/bidi'; -import { - ESCAPE, - LEFT_ARROW, - RIGHT_ARROW, - DOWN_ARROW, - UP_ARROW, - hasModifierKey, -} from '@angular/cdk/keycodes'; import {merge, Observable, Subject} from 'rxjs'; import {startWith, switchMap} from 'rxjs/operators'; +import {matMenuAnimations} from './menu-animations'; +import {MAT_MENU_CONTENT, MatMenuContent} from './menu-content'; +import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors'; import {MatMenuItem} from './menu-item'; -import {MatMenuPanel, MAT_MENU_PANEL} from './menu-panel'; +import {MAT_MENU_PANEL, MatMenuPanel} from './menu-panel'; import {MenuPositionX, MenuPositionY} from './menu-positions'; -import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors'; -import {MatMenuContent, MAT_MENU_CONTENT} from './menu-content'; -import {matMenuAnimations} from './menu-animations'; - -let menuPanelUid = 0; /** Reason why the menu was closed. */ export type MenuCloseReason = void | 'click' | 'keydown' | 'tab'; @@ -114,6 +112,9 @@ export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions { standalone: true, }) export class MatMenu implements AfterContentInit, MatMenuPanel, OnInit, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _keyManager: FocusKeyManager; private _xPosition: MenuPositionX; private _yPosition: MenuPositionY; @@ -270,7 +271,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnI */ @Output() readonly close: EventEmitter = this.closed; - readonly panelId = `mat-menu-panel-${menuPanelUid++}`; + readonly panelId = this._idGenerator.getId('mat-menu-panel-'); private _injector = inject(Injector); diff --git a/src/material/paginator/paginator.ts b/src/material/paginator/paginator.ts index 0e589fdb4b5d..eb0312f0d196 100644 --- a/src/material/paginator/paginator.ts +++ b/src/material/paginator/paginator.ts @@ -6,27 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ +import {IdGenerator} from '@angular/cdk/a11y'; import { + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, + inject, InjectionToken, Input, + numberAttribute, OnDestroy, OnInit, Optional, Output, ViewEncapsulation, - booleanAttribute, - numberAttribute, } from '@angular/core'; +import {MatIconButton} from '@angular/material/button'; import {MatOption, ThemePalette} from '@angular/material/core'; +import {MatFormField, MatFormFieldAppearance} from '@angular/material/form-field'; import {MatSelect} from '@angular/material/select'; -import {MatIconButton} from '@angular/material/button'; import {MatTooltip} from '@angular/material/tooltip'; -import {MatFormField, MatFormFieldAppearance} from '@angular/material/form-field'; import {Observable, ReplaySubject, Subscription} from 'rxjs'; import {MatPaginatorIntl} from './paginator-intl'; @@ -90,8 +92,6 @@ export const MAT_PAGINATOR_DEFAULT_OPTIONS = new InjectionToken { 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..3129d35c611b 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -9,6 +9,7 @@ import { ActiveDescendantKeyManager, addAriaReferencedId, + IdGenerator, LiveAnnouncer, removeAriaReferencedId, } from '@angular/cdk/a11y'; @@ -32,6 +33,7 @@ import { ScrollStrategy, } from '@angular/cdk/overlay'; import {ViewportRuler} from '@angular/cdk/scrolling'; +import {NgClass} from '@angular/common'; import { AfterContentInit, Attribute, @@ -98,9 +100,6 @@ import { getMatSelectNonArrayValueError, getMatSelectNonFunctionValueError, } from './select-errors'; -import {NgClass} from '@angular/common'; - -let nextUniqueId = 0; /** Injection token that determines the scroll handling while a select is open. */ export const MAT_SELECT_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( @@ -217,6 +216,9 @@ export class MatSelect ControlValueAccessor, MatFormFieldControl { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** All of the defined select options. */ @ContentChildren(MatOption, {descendants: true}) options: QueryList; @@ -308,7 +310,7 @@ export class MatSelect private _compareWith = (o1: any, o2: any) => o1 === o2; /** Unique id for this input. */ - private _uid = `mat-select-${nextUniqueId++}`; + private _uid = this._idGenerator.getId('mat-select-'); /** Current `aria-labelledby` value for the select trigger. */ private _triggerAriaLabelledBy: string | null = null; @@ -363,7 +365,7 @@ export class MatSelect _onTouched = () => {}; /** ID for the DOM node containing the select's value. */ - _valueId = `mat-select-value-${nextUniqueId++}`; + _valueId = this._idGenerator.getId('mat-select-value-'); /** 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..d5cad0d0fb14 100644 --- a/src/material/slide-toggle/slide-toggle.ts +++ b/src/material/slide-toggle/slide-toggle.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {FocusMonitor, IdGenerator} from '@angular/cdk/a11y'; import { AfterContentInit, + ANIMATION_MODULE_TYPE, Attribute, booleanAttribute, ChangeDetectionStrategy, @@ -17,6 +19,7 @@ import { EventEmitter, forwardRef, Inject, + inject, Input, numberAttribute, OnChanges, @@ -26,7 +29,6 @@ import { SimpleChanges, ViewChild, ViewEncapsulation, - ANIMATION_MODULE_TYPE, } from '@angular/core'; import { AbstractControl, @@ -36,12 +38,11 @@ import { ValidationErrors, Validator, } from '@angular/forms'; -import {FocusMonitor} from '@angular/cdk/a11y'; +import {_MatInternalFormField, MatRipple} from '@angular/material/core'; import { MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, MatSlideToggleDefaultOptions, } from './slide-toggle-config'; -import {_MatInternalFormField, MatRipple} from '@angular/material/core'; /** * @deprecated Will stop being exported. @@ -64,7 +65,6 @@ export class MatSlideToggleChange { } // Increasing integer for generating unique ids for slide-toggle components. -let nextUniqueId = 0; @Component({ selector: 'mat-slide-toggle', @@ -100,6 +100,9 @@ let nextUniqueId = 0; export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _onChange = (_: any) => {}; private _onTouched = () => {}; private _validatorOnChange = () => {}; @@ -213,7 +216,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 = this._idGenerator.getId('mat-mdc-slide-toggle-'); 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..f74a7753cdbb 100644 --- a/src/material/snack-bar/snack-bar-container.ts +++ b/src/material/snack-bar/snack-bar-container.ts @@ -6,6 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ +import {AnimationEvent} from '@angular/animations'; +import {AriaLivePoliteness, IdGenerator} from '@angular/cdk/a11y'; +import {Platform} from '@angular/cdk/platform'; +import { + BasePortalOutlet, + CdkPortalOutlet, + ComponentPortal, + DomPortal, + TemplatePortal, +} from '@angular/cdk/portal'; +import {DOCUMENT} from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -19,23 +30,10 @@ import { ViewChild, ViewEncapsulation, } from '@angular/core'; -import {DOCUMENT} from '@angular/common'; -import {matSnackBarAnimations} from './snack-bar-animations'; -import { - BasePortalOutlet, - CdkPortalOutlet, - ComponentPortal, - DomPortal, - TemplatePortal, -} from '@angular/cdk/portal'; import {Observable, Subject} from 'rxjs'; -import {AriaLivePoliteness} from '@angular/cdk/a11y'; -import {Platform} from '@angular/cdk/platform'; -import {AnimationEvent} from '@angular/animations'; +import {matSnackBarAnimations} from './snack-bar-animations'; import {MatSnackBarConfig} from './snack-bar-config'; -let uniqueId = 0; - /** * Internal component that wraps user-provided snack bar content. * @docs-private @@ -60,6 +58,9 @@ let uniqueId = 0; }, }) export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private _document = inject(DOCUMENT); private _trackedModals = new Set(); @@ -104,7 +105,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 = this._idGenerator.getId('mat-snack-bar-container-live-'); constructor( private _ngZone: NgZone, diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts index 1f2f5fa0a69a..0a3166e2f4e4 100644 --- a/src/material/tabs/tab-group.ts +++ b/src/material/tabs/tab-group.ts @@ -6,9 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ +import {CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y'; +import {Platform} from '@angular/cdk/platform'; +import {CdkPortalOutlet} from '@angular/cdk/portal'; import { AfterContentChecked, AfterContentInit, + ANIMATION_MODULE_TYPE, + APP_ID, + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -16,29 +22,24 @@ import { ElementRef, EventEmitter, Inject, + inject, Input, + numberAttribute, OnDestroy, Optional, Output, QueryList, ViewChild, ViewEncapsulation, - booleanAttribute, - inject, - numberAttribute, - ANIMATION_MODULE_TYPE, } from '@angular/core'; -import {MAT_TAB_GROUP, MatTab} from './tab'; -import {MatTabHeader} from './tab-header'; -import {ThemePalette, MatRipple} from '@angular/material/core'; +import {MatRipple, ThemePalette} from '@angular/material/core'; import {merge, Subscription} from 'rxjs'; -import {MAT_TABS_CONFIG, MatTabsConfig} from './tab-config'; import {startWith} from 'rxjs/operators'; -import {CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y'; +import {MAT_TAB_GROUP, MatTab} from './tab'; import {MatTabBody} from './tab-body'; -import {CdkPortalOutlet} from '@angular/cdk/portal'; +import {MAT_TABS_CONFIG, MatTabsConfig} from './tab-config'; +import {MatTabHeader} from './tab-header'; import {MatTabLabelWrapper} from './tab-label-wrapper'; -import {Platform} from '@angular/cdk/platform'; /** Used to generate unique ID's for each tab component */ let nextId = 0; @@ -94,6 +95,9 @@ const ENABLE_BACKGROUND_INPUT = true; ], }) export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDestroy { + /** Generator for assigning unique IDs to DOM elements. */ + 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..b7e33865835e 100644 --- a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts @@ -5,10 +5,17 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FocusableOption, FocusMonitor, IdGenerator} from '@angular/cdk/a11y'; +import {Directionality} from '@angular/cdk/bidi'; +import {ENTER, SPACE} from '@angular/cdk/keycodes'; +import {CdkObserveContent} from '@angular/cdk/observers'; +import {Platform} from '@angular/cdk/platform'; +import {ViewportRuler} from '@angular/cdk/scrolling'; import { AfterContentChecked, AfterContentInit, AfterViewInit, + ANIMATION_MODULE_TYPE, Attribute, booleanAttribute, ChangeDetectionStrategy, @@ -18,6 +25,7 @@ import { ElementRef, forwardRef, Inject, + inject, Input, NgZone, numberAttribute, @@ -26,7 +34,6 @@ import { QueryList, ViewChild, ViewEncapsulation, - ANIMATION_MODULE_TYPE, } from '@angular/core'; import { MAT_RIPPLE_GLOBAL_OPTIONS, @@ -36,20 +43,11 @@ import { RippleTarget, ThemePalette, } from '@angular/material/core'; -import {FocusableOption, FocusMonitor} from '@angular/cdk/a11y'; -import {Directionality} from '@angular/cdk/bidi'; -import {ViewportRuler} from '@angular/cdk/scrolling'; -import {Platform} from '@angular/cdk/platform'; -import {MatInkBar, InkBarItem} from '../ink-bar'; import {BehaviorSubject, Subject} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; -import {ENTER, SPACE} from '@angular/cdk/keycodes'; -import {MAT_TABS_CONFIG, MatTabsConfig} from '../tab-config'; +import {InkBarItem, MatInkBar} from '../ink-bar'; import {MatPaginatedTabHeader} from '../paginated-tab-header'; -import {CdkObserveContent} from '@angular/cdk/observers'; - -// Increasing integer for generating unique ids for tab nav components. -let nextUniqueId = 0; +import {MAT_TABS_CONFIG, MatTabsConfig} from '../tab-config'; /** * Navigation component matching the styles of the tab group header. @@ -265,6 +263,9 @@ export class MatTabLink extends InkBarItem implements AfterViewInit, OnDestroy, RippleTarget, FocusableOption { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + private readonly _destroyed = new Subject(); /** Whether the tab link is active or not. */ @@ -318,7 +319,7 @@ export class MatTabLink } /** Unique id for the tab. */ - @Input() id = `mat-tab-link-${nextUniqueId++}`; + @Input() id = this._idGenerator.getId('mat-tab-link-'); constructor( private _tabNavBar: MatTabNav, @@ -431,8 +432,11 @@ export class MatTabLink standalone: true, }) export class MatTabNavPanel { + /** Generator for assigning unique IDs to DOM elements. */ + private _idGenerator = inject(IdGenerator); + /** Unique id for the tab panel. */ - @Input() id = `mat-tab-nav-panel-${nextUniqueId++}`; + @Input() id = this._idGenerator.getId('mat-tab-nav-panel-'); /** Id of the active tab in the nav bar. */ _activeTabId?: string; diff --git a/tools/public_api_guard/cdk/accordion.md b/tools/public_api_guard/cdk/accordion.md index 716cac8f1b5a..54f3362d9ed5 100644 --- a/tools/public_api_guard/cdk/accordion.md +++ b/tools/public_api_guard/cdk/accordion.md @@ -7,6 +7,7 @@ import { ChangeDetectorRef } from '@angular/core'; import { EventEmitter } from '@angular/core'; import * as i0 from '@angular/core'; +import { IdGenerator } from '@angular/cdk/a11y'; import { InjectionToken } from '@angular/core'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; @@ -53,6 +54,8 @@ export class CdkAccordionItem implements OnDestroy { protected _expansionDispatcher: UniqueSelectionDispatcher; readonly id: string; // (undocumented) + protected _idGenerator: IdGenerator; + // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) static ngAcceptInputType_expanded: unknown;