Skip to content

Commit 06c2164

Browse files
EmmanuelRouxamysorto
authored andcommitted
feat(cdk/a11y): add named export and public property to CdkMonitorFocus directive (#25427)
(cherry picked from commit eb2d821)
1 parent 1bfb502 commit 06c2164

File tree

3 files changed

+95
-4
lines changed

3 files changed

+95
-4
lines changed

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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
dispatchEvent,
99
} from '../../testing/private';
1010
import {DOCUMENT} from '@angular/common';
11-
import {Component, NgZone} from '@angular/core';
11+
import {Component, NgZone, ViewChild} from '@angular/core';
1212
import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
1313
import {By} from '@angular/platform-browser';
14-
import {A11yModule} from '../index';
14+
import {A11yModule, CdkMonitorFocus} from '../index';
1515
import {TOUCH_BUFFER_MS} from '../input-modality/input-modality-detector';
1616
import {
1717
FocusMonitor,
@@ -515,6 +515,7 @@ describe('cdkMonitorFocus', () => {
515515
ComplexComponentWithMonitorSubtreeFocus,
516516
ComplexComponentWithMonitorSubtreeFocusAndMonitorElementFocus,
517517
FocusMonitorOnCommentNode,
518+
ExportedFocusMonitor,
518519
],
519520
}).compileComponents();
520521
});
@@ -737,6 +738,77 @@ describe('cdkMonitorFocus', () => {
737738
}));
738739
});
739740

741+
describe('button with exported cdkMonitorElementFocus', () => {
742+
let fixture: ComponentFixture<ExportedFocusMonitor>;
743+
let buttonElement: HTMLElement;
744+
745+
beforeEach(() => {
746+
fixture = TestBed.createComponent(ExportedFocusMonitor);
747+
fixture.detectChanges();
748+
749+
buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement;
750+
patchElementFocus(buttonElement);
751+
});
752+
753+
it('should initially not be focused', () => {
754+
expect(fixture.componentInstance.exportedDirRef.focusOrigin)
755+
.withContext('initial focus origin should be null')
756+
.toBeNull();
757+
});
758+
759+
it('should detect focus via keyboard', fakeAsync(() => {
760+
// Simulate focus via keyboard.
761+
dispatchKeyboardEvent(document, 'keydown', TAB);
762+
buttonElement.focus();
763+
fixture.detectChanges();
764+
flush();
765+
766+
expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('keyboard');
767+
}));
768+
769+
it('should detect focus via mouse', fakeAsync(() => {
770+
// Simulate focus via mouse.
771+
dispatchMouseEvent(buttonElement, 'mousedown');
772+
buttonElement.focus();
773+
fixture.detectChanges();
774+
flush();
775+
776+
expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('mouse');
777+
}));
778+
779+
it('should detect focus via touch', fakeAsync(() => {
780+
// Simulate focus via touch.
781+
dispatchFakeEvent(buttonElement, 'touchstart');
782+
buttonElement.focus();
783+
fixture.detectChanges();
784+
tick(TOUCH_BUFFER_MS);
785+
786+
expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('touch');
787+
}));
788+
789+
it('should detect programmatic focus', fakeAsync(() => {
790+
// Programmatically focus.
791+
buttonElement.focus();
792+
fixture.detectChanges();
793+
tick();
794+
795+
expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('program');
796+
}));
797+
798+
it('should remove focus classes on blur', fakeAsync(() => {
799+
buttonElement.focus();
800+
fixture.detectChanges();
801+
tick();
802+
803+
expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual('program');
804+
805+
buttonElement.blur();
806+
fixture.detectChanges();
807+
808+
expect(fixture.componentInstance.exportedDirRef.focusOrigin).toEqual(null);
809+
}));
810+
});
811+
740812
it('should not throw when trying to monitor focus on a non-element node', () => {
741813
expect(() => {
742814
const fixture = TestBed.createComponent(FocusMonitorOnCommentNode);
@@ -862,3 +934,10 @@ class FocusMonitorOnCommentNode {}
862934
`,
863935
})
864936
class CheckboxWithLabel {}
937+
938+
@Component({
939+
template: `<button cdkMonitorElementFocus #exportedDir="cdkMonitorFocus"></button>`,
940+
})
941+
class ExportedFocusMonitor {
942+
@ViewChild('exportedDir') exportedDirRef: CdkMonitorFocus;
943+
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,18 +614,28 @@ export class FocusMonitor implements OnDestroy {
614614
*/
615615
@Directive({
616616
selector: '[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]',
617+
exportAs: 'cdkMonitorFocus',
617618
})
618619
export class CdkMonitorFocus implements AfterViewInit, OnDestroy {
619620
private _monitorSubscription: Subscription;
621+
private _focusOrigin: FocusOrigin = null;
622+
620623
@Output() readonly cdkFocusChange = new EventEmitter<FocusOrigin>();
621624

622625
constructor(private _elementRef: ElementRef<HTMLElement>, private _focusMonitor: FocusMonitor) {}
623626

627+
get focusOrigin(): FocusOrigin {
628+
return this._focusOrigin;
629+
}
630+
624631
ngAfterViewInit() {
625632
const element = this._elementRef.nativeElement;
626633
this._monitorSubscription = this._focusMonitor
627634
.monitor(element, element.nodeType === 1 && element.hasAttribute('cdkMonitorSubtreeFocus'))
628-
.subscribe(origin => this.cdkFocusChange.emit(origin));
635+
.subscribe(origin => {
636+
this._focusOrigin = origin;
637+
this.cdkFocusChange.emit(origin);
638+
});
629639
}
630640

631641
ngOnDestroy() {

tools/public_api_guard/cdk/a11y.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ export class CdkMonitorFocus implements AfterViewInit, OnDestroy {
8484
// (undocumented)
8585
readonly cdkFocusChange: EventEmitter<FocusOrigin>;
8686
// (undocumented)
87+
get focusOrigin(): FocusOrigin;
88+
// (undocumented)
8789
ngAfterViewInit(): void;
8890
// (undocumented)
8991
ngOnDestroy(): void;
9092
// (undocumented)
91-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMonitorFocus, "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", never, {}, { "cdkFocusChange": "cdkFocusChange"; }, never, never, false>;
93+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMonitorFocus, "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", ["cdkMonitorFocus"], {}, { "cdkFocusChange": "cdkFocusChange"; }, never, never, false>;
9294
// (undocumented)
9395
static ɵfac: i0.ɵɵFactoryDeclaration<CdkMonitorFocus, never>;
9496
}

0 commit comments

Comments
 (0)