Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have to repeat the docs comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK otherwise it won't display for all signatures in the IDE autocompletion.

* 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 @@ -79,7 +79,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 @@ -129,7 +129,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 @@ -230,7 +230,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 @@ -261,7 +261,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