Skip to content

Commit

Permalink
feat(focus-monitor): support monitoring ElementRef (#12712)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto authored and jelbourn committed Aug 21, 2018
1 parent 636d27e commit 932211e
Show file tree
Hide file tree
Showing 15 changed files with 74 additions and 46 deletions.
60 changes: 44 additions & 16 deletions src/cdk/a11y/focus-monitor/focus-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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();
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -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;
}
}


Expand All @@ -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();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/button-toggle/button-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
4 changes: 2 additions & 2 deletions src/lib/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ 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;
}
}

ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}

/** Focuses the button. */
Expand Down
4 changes: 2 additions & 2 deletions src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/lib/expansion/expansion-panel-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -133,7 +133,7 @@ export class MatExpansionPanelHeader implements OnDestroy {

ngOnDestroy() {
this._parentChangeSubscription.unsubscribe();
this._focusMonitor.stopMonitoring(this._element.nativeElement);
this._focusMonitor.stopMonitoring(this._element);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/radio/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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. */
Expand Down
4 changes: 2 additions & 2 deletions src/lib/slider/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -478,7 +478,7 @@ export class MatSlider extends _MatSliderMixinBase
}

ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
this._dirChangeSubscription.unsubscribe();
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
4 changes: 2 additions & 2 deletions src/lib/tabs/tab-nav-bar/tab-nav-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,15 @@ export class MatTabLink extends _MatTabLinkMixinBase
}

if (_focusMonitor) {
_focusMonitor.monitor(_elementRef.nativeElement);
_focusMonitor.monitor(_elementRef);
}
}

ngOnDestroy() {
this._tabLinkRipple._removeTriggerEvents();

if (this._focusMonitor) {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
this._focusMonitor.stopMonitoring(this._elementRef);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ 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();
}));
}

ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.monitoredEl.nativeElement);
this.focusMonitor.stopMonitoring(this.monitoredEl);
}

formatOrigin(origin: FocusOrigin): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ 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();
}));
}

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ 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();
});
}

ngOnDestroy() {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef.nativeElement);
this.fm.stopMonitoring(this.elRef);
}

setDescribedByIds(ids: string[]) {
Expand Down

0 comments on commit 932211e

Please sign in to comment.