From 932211e26f5df6b028240b1240b15f48e7da417d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 21 Aug 2018 20:39:44 +0200 Subject: [PATCH] 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. --- src/cdk/a11y/focus-monitor/focus-monitor.ts | 60 ++++++++++++++----- src/lib/button-toggle/button-toggle.ts | 4 +- src/lib/button/button.ts | 4 +- src/lib/checkbox/checkbox.ts | 4 +- src/lib/expansion/expansion-panel-header.ts | 4 +- src/lib/menu/menu-item.ts | 4 +- src/lib/radio/radio.ts | 4 +- src/lib/slide-toggle/slide-toggle.ts | 4 +- src/lib/slider/slider.ts | 4 +- src/lib/stepper/step-header.ts | 4 +- src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 4 +- src/lib/tooltip/tooltip.ts | 4 +- .../focus-monitor-focus-via-example.ts | 4 +- .../focus-monitor-overview-example.ts | 8 +-- .../form-field-custom-control-example.ts | 4 +- 15 files changed, 74 insertions(+), 46 deletions(-) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 08899712b26..9a0a1fae831 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -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 { + monitor(element: HTMLElement, checkChildren?: boolean): Observable; + + /** + * 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, checkChildren?: boolean): Observable; + + monitor(element: HTMLElement | ElementRef, + checkChildren: boolean = false): Observable { // 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() }; - 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): void; + + stopMonitoring(element: HTMLElement | ElementRef): 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 { + 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(); } } diff --git a/src/lib/button-toggle/button-toggle.ts b/src/lib/button-toggle/button-toggle.ts index 225c40be564..23abf0513db 100644 --- a/src/lib/button-toggle/button-toggle.ts +++ b/src/lib/button-toggle/button-toggle.ts @@ -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. */ diff --git a/src/lib/button/button.ts b/src/lib/button/button.ts index e6c0dc71ee7..50823689ffb 100644 --- a/src/lib/button/button.ts +++ b/src/lib/button/button.ts @@ -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. */ diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts index 9203f7a2fca..51918750d7b 100644 --- a/src/lib/checkbox/checkbox.ts +++ b/src/lib/checkbox/checkbox.ts @@ -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); } /** diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts index c871cb83d9c..578fa78f165 100644 --- a/src/lib/expansion/expansion-panel-header.ts +++ b/src/lib/expansion/expansion-panel-header.ts @@ -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); } } diff --git a/src/lib/menu/menu-item.ts b/src/lib/menu/menu-item.ts index 4a721fc1153..227417c20ff 100644 --- a/src/lib/menu/menu-item.ts +++ b/src/lib/menu/menu-item.ts @@ -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) { diff --git a/src/lib/radio/radio.ts b/src/lib/radio/radio.ts index 6ab92d24163..5c130621962 100644 --- a/src/lib/radio/radio.ts +++ b/src/lib/radio/radio.ts @@ -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(); } diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts index 598faab208a..bd7f8b733e7 100644 --- a/src/lib/slide-toggle/slide-toggle.ts +++ b/src/lib/slide-toggle/slide-toggle.ts @@ -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. */ diff --git a/src/lib/slider/slider.ts b/src/lib/slider/slider.ts index 010cd84445f..ff2fd7aef4e 100644 --- a/src/lib/slider/slider.ts +++ b/src/lib/slider/slider.ts @@ -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(); } diff --git a/src/lib/stepper/step-header.ts b/src/lib/stepper/step-header.ts index e28acbd13e1..d152e949448 100644 --- a/src/lib/stepper/step-header.ts +++ b/src/lib/stepper/step-header.ts @@ -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. */ diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index f1da8e229da..a1a85d3010b 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -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); } } diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index b7a8b26a142..c83abbdca97 100644 --- a/src/lib/tooltip/tooltip.ts +++ b/src/lib/tooltip/tooltip.ts @@ -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 */ diff --git a/src/material-examples/focus-monitor-focus-via/focus-monitor-focus-via-example.ts b/src/material-examples/focus-monitor-focus-via/focus-monitor-focus-via-example.ts index 78c520045b0..f2a81c13a43 100644 --- a/src/material-examples/focus-monitor-focus-via/focus-monitor-focus-via-example.ts +++ b/src/material-examples/focus-monitor-focus-via/focus-monitor-focus-via-example.ts @@ -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 { diff --git a/src/material-examples/focus-monitor-overview/focus-monitor-overview-example.ts b/src/material-examples/focus-monitor-overview/focus-monitor-overview-example.ts index 7e4abd64864..b9f10ac2046 100644 --- a/src/material-examples/focus-monitor-overview/focus-monitor-overview-example.ts +++ b/src/material-examples/focus-monitor-overview/focus-monitor-overview-example.ts @@ -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 { diff --git a/src/material-examples/form-field-custom-control/form-field-custom-control-example.ts b/src/material-examples/form-field-custom-control/form-field-custom-control-example.ts index 314611d7733..a077ce7020d 100644 --- a/src/material-examples/form-field-custom-control/form-field-custom-control-example.ts +++ b/src/material-examples/form-field-custom-control/form-field-custom-control-example.ts @@ -88,7 +88,7 @@ export class MyTelInput implements MatFormFieldControl, 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, OnDestroy { ngOnDestroy() { this.stateChanges.complete(); - this.fm.stopMonitoring(this.elRef.nativeElement); + this.fm.stopMonitoring(this.elRef); } setDescribedByIds(ids: string[]) {