diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts b/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts index 9d80e5fb65b5..72cd7cb1847f 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts @@ -8,10 +8,10 @@ import { dispatchEvent, } from '../../testing/private'; import {DOCUMENT} from '@angular/common'; -import {Component, NgZone} from '@angular/core'; +import {Component, NgZone, ViewChild} from '@angular/core'; import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {A11yModule} from '../index'; +import {A11yModule, CdkMonitorFocus} from '../index'; import {TOUCH_BUFFER_MS} from '../input-modality/input-modality-detector'; import { FocusMonitor, @@ -515,6 +515,7 @@ describe('cdkMonitorFocus', () => { ComplexComponentWithMonitorSubtreeFocus, ComplexComponentWithMonitorSubtreeFocusAndMonitorElementFocus, FocusMonitorOnCommentNode, + ExportedFocusMonitor, ], }).compileComponents(); }); @@ -737,6 +738,77 @@ describe('cdkMonitorFocus', () => { })); }); + describe('button with exported cdkMonitorElementFocus', () => { + let fixture: ComponentFixture; + let buttonElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(ExportedFocusMonitor); + fixture.detectChanges(); + + buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement; + patchElementFocus(buttonElement); + }); + + it('should initially not be focused', () => { + expect(fixture.componentInstance.exportedDirRef.focusOrigin) + .withContext('initial focus origin should be null') + .toBeNull(); + }); + + it('should detect focus via keyboard', fakeAsync(() => { + // Simulate focus via keyboard. + dispatchKeyboardEvent(document, 'keydown', TAB); + buttonElement.focus(); + fixture.detectChanges(); + flush(); + + expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('keyboard'); + })); + + it('should detect focus via mouse', fakeAsync(() => { + // Simulate focus via mouse. + dispatchMouseEvent(buttonElement, 'mousedown'); + buttonElement.focus(); + fixture.detectChanges(); + flush(); + + expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('mouse'); + })); + + it('should detect focus via touch', fakeAsync(() => { + // Simulate focus via touch. + dispatchFakeEvent(buttonElement, 'touchstart'); + buttonElement.focus(); + fixture.detectChanges(); + tick(TOUCH_BUFFER_MS); + + expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('touch'); + })); + + it('should detect programmatic focus', fakeAsync(() => { + // Programmatically focus. + buttonElement.focus(); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('program'); + })); + + it('should remove focus classes on blur', fakeAsync(() => { + buttonElement.focus(); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('program'); + + buttonElement.blur(); + fixture.detectChanges(); + + expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual(null); + })); + }); + it('should not throw when trying to monitor focus on a non-element node', () => { expect(() => { const fixture = TestBed.createComponent(FocusMonitorOnCommentNode); @@ -862,3 +934,10 @@ class FocusMonitorOnCommentNode {} `, }) class CheckboxWithLabel {} + +@Component({ + template: ``, +}) +class ExportedFocusMonitor { + @ViewChild('exportedDir') exportedDirRef: CdkMonitorFocus; +} diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 751b7f651001..503e7707c3e2 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -614,18 +614,28 @@ export class FocusMonitor implements OnDestroy { */ @Directive({ selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]', + exportAs: 'cdkMonitorFocus', }) export class CdkMonitorFocus implements AfterViewInit, OnDestroy { private _monitorSubscription: Subscription; + private _focusOrigin: FocusOrigin = null; + @Output() readonly cdkFocusChange = new EventEmitter(); constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) {} + get focusOrigin(): FocusOrigin { + return this._focusOrigin; + } + ngAfterViewInit() { const element = this._elementRef.nativeElement; this._monitorSubscription = this._focusMonitor .monitor(element, element.nodeType === 1 && element.hasAttribute('cdkMonitorSubtreeFocus')) - .subscribe(origin => this.cdkFocusChange.emit(origin)); + .subscribe(origin => { + this._focusOrigin = origin; + this.cdkFocusChange.emit(origin); + }); } ngOnDestroy() { diff --git a/tools/public_api_guard/cdk/a11y.md b/tools/public_api_guard/cdk/a11y.md index 3530215ef501..56716d3a82ff 100644 --- a/tools/public_api_guard/cdk/a11y.md +++ b/tools/public_api_guard/cdk/a11y.md @@ -84,11 +84,13 @@ export class CdkMonitorFocus implements AfterViewInit, OnDestroy { // (undocumented) readonly cdkFocusChange: EventEmitter; // (undocumented) + get focusOrigin(): FocusOrigin; + // (undocumented) ngAfterViewInit(): void; // (undocumented) ngOnDestroy(): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }