Skip to content

Commit c704d17

Browse files
authored
fix(a11y): focus trap incorrectly moving focus internally if focus was already moved into it (#18541)
The new `ConfigurableFocusTrap` uses a timeout to allow for focus to be moved before it traps focus into itself, however if focus was moved into it before the timeout has fired, focus will be incorrectly moved to the first element in the trap. These changes add an extra check that ensures that focus isn't moved again if it's already inside the trap. Relates to #18538.
1 parent a1bfc4e commit c704d17

File tree

2 files changed

+21
-3
lines changed

2 files changed

+21
-3
lines changed

src/cdk/a11y/focus-trap/event-listener-inert-strategy.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ describe('EventListenerFocusTrapInertStrategy', () => {
4141
componentInstance.secondFocusableElement.nativeElement,
4242
'Expected second focusable element to be focused');
4343
}));
44+
45+
it('should not intercept focus if it moved outside the trap and back in again',
46+
fakeAsync(() => {
47+
const fixture = createComponent(SimpleFocusTrap, providers);
48+
fixture.detectChanges();
49+
const {secondFocusableElement, outsideFocusableElement} = fixture.componentInstance;
50+
51+
outsideFocusableElement.nativeElement.focus();
52+
secondFocusableElement.nativeElement.focus();
53+
flush();
54+
55+
expect(fixture.componentInstance.activeElement).toBe(secondFocusableElement.nativeElement,
56+
'Expected second focusable element to be focused');
57+
}));
58+
4459
});
4560

4661
function createComponent<T>(componentType: Type<T>, providers: Array<Object> = []
@@ -91,6 +106,7 @@ class SimpleFocusTrap implements AfterViewInit {
91106
});
92107

93108
this.focusTrap = this._focusTrapFactory.create(this.focusTrapElement.nativeElement);
109+
spyOnProperty(document, 'activeElement', 'get').and.callFake(() => this.activeElement);
94110
this.focusTrap.focusFirstTabbableElementWhenReady();
95111
}
96112
}

src/cdk/a11y/focus-trap/event-listener-inert-strategy.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ export class EventListenerFocusTrapInertStrategy implements FocusTrapInertStrate
4949
*/
5050
private _trapFocus(focusTrap: ConfigurableFocusTrap, event: FocusEvent) {
5151
const target = event.target as HTMLElement;
52+
const focusTrapRoot = focusTrap._element;
53+
5254
// Don't refocus if target was in an overlay, because the overlay might be associated
5355
// with an element inside the FocusTrap, ex. mat-select.
54-
if (!focusTrap._element.contains(target) &&
55-
closest(target, 'div.cdk-overlay-pane') === null) {
56+
if (!focusTrapRoot.contains(target) && closest(target, 'div.cdk-overlay-pane') === null) {
5657
// Some legacy FocusTrap usages have logic that focuses some element on the page
5758
// just before FocusTrap is destroyed. For backwards compatibility, wait
5859
// to be sure FocusTrap is still enabled before refocusing.
5960
setTimeout(() => {
60-
if (focusTrap.enabled) {
61+
// Check whether focus wasn't put back into the focus trap while the timeout was pending.
62+
if (focusTrap.enabled && !focusTrapRoot.contains(focusTrap._document.activeElement)) {
6163
focusTrap.focusFirstTabbableElement();
6264
}
6365
});

0 commit comments

Comments
 (0)