From e27c285e4e63e6a8dd177545ea590d8150e18985 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 12:50:52 +0100 Subject: [PATCH 01/30] refactor(cdk/a11y): add ID generator service Adds a service to generate unique IDs. --- src/cdk/a11y/id-generator.ts | 40 ++++++++++++++++++++++++++++++ src/cdk/a11y/public-api.ts | 1 + tools/public_api_guard/cdk/a11y.md | 9 +++++++ 3 files changed, 50 insertions(+) create mode 100644 src/cdk/a11y/id-generator.ts diff --git a/src/cdk/a11y/id-generator.ts b/src/cdk/a11y/id-generator.ts new file mode 100644 index 000000000000..55bae7a0a551 --- /dev/null +++ b/src/cdk/a11y/id-generator.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {APP_ID, inject, Injectable} from '@angular/core'; + +/** + * Keeps track of the ID count per prefix. This helps us make the IDs a bit more deterministic + * like they were before the service was introduced. Note that ideally we wouldn't have to do + * this, but there are some internal tests that rely on the IDs. + */ +const counters: Record = {}; + +/** Service that generates unique IDs for DOM nodes. */ +@Injectable({providedIn: 'root'}) +export class _IdGenerator { + private _appId = inject(APP_ID); + + /** + * Generates a unique ID with a specific prefix. + * @param prefix Prefix to add to the ID. + */ + getId(prefix: string): string { + // Omit the app ID if it's the default `ng`. Since the vast majority of pages have one + // Angular app on them, we can reduce the amount of breakages by not adding it. + if (this._appId !== 'ng') { + prefix += this._appId; + } + + if (!counters.hasOwnProperty(prefix)) { + counters[prefix] = 0; + } + + return `${prefix}${counters[prefix]++}`; + } +} diff --git a/src/cdk/a11y/public-api.ts b/src/cdk/a11y/public-api.ts index ead2d81e9b63..df192ec2c339 100644 --- a/src/cdk/a11y/public-api.ts +++ b/src/cdk/a11y/public-api.ts @@ -36,3 +36,4 @@ export { HighContrastModeDetector, HighContrastMode, } from './high-contrast-mode/high-contrast-mode-detector'; +export {_IdGenerator} from './id-generator'; diff --git a/tools/public_api_guard/cdk/a11y.md b/tools/public_api_guard/cdk/a11y.md index 22d42d45c361..d447dca237d1 100644 --- a/tools/public_api_guard/cdk/a11y.md +++ b/tools/public_api_guard/cdk/a11y.md @@ -291,6 +291,15 @@ export interface Highlightable extends ListKeyManagerOption { setInactiveStyles(): void; } +// @public +export class _IdGenerator { + getId(prefix: string): string; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration<_IdGenerator, never>; + // (undocumented) + static ɵprov: i0.ɵɵInjectableDeclaration<_IdGenerator>; +} + // @public export const INPUT_MODALITY_DETECTOR_DEFAULT_OPTIONS: InputModalityDetectorOptions; From 3a7c9db75b6f770b655c848b8fbf39d3b2dc4e39 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:56:59 +0100 Subject: [PATCH 02/30] refactor(cdk/accordion): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/accordion/BUILD.bazel | 1 + src/cdk/accordion/accordion-item.ts | 6 ++---- src/cdk/accordion/accordion.ts | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) 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 1bf8f117601f..9c1a856b24c0 100644 --- a/src/cdk/accordion/accordion-item.ts +++ b/src/cdk/accordion/accordion-item.ts @@ -17,13 +17,11 @@ import { inject, OnInit, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; 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; - /** * A basic directive expected to be extended and decorated as a component. Sets up all * events and attributes needed to be managed by a CdkAccordion parent. @@ -59,7 +57,7 @@ export class CdkAccordionItem implements OnInit, OnDestroy { @Output() readonly expandedChange: EventEmitter = new EventEmitter(); /** The unique AccordionItem id. */ - readonly id: string = `cdk-accordion-child-${nextId++}`; + readonly id: string = inject(_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 890634dec198..b110380f67c8 100644 --- a/src/cdk/accordion/accordion.ts +++ b/src/cdk/accordion/accordion.ts @@ -14,12 +14,11 @@ import { OnDestroy, SimpleChanges, booleanAttribute, + inject, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; 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 @@ -43,7 +42,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 = inject(_IdGenerator).getId('cdk-accordion-'); /** Whether the accordion should allow multiple expanded accordion items simultaneously. */ @Input({transform: booleanAttribute}) multi: boolean = false; From e7b57ee7cc1caf86e12076467f26fd2101319e4c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:57:12 +0100 Subject: [PATCH 03/30] refactor(cdk/stepper): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/stepper/stepper.ts | 11 ++++------- tools/public_api_guard/cdk/stepper.md | 1 - 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 49393aafeefe..95f08f9942a3 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusableOption, FocusKeyManager} from '@angular/cdk/a11y'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes'; import { @@ -46,9 +46,6 @@ import {startWith, takeUntil} from 'rxjs/operators'; import {CdkStepHeader} from './step-header'; import {CdkStepLabel} from './step-label'; -/** Used to generate unique ID for each stepper component. */ -let nextId = 0; - /** * Position state of the content of each step in stepper that is used for transitioning * the content into correct position upon step selection change. @@ -324,7 +321,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { @Output() readonly selectedIndexChange: EventEmitter = new EventEmitter(); /** Used to track unique ID for each stepper component. */ - _groupId = nextId++; + private _groupId = inject(_IdGenerator).getId('cdk-stepper-'); /** Orientation of the stepper. */ @Input() @@ -434,12 +431,12 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { /** Returns a unique id for each step label element. */ _getStepLabelId(i: number): string { - return `cdk-step-label-${this._groupId}-${i}`; + return `${this._groupId}-label-${i}`; } /** Returns unique id for each step content element. */ _getStepContentId(i: number): string { - return `cdk-step-content-${this._groupId}-${i}`; + return `${this._groupId}-content-${i}`; } /** Marks the component to be change detected. */ diff --git a/tools/public_api_guard/cdk/stepper.md b/tools/public_api_guard/cdk/stepper.md index 6c82b9b106b9..5279141041e4 100644 --- a/tools/public_api_guard/cdk/stepper.md +++ b/tools/public_api_guard/cdk/stepper.md @@ -100,7 +100,6 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { _getIndicatorType(index: number, state?: StepState): StepState; _getStepContentId(i: number): string; _getStepLabelId(i: number): string; - _groupId: number; linear: boolean; next(): void; // (undocumented) From 17eeadee2486bc31c4ee6d61e92665dab8a55697 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:57:33 +0100 Subject: [PATCH 04/30] refactor(material/datepicker): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/datepicker/calendar-body.ts | 20 +++++++++++-------- .../datepicker/calendar-header.spec.ts | 4 +++- src/material/datepicker/calendar.ts | 8 ++------ src/material/datepicker/date-range-input.ts | 6 ++---- src/material/datepicker/datepicker-base.ts | 7 ++----- tools/public_api_guard/material/datepicker.md | 2 -- 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts index 2d23941cb471..6e67911f0f1d 100644 --- a/src/material/datepicker/calendar-body.ts +++ b/src/material/datepicker/calendar-body.ts @@ -24,6 +24,7 @@ import { afterNextRender, Injector, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {NgClass} from '@angular/common'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {_StructuralStylesLoader} from '@angular/material/core'; @@ -63,8 +64,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, @@ -195,6 +194,12 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView /** Width of an individual cell. */ _cellWidth: string; + /** ID for the start date label. */ + _startDateLabelId: string; + + /** ID for the end date label. */ + _endDateLabelId: string; + private _didDragSinceMouseDown = false; private _injector = inject(Injector); @@ -209,7 +214,12 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView constructor(...args: unknown[]); constructor() { + const idGenerator = inject(_IdGenerator); + this._startDateLabelId = idGenerator.getId('mat-calendar-body-start-'); + this._endDateLabelId = idGenerator.getId('mat-calendar-body-start-'); + inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); + this._ngZone.runOutsideAngular(() => { const element = this._elementRef.nativeElement; @@ -597,12 +607,6 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView return null; } - - private _id = `mat-calendar-body-${calendarBodyId++}`; - - _startDateLabelId = `${this._id}-start-date`; - - _endDateLabelId = `${this._id}-end-date`; } /** Checks whether a node is a table cell element. */ diff --git a/src/material/datepicker/calendar-header.spec.ts b/src/material/datepicker/calendar-header.spec.ts index a03bbcaf935f..c899dcd1285b 100644 --- a/src/material/datepicker/calendar-header.spec.ts +++ b/src/material/datepicker/calendar-header.spec.ts @@ -199,7 +199,9 @@ 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-period-label-\w+[0-9]+/i, + ); }); }); diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts index 7e4302d2e90a..18c97679e604 100644 --- a/src/material/datepicker/calendar.ts +++ b/src/material/datepicker/calendar.ts @@ -39,11 +39,9 @@ import { 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'; +import {_IdGenerator, CdkMonitorFocus} from '@angular/cdk/a11y'; import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private'; -let calendarHeaderId = 1; - /** * Possible views for the calendar. * @docs-private @@ -222,9 +220,7 @@ export class MatCalendarHeader { return [minYearLabel, maxYearLabel]; } - private _id = `mat-calendar-header-${calendarHeaderId++}`; - - _periodButtonLabelId = `${this._id}-period-label`; + _periodButtonLabelId = inject(_IdGenerator).getId('mat-calendar-period-label-'); } /** A calendar that is used as part of the datepicker. */ diff --git a/src/material/datepicker/date-range-input.ts b/src/material/datepicker/date-range-input.ts index d1bdb1416a87..935726662b54 100644 --- a/src/material/datepicker/date-range-input.ts +++ b/src/material/datepicker/date-range-input.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y'; +import {_IdGenerator, CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y'; import { AfterContentInit, ChangeDetectionStrategy, @@ -39,8 +39,6 @@ import {MatDatepickerControl, MatDatepickerPanel} from './datepicker-base'; import {createMissingDateImplError} from './datepicker-errors'; import {DateFilterFn, _MatFormFieldPartial, dateInputsHaveChanged} from './datepicker-input-base'; -let nextUniqueId = 0; - @Component({ selector: 'mat-date-range-input', templateUrl: 'date-range-input.html', @@ -90,7 +88,7 @@ export class MatDateRangeInput } /** Unique ID for the group. */ - id = `mat-date-range-input-${nextUniqueId++}`; + id: string = inject(_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 b63170cad814..7ccbbbbf3a03 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 {_IdGenerator, CdkTrapFocus} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceStringArray} from '@angular/cdk/coercion'; import { @@ -76,9 +76,6 @@ import {DateFilterFn} from './datepicker-input-base'; import {MatDatepickerIntl} from './datepicker-intl'; import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private'; -/** 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', @@ -497,7 +494,7 @@ export abstract class MatDatepickerBase< private _opened = false; /** The id for the datepicker calendar. */ - id: string = `mat-datepicker-${datepickerUid++}`; + id: string = inject(_IdGenerator).getId('mat-datepicker-'); /** The minimum selectable date. */ _getMinDate(): D | null { diff --git a/tools/public_api_guard/material/datepicker.md b/tools/public_api_guard/material/datepicker.md index 43c985a3f525..a0c8a859fdad 100644 --- a/tools/public_api_guard/material/datepicker.md +++ b/tools/public_api_guard/material/datepicker.md @@ -203,7 +203,6 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView // (undocumented) _emitActiveDateChange(cell: MatCalendarCell, event: FocusEvent): void; endDateAccessibleName: string | null; - // (undocumented) _endDateLabelId: string; endValue: number; _firstRowOffset: number; @@ -240,7 +239,6 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterView _scheduleFocusActiveCellAfterViewChecked(): void; readonly selectedValueChange: EventEmitter>; startDateAccessibleName: string | null; - // (undocumented) _startDateLabelId: string; startValue: number; todayValue: number; From 33328988b5954fded69012dcd513616f8f14bca8 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:57:50 +0100 Subject: [PATCH 05/30] refactor(material/badge): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/badge/badge.ts | 10 +++------- tools/public_api_guard/material/badge.md | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/material/badge/badge.ts b/src/material/badge/badge.ts index f6d8b2b44eee..7a08cbbf9d0a 100644 --- a/src/material/badge/badge.ts +++ b/src/material/badge/badge.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {AriaDescriber, InteractivityChecker} from '@angular/cdk/a11y'; +import {_IdGenerator, AriaDescriber, InteractivityChecker} from '@angular/cdk/a11y'; import {DOCUMENT} from '@angular/common'; import { booleanAttribute, @@ -26,8 +26,6 @@ import { import {ThemePalette} from '@angular/material/core'; import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private'; -let nextId = 0; - /** Allowed position options for matBadgePosition */ export type MatBadgePosition = | 'above after' @@ -79,6 +77,7 @@ export class MatBadge implements OnInit, OnDestroy { private _ariaDescriber = inject(AriaDescriber); private _renderer = inject(Renderer2); private _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true}); + private _idGenerator = inject(_IdGenerator); /** * Theme color of the badge. This API is supported in M2 themes only, it @@ -135,9 +134,6 @@ export class MatBadge implements OnInit, OnDestroy { /** Whether the badge is hidden. */ @Input({alias: 'matBadgeHidden', transform: booleanAttribute}) hidden: boolean; - /** Unique id for the badge */ - _id: number = nextId++; - /** Visible badge element. */ private _badgeElement: HTMLElement | undefined; @@ -236,7 +232,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/tools/public_api_guard/material/badge.md b/tools/public_api_guard/material/badge.md index 85c9f45e0869..e42871f140e8 100644 --- a/tools/public_api_guard/material/badge.md +++ b/tools/public_api_guard/material/badge.md @@ -23,7 +23,6 @@ export class MatBadge implements OnInit, OnDestroy { disabled: boolean; getBadgeElement(): HTMLElement | undefined; hidden: boolean; - _id: number; isAbove(): boolean; isAfter(): boolean; // (undocumented) From b15ee194a12142134f6be27c7e33867af79ab27b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:58:20 +0100 Subject: [PATCH 06/30] refactor(cdk/dialog): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/dialog/dialog.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cdk/dialog/dialog.ts b/src/cdk/dialog/dialog.ts index 468e0f81a76f..0c9e9f06c8e2 100644 --- a/src/cdk/dialog/dialog.ts +++ b/src/cdk/dialog/dialog.ts @@ -21,6 +21,7 @@ import {of as observableOf, Observable, Subject, defer} from 'rxjs'; import {DialogRef} from './dialog-ref'; import {DialogConfig} from './dialog-config'; import {Directionality} from '@angular/cdk/bidi'; +import {_IdGenerator} from '@angular/cdk/a11y'; import { ComponentType, Overlay, @@ -33,9 +34,6 @@ import {startWith} from 'rxjs/operators'; import {DEFAULT_DIALOG_CONFIG, DIALOG_DATA, DIALOG_SCROLL_STRATEGY} from './dialog-injectors'; import {CdkDialogContainer} from './dialog-container'; -/** Unique id for the created dialog. */ -let uniqueId = 0; - @Injectable({providedIn: 'root'}) export class Dialog implements OnDestroy { private _overlay = inject(Overlay); @@ -43,6 +41,7 @@ export class Dialog implements OnDestroy { private _defaultOptions = inject(DEFAULT_DIALOG_CONFIG, {optional: true}); private _parentDialog = inject(Dialog, {optional: true, skipSelf: true}); private _overlayContainer = inject(OverlayContainer); + private _idGenerator = inject(_IdGenerator); private _openDialogsAtThisLevel: DialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); @@ -110,7 +109,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 && From 7d13dd8f7ed2fd1471ebdc61cd60f8169aee3ad8 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:58:31 +0100 Subject: [PATCH 07/30] refactor(cdk/drag-drop): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/drag-drop/directives/drop-list.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index f4fce01c96f7..1e6e6ff315e2 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -19,6 +19,7 @@ import { inject, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import {CDK_DROP_LIST, CdkDrag} from './drag'; import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from '../drag-events'; @@ -31,9 +32,6 @@ import {merge, Subject} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; import {assertElementNode} from './assertions'; -/** Counter used to generate unique ids for drop zones. */ -let _uniqueIdCounter = 0; - /** Container that wraps a set of draggable items. */ @Directive({ selector: '[cdkDropList], cdk-drop-list', @@ -91,7 +89,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 = inject(_IdGenerator).getId('cdk-drop-list-'); /** Locks the position of the draggable elements inside the container along the specified axis. */ @Input('cdkDropListLockAxis') lockAxis: DragAxis; From eb15a298c68e04eba3470afcc47d0d5c1e3bc007 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:58:39 +0100 Subject: [PATCH 08/30] refactor(cdk/listbox): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/listbox/listbox.spec.ts | 4 ++-- src/cdk/listbox/listbox.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/cdk/listbox/listbox.spec.ts b/src/cdk/listbox/listbox.spec.ts index 92a545461b3f..46e304fae7f6 100644 --- a/src/cdk/listbox/listbox.spec.ts +++ b/src/cdk/listbox/listbox.spec.ts @@ -45,10 +45,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+\d+/); } expect(listbox.id).toEqual(listboxEl.id); - expect(listbox.id).toMatch(/cdk-listbox-\d+/); + expect(listbox.id).toMatch(/cdk-listbox-\w+\d+/); }); it('should not overwrite user given ids', () => { diff --git a/src/cdk/listbox/listbox.ts b/src/cdk/listbox/listbox.ts index c27bef5bcaee..8a342736674f 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.dev/license */ -import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y'; +import { + _IdGenerator, + ActiveDescendantKeyManager, + Highlightable, + 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 @@ -104,7 +106,7 @@ export class CdkOption implements ListKeyManagerOption, Highlightab this._id = value; } private _id: string; - private _generatedId = `cdk-option-${nextId++}`; + private _generatedId = inject(_IdGenerator).getId('cdk-option-'); /** The value of this option. */ @Input('cdkOption') value: T; @@ -262,7 +264,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, Con this._id = value; } private _id: string; - private _generatedId = `cdk-listbox-${nextId++}`; + private _generatedId = inject(_IdGenerator).getId('cdk-listbox-'); /** The tabindex to use when the listbox is enabled. */ @Input('tabindex') From 6a7fd7b6466902788935e6cff12c05de89d57c6a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:58:47 +0100 Subject: [PATCH 09/30] refactor(cdk/menu): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/menu/menu-base.ts | 7 ++----- src/cdk/menu/menu-item-radio.ts | 8 +++----- src/cdk/menu/menu-stack.ts | 8 +++----- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/cdk/menu/menu-base.ts b/src/cdk/menu/menu-base.ts index ecd908a08466..93be3a9819e1 100644 --- a/src/cdk/menu/menu-base.ts +++ b/src/cdk/menu/menu-base.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { AfterContentInit, @@ -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. @@ -70,7 +67,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: string = inject(_IdGenerator).getId('cdk-menu-'); /** All child MenuItem elements nested in this Menu. */ @ContentChildren(CdkMenuItem, {descendants: true}) diff --git a/src/cdk/menu/menu-item-radio.ts b/src/cdk/menu/menu-item-radio.ts index ce6d8348f2f8..a5c55c6d804e 100644 --- a/src/cdk/menu/menu-item-radio.ts +++ b/src/cdk/menu/menu-item-radio.ts @@ -6,14 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import {Directive, inject, OnDestroy} from '@angular/core'; +import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {CdkMenuItemSelectable} from './menu-item-selectable'; import {CdkMenuItem} from './menu-item'; -/** Counter used to set a unique id and name for a selectable item */ -let nextId = 0; - /** * A directive providing behavior for the "menuitemradio" ARIA role, which behaves similarly to * a conventional radio-button. Any sibling `CdkMenuItemRadio` instances within the same `CdkMenu` @@ -36,7 +34,7 @@ export class CdkMenuItemRadio extends CdkMenuItemSelectable implements OnDestroy private readonly _selectionDispatcher = inject(UniqueSelectionDispatcher); /** An ID to identify this radio item to the `UniqueSelectionDispatcher`. */ - private _id = `${nextId++}`; + private _id = inject(_IdGenerator).getId('cdk-menu-item-radio-'); /** Function to unregister the selection dispatcher */ private _removeDispatcherListener: () => void; diff --git a/src/cdk/menu/menu-stack.ts b/src/cdk/menu/menu-stack.ts index 7d3b48e71e48..b9c397b5ff2f 100644 --- a/src/cdk/menu/menu-stack.ts +++ b/src/cdk/menu/menu-stack.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Inject, Injectable, InjectionToken, Optional, SkipSelf} from '@angular/core'; +import {inject, Inject, Injectable, InjectionToken, Optional, SkipSelf} from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {Observable, Subject} from 'rxjs'; import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators'; @@ -58,9 +59,6 @@ export interface MenuStackCloseEvent { focusParentTrigger?: boolean; } -/** The next available menu stack ID. */ -let nextId = 0; - /** * MenuStack allows subscribers to listen for close events (when a MenuStackItem is popped off * of the stack) in order to perform closing actions. Upon the MenuStack being empty it emits @@ -70,7 +68,7 @@ let nextId = 0; @Injectable() export class MenuStack { /** The ID of this menu stack. */ - readonly id = `${nextId++}`; + readonly id = inject(_IdGenerator).getId('cdk-menu-stack-'); /** All MenuStackItems tracked by this MenuStack. */ private readonly _elements: MenuStackItem[] = []; From 7a8f780fec727c28e5fbdf28f18c29b82d093c46 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:58:54 +0100 Subject: [PATCH 10/30] refactor(cdk/overlay): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk/overlay/BUILD.bazel | 1 + src/cdk/overlay/overlay.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cdk/overlay/BUILD.bazel b/src/cdk/overlay/BUILD.bazel index 03eefa013649..d3a7e69da539 100644 --- a/src/cdk/overlay/BUILD.bazel +++ b/src/cdk/overlay/BUILD.bazel @@ -23,6 +23,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 35cf6c7b18fd..b7cb242a5574 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -18,6 +18,7 @@ import { EnvironmentInjector, inject, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher'; import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher'; @@ -27,9 +28,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; - /** * Service to create Overlays. Overlays are dynamically added pieces of floating UI, meant to be * used as a low-level building block for other components. Dialogs, tooltips, menus, @@ -51,6 +49,7 @@ export class Overlay { private _location = inject(Location); private _outsideClickDispatcher = inject(OverlayOutsideClickDispatcher); private _animationsModuleType = inject(ANIMATION_MODULE_TYPE, {optional: true}); + private _idGenerator = inject(_IdGenerator); private _appRef: ApplicationRef; private _styleLoader = inject(_CdkPrivateStyleLoader); @@ -106,7 +105,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); From 600dec54599d566b633f389ae750ff6d608dfcd3 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:59:16 +0100 Subject: [PATCH 11/30] refactor(cdk-experimental/column-resize): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk-experimental/column-resize/column-resize.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cdk-experimental/column-resize/column-resize.ts b/src/cdk-experimental/column-resize/column-resize.ts index 4b084dcc17a0..0ec6b7a7c3fc 100644 --- a/src/cdk-experimental/column-resize/column-resize.ts +++ b/src/cdk-experimental/column-resize/column-resize.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import {AfterViewInit, Directive, ElementRef, NgZone, OnDestroy} from '@angular/core'; +import {AfterViewInit, Directive, ElementRef, inject, NgZone, OnDestroy} from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {fromEvent, merge, Subject} from 'rxjs'; import {filter, map, mapTo, pairwise, startWith, take, takeUntil} from 'rxjs/operators'; @@ -19,14 +20,13 @@ import {HeaderRowEventDispatcher} from './event-dispatcher'; const HOVER_OR_ACTIVE_CLASS = 'cdk-column-resize-hover-or-active'; const WITH_RESIZED_COLUMN_CLASS = 'cdk-column-resize-with-resized-column'; -let nextId = 0; - /** * Base class for ColumnResize directives which attach to mat-table elements to * provide common events and services for column resizing. */ @Directive() export abstract class ColumnResize implements AfterViewInit, OnDestroy { + private _idGenerator = inject(_IdGenerator); protected readonly destroyed = new Subject(); /* Publicly accessible interface for triggering and being notified of resizes. */ @@ -40,7 +40,7 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy { protected abstract readonly notifier: ColumnResizeNotifierSource; /** Unique ID for this table instance. */ - protected readonly selectorId = `${++nextId}`; + protected readonly selectorId = this._idGenerator.getId('cdk-column-resize-'); /** The id attribute of the table, if specified. */ id?: string; @@ -60,7 +60,7 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy { /** Gets the unique CSS class name for this table instance. */ getUniqueCssClass() { - return `cdk-column-resize-${this.selectorId}`; + return this.selectorId; } /** Called when a column in the table is resized. Applies a css class to the table element. */ From 410cc6b46c99fef510a20e68fe2466995461f939 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:59:27 +0100 Subject: [PATCH 12/30] refactor(cdk-experimental/combobox): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk-experimental/combobox/combobox-popup.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cdk-experimental/combobox/combobox-popup.ts b/src/cdk-experimental/combobox/combobox-popup.ts index c452e713d8ee..0f5912611c43 100644 --- a/src/cdk-experimental/combobox/combobox-popup.ts +++ b/src/cdk-experimental/combobox/combobox-popup.ts @@ -7,10 +7,9 @@ */ import {Directive, ElementRef, Input, OnInit, inject} from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {AriaHasPopupValue, CDK_COMBOBOX, CdkCombobox} from './combobox'; -let nextId = 0; - @Directive({ selector: '[cdkComboboxPopup]', exportAs: 'cdkComboboxPopup', @@ -44,7 +43,7 @@ export class CdkComboboxPopup implements OnInit { } private _firstFocusElement: HTMLElement; - @Input() id = `cdk-combobox-popup-${nextId++}`; + @Input() id: string = inject(_IdGenerator).getId('cdk-combobox-popup-'); ngOnInit() { this.registerWithPanel(); From 662850c743b59de98bdbb6ef45faf4b0cf571812 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 13:59:53 +0100 Subject: [PATCH 13/30] refactor(cdk-experimental/table-scroll-container): use ID generator Switches to using the ID generator service to create unique IDs. --- src/cdk-experimental/table-scroll-container/BUILD.bazel | 1 + .../table-scroll-container/table-scroll-container.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) 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 0b88717878bc..2182bc795ebc 100644 --- a/src/cdk-experimental/table-scroll-container/table-scroll-container.ts +++ b/src/cdk-experimental/table-scroll-container/table-scroll-container.ts @@ -7,6 +7,7 @@ */ import {CSP_NONCE, Directive, ElementRef, OnDestroy, OnInit, inject} from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {DOCUMENT} from '@angular/common'; import {Directionality} from '@angular/cdk/bidi'; import {_getShadowRoot} from '@angular/cdk/platform'; @@ -17,8 +18,6 @@ import { StickyUpdate, } from '@angular/cdk/table'; -let nextId = 0; - /** * Applies styles to the host element that make its scrollbars match up with * the non-sticky scrollable portions of the CdkTable contained within. @@ -43,7 +42,7 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes private readonly _directionality = inject(Directionality, {optional: true}); private readonly _nonce = inject(CSP_NONCE, {optional: true}); - private readonly _uniqueClassName = `cdk-table-scroll-container-${++nextId}`; + private readonly _uniqueClassName = inject(_IdGenerator).getId('cdk-table-scroll-container-'); private _styleRoot!: Node; private _styleElement?: HTMLStyleElement; From e4ce1a02cc6683589bf0533e695c1205f45ab14c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:01 +0100 Subject: [PATCH 14/30] refactor(material/autocomplete): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/autocomplete/autocomplete.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts index 4565f4e18a4f..d9a1af93344d 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -33,17 +33,11 @@ import { MatOption, ThemePalette, } from '@angular/material/core'; -import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; +import {_IdGenerator, 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; - /** Event object that is emitted when an autocomplete option is selected. */ export class MatAutocompleteSelectedEvent { constructor( @@ -247,7 +241,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 = inject(_IdGenerator).getId('mat-autocomplete-'); /** * Tells any descendant `mat-optgroup` to use the inert a11y pattern. From ac0ea706744dfe729a8dcbb8c978bbb9ca92640d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:09 +0100 Subject: [PATCH 15/30] refactor(material/button-toggle): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/button-toggle/button-toggle.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/material/button-toggle/button-toggle.ts b/src/material/button-toggle/button-toggle.ts index bd03ff5ec284..8a9c9dba76d7 100644 --- a/src/material/button-toggle/button-toggle.ts +++ b/src/material/button-toggle/button-toggle.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {FocusMonitor} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusMonitor} from '@angular/cdk/a11y'; import {SelectionModel} from '@angular/cdk/collections'; import {DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, SPACE, ENTER} from '@angular/cdk/keycodes'; import { @@ -104,9 +104,6 @@ export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = { multi: true, }; -// Counter used to generate unique IDs. -let uniqueIdCounter = 0; - /** Change event object emitted by button toggle. */ export class MatButtonToggleChange { constructor( @@ -181,7 +178,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After this._name = value; this._markButtonsForCheck(); } - private _name = `mat-button-toggle-group-${uniqueIdCounter++}`; + private _name = inject(_IdGenerator).getId('mat-button-toggle-group-'); /** Whether the toggle group is vertical. */ @Input({transform: booleanAttribute}) vertical: boolean; @@ -562,6 +559,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy { private _changeDetectorRef = inject(ChangeDetectorRef); private _elementRef = inject>(ElementRef); private _focusMonitor = inject(FocusMonitor); + private _idGenerator = inject(_IdGenerator); private _checked = false; @@ -685,7 +683,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy { ngOnInit() { const group = this.buttonToggleGroup; - this.id = this.id || `mat-button-toggle-${uniqueIdCounter++}`; + this.id = this.id || this._idGenerator.getId('mat-button-toggle-'); if (group) { if (group._isPrechecked(this)) { From d13ce953227bc25f9b3721cea2da2fb41548e56f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:17 +0100 Subject: [PATCH 16/30] refactor(material/checkbox): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/checkbox/checkbox.spec.ts | 6 +++--- src/material/checkbox/checkbox.ts | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/material/checkbox/checkbox.spec.ts b/src/material/checkbox/checkbox.spec.ts index 35fb4441d91e..9b2c2aa5b4f4 100644 --- a/src/material/checkbox/checkbox.spec.ts +++ b/src/material/checkbox/checkbox.spec.ts @@ -298,7 +298,7 @@ describe('MatCheckbox', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - expect(checkboxInstance.inputId).toMatch(/mat-mdc-checkbox-\d+/); + expect(checkboxInstance.inputId).toMatch(/mat-mdc-checkbox-\w+\d+/); expect(inputElement.id).toBe(checkboxInstance.inputId); })); @@ -965,8 +965,8 @@ describe('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+\d+-input/); + expect(secondId).toMatch(/mat-mdc-checkbox-\w+\d+-input/); expect(firstId).not.toEqual(secondId); })); }); diff --git a/src/material/checkbox/checkbox.ts b/src/material/checkbox/checkbox.ts index 4f029f8fe5e6..a4cad5036e76 100644 --- a/src/material/checkbox/checkbox.ts +++ b/src/material/checkbox/checkbox.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {FocusableOption} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusableOption} from '@angular/cdk/a11y'; import { ANIMATION_MODULE_TYPE, AfterViewInit, @@ -77,9 +77,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(); @@ -255,7 +252,7 @@ export class MatCheckbox this._options = this._options || defaults; this.color = this._options.color || defaults.color; this.tabIndex = tabIndex == null ? 0 : parseInt(tabIndex) || 0; - this.id = this._uniqueId = `mat-mdc-checkbox-${++nextUniqueId}`; + this.id = this._uniqueId = inject(_IdGenerator).getId('mat-mdc-checkbox-'); this.disabledInteractive = this._options?.disabledInteractive ?? false; } From 62e67635d8b9d83ef8f21165f1d6e469baaf104f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:25 +0100 Subject: [PATCH 17/30] refactor(material/chips): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/chips/chip-input.ts | 6 ++---- src/material/chips/chip.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index ce79c55d40fe..6c6c68be4063 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -18,6 +18,7 @@ import { booleanAttribute, inject, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field'; import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; import {MatChipGrid} from './chip-grid'; @@ -39,9 +40,6 @@ export interface MatChipInputEvent { chipInput: MatChipInput; } -// Increasing integer for generating unique ids. -let nextUniqueId = 0; - /** * Directive that adds chip-specific behaviors to an input element inside ``. * May be placed inside or outside of a ``. @@ -107,7 +105,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 = inject(_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 9f6434ac5d5d..df865cb1ad68 100644 --- a/src/material/chips/chip.ts +++ b/src/material/chips/chip.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {FocusMonitor} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusMonitor} from '@angular/cdk/a11y'; import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; import {DOCUMENT} from '@angular/common'; import { @@ -46,8 +46,6 @@ import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons'; import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private'; -let uid = 0; - /** Represents an event fired on an individual `mat-chip`. */ export interface MatChipEvent { /** The chip the event was fired on. */ @@ -142,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 = inject(_IdGenerator).getId('mat-mdc-chip-'); // TODO(#26104): Consider deprecating and using `_computeAriaAccessibleName` instead. // `ariaLabel` may be unnecessary, and `_computeAriaAccessibleName` only supports From 0200e8ba657306365b0a27f5f051ba478543df43 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:35 +0100 Subject: [PATCH 18/30] refactor(material/core): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/core/option/optgroup.ts | 6 ++---- src/material/core/option/option.ts | 10 ++-------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/material/core/option/optgroup.ts b/src/material/core/option/optgroup.ts index 9a55b679cdfa..4e7b7fe154e6 100644 --- a/src/material/core/option/optgroup.ts +++ b/src/material/core/option/optgroup.ts @@ -15,6 +15,7 @@ import { booleanAttribute, inject, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent'; // Notes on the accessibility pattern used for `mat-optgroup`. @@ -37,9 +38,6 @@ import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-pa // 3. ` { constructor( @@ -110,7 +104,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 = inject(_IdGenerator).getId('mat-option-'); /** Whether the option is disabled. */ @Input({transform: booleanAttribute}) From 6d3c9369dd6a632bb4d006c8c3d10903b4eaead2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:42 +0100 Subject: [PATCH 19/30] refactor(material/dialog): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/dialog/dialog-content-directives.ts | 6 ++---- src/material/dialog/dialog.ts | 7 +++---- src/material/dialog/testing/dialog-harness.spec.ts | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/material/dialog/dialog-content-directives.ts b/src/material/dialog/dialog-content-directives.ts index 9ab4f87f7272..97aba9812f9b 100644 --- a/src/material/dialog/dialog-content-directives.ts +++ b/src/material/dialog/dialog-content-directives.ts @@ -16,14 +16,12 @@ import { SimpleChanges, inject, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; 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. */ @@ -137,7 +135,7 @@ export abstract class MatDialogLayoutSection implements OnInit, OnDestroy { }, }) export class MatDialogTitle extends MatDialogLayoutSection { - @Input() id: string = `mat-mdc-dialog-title-${dialogElementUid++}`; + @Input() id: string = inject(_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 89dea3732c39..49840489d23a 100644 --- a/src/material/dialog/dialog.ts +++ b/src/material/dialog/dialog.ts @@ -22,6 +22,7 @@ import {MatDialogRef} from './dialog-ref'; import {defer, Observable, Subject} from 'rxjs'; import {Dialog, DialogConfig} from '@angular/cdk/dialog'; import {startWith} from 'rxjs/operators'; +import {_IdGenerator} from '@angular/cdk/a11y'; /** 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'); @@ -65,9 +66,6 @@ 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. */ @@ -77,6 +75,7 @@ export class MatDialog implements OnDestroy { private _defaultOptions = inject(MAT_DIALOG_DEFAULT_OPTIONS, {optional: true}); private _scrollStrategy = inject(MAT_DIALOG_SCROLL_STRATEGY); private _parentDialog = inject(MatDialog, {optional: true, skipSelf: true}); + private _idGenerator = inject(_IdGenerator); protected _dialog = inject(Dialog); private readonly _openDialogsAtThisLevel: MatDialogRef[] = []; @@ -154,7 +153,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 a42b30376c44..9a166ebd0037 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+\d+/); expect(await dialogs[1].getAriaLabelledby()).toBe('dialog-label'); }); From bd9310b05ad74854208249c10f6f6d37d705369d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:00:53 +0100 Subject: [PATCH 20/30] refactor(material/expansion): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/expansion/expansion-panel.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/material/expansion/expansion-panel.ts b/src/material/expansion/expansion-panel.ts index 83f792243841..3cb9bc07f8a0 100644 --- a/src/material/expansion/expansion-panel.ts +++ b/src/material/expansion/expansion-panel.ts @@ -32,6 +32,7 @@ import { ANIMATION_MODULE_TYPE, inject, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {Subject} from 'rxjs'; import {filter, startWith, take} from 'rxjs/operators'; import {MatAccordionBase, MatAccordionTogglePosition, MAT_ACCORDION} from './accordion-base'; @@ -42,9 +43,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. @@ -145,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: string = inject(_IdGenerator).getId('mat-expansion-panel-header-'); constructor(...args: unknown[]); From 7af752aa71973d62c91bc01cb5349b33e1ae95f7 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:01:01 +0100 Subject: [PATCH 21/30] refactor(material/form-field): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/form-field/directives/error.ts | 5 ++--- src/material/form-field/directives/hint.ts | 7 +++---- src/material/form-field/form-field.ts | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/material/form-field/directives/error.ts b/src/material/form-field/directives/error.ts index 05304db89400..a304e90a8171 100644 --- a/src/material/form-field/directives/error.ts +++ b/src/material/form-field/directives/error.ts @@ -14,8 +14,7 @@ import { HostAttributeToken, inject, } from '@angular/core'; - -let nextUniqueId = 0; +import {_IdGenerator} from '@angular/cdk/a11y'; /** * Injection token that can be used to reference instances of `MatError`. It serves as @@ -35,7 +34,7 @@ export const MAT_ERROR = new InjectionToken('MatError'); providers: [{provide: MAT_ERROR, useExisting: MatError}], }) export class MatError { - @Input() id: string = `mat-mdc-error-${nextUniqueId++}`; + @Input() id: string = inject(_IdGenerator).getId('mat-mdc-error-'); constructor(...args: unknown[]); diff --git a/src/material/form-field/directives/hint.ts b/src/material/form-field/directives/hint.ts index 159fc054ce4a..147bd77b86e1 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.dev/license */ -import {Directive, Input} from '@angular/core'; - -let nextUniqueId = 0; +import {Directive, inject, Input} from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; /** Hint text to be shown underneath the form field control. */ @Directive({ @@ -26,5 +25,5 @@ export class MatHint { @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 = inject(_IdGenerator).getId('mat-mdc-hint-'); } diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 0d827d164f2b..8b20dc2ccd02 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -34,6 +34,7 @@ import { } from '@angular/core'; import {AbstractControlDirective} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {Subject, Subscription, merge} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {MAT_ERROR, MatError} from './directives/error'; @@ -105,8 +106,6 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken(MAT_FORM_FIELD_DEFAULT_OPTIONS, { optional: true, }); @@ -305,10 +305,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 = ''; From b710211edc4da3f1829338c7ac5bbb3fe2597861 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:01:09 +0100 Subject: [PATCH 22/30] refactor(material/input): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/input/input.spec.ts | 2 +- src/material/input/input.ts | 5 ++--- src/material/input/testing/input-harness.spec.ts | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 1921d48a1e38..93ed2c7a723a 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -607,7 +607,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+\d+$/); fixture.componentInstance.label = ''; fixture.componentInstance.userDescribedByValue = ''; diff --git a/src/material/input/input.ts b/src/material/input/input.ts index f1b4f340d7f3..568b49c6e7bf 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -25,6 +25,7 @@ import { OnDestroy, WritableSignal, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; 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'; @@ -45,8 +46,6 @@ const MAT_INPUT_INVALID_TYPES = [ 'submit', ]; -let nextUniqueId = 0; - /** Object that can be used to configure the default options for the input. */ export interface MatInputConfig { /** Whether disabled inputs should be interactive. */ @@ -103,7 +102,7 @@ export class MatInput private _ngZone = inject(NgZone); protected _formField? = inject(MAT_FORM_FIELD, {optional: true}); - protected _uid = `mat-input-${nextUniqueId++}`; + protected _uid = inject(_IdGenerator).getId('mat-input-'); protected _previousNativeValue: any; private _inputValueAccessor: {value: any}; private _signalBasedValueAccessor?: {value: WritableSignal}; diff --git a/src/material/input/testing/input-harness.spec.ts b/src/material/input/testing/input-harness.spec.ts index 0224d6dc2500..49232e69ae02 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+\d+/); + expect(await inputs[1].getId()).toMatch(/mat-input-\w+\d+/); 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+\d+/); expect(await inputs[5].getId()).toBe('has-ng-model'); }); From 93e36ffb97a952b6bf4fb7c1c72bcad1efcdbf1b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:01:18 +0100 Subject: [PATCH 23/30] refactor(material/menu): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/menu/menu.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index 4aa5999155af..678f46fbabc7 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -31,7 +31,7 @@ import { Injector, } from '@angular/core'; import {AnimationEvent} from '@angular/animations'; -import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y'; import {Direction} from '@angular/cdk/bidi'; import { ESCAPE, @@ -50,8 +50,6 @@ import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu 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'; @@ -270,7 +268,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnI */ @Output() readonly close: EventEmitter = this.closed; - readonly panelId = `mat-menu-panel-${menuPanelUid++}`; + readonly panelId: string = inject(_IdGenerator).getId('mat-menu-panel-'); private _injector = inject(Injector); From c28615e7d01e55f835f642c8a5ca97573ecc4fd4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:01:26 +0100 Subject: [PATCH 24/30] refactor(material/paginator): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/paginator/paginator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/material/paginator/paginator.ts b/src/material/paginator/paginator.ts index 0becbb4d065a..80efe631b232 100644 --- a/src/material/paginator/paginator.ts +++ b/src/material/paginator/paginator.ts @@ -20,8 +20,10 @@ import { Output, ViewEncapsulation, booleanAttribute, + inject, numberAttribute, } from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {MatOption, ThemePalette} from '@angular/material/core'; import {MatSelect} from '@angular/material/select'; import {MatIconButton} from '@angular/material/button'; @@ -90,8 +92,6 @@ export const MAT_PAGINATOR_DEFAULT_OPTIONS = new InjectionToken Date: Thu, 31 Oct 2024 14:01:34 +0100 Subject: [PATCH 25/30] refactor(material/radio): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/radio/radio.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/material/radio/radio.ts b/src/material/radio/radio.ts index d569a1e311da..97d26cb3dec5 100644 --- a/src/material/radio/radio.ts +++ b/src/material/radio/radio.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusMonitor, FocusOrigin} from '@angular/cdk/a11y'; import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import { ANIMATION_MODULE_TYPE, @@ -47,9 +47,6 @@ import { import {Subscription} from 'rxjs'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; -// Increasing integer for generating unique ids for radio components. -let nextUniqueId = 0; - /** Change event object emitted by radio button and radio group. */ export class MatRadioChange { constructor( @@ -129,7 +126,7 @@ export class MatRadioGroup implements AfterContentInit, OnDestroy, ControlValueA 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 = inject(_IdGenerator).getId('mat-radio-group-'); /** The currently selected radio button. Should match value. */ private _selected: MatRadioButton | null = null; @@ -422,7 +419,7 @@ export class MatRadioButton implements OnInit, AfterViewInit, DoCheck, OnDestroy }); private _ngZone = inject(NgZone); - private _uniqueId: string = `mat-radio-${++nextUniqueId}`; + private _uniqueId: string = inject(_IdGenerator).getId('mat-radio-'); /** The unique ID for the radio button. */ @Input() id: string = this._uniqueId; From a0597224fd912d87ce48ee4ca0714bd684ed9ef2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:01:41 +0100 Subject: [PATCH 26/30] refactor(material/select): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/select/select.spec.ts | 2 +- src/material/select/select.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts index 9fce7d326cba..c865d3f74dcf 100644 --- a/src/material/select/select.spec.ts +++ b/src/material/select/select.spec.ts @@ -220,7 +220,7 @@ describe('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+\d+$/); }); it('should support user binding to `aria-describedby`', () => { diff --git a/src/material/select/select.ts b/src/material/select/select.ts index c0fb76b45e10..c81a601a8954 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -7,6 +7,7 @@ */ import { + _IdGenerator, ActiveDescendantKeyManager, addAriaReferencedId, LiveAnnouncer, @@ -96,8 +97,6 @@ import { } 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>( 'mat-select-scroll-strategy', @@ -215,6 +214,7 @@ export class MatSelect protected _changeDetectorRef = inject(ChangeDetectorRef); readonly _elementRef = inject(ElementRef); private _dir = inject(Directionality, {optional: true}); + private _idGenerator = inject(_IdGenerator); protected _parentFormField = inject(MAT_FORM_FIELD, {optional: true}); ngControl = inject(NgControl, {self: true, optional: true})!; private _liveAnnouncer = inject(LiveAnnouncer); @@ -312,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 = this._idGenerator.getId('mat-select-'); /** Current `aria-labelledby` value for the select trigger. */ private _triggerAriaLabelledBy: string | null = null; @@ -367,7 +367,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(); From 024995bfc54581a39fd7bbe2094fd24cbec3a3b7 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:01:50 +0100 Subject: [PATCH 27/30] refactor(material/slide-toggle): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/slide-toggle/slide-toggle.spec.ts | 4 ++-- src/material/slide-toggle/slide-toggle.ts | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/material/slide-toggle/slide-toggle.spec.ts b/src/material/slide-toggle/slide-toggle.spec.ts index 1433026fadc4..8a775f2f66f8 100644 --- a/src/material/slide-toggle/slide-toggle.spec.ts +++ b/src/material/slide-toggle/slide-toggle.spec.ts @@ -173,7 +173,7 @@ describe('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+\d+-button/); }); it('should forward the tabIndex to the underlying element', () => { @@ -235,7 +235,7 @@ describe('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+\d+-label/, ); }); diff --git a/src/material/slide-toggle/slide-toggle.ts b/src/material/slide-toggle/slide-toggle.ts index b5969838eea4..b1fe596d1bb0 100644 --- a/src/material/slide-toggle/slide-toggle.ts +++ b/src/material/slide-toggle/slide-toggle.ts @@ -35,7 +35,7 @@ import { ValidationErrors, Validator, } from '@angular/forms'; -import {FocusMonitor} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusMonitor} from '@angular/cdk/a11y'; import { MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, MatSlideToggleDefaultOptions, @@ -63,9 +63,6 @@ export class MatSlideToggleChange { ) {} } -// Increasing integer for generating unique ids for slide-toggle components. -let nextUniqueId = 0; - @Component({ selector: 'mat-slide-toggle', templateUrl: 'slide-toggle.html', @@ -220,7 +217,7 @@ export class MatSlideToggle this.tabIndex = tabIndex == null ? 0 : 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 = inject(_IdGenerator).getId('mat-mdc-slide-toggle-'); this.hideIcon = defaults.hideIcon ?? false; this.disabledInteractive = defaults.disabledInteractive ?? false; this._labelId = this._uniqueId + '-label'; From c0befc61102cdb9b2d34075b4cebb2253544db07 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:02:00 +0100 Subject: [PATCH 28/30] refactor(material/snack-bar): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/snack-bar/snack-bar-container.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/material/snack-bar/snack-bar-container.ts b/src/material/snack-bar/snack-bar-container.ts index 2a0ca64ab2d3..0f205c075058 100644 --- a/src/material/snack-bar/snack-bar-container.ts +++ b/src/material/snack-bar/snack-bar-container.ts @@ -29,13 +29,11 @@ import { TemplatePortal, } from '@angular/cdk/portal'; import {Observable, Subject} from 'rxjs'; -import {AriaLivePoliteness} from '@angular/cdk/a11y'; +import {_IdGenerator, AriaLivePoliteness} from '@angular/cdk/a11y'; import {Platform} from '@angular/cdk/platform'; import {AnimationEvent} from '@angular/animations'; import {MatSnackBarConfig} from './snack-bar-config'; -let uniqueId = 0; - /** * Internal component that wraps user-provided snack bar content. * @docs-private @@ -109,7 +107,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 = inject(_IdGenerator).getId('mat-snack-bar-container-live-'); constructor(...args: unknown[]); From a16545397fb0c192c0eada9f931dd832a6d51b47 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:02:07 +0100 Subject: [PATCH 29/30] refactor(material/tabs): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/tabs/tab-group.ts | 13 +++++-------- src/material/tabs/tab-nav-bar/tab-nav-bar.ts | 9 +++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts index 023550b73109..20492a9887a3 100644 --- a/src/material/tabs/tab-group.ts +++ b/src/material/tabs/tab-group.ts @@ -32,15 +32,12 @@ import {ThemePalette, MatRipple} 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 {_IdGenerator, CdkMonitorFocus, FocusOrigin} from '@angular/cdk/a11y'; import {MatTabBody} from './tab-body'; import {CdkPortalOutlet} from '@angular/cdk/portal'; 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; - /** @docs-private */ export interface MatTabGroupBaseHeader { _alignInkBarToSelectedTab(): void; @@ -268,7 +265,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes @Output() readonly selectedTabChange: EventEmitter = new EventEmitter(true); - private _groupId: number; + private _groupId: string; /** Whether the tab group is rendered on the server. */ protected _isServer: boolean = !inject(Platform).isBrowser; @@ -278,7 +275,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes constructor() { const defaultConfig = inject(MAT_TABS_CONFIG, {optional: true}); - this._groupId = nextId++; + this._groupId = inject(_IdGenerator).getId('mat-tab-group-'); this.animationDuration = defaultConfig && defaultConfig.animationDuration ? defaultConfig.animationDuration : '500ms'; this.disablePagination = @@ -492,12 +489,12 @@ 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 `${this._groupId}-label-${i}`; } /** Returns a unique id for each tab content element */ _getTabContentId(i: number): string { - return `mat-tab-content-${this._groupId}-${i}`; + return `${this._groupId}-content-${i}`; } /** 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 f8f456ba4a29..feb30ffb6aec 100644 --- a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts @@ -36,7 +36,7 @@ import { ThemePalette, _StructuralStylesLoader, } from '@angular/material/core'; -import {FocusableOption, FocusMonitor} from '@angular/cdk/a11y'; +import {_IdGenerator, FocusableOption, FocusMonitor} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {ViewportRuler} from '@angular/cdk/scrolling'; import {Platform} from '@angular/cdk/platform'; @@ -49,9 +49,6 @@ import {MatPaginatedTabHeader} from '../paginated-tab-header'; import {CdkObserveContent} from '@angular/cdk/observers'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; -// Increasing integer for generating unique ids for tab nav components. -let nextUniqueId = 0; - /** * Navigation component matching the styles of the tab group header. * Provides anchored navigation with animated ink bar. @@ -329,7 +326,7 @@ export class MatTabLink } /** Unique id for the tab. */ - @Input() id = `mat-tab-link-${nextUniqueId++}`; + @Input() id: string = inject(_IdGenerator).getId('mat-tab-link-'); constructor(...args: unknown[]); @@ -444,7 +441,7 @@ export class MatTabLink }) export class MatTabNavPanel { /** Unique id for the tab panel. */ - @Input() id = `mat-tab-nav-panel-${nextUniqueId++}`; + @Input() id: string = inject(_IdGenerator).getId('mat-tab-nav-panel-'); /** Id of the active tab in the nav bar. */ _activeTabId?: string; From 74ea762917eabd500a0b62f4db20f21503eb1e65 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 31 Oct 2024 14:02:14 +0100 Subject: [PATCH 30/30] refactor(material/timepicker): use ID generator Switches to using the ID generator service to create unique IDs. --- src/material/timepicker/timepicker.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index 23efc3e45565..e1628a2ef7b3 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -44,7 +44,7 @@ import {Overlay, OverlayRef} from '@angular/cdk/overlay'; import {TemplatePortal} from '@angular/cdk/portal'; import {_getEventTarget} from '@angular/cdk/platform'; import {ENTER, ESCAPE, hasModifierKey, TAB} from '@angular/cdk/keycodes'; -import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; +import {_IdGenerator, ActiveDescendantKeyManager} from '@angular/cdk/a11y'; import type {MatTimepickerInput} from './timepicker-input'; import { generateOptions, @@ -55,9 +55,6 @@ import { } from './util'; import {Subscription} from 'rxjs'; -/** Counter used to generate unique IDs. */ -let uniqueId = 0; - /** Event emitted when a value is selected in the timepicker. */ export interface MatTimepickerSelected { value: D; @@ -157,7 +154,7 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { readonly activeDescendant: Signal = this._activeDescendant.asReadonly(); /** Unique ID of the timepicker's panel */ - readonly panelId = `mat-timepicker-panel-${uniqueId++}`; + readonly panelId: string = inject(_IdGenerator).getId('mat-timepicker-panel-'); /** Whether ripples within the timepicker should be disabled. */ readonly disableRipple: InputSignalWithTransform = input(