Skip to content

Commit fe24ce7

Browse files
authored
fix(cdk/stepper): focus management not working with shadow dom encapsulation (#23047)
The CDK stepper focus management checks against the document to find the focused element which won't work if the stepper is inside the shadow DOM. These changes use our shadow DOM helper to resolve the element instead.
1 parent d6b6b5a commit fe24ce7

File tree

5 files changed

+49
-8
lines changed

5 files changed

+49
-8
lines changed

src/cdk/stepper/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ng_module(
1515
"//src/cdk/bidi",
1616
"//src/cdk/coercion",
1717
"//src/cdk/keycodes",
18+
"//src/cdk/platform",
1819
"@npm//@angular/core",
1920
"@npm//@angular/forms",
2021
"@npm//rxjs",

src/cdk/stepper/stepper.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
ViewEncapsulation,
4141
AfterContentInit,
4242
} from '@angular/core';
43+
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
4344
import {Observable, of as observableOf, Subject} from 'rxjs';
4445
import {startWith, takeUntil} from 'rxjs/operators';
4546

@@ -262,8 +263,6 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
262263
/** Used for managing keyboard focus. */
263264
private _keyManager: FocusKeyManager<FocusableOption>;
264265

265-
private _document: Document;
266-
267266
/** Full list of steps inside the stepper, including inside nested steppers. */
268267
@ContentChildren(CdkStep, {descendants: true}) _steps: QueryList<CdkStep>;
269268

@@ -344,9 +343,13 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
344343

345344
constructor(
346345
@Optional() private _dir: Directionality, private _changeDetectorRef: ChangeDetectorRef,
347-
private _elementRef: ElementRef<HTMLElement>, @Inject(DOCUMENT) _document: any) {
346+
private _elementRef: ElementRef<HTMLElement>,
347+
/**
348+
* @deprecated No longer in use, to be removed.
349+
* @breaking-change 13.0.0
350+
*/
351+
@Inject(DOCUMENT) _document: any) {
348352
this._groupId = nextId++;
349-
this._document = _document;
350353
}
351354

352355
ngAfterContentInit() {
@@ -534,7 +537,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
534537
/** Checks whether the stepper contains the focused element. */
535538
private _containsFocus(): boolean {
536539
const stepperElement = this._elementRef.nativeElement;
537-
const focusedElement = this._document.activeElement;
540+
const focusedElement = _getFocusedElementPierceShadowDom();
538541
return stepperElement === focusedElement || stepperElement.contains(focusedElement);
539542
}
540543

src/material/stepper/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ ng_test_library(
7676
":stepper",
7777
"//src/cdk/bidi",
7878
"//src/cdk/keycodes",
79+
"//src/cdk/platform",
7980
"//src/cdk/stepper",
8081
"//src/cdk/testing/private",
8182
"//src/material/core",

src/material/stepper/stepper.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ViewChildren,
3131
QueryList,
3232
ViewChild,
33+
ViewEncapsulation,
3334
} from '@angular/core';
3435
import {ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing';
3536
import {
@@ -45,6 +46,7 @@ import {
4546
import {MatRipple, ThemePalette} from '@angular/material/core';
4647
import {By} from '@angular/platform-browser';
4748
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
49+
import {_supportsShadowDom} from '@angular/cdk/platform';
4850
import {merge, Observable, Subject} from 'rxjs';
4951
import {map, take} from 'rxjs/operators';
5052
import {MatStepHeader, MatStepperModule} from './index';
@@ -287,6 +289,31 @@ describe('MatStepper', () => {
287289
expect(stepHeaderEl.focus).toHaveBeenCalled();
288290
});
289291

292+
it('should focus next step header if focus is inside the stepper with shadow DOM', () => {
293+
if (!_supportsShadowDom()) {
294+
return;
295+
}
296+
297+
fixture.destroy();
298+
TestBed.resetTestingModule();
299+
fixture = createComponent(SimpleMatVerticalStepperApp, [], [], ViewEncapsulation.ShadowDom);
300+
fixture.detectChanges();
301+
302+
const stepperComponent =
303+
fixture.debugElement.query(By.directive(MatStepper))!.componentInstance;
304+
const stepHeaderEl =
305+
fixture.debugElement.queryAll(By.css('mat-step-header'))[1].nativeElement;
306+
const nextButtonNativeEl = fixture.debugElement
307+
.queryAll(By.directive(MatStepperNext))[0].nativeElement;
308+
spyOn(stepHeaderEl, 'focus');
309+
nextButtonNativeEl.focus();
310+
nextButtonNativeEl.click();
311+
fixture.detectChanges();
312+
313+
expect(stepperComponent.selectedIndex).toBe(1);
314+
expect(stepHeaderEl.focus).toHaveBeenCalled();
315+
});
316+
290317
it('should only be able to return to a previous step if it is editable', () => {
291318
const stepperComponent =
292319
fixture.debugElement.query(By.directive(MatStepper))!.componentInstance;
@@ -1559,7 +1586,8 @@ function asyncValidator(minLength: number, validationTrigger: Subject<void>): As
15591586

15601587
function createComponent<T>(component: Type<T>,
15611588
providers: Provider[] = [],
1562-
imports: any[] = []): ComponentFixture<T> {
1589+
imports: any[] = [],
1590+
encapsulation?: ViewEncapsulation): ComponentFixture<T> {
15631591
TestBed.configureTestingModule({
15641592
imports: [
15651593
MatStepperModule,
@@ -1572,8 +1600,15 @@ function createComponent<T>(component: Type<T>,
15721600
{provide: Directionality, useFactory: () => dir},
15731601
...providers
15741602
],
1575-
}).compileComponents();
1603+
});
1604+
1605+
if (encapsulation != null) {
1606+
TestBed.overrideComponent(component, {
1607+
set: {encapsulation}
1608+
});
1609+
}
15761610

1611+
TestBed.compileComponents();
15771612
return TestBed.createComponent<T>(component);
15781613
}
15791614

tools/public_api_guard/cdk/stepper.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export declare class CdkStepper implements AfterContentInit, AfterViewInit, OnDe
6565
set selectedIndex(index: number);
6666
readonly selectionChange: EventEmitter<StepperSelectionEvent>;
6767
readonly steps: QueryList<CdkStep>;
68-
constructor(_dir: Directionality, _changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef<HTMLElement>, _document: any);
68+
constructor(_dir: Directionality, _changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef<HTMLElement>,
69+
_document: any);
6970
_getAnimationDirection(index: number): StepContentPositionState;
7071
_getFocusIndex(): number | null;
7172
_getIndicatorType(index: number, state?: StepState): StepState;

0 commit comments

Comments
 (0)