Skip to content

Commit

Permalink
fix(multiple): include APP_ID in element IDs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jelbourn committed Jun 14, 2024
1 parent ba5a4f9 commit 9e90bdb
Show file tree
Hide file tree
Showing 46 changed files with 254 additions and 64 deletions.
7 changes: 5 additions & 2 deletions src/cdk-experimental/combobox/combobox-popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef, Inject, Input, OnInit} from '@angular/core';
import {APP_ID, Directive, ElementRef, inject, Inject, Input, OnInit} from '@angular/core';
import {AriaHasPopupValue, CDK_COMBOBOX, CdkCombobox} from './combobox';

let nextId = 0;
Expand All @@ -24,6 +24,9 @@ let nextId = 0;
standalone: true,
})
export class CdkComboboxPopup<T = unknown> implements OnInit {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

@Input()
get role(): AriaHasPopupValue {
return this._role;
Expand All @@ -42,7 +45,7 @@ export class CdkComboboxPopup<T = unknown> implements OnInit {
}
private _firstFocusElement: HTMLElement;

@Input() id = `cdk-combobox-popup-${nextId++}`;
@Input() id = `cdk-combobox-popup-${this._appId}${nextId++}`;

constructor(
private readonly _elementRef: ElementRef<HTMLElement>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/

import {CSP_NONCE, Directive, ElementRef, Inject, OnDestroy, OnInit, Optional} from '@angular/core';
import {
APP_ID,
CSP_NONCE,
Directive,
ElementRef,
inject,
Inject,
OnDestroy,
OnInit,
Optional,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {Directionality} from '@angular/cdk/bidi';
import {_getShadowRoot} from '@angular/cdk/platform';
Expand Down Expand Up @@ -39,6 +49,9 @@ let nextId = 0;
standalone: true,
})
export class CdkTableScrollContainer implements StickyPositioningListener, OnDestroy, OnInit {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

private readonly _uniqueClassName: string;
private _styleRoot!: Node;
private _styleElement?: HTMLStyleElement;
Expand All @@ -55,7 +68,7 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes
@Optional() private readonly _directionality?: Directionality,
@Optional() @Inject(CSP_NONCE) private readonly _nonce?: string | null,
) {
this._uniqueClassName = `cdk-table-scroll-container-${++nextId}`;
this._uniqueClassName = `cdk-table-scroll-container-${this._appId}${++nextId}`;
_elementRef.nativeElement.classList.add(this._uniqueClassName);
}

Expand Down
7 changes: 6 additions & 1 deletion src/cdk/a11y/live-announcer/live-announcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import {ContentObserver} from '@angular/cdk/observers';
import {DOCUMENT} from '@angular/common';
import {
APP_ID,
Directive,
ElementRef,
inject,
Inject,
Injectable,
Input,
Expand All @@ -30,6 +32,9 @@ let uniqueIds = 0;

@Injectable({providedIn: 'root'})
export class LiveAnnouncer implements OnDestroy {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

private _liveElement: HTMLElement;
private _document: Document;
private _previousTimeout: number;
Expand Down Expand Up @@ -179,7 +184,7 @@ export class LiveAnnouncer implements OnDestroy {

liveEl.setAttribute('aria-atomic', 'true');
liveEl.setAttribute('aria-live', 'polite');
liveEl.id = `cdk-live-announcer-${uniqueIds++}`;
liveEl.id = `cdk-live-announcer-${this._appId}${uniqueIds++}`;

this._document.body.appendChild(liveEl);

Expand Down
6 changes: 5 additions & 1 deletion src/cdk/accordion/accordion-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
SkipSelf,
Inject,
booleanAttribute,
inject,
APP_ID,
} from '@angular/core';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
import {CDK_ACCORDION, CdkAccordion} from './accordion';
Expand All @@ -40,6 +42,8 @@ let nextId = 0;
standalone: true,
})
export class CdkAccordionItem implements OnDestroy {
protected _appId = inject(APP_ID);

/** Subscription to openAll/closeAll events. */
private _openCloseAllSubscription = Subscription.EMPTY;
/** Event emitted every time the AccordionItem is closed. */
Expand All @@ -57,7 +61,7 @@ export class CdkAccordionItem implements OnDestroy {
@Output() readonly expandedChange: EventEmitter<boolean> = new EventEmitter<boolean>();

/** The unique AccordionItem id. */
readonly id: string = `cdk-accordion-child-${nextId++}`;
readonly id: string = `cdk-accordion-child-${this._appId}${nextId++}`;

/** Whether the AccordionItem is expanded. */
@Input({transform: booleanAttribute})
Expand Down
7 changes: 6 additions & 1 deletion src/cdk/accordion/accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
OnDestroy,
SimpleChanges,
booleanAttribute,
inject,
APP_ID,
} from '@angular/core';
import {Subject} from 'rxjs';

Expand All @@ -37,14 +39,17 @@ export const CDK_ACCORDION = new InjectionToken<CdkAccordion>('CdkAccordion');
standalone: true,
})
export class CdkAccordion implements OnDestroy, OnChanges {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

/** Emits when the state of the accordion changes */
readonly _stateChanges = new Subject<SimpleChanges>();

/** Stream that emits true/false when openAll/closeAll is triggered. */
readonly _openCloseAllActions: Subject<boolean> = new Subject<boolean>();

/** A readonly id value to use for unique selection coordination. */
readonly id: string = `cdk-accordion-${nextId++}`;
readonly id: string = `cdk-accordion-${this._appId}${nextId++}`;

/** Whether the accordion should allow multiple expanded accordion items simultaneously. */
@Input({transform: booleanAttribute}) multi: boolean = false;
Expand Down
7 changes: 6 additions & 1 deletion src/cdk/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
Optional,
SkipSelf,
ComponentRef,
APP_ID,
inject,
} from '@angular/core';
import {BasePortalOutlet, ComponentPortal, TemplatePortal} from '@angular/cdk/portal';
import {of as observableOf, Observable, Subject, defer} from 'rxjs';
Expand All @@ -41,6 +43,9 @@ let uniqueId = 0;

@Injectable({providedIn: 'root'})
export class Dialog implements OnDestroy {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

private _openDialogsAtThisLevel: DialogRef<any, any>[] = [];
private readonly _afterAllClosedAtThisLevel = new Subject<void>();
private readonly _afterOpenedAtThisLevel = new Subject<DialogRef>();
Expand Down Expand Up @@ -114,7 +119,7 @@ export class Dialog implements OnDestroy {
DialogRef<R, C>
>;
config = {...defaults, ...config};
config.id = config.id || `cdk-dialog-${uniqueId++}`;
config.id = config.id || `cdk-dialog-${this._appId}${uniqueId++}`;

if (
config.id &&
Expand Down
7 changes: 6 additions & 1 deletion src/cdk/drag-drop/directives/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
SkipSelf,
Inject,
booleanAttribute,
inject,
APP_ID,
} from '@angular/core';
import {Directionality} from '@angular/cdk/bidi';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
Expand Down Expand Up @@ -55,6 +57,9 @@ let _uniqueIdCounter = 0;
},
})
export class CdkDropList<T = any> implements OnDestroy {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

/** Emits when the list has been destroyed. */
private readonly _destroyed = new Subject<void>();

Expand Down Expand Up @@ -85,7 +90,7 @@ export class CdkDropList<T = any> implements OnDestroy {
* Unique ID for the drop zone. Can be used as a reference
* in the `connectedTo` of another `CdkDropList`.
*/
@Input() id: string = `cdk-drop-list-${_uniqueIdCounter++}`;
@Input() id: string = `cdk-drop-list-${this._appId}${_uniqueIdCounter++}`;

/** Locks the position of the draggable elements inside the container along the specified axis. */
@Input('cdkDropListLockAxis') lockAxis: DragAxis;
Expand Down
4 changes: 2 additions & 2 deletions src/cdk/listbox/listbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
11 changes: 9 additions & 2 deletions src/cdk/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {Platform} from '@angular/cdk/platform';
import {
AfterContentInit,
APP_ID,
booleanAttribute,
ChangeDetectorRef,
ContentChildren,
Expand Down Expand Up @@ -96,6 +97,9 @@ class ListboxSelectionModel<T> extends SelectionModel<T> {
},
})
export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightable, OnDestroy {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

/** The id of the option's host element. */
@Input()
get id() {
Expand All @@ -105,7 +109,7 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
this._id = value;
}
private _id: string;
private _generatedId = `cdk-option-${nextId++}`;
private _generatedId = `cdk-option-${this._appId}${nextId++}`;

/** The value of this option. */
@Input('cdkOption') value: T;
Expand Down Expand Up @@ -249,6 +253,9 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
],
})
export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, ControlValueAccessor {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

/** The id of the option's host element. */
@Input()
get id() {
Expand All @@ -258,7 +265,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
this._id = value;
}
private _id: string;
private _generatedId = `cdk-listbox-${nextId++}`;
private _generatedId = `cdk-listbox-${this._appId}${nextId++}`;

/** The tabindex to use when the listbox is enabled. */
@Input('tabindex')
Expand Down
6 changes: 5 additions & 1 deletion src/cdk/menu/menu-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
computed,
inject,
signal,
APP_ID,
} from '@angular/core';
import {Subject, merge} from 'rxjs';
import {mapTo, mergeAll, mergeMap, startWith, switchMap, takeUntil} from 'rxjs/operators';
Expand Down Expand Up @@ -55,6 +56,9 @@ export abstract class CdkMenuBase
extends CdkMenuGroup
implements Menu, AfterContentInit, OnDestroy
{
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

/** The menu's native DOM host element. */
readonly nativeElement: HTMLElement = inject(ElementRef).nativeElement;

Expand All @@ -71,7 +75,7 @@ export abstract class CdkMenuBase
protected readonly dir = inject(Directionality, {optional: true});

/** The id of the menu's host element. */
@Input() id = `cdk-menu-${nextId++}`;
@Input() id = `cdk-menu-${this._appId}${nextId++}`;

/** All child MenuItem elements nested in this Menu. */
@ContentChildren(CdkMenuItem, {descendants: true})
Expand Down
7 changes: 6 additions & 1 deletion src/cdk/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
ANIMATION_MODULE_TYPE,
Optional,
EnvironmentInjector,
APP_ID,
inject,
} from '@angular/core';
import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher';
import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher';
Expand All @@ -44,6 +46,9 @@ let nextUniqueId = 0;
*/
@Injectable({providedIn: 'root'})
export class Overlay {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

private _appRef: ApplicationRef;

constructor(
Expand Down Expand Up @@ -106,7 +111,7 @@ export class Overlay {
private _createPaneElement(host: HTMLElement): HTMLElement {
const pane = this._document.createElement('div');

pane.id = `cdk-overlay-${nextUniqueId++}`;
pane.id = `cdk-overlay-${this._appId}${nextUniqueId++}`;
pane.classList.add('cdk-overlay-pane');
host.appendChild(pane);

Expand Down
7 changes: 6 additions & 1 deletion src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
AfterContentInit,
booleanAttribute,
numberAttribute,
APP_ID,
inject,
} from '@angular/core';
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
import {Observable, of as observableOf, Subject} from 'rxjs';
Expand Down Expand Up @@ -236,6 +238,9 @@ export class CdkStep implements OnChanges {
standalone: true,
})
export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

/** Emits when the component is destroyed. */
protected readonly _destroyed = new Subject<void>();

Expand Down Expand Up @@ -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. */
Expand Down
7 changes: 6 additions & 1 deletion src/material/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
ViewChild,
ViewEncapsulation,
booleanAttribute,
inject,
APP_ID,
} from '@angular/core';
import {AnimationEvent} from '@angular/animations';
import {
Expand Down Expand Up @@ -119,6 +121,9 @@ export function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): MatAutocompleteDefau
standalone: true,
})
export class MatAutocomplete implements AfterContentInit, OnDestroy {
/** Unique APP_ID to generate page-unique IDs if multiple Angular instances are on the page. */
private _appId = inject(APP_ID);

private _activeOptionChanges = Subscription.EMPTY;

/** Emits when the panel animation is done. Null if the panel doesn't animate. */
Expand Down Expand Up @@ -244,7 +249,7 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy {
}

/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
id: string = `mat-autocomplete-${_uniqueAutocompleteIdCounter++}`;
id: string = `mat-autocomplete-${this._appId}${_uniqueAutocompleteIdCounter++}`;

/**
* Tells any descendant `mat-optgroup` to use the inert a11y pattern.
Expand Down
Loading

0 comments on commit 9e90bdb

Please sign in to comment.