Skip to content

Commit d7db7c8

Browse files
authored
feat(cdk/a11y): allow focus options to be passed in to focus trap (#21769)
Allows for an optional `FocusOptions` object to be passed into the various focus trap methods. Fixes #21767.
1 parent 0dc5e04 commit d7db7c8

File tree

3 files changed

+44
-20
lines changed

3 files changed

+44
-20
lines changed

src/cdk/a11y/focus-trap/focus-trap.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,20 +129,44 @@ describe('FocusTrap', () => {
129129
expect(document.activeElement!.id).toBe('middle');
130130
});
131131

132+
it('should be able to pass in focus options to initial focusable element', () => {
133+
const options = {preventScroll: true};
134+
const spy = spyOn(fixture.nativeElement.querySelector('#middle'), 'focus').and.callThrough();
135+
136+
focusTrapInstance.focusInitialElement(options);
137+
expect(spy).toHaveBeenCalledWith(options);
138+
});
139+
132140
it('should be able to prioritize the first focus target', () => {
133141
// Because we can't mimic a real tab press focus change in a unit test, just call the
134142
// focus event handler directly.
135143
focusTrapInstance.focusFirstTabbableElement();
136144
expect(document.activeElement!.id).toBe('first');
137145
});
138146

147+
it('should be able to pass in focus options to first focusable element', () => {
148+
const options = {preventScroll: true};
149+
const spy = spyOn(fixture.nativeElement.querySelector('#first'), 'focus').and.callThrough();
150+
151+
focusTrapInstance.focusFirstTabbableElement(options);
152+
expect(spy).toHaveBeenCalledWith(options);
153+
});
154+
139155
it('should be able to prioritize the last focus target', () => {
140156
// Because we can't mimic a real tab press focus change in a unit test, just call the
141157
// focus event handler directly.
142158
focusTrapInstance.focusLastTabbableElement();
143159
expect(document.activeElement!.id).toBe('last');
144160
});
145161

162+
it('should be able to pass in focus options to last focusable element', () => {
163+
const options = {preventScroll: true};
164+
const spy = spyOn(fixture.nativeElement.querySelector('#last'), 'focus').and.callThrough();
165+
166+
focusTrapInstance.focusLastTabbableElement(options);
167+
expect(spy).toHaveBeenCalledWith(options);
168+
});
169+
146170
it('should warn if the initial focus target is not focusable', () => {
147171
const alternateFixture = TestBed.createComponent(FocusTrapUnfocusableTarget);
148172
alternateFixture.detectChanges();

src/cdk/a11y/focus-trap/focus-trap.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ export class FocusTrap {
132132
* @returns Returns a promise that resolves with a boolean, depending
133133
* on whether focus was moved successfully.
134134
*/
135-
focusInitialElementWhenReady(): Promise<boolean> {
135+
focusInitialElementWhenReady(options?: FocusOptions): Promise<boolean> {
136136
return new Promise<boolean>(resolve => {
137-
this._executeOnStable(() => resolve(this.focusInitialElement()));
137+
this._executeOnStable(() => resolve(this.focusInitialElement(options)));
138138
});
139139
}
140140

@@ -144,9 +144,9 @@ export class FocusTrap {
144144
* @returns Returns a promise that resolves with a boolean, depending
145145
* on whether focus was moved successfully.
146146
*/
147-
focusFirstTabbableElementWhenReady(): Promise<boolean> {
147+
focusFirstTabbableElementWhenReady(options?: FocusOptions): Promise<boolean> {
148148
return new Promise<boolean>(resolve => {
149-
this._executeOnStable(() => resolve(this.focusFirstTabbableElement()));
149+
this._executeOnStable(() => resolve(this.focusFirstTabbableElement(options)));
150150
});
151151
}
152152

@@ -156,9 +156,9 @@ export class FocusTrap {
156156
* @returns Returns a promise that resolves with a boolean, depending
157157
* on whether focus was moved successfully.
158158
*/
159-
focusLastTabbableElementWhenReady(): Promise<boolean> {
159+
focusLastTabbableElementWhenReady(options?: FocusOptions): Promise<boolean> {
160160
return new Promise<boolean>(resolve => {
161-
this._executeOnStable(() => resolve(this.focusLastTabbableElement()));
161+
this._executeOnStable(() => resolve(this.focusLastTabbableElement(options)));
162162
});
163163
}
164164

@@ -197,7 +197,7 @@ export class FocusTrap {
197197
* Focuses the element that should be focused when the focus trap is initialized.
198198
* @returns Whether focus was moved successfully.
199199
*/
200-
focusInitialElement(): boolean {
200+
focusInitialElement(options?: FocusOptions): boolean {
201201
// Contains the deprecated version of selector, for temporary backwards comparability.
202202
const redirectToElement = this._element.querySelector(`[cdk-focus-initial], ` +
203203
`[cdkFocusInitial]`) as HTMLElement;
@@ -219,26 +219,26 @@ export class FocusTrap {
219219

220220
if (!this._checker.isFocusable(redirectToElement)) {
221221
const focusableChild = this._getFirstTabbableElement(redirectToElement) as HTMLElement;
222-
focusableChild?.focus();
222+
focusableChild?.focus(options);
223223
return !!focusableChild;
224224
}
225225

226-
redirectToElement.focus();
226+
redirectToElement.focus(options);
227227
return true;
228228
}
229229

230-
return this.focusFirstTabbableElement();
230+
return this.focusFirstTabbableElement(options);
231231
}
232232

233233
/**
234234
* Focuses the first tabbable element within the focus trap region.
235235
* @returns Whether focus was moved successfully.
236236
*/
237-
focusFirstTabbableElement(): boolean {
237+
focusFirstTabbableElement(options?: FocusOptions): boolean {
238238
const redirectToElement = this._getRegionBoundary('start');
239239

240240
if (redirectToElement) {
241-
redirectToElement.focus();
241+
redirectToElement.focus(options);
242242
}
243243

244244
return !!redirectToElement;
@@ -248,11 +248,11 @@ export class FocusTrap {
248248
* Focuses the last tabbable element within the focus trap region.
249249
* @returns Whether focus was moved successfully.
250250
*/
251-
focusLastTabbableElement(): boolean {
251+
focusLastTabbableElement(options?: FocusOptions): boolean {
252252
const redirectToElement = this._getRegionBoundary('end');
253253

254254
if (redirectToElement) {
255-
redirectToElement.focus();
255+
redirectToElement.focus(options);
256256
}
257257

258258
return !!redirectToElement;

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,12 @@ export declare class FocusTrap {
140140
constructor(_element: HTMLElement, _checker: InteractivityChecker, _ngZone: NgZone, _document: Document, deferAnchors?: boolean);
141141
attachAnchors(): boolean;
142142
destroy(): void;
143-
focusFirstTabbableElement(): boolean;
144-
focusFirstTabbableElementWhenReady(): Promise<boolean>;
145-
focusInitialElement(): boolean;
146-
focusInitialElementWhenReady(): Promise<boolean>;
147-
focusLastTabbableElement(): boolean;
148-
focusLastTabbableElementWhenReady(): Promise<boolean>;
143+
focusFirstTabbableElement(options?: FocusOptions): boolean;
144+
focusFirstTabbableElementWhenReady(options?: FocusOptions): Promise<boolean>;
145+
focusInitialElement(options?: FocusOptions): boolean;
146+
focusInitialElementWhenReady(options?: FocusOptions): Promise<boolean>;
147+
focusLastTabbableElement(options?: FocusOptions): boolean;
148+
focusLastTabbableElementWhenReady(options?: FocusOptions): Promise<boolean>;
149149
hasAttached(): boolean;
150150
protected toggleAnchors(enabled: boolean): void;
151151
}

0 commit comments

Comments
 (0)