Skip to content
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
4 changes: 1 addition & 3 deletions src/cdk/a11y/focus-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {A11yModule} from './index';
describe('FocusMonitor', () => {
let fixture: ComponentFixture<PlainButton>;
let buttonElement: HTMLElement;
let buttonRenderer: Renderer2;
let focusMonitor: FocusMonitor;
let changeHandler: (origin: FocusOrigin) => void;

Expand All @@ -28,11 +27,10 @@ describe('FocusMonitor', () => {
fixture.detectChanges();

buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
buttonRenderer = fixture.componentInstance.renderer;
focusMonitor = fm;

changeHandler = jasmine.createSpy('focus origin change handler');
focusMonitor.monitor(buttonElement, buttonRenderer, false).subscribe(changeHandler);
focusMonitor.monitor(buttonElement, false).subscribe(changeHandler);
patchElementFocus(buttonElement);
}));

Expand Down
118 changes: 82 additions & 36 deletions src/cdk/a11y/focus-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null;
type MonitoredElementInfo = {
unlisten: Function,
checkChildren: boolean,
renderer: Renderer2,
subject: Subject<FocusOrigin>
};

Expand All @@ -62,22 +61,38 @@ export class FocusMonitor {
/** Weak map of elements being monitored to their info. */
private _elementInfo = new WeakMap<Element, MonitoredElementInfo>();

constructor(private _ngZone: NgZone, private _platform: Platform) {
this._ngZone.runOutsideAngular(() => this._registerDocumentEvents());
}
/** A map of global objects to lists of current listeners. */
private _unregisterGlobalListeners = () => {};

/** The number of elements currently being monitored. */
private _monitoredElementCount = 0;

constructor(private _ngZone: NgZone, private _platform: Platform) {}

/**
* @docs-private
* @deprecated renderer param no longer needed.
*/
monitor(element: HTMLElement, renderer: Renderer2, checkChildren: boolean):
Observable<FocusOrigin>;
/**
* Monitors focus on an element and applies appropriate CSS classes.
* @param element The element to monitor
* @param renderer The renderer to use to apply CSS classes to the element.
* @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: HTMLElement, checkChildren: boolean): Observable<FocusOrigin>;
monitor(
element: HTMLElement,
renderer: Renderer2,
checkChildren: boolean): Observable<FocusOrigin> {
renderer: Renderer2 | boolean,
checkChildren?: boolean): Observable<FocusOrigin> {
// TODO(mmalerba): clean up after deprecated signature is removed.
if (!(renderer instanceof Renderer2)) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you add this to the google cleanups tracker?

checkChildren = renderer;
}
checkChildren = !!checkChildren;

// Do nothing if we're not on the browser platform.
if (!this._platform.isBrowser) {
return observableOf(null);
Expand All @@ -93,10 +108,10 @@ export class FocusMonitor {
let info: MonitoredElementInfo = {
unlisten: () => {},
checkChildren: checkChildren,
renderer: renderer,
subject: new Subject<FocusOrigin>()
};
this._elementInfo.set(element, 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);
Expand Down Expand Up @@ -128,6 +143,7 @@ export class FocusMonitor {

this._setClasses(element);
this._elementInfo.delete(element);
this._decrementMonitoredElementCount();
}
}

Expand All @@ -142,49 +158,69 @@ export class FocusMonitor {
}

/** Register necessary event listeners on the document and window. */
private _registerDocumentEvents() {
private _registerGlobalListeners() {
// Do nothing if we're not on the browser platform.
if (!this._platform.isBrowser) {
return;
}

// Note: we listen to events in the capture phase so we can detect them even if the user stops
// propagation.

// On keydown record the origin and clear any touch event that may be in progress.
document.addEventListener('keydown', () => {
let documentKeydownListener = () => {
this._lastTouchTarget = null;
this._setOriginForCurrentEventQueue('keyboard');
}, true);
};

// On mousedown record the origin only if there is not touch target, since a mousedown can
// happen as a result of a touch event.
document.addEventListener('mousedown', () => {
let documentMousedownListener = () => {
if (!this._lastTouchTarget) {
this._setOriginForCurrentEventQueue('mouse');
}
}, true);
};

// When the touchstart event fires the focus event is not yet in the event queue. This means
// we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
// see if a focus happens.
document.addEventListener('touchstart', (event: TouchEvent) => {
let documentTouchstartListener = (event: TouchEvent) => {
if (this._touchTimeout != null) {
clearTimeout(this._touchTimeout);
}
this._lastTouchTarget = event.target;
this._touchTimeout = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);

// Note that we need to cast the event options to `any`, because at the time of writing
// (TypeScript 2.5), the built-in types don't support the `addEventListener` options param.
}, supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
};

// Make a note of when the window regains focus, so we can restore the origin info for the
// focused element.
window.addEventListener('focus', () => {
let windowFocusListener = () => {
this._windowFocused = true;
setTimeout(() => this._windowFocused = false, 0);
};

// Note: we listen to events in the capture phase so we can detect them even if the user stops
// propagation.
this._ngZone.runOutsideAngular(() => {
document.addEventListener('keydown', documentKeydownListener, true);
document.addEventListener('mousedown', documentMousedownListener, true);
document.addEventListener('touchstart', documentTouchstartListener,
supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
window.addEventListener('focus', windowFocusListener);
});

this._unregisterGlobalListeners = () => {
document.removeEventListener('keydown', documentKeydownListener, true);
document.removeEventListener('mousedown', documentMousedownListener, true);
document.removeEventListener('touchstart', documentTouchstartListener,
supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
window.removeEventListener('focus', windowFocusListener);
};
}

private _toggleClass(element: Element, className: string, shouldSet: boolean) {
if (shouldSet) {
element.classList.add(className);
} else {
element.classList.remove(className);
}
}

/**
Expand All @@ -196,16 +232,11 @@ export class FocusMonitor {
const elementInfo = this._elementInfo.get(element);

if (elementInfo) {
const toggleClass = (className: string, shouldSet: boolean) => {
shouldSet ? elementInfo.renderer.addClass(element, className) :
elementInfo.renderer.removeClass(element, className);
};

toggleClass('cdk-focused', !!origin);
toggleClass('cdk-touch-focused', origin === 'touch');
toggleClass('cdk-keyboard-focused', origin === 'keyboard');
toggleClass('cdk-mouse-focused', origin === 'mouse');
toggleClass('cdk-program-focused', origin === 'program');
this._toggleClass(element, 'cdk-focused', !!origin);
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
}
}

Expand Down Expand Up @@ -235,7 +266,7 @@ export class FocusMonitor {
// result, this code will still consider it to have been caused by the touch event and will
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
// relatively small edge-case that can be worked around by using
// focusVia(parentEl, renderer, 'program') to focus the parent element.
// focusVia(parentEl, 'program') to focus the parent element.
//
// If we decide that we absolutely must handle this case correctly, we can do so by listening
// for the first focus event after the touchstart, and then the first blur event after that
Expand Down Expand Up @@ -304,6 +335,22 @@ export class FocusMonitor {
this._setClasses(element);
elementInfo.subject.next(null);
}

private _incrementMonitoredElementCount() {
// Register global listeners when first element is monitored.
if (++this._monitoredElementCount == 1) {
this._registerGlobalListeners();
}
}

private _decrementMonitoredElementCount() {
// Unregister global listeners when last element is unmonitored.
if (!--this._monitoredElementCount) {
this._unregisterGlobalListeners();
this._unregisterGlobalListeners = () => {};
}
}

}


Expand All @@ -323,10 +370,9 @@ export class CdkMonitorFocus implements OnDestroy {
private _monitorSubscription: Subscription;
@Output() cdkFocusChange = new EventEmitter<FocusOrigin>();

constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor,
renderer: Renderer2) {
constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) {
this._monitorSubscription = this._focusMonitor.monitor(
this._elementRef.nativeElement, renderer,
this._elementRef.nativeElement,
this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
.subscribe(origin => this.cdkFocusChange.emit(origin));
}
Expand Down
20 changes: 9 additions & 11 deletions src/lib/button-toggle/button-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,29 @@
* found in the LICENSE file at https://angular.io/license
*/

import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
Directive,
ElementRef,
Renderer2,
EventEmitter,
forwardRef,
Input,
OnInit,
OnDestroy,
OnInit,
Optional,
Output,
QueryList,
ViewChild,
ViewEncapsulation,
forwardRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {CanDisable, mixinDisabled} from '@angular/material/core';
import {FocusMonitor} from '@angular/cdk/a11y';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';

/** Acceptable types for a button toggle. */
export type ToggleType = 'checkbox' | 'radio';
Expand Down Expand Up @@ -386,7 +385,6 @@ export class MatButtonToggle implements OnInit, OnDestroy {
@Optional() toggleGroupMultiple: MatButtonToggleGroupMultiple,
private _changeDetectorRef: ChangeDetectorRef,
private _buttonToggleDispatcher: UniqueSelectionDispatcher,
private _renderer: Renderer2,
private _elementRef: ElementRef,
private _focusMonitor: FocusMonitor) {

Expand Down Expand Up @@ -421,7 +419,7 @@ export class MatButtonToggle implements OnInit, OnDestroy {
if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) {
this._checked = true;
}
this._focusMonitor.monitor(this._elementRef.nativeElement, this._renderer, true);
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
}

/** Focuses the button. */
Expand Down
6 changes: 3 additions & 3 deletions src/lib/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {FocusMonitor} from '@angular/cdk/a11y';
import {Platform} from '@angular/cdk/platform';
import {
ChangeDetectionStrategy,
Component,
Expand All @@ -19,7 +21,6 @@ import {
Self,
ViewEncapsulation,
} from '@angular/core';
import {Platform} from '@angular/cdk/platform';
import {
CanColor,
CanDisable,
Expand All @@ -28,7 +29,6 @@ import {
mixinDisabled,
mixinDisableRipple
} from '@angular/material/core';
import {FocusMonitor} from '@angular/cdk/a11y';


// TODO(kara): Convert attribute selectors to classes when attr maps become available
Expand Down Expand Up @@ -141,7 +141,7 @@ export class MatButton extends _MatButtonMixinBase
private _platform: Platform,
private _focusMonitor: FocusMonitor) {
super(renderer, elementRef);
this._focusMonitor.monitor(this._elementRef.nativeElement, this._renderer, true);
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
}

ngOnDestroy() {
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 @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {
AfterViewInit,
Expand Down Expand Up @@ -36,7 +37,6 @@ import {
mixinTabIndex,
RippleRef,
} from '@angular/material/core';
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';


// Increasing integer for generating unique ids for checkbox components.
Expand Down Expand Up @@ -209,7 +209,7 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc

ngAfterViewInit() {
this._focusMonitor
.monitor(this._inputElement.nativeElement, this._renderer, false)
.monitor(this._inputElement.nativeElement, false)
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
}

Expand Down
4 changes: 1 addition & 3 deletions src/lib/expansion/expansion-panel-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
Host,
Input,
OnDestroy,
Renderer2,
ViewEncapsulation,
} from '@angular/core';
import {merge} from 'rxjs/observable/merge';
Expand Down Expand Up @@ -85,7 +84,6 @@ export class MatExpansionPanelHeader implements OnDestroy {
private _parentChangeSubscription = Subscription.EMPTY;

constructor(
renderer: Renderer2,
@Host() public panel: MatExpansionPanel,
private _element: ElementRef,
private _focusMonitor: FocusMonitor,
Expand All @@ -100,7 +98,7 @@ export class MatExpansionPanelHeader implements OnDestroy {
)
.subscribe(() => this._changeDetectorRef.markForCheck());

_focusMonitor.monitor(_element.nativeElement, renderer, false);
_focusMonitor.monitor(_element.nativeElement, false);
}

/** Height of the header while the panel is expanded. */
Expand Down
Loading