feat(focus-monitor): support monitoring ElementRef (#12712)

Allows for an `ElementRef` to be monitored by the `FocusMonitor`. This makes it more convenient, because most of the time we're dealing with `ElementRef` anyway.
This commit is contained in:
Kristiyan Kostadinov
2018-08-21 20:39:44 +02:00
committed by Jeremy Elbourn
parent 636d27e3ae
commit 932211e26f
15 changed files with 74 additions and 46 deletions
+44 -16
View File
@@ -88,14 +88,29 @@ export class FocusMonitor implements OnDestroy {
* @returns An observable that emits when the focus state of the element changes.
* When the element is blurred, null will be emitted.
*/
monitor(element: HTMLElement, checkChildren: boolean = false): Observable<FocusOrigin> {
monitor(element: HTMLElement, checkChildren?: boolean): Observable<FocusOrigin>;
/**
* Monitors focus on an element and applies appropriate CSS classes.
* @param element The element to monitor
* @param checkChildren Whether to count the element as focused when its children are focused.
* @returns An observable that emits when the focus state of the element changes.
* When the element is blurred, null will be emitted.
*/
monitor(element: ElementRef<HTMLElement>, checkChildren?: boolean): Observable<FocusOrigin>;
monitor(element: HTMLElement | ElementRef<HTMLElement>,
checkChildren: boolean = false): Observable<FocusOrigin> {
// Do nothing if we're not on the browser platform.
if (!this._platform.isBrowser) {
return observableOf(null);
}
const nativeElement = this._getNativeElement(element);
// Check if we're already monitoring this element.
if (this._elementInfo.has(element)) {
let cachedInfo = this._elementInfo.get(element);
if (this._elementInfo.has(nativeElement)) {
let cachedInfo = this._elementInfo.get(nativeElement);
cachedInfo!.checkChildren = checkChildren;
return cachedInfo!.subject.asObservable();
}
@@ -106,21 +121,21 @@ export class FocusMonitor implements OnDestroy {
checkChildren: checkChildren,
subject: new Subject<FocusOrigin>()
};
this._elementInfo.set(element, info);
this._elementInfo.set(nativeElement, info);
this._incrementMonitoredElementCount();
// Start listening. We need to listen in capture phase since focus events don't bubble.
let focusListener = (event: FocusEvent) => this._onFocus(event, element);
let blurListener = (event: FocusEvent) => this._onBlur(event, element);
let focusListener = (event: FocusEvent) => this._onFocus(event, nativeElement);
let blurListener = (event: FocusEvent) => this._onBlur(event, nativeElement);
this._ngZone.runOutsideAngular(() => {
element.addEventListener('focus', focusListener, true);
element.addEventListener('blur', blurListener, true);
nativeElement.addEventListener('focus', focusListener, true);
nativeElement.addEventListener('blur', blurListener, true);
});
// Create an unlisten function for later.
info.unlisten = () => {
element.removeEventListener('focus', focusListener, true);
element.removeEventListener('blur', blurListener, true);
nativeElement.removeEventListener('focus', focusListener, true);
nativeElement.removeEventListener('blur', blurListener, true);
};
return info.subject.asObservable();
@@ -130,15 +145,24 @@ export class FocusMonitor implements OnDestroy {
* Stops monitoring an element and removes all focus classes.
* @param element The element to stop monitoring.
*/
stopMonitoring(element: HTMLElement): void {
const elementInfo = this._elementInfo.get(element);
stopMonitoring(element: HTMLElement): void;
/**
* Stops monitoring an element and removes all focus classes.
* @param element The element to stop monitoring.
*/
stopMonitoring(element: ElementRef<HTMLElement>): void;
stopMonitoring(element: HTMLElement | ElementRef<HTMLElement>): void {
const nativeElement = this._getNativeElement(element);
const elementInfo = this._elementInfo.get(nativeElement);
if (elementInfo) {
elementInfo.unlisten();
elementInfo.subject.complete();
this._setClasses(element);
this._elementInfo.delete(element);
this._setClasses(nativeElement);
this._elementInfo.delete(nativeElement);
this._decrementMonitoredElementCount();
}
}
@@ -370,6 +394,10 @@ export class FocusMonitor implements OnDestroy {
this._unregisterGlobalListeners = () => {};
}
}
private _getNativeElement(element: HTMLElement | ElementRef<HTMLElement>): HTMLElement {
return element instanceof ElementRef ? element.nativeElement : element;
}
}
@@ -391,13 +419,13 @@ export class CdkMonitorFocus implements OnDestroy {
constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) {
this._monitorSubscription = this._focusMonitor.monitor(
this._elementRef.nativeElement,
this._elementRef,
this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
.subscribe(origin => this.cdkFocusChange.emit(origin));
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
this._monitorSubscription.unsubscribe();
}
}
+2 -2
View File
@@ -433,11 +433,11 @@ export class MatButtonToggle extends _MatButtonToggleMixinBase implements OnInit
this.checked = true;
}
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
this._focusMonitor.monitor(this._elementRef, true);
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}
/** Focuses the button. */
+2 -2
View File
@@ -106,7 +106,7 @@ export class MatButton extends _MatButtonMixinBase
}
}
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
this._focusMonitor.monitor(this._elementRef, true);
if (this.isRoundButton) {
this.color = DEFAULT_ROUND_BUTTON_COLOR;
@@ -114,7 +114,7 @@ export class MatButton extends _MatButtonMixinBase
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}
/** Focuses the button. */
+2 -2
View File
@@ -197,12 +197,12 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
ngAfterViewInit() {
this._focusMonitor
.monitor(this._inputElement.nativeElement)
.monitor(this._inputElement)
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._inputElement.nativeElement);
this._focusMonitor.stopMonitoring(this._inputElement);
}
/**
+2 -2
View File
@@ -83,7 +83,7 @@ export class MatExpansionPanelHeader implements OnDestroy {
)
.subscribe(() => this._changeDetectorRef.markForCheck());
_focusMonitor.monitor(_element.nativeElement);
_focusMonitor.monitor(_element);
}
/** Height of the header while the panel is expanded. */
@@ -133,7 +133,7 @@ export class MatExpansionPanelHeader implements OnDestroy {
ngOnDestroy() {
this._parentChangeSubscription.unsubscribe();
this._focusMonitor.stopMonitoring(this._element.nativeElement);
this._focusMonitor.stopMonitoring(this._element);
}
}
+2 -2
View File
@@ -82,7 +82,7 @@ export class MatMenuItem extends _MatMenuItemMixinBase
// Start monitoring the element so it gets the appropriate focused classes. We want
// to show the focus style for menu items only when the focus was not caused by a
// mouse or touch interaction.
_focusMonitor.monitor(this._getHostElement(), false);
_focusMonitor.monitor(this._elementRef, false);
}
if (_parentMenu && _parentMenu.addItem) {
@@ -103,7 +103,7 @@ export class MatMenuItem extends _MatMenuItemMixinBase
ngOnDestroy() {
if (this._focusMonitor) {
this._focusMonitor.stopMonitoring(this._getHostElement());
this._focusMonitor.stopMonitoring(this._elementRef);
}
if (this._parentMenu && this._parentMenu.removeItem) {
+2 -2
View File
@@ -518,12 +518,12 @@ export class MatRadioButton extends _MatRadioButtonMixinBase
ngAfterViewInit() {
this._focusMonitor
.monitor(this._inputElement.nativeElement)
.monitor(this._inputElement)
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._inputElement.nativeElement);
this._focusMonitor.stopMonitoring(this._inputElement);
this._removeUniqueSelectionListener();
}
+2 -2
View File
@@ -194,7 +194,7 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
ngAfterContentInit() {
this._focusMonitor
.monitor(this._elementRef.nativeElement, true)
.monitor(this._elementRef, true)
.subscribe(focusOrigin => {
if (!focusOrigin) {
// When a focused element becomes disabled, the browser *immediately* fires a blur event.
@@ -208,7 +208,7 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}
/** Method being called whenever the underlying input emits a change event. */
+2 -2
View File
@@ -465,7 +465,7 @@ export class MatSlider extends _MatSliderMixinBase
ngOnInit() {
this._focusMonitor
.monitor(this._elementRef.nativeElement, true)
.monitor(this._elementRef, true)
.subscribe((origin: FocusOrigin) => {
this._isActive = !!origin && origin !== 'keyboard';
this._changeDetectorRef.detectChanges();
@@ -478,7 +478,7 @@ export class MatSlider extends _MatSliderMixinBase
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
this._dirChangeSubscription.unsubscribe();
}
+2 -2
View File
@@ -64,13 +64,13 @@ export class MatStepHeader implements OnDestroy {
private _focusMonitor: FocusMonitor,
private _element: ElementRef,
changeDetectorRef: ChangeDetectorRef) {
_focusMonitor.monitor(_element.nativeElement, true);
_focusMonitor.monitor(_element, true);
this._intlSubscription = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
}
ngOnDestroy() {
this._intlSubscription.unsubscribe();
this._focusMonitor.stopMonitoring(this._element.nativeElement);
this._focusMonitor.stopMonitoring(this._element);
}
/** Returns string label of given step if it is a text label. */
+2 -2
View File
@@ -247,7 +247,7 @@ export class MatTabLink extends _MatTabLinkMixinBase
}
if (_focusMonitor) {
_focusMonitor.monitor(_elementRef.nativeElement);
_focusMonitor.monitor(_elementRef);
}
}
@@ -255,7 +255,7 @@ export class MatTabLink extends _MatTabLinkMixinBase
this._tabLinkRipple._removeTriggerEvents();
if (this._focusMonitor) {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}
}
+2 -2
View File
@@ -234,7 +234,7 @@ export class MatTooltip implements OnDestroy {
element.style['webkitUserDrag'] = '';
}
_focusMonitor.monitor(element).pipe(takeUntil(this._destroyed)).subscribe(origin => {
_focusMonitor.monitor(_elementRef).pipe(takeUntil(this._destroyed)).subscribe(origin => {
// Note that the focus monitor runs outside the Angular zone.
if (!origin) {
_ngZone.run(() => this.hide(0));
@@ -265,7 +265,7 @@ export class MatTooltip implements OnDestroy {
this._destroyed.complete();
this._ariaDescriber.removeDescription(this._elementRef.nativeElement, this.message);
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}
/** Shows the tooltip after the delay in ms, defaults to tooltip-delay-show or 0ms if no input */
@@ -25,7 +25,7 @@ export class FocusMonitorFocusViaExample implements OnDestroy, OnInit {
private ngZone: NgZone) {}
ngOnInit() {
this.focusMonitor.monitor(this.monitoredEl.nativeElement)
this.focusMonitor.monitor(this.monitoredEl)
.subscribe(origin => this.ngZone.run(() => {
this.origin = this.formatOrigin(origin);
this.cdr.markForCheck();
@@ -33,7 +33,7 @@ export class FocusMonitorFocusViaExample implements OnDestroy, OnInit {
}
ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.monitoredEl.nativeElement);
this.focusMonitor.stopMonitoring(this.monitoredEl);
}
formatOrigin(origin: FocusOrigin): string {
@@ -27,12 +27,12 @@ export class FocusMonitorOverviewExample implements OnDestroy, OnInit {
private ngZone: NgZone) {}
ngOnInit() {
this.focusMonitor.monitor(this.element.nativeElement)
this.focusMonitor.monitor(this.element)
.subscribe(origin => this.ngZone.run(() => {
this.elementOrigin = this.formatOrigin(origin);
this.cdr.markForCheck();
}));
this.focusMonitor.monitor(this.subtree.nativeElement, true)
this.focusMonitor.monitor(this.subtree, true)
.subscribe(origin => this.ngZone.run(() => {
this.subtreeOrigin = this.formatOrigin(origin);
this.cdr.markForCheck();
@@ -40,8 +40,8 @@ export class FocusMonitorOverviewExample implements OnDestroy, OnInit {
}
ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.element.nativeElement);
this.focusMonitor.stopMonitoring(this.subtree.nativeElement);
this.focusMonitor.stopMonitoring(this.element);
this.focusMonitor.stopMonitoring(this.subtree);
}
formatOrigin(origin: FocusOrigin): string {
@@ -88,7 +88,7 @@ export class MyTelInput implements MatFormFieldControl<MyTel>, OnDestroy {
subscriber: '',
});
fm.monitor(elRef.nativeElement, true).subscribe(origin => {
fm.monitor(elRef, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
@@ -96,7 +96,7 @@ export class MyTelInput implements MatFormFieldControl<MyTel>, OnDestroy {
ngOnDestroy() {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef.nativeElement);
this.fm.stopMonitoring(this.elRef);
}
setDescribedByIds(ids: string[]) {