Skip to content

Commit 612cdc1

Browse files
crisbetoannieyw
authored andcommitted
fix(cdk/a11y): focusVia not accounting for focused child node (#21512)
In #20966 some logic was added so that calling `focusVia` on an element that already has focus would change the origin to the passed-in one. The problem is that the new logic doesn't account for when a parent element is monitored and `focusVia` is called on a child. These changes add some more logic that will look through all the monitored elements that have `checkChildren: true` and will switch the origin accordingly. Fixes #21500. (cherry picked from commit f8df9f8)
1 parent 8e57fc8 commit 612cdc1

File tree

2 files changed

+54
-2
lines changed

2 files changed

+54
-2
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,40 @@ describe('FocusMonitor', () => {
352352
expect(buttonElement.focus).toHaveBeenCalledTimes(1);
353353
}));
354354

355+
it('focusVia should change the focus origin when called a focused child node', fakeAsync(() => {
356+
const parent = fixture.nativeElement.querySelector('.parent');
357+
focusMonitor.stopMonitoring(buttonElement); // The button gets monitored by default.
358+
focusMonitor.monitor(parent, true).subscribe(changeHandler);
359+
spyOn(buttonElement, 'focus').and.callThrough();
360+
focusMonitor.focusVia(buttonElement, 'keyboard');
361+
flush();
362+
fakeActiveElement = buttonElement;
363+
364+
expect(parent.classList.length)
365+
.toBe(3, 'Parent should have exactly 2 focus classes and the `parent` class');
366+
expect(parent.classList.contains('cdk-focused'))
367+
.toBe(true, 'Parent should have cdk-focused class');
368+
expect(parent.classList.contains('cdk-keyboard-focused'))
369+
.toBe(true, 'Parent should have cdk-keyboard-focused class');
370+
expect(changeHandler).toHaveBeenCalledTimes(1);
371+
expect(changeHandler).toHaveBeenCalledWith('keyboard');
372+
expect(buttonElement.focus).toHaveBeenCalledTimes(1);
373+
374+
focusMonitor.focusVia(buttonElement, 'mouse');
375+
flush();
376+
fakeActiveElement = buttonElement;
377+
378+
expect(parent.classList.length)
379+
.toBe(3, 'Parent should have exactly 2 focus classes and the `parent` class');
380+
expect(parent.classList.contains('cdk-focused'))
381+
.toBe(true, 'Parent should have cdk-focused class');
382+
expect(parent.classList.contains('cdk-mouse-focused'))
383+
.toBe(true, 'Parent should have cdk-mouse-focused class');
384+
expect(changeHandler).toHaveBeenCalledTimes(2);
385+
expect(changeHandler).toHaveBeenCalledWith('mouse');
386+
expect(buttonElement.focus).toHaveBeenCalledTimes(1);
387+
}));
388+
355389
});
356390

357391
describe('FocusMonitor with "eventual" detection', () => {

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,9 @@ export class FocusMonitor implements OnDestroy {
313313
// If the element is focused already, calling `focus` again won't trigger the event listener
314314
// which means that the focus classes won't be updated. If that's the case, update the classes
315315
// directly without waiting for an event.
316-
if (nativeElement === focusedElement && this._elementInfo.has(nativeElement)) {
317-
this._originChanged(nativeElement, origin, this._elementInfo.get(nativeElement)!);
316+
if (nativeElement === focusedElement) {
317+
this._getClosestElementsInfo(nativeElement)
318+
.forEach(([currentElement, info]) => this._originChanged(currentElement, origin, info));
318319
} else {
319320
this._setOriginForCurrentEventQueue(origin);
320321

@@ -553,6 +554,23 @@ export class FocusMonitor implements OnDestroy {
553554
this._emitOrigin(elementInfo.subject, origin);
554555
this._lastFocusOrigin = origin;
555556
}
557+
558+
/**
559+
* Collects the `MonitoredElementInfo` of a particular element and
560+
* all of its ancestors that have enabled `checkChildren`.
561+
* @param element Element from which to start the search.
562+
*/
563+
private _getClosestElementsInfo(element: HTMLElement): [HTMLElement, MonitoredElementInfo][] {
564+
const results: [HTMLElement, MonitoredElementInfo][] = [];
565+
566+
this._elementInfo.forEach((info, currentElement) => {
567+
if (currentElement === element || (info.checkChildren && currentElement.contains(element))) {
568+
results.push([currentElement, info]);
569+
}
570+
});
571+
572+
return results;
573+
}
556574
}
557575

558576
/** Gets the target of an event, accounting for Shadow DOM. */

0 commit comments

Comments
 (0)