Skip to content

Commit

Permalink
feat(cdk/a11y): add named export and public property to CdkMonitorFoc…
Browse files Browse the repository at this point in the history
…us directive (#25427)

(cherry picked from commit eb2d821)
  • Loading branch information
EmmanuelRoux authored and amysorto committed Aug 23, 2022
1 parent 1bfb502 commit 06c2164
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 4 deletions.
83 changes: 81 additions & 2 deletions src/cdk/a11y/focus-monitor/focus-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -515,6 +515,7 @@ describe('cdkMonitorFocus', () => {
ComplexComponentWithMonitorSubtreeFocus,
ComplexComponentWithMonitorSubtreeFocusAndMonitorElementFocus,
FocusMonitorOnCommentNode,
ExportedFocusMonitor,
],
}).compileComponents();
});
Expand Down Expand Up @@ -737,6 +738,77 @@ describe('cdkMonitorFocus', () => {
}));
});

describe('button with exported cdkMonitorElementFocus', () => {
let fixture: ComponentFixture<ExportedFocusMonitor>;
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);
Expand Down Expand Up @@ -862,3 +934,10 @@ class FocusMonitorOnCommentNode {}
`,
})
class CheckboxWithLabel {}

@Component({
template: `<button cdkMonitorElementFocus #exportedDir="cdkMonitorFocus"></button>`,
})
class ExportedFocusMonitor {
@ViewChild('exportedDir') exportedDirRef: CdkMonitorFocus;
}
12 changes: 11 additions & 1 deletion src/cdk/a11y/focus-monitor/focus-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FocusOrigin>();

constructor(private _elementRef: ElementRef<HTMLElement>, 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() {
Expand Down
4 changes: 3 additions & 1 deletion tools/public_api_guard/cdk/a11y.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ export class CdkMonitorFocus implements AfterViewInit, OnDestroy {
// (undocumented)
readonly cdkFocusChange: EventEmitter<FocusOrigin>;
// (undocumented)
get focusOrigin(): FocusOrigin;
// (undocumented)
ngAfterViewInit(): void;
// (undocumented)
ngOnDestroy(): void;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMonitorFocus, "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", never, {}, { "cdkFocusChange": "cdkFocusChange"; }, never, never, false>;
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMonitorFocus, "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", ["cdkMonitorFocus"], {}, { "cdkFocusChange": "cdkFocusChange"; }, never, never, false>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CdkMonitorFocus, never>;
}
Expand Down

0 comments on commit 06c2164

Please sign in to comment.