From 3363c86e3ffe12c308b0ff07d48c451f05bc512b Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 13 Jan 2017 17:38:13 -0800 Subject: [PATCH 01/12] Add directive to determine how elements were focused. --- src/demo-app/demo-app-module.ts | 5 +- src/demo-app/demo-app/demo-app.ts | 3 +- src/demo-app/demo-app/routes.ts | 4 +- src/demo-app/style/style-demo.html | 3 + src/demo-app/style/style-demo.scss | 15 +++ src/demo-app/style/style-demo.ts | 10 ++ src/lib/core/core.ts | 2 +- src/lib/core/style/add-focus-classes.spec.ts | 112 +++++++++++++++++++ src/lib/core/style/add-focus-classes.ts | 80 +++++++++++++ src/lib/core/style/index.ts | 15 +++ src/lib/module.ts | 2 + 11 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 src/demo-app/style/style-demo.html create mode 100644 src/demo-app/style/style-demo.scss create mode 100644 src/demo-app/style/style-demo.ts create mode 100644 src/lib/core/style/add-focus-classes.spec.ts create mode 100644 src/lib/core/style/add-focus-classes.ts create mode 100644 src/lib/core/style/index.ts diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 47b54dbb1036..541909dbf171 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -4,8 +4,7 @@ import {HttpModule} from '@angular/http'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {DemoApp, Home} from './demo-app/demo-app'; import {RouterModule} from '@angular/router'; -import {MaterialModule, OverlayContainer, - FullscreenOverlayContainer} from '@angular/material'; +import {MaterialModule, OverlayContainer, FullscreenOverlayContainer} from '@angular/material'; import {DEMO_APP_ROUTES} from './demo-app/routes'; import {ProgressBarDemo} from './progress-bar/progress-bar-demo'; import {JazzDialog, ContentElementDialog, DialogDemo, IFrameDialog} from './dialog/dialog-demo'; @@ -38,6 +37,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d import {PlatformDemo} from './platform/platform-demo'; import {AutocompleteDemo} from './autocomplete/autocomplete-demo'; import {InputContainerDemo} from './input/input-container-demo'; +import {StyleDemo} from './style/style-demo'; @NgModule({ imports: [ @@ -86,6 +86,7 @@ import {InputContainerDemo} from './input/input-container-demo'; SliderDemo, SlideToggleDemo, SpagettiPanel, + StyleDemo, ToolbarDemo, TooltipDemo, TabsDemo, diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts index 98c029d770cc..cdbdcd904484 100644 --- a/src/demo-app/demo-app/demo-app.ts +++ b/src/demo-app/demo-app/demo-app.ts @@ -50,7 +50,8 @@ export class DemoApp { {name: 'Tabs', route: 'tabs'}, {name: 'Toolbar', route: 'toolbar'}, {name: 'Tooltip', route: 'tooltip'}, - {name: 'Platform', route: 'platform'} + {name: 'Platform', route: 'platform'}, + {name: 'Style', route: 'style'} ]; constructor(private _element: ElementRef) { diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts index 3a1b3ba916b6..c1d92d0fab43 100644 --- a/src/demo-app/demo-app/routes.ts +++ b/src/demo-app/demo-app/routes.ts @@ -32,6 +32,7 @@ import {TABS_DEMO_ROUTES} from '../tabs/routes'; import {PlatformDemo} from '../platform/platform-demo'; import {AutocompleteDemo} from '../autocomplete/autocomplete-demo'; import {InputContainerDemo} from '../input/input-container-demo'; +import {StyleDemo} from '../style/style-demo'; export const DEMO_APP_ROUTES: Routes = [ {path: '', component: Home}, @@ -65,5 +66,6 @@ export const DEMO_APP_ROUTES: Routes = [ {path: 'dialog', component: DialogDemo}, {path: 'tooltip', component: TooltipDemo}, {path: 'snack-bar', component: SnackBarDemo}, - {path: 'platform', component: PlatformDemo} + {path: 'platform', component: PlatformDemo}, + {path: 'style', component: StyleDemo}, ]; diff --git a/src/demo-app/style/style-demo.html b/src/demo-app/style/style-demo.html new file mode 100644 index 000000000000..509472ae91a6 --- /dev/null +++ b/src/demo-app/style/style-demo.html @@ -0,0 +1,3 @@ + + +
Active classes: {{b.classList}}
diff --git a/src/demo-app/style/style-demo.scss b/src/demo-app/style/style-demo.scss new file mode 100644 index 000000000000..e11216c5614a --- /dev/null +++ b/src/demo-app/style/style-demo.scss @@ -0,0 +1,15 @@ +.demo-button.cdk-focused { + border: 2px solid red; +} + +.demo-button.cdk-mouse-focused { + background: green; +} + +.demo-button.cdk-keyboard-focused { + background: yellow; +} + +.demo-button.cdk-programmatically-focused { + background: blue; +} diff --git a/src/demo-app/style/style-demo.ts b/src/demo-app/style/style-demo.ts new file mode 100644 index 000000000000..1705c448213c --- /dev/null +++ b/src/demo-app/style/style-demo.ts @@ -0,0 +1,10 @@ +import {Component} from '@angular/core'; + + +@Component({ + moduleId: module.id, + selector: 'style-demo', + templateUrl: 'style-demo.html', + styleUrls: ['style-demo.css'], +}) +export class StyleDemo {} diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index b0c08443f1b0..38aba4c9665f 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -99,7 +99,7 @@ export { export {MdLineModule, MdLine, MdLineSetter} from './line/line'; // Style -export {applyCssTransform} from './style/apply-transform'; +export * from './style/index'; // Error export {MdError} from './errors/error'; diff --git a/src/lib/core/style/add-focus-classes.spec.ts b/src/lib/core/style/add-focus-classes.spec.ts new file mode 100644 index 000000000000..52597aa7106f --- /dev/null +++ b/src/lib/core/style/add-focus-classes.spec.ts @@ -0,0 +1,112 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {StyleModule} from './index'; +import {By} from '@angular/platform-browser'; + + +describe('MdSlider', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [StyleModule], + declarations: [ + ButtonWithFocusClasses, + ], + }); + + TestBed.compileComponents(); + })); + + describe('cdkAddFocusClasses', () => { + let fixture: ComponentFixture; + let buttonElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(ButtonWithFocusClasses); + fixture.detectChanges(); + + buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; + }); + + it('should initially not be focused', () => { + expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes'); + }); + + it('should detect focus via keyboard', async(() => { + // Simulate focus via keyboard. + dispatchKeydownEvent(document, 9 /* tab */); + buttonElement.focus(); + fixture.detectChanges(); + + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }, 0); + })); + + it('should detect focus via mouse', async(() => { + // Simulate focus via mouse. + dispatchMousedownEvent(document); + buttonElement.focus(); + fixture.detectChanges(); + + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }, 0); + })); + + it('should detect programmatic focus', async(() => { + // Programmatically focus. + buttonElement.focus(); + fixture.detectChanges(); + + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-programmatically-focused')) + .toBe(true, 'button should have cdk-programmatically-focused class'); + }, 0); + })); + }); +}); + + +@Component({template: ``}) +class ButtonWithFocusClasses {} + + +/** Dispatches a mousedown event on the specified element. */ +function dispatchMousedownEvent(element: Node) { + let event = document.createEvent('MouseEvent'); + event.initMouseEvent( + 'mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + element.dispatchEvent(event); +} + + +/** Dispatches a keydown event on the specified element. */ +function dispatchKeydownEvent(element: Node, keyCode: number) { + let event: any = document.createEvent('KeyboardEvent'); + (event.initKeyEvent || event.initKeyboardEvent).bind(event)( + 'keydown', true, true, window, 0, 0, 0, 0, 0, keyCode); + Object.defineProperty(event, 'keyCode', { + get: function() { return keyCode; } + }); + element.dispatchEvent(event); +} diff --git a/src/lib/core/style/add-focus-classes.ts b/src/lib/core/style/add-focus-classes.ts new file mode 100644 index 000000000000..673e23cbd3ac --- /dev/null +++ b/src/lib/core/style/add-focus-classes.ts @@ -0,0 +1,80 @@ +import {Directive, Injectable, Optional, SkipSelf} from '@angular/core'; + + +/** Singleton that allows all instances of CdkAddFocusClasses to share document event listeners. */ +@Injectable() +export class CdkFocusCauseDetector { + /** Whether a keydown event has just occurred. */ + get keydownOccurred() { return this._keydownOccurred; } + private _keydownOccurred = false; + + get mousedownOccurred() { return this._mousedownOccurred; } + private _mousedownOccurred = false; + + constructor() { + document.addEventListener('keydown', () => { + this._keydownOccurred = true; + setTimeout(() => this._keydownOccurred = false, 0); + }, true); + + document.addEventListener('mousedown', () => { + this._mousedownOccurred = true; + setTimeout(() => this._mousedownOccurred = false, 0); + }, true); + } +} + + +/** + * Directive that determines how a particular element was focused (via keyboard, mouse, or + * programmatically) and adds corresponding classes to the element. + */ +@Directive({ + selector: '[cdkAddFocusClasses]', + host: { + '[class.cdk-focused]': 'keyboardFocused || mouseFocused || programmaticallyFocused', + '[class.cdk-keyboard-focused]': 'keyboardFocused', + '[class.cdk-mouse-focused]': 'mouseFocused', + '[class.cdk-programmatically-focused]': 'programmaticallyFocused', + '(focus)': '_onFocus()', + '(blur)': '_onBlur()', + } +}) +export class CdkAddFocusClasses { + /** Whether the elmenet is focused due to a keyboard event. */ + keyboardFocused = false; + + /** Whether the element is focused due to a mouse event. */ + mouseFocused = false; + + /** Whether the has been programmatically focused. */ + programmaticallyFocused = false; + + constructor(private _focusCauseDetector: CdkFocusCauseDetector) {} + + /** Handles focus event on the element. */ + _onFocus() { + this.keyboardFocused = this._focusCauseDetector.keydownOccurred; + this.mouseFocused = this._focusCauseDetector.mousedownOccurred; + this.programmaticallyFocused = !this.keyboardFocused && !this.mouseFocused; + } + + /** Handles blur event on the element. */ + _onBlur() { + this.keyboardFocused = this.mouseFocused = this.programmaticallyFocused = false; + } +} + + +export function FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY( + parentDispatcher: CdkFocusCauseDetector) { + return parentDispatcher || new CdkFocusCauseDetector(); +} + + +export const FOCUS_CAUSE_DETECTOR_PROVIDER = { + // If there is already a CdkFocusCauseDetector available, use that. Otherwise, provide a new one. + provide: CdkFocusCauseDetector, + deps: [[new Optional(), new SkipSelf(), CdkFocusCauseDetector]], + useFactory: FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY +}; diff --git a/src/lib/core/style/index.ts b/src/lib/core/style/index.ts new file mode 100644 index 000000000000..38ff2bd72266 --- /dev/null +++ b/src/lib/core/style/index.ts @@ -0,0 +1,15 @@ +import {NgModule} from '@angular/core'; +import {CdkAddFocusClasses, FOCUS_CAUSE_DETECTOR_PROVIDER} from './add-focus-classes'; +import {DefaultStyleCompatibilityModeModule} from '../compatibility/default-mode'; + +export * from './add-focus-classes'; +export * from './apply-transform'; + + +@NgModule({ + imports: [DefaultStyleCompatibilityModeModule], + declarations: [CdkAddFocusClasses], + exports: [CdkAddFocusClasses, DefaultStyleCompatibilityModeModule], + providers: [FOCUS_CAUSE_DETECTOR_PROVIDER], +}) +export class StyleModule {} diff --git a/src/lib/module.ts b/src/lib/module.ts index 2c395929e1a5..490e419cb133 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -35,6 +35,7 @@ import {MdMenuModule} from './menu/index'; import {MdDialogModule} from './dialog/index'; import {PlatformModule} from './core/platform/index'; import {MdAutocompleteModule} from './autocomplete/index'; +import {StyleModule} from './core/style/index'; const MATERIAL_MODULES = [ MdAutocompleteModule, @@ -64,6 +65,7 @@ const MATERIAL_MODULES = [ OverlayModule, PortalModule, RtlModule, + StyleModule, A11yModule, PlatformModule, ProjectionModule, From fec6cbb4641c180338519f129237809c3c32a6dc Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 24 Jan 2017 15:31:13 -0800 Subject: [PATCH 02/12] addressed some comments --- src/lib/core/style/add-focus-classes.ts | 20 +++++++++------- src/lib/core/style/perf_results | 26 +++++++++++++++++++++ src/lib/core/style/schedule_async_perf.html | 26 +++++++++++++++++++++ src/lib/core/style/timestamp_perf.html | 25 ++++++++++++++++++++ 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 src/lib/core/style/perf_results create mode 100644 src/lib/core/style/schedule_async_perf.html create mode 100644 src/lib/core/style/timestamp_perf.html diff --git a/src/lib/core/style/add-focus-classes.ts b/src/lib/core/style/add-focus-classes.ts index 673e23cbd3ac..e60c9607f1df 100644 --- a/src/lib/core/style/add-focus-classes.ts +++ b/src/lib/core/style/add-focus-classes.ts @@ -3,7 +3,7 @@ import {Directive, Injectable, Optional, SkipSelf} from '@angular/core'; /** Singleton that allows all instances of CdkAddFocusClasses to share document event listeners. */ @Injectable() -export class CdkFocusCauseDetector { +export class FocusOriginMonitor { /** Whether a keydown event has just occurred. */ get keydownOccurred() { return this._keydownOccurred; } private _keydownOccurred = false; @@ -12,14 +12,16 @@ export class CdkFocusCauseDetector { private _mousedownOccurred = false; constructor() { + // Listen to keydown and mousedown in the capture phase so we can detect them even if the user + // stops propagation. document.addEventListener('keydown', () => { this._keydownOccurred = true; - setTimeout(() => this._keydownOccurred = false, 0); + Promise.resolve().then(() => this._keydownOccurred = false); }, true); document.addEventListener('mousedown', () => { this._mousedownOccurred = true; - setTimeout(() => this._mousedownOccurred = false, 0); + Promise.resolve().then(() => this._mousedownOccurred = false); }, true); } } @@ -50,7 +52,7 @@ export class CdkAddFocusClasses { /** Whether the has been programmatically focused. */ programmaticallyFocused = false; - constructor(private _focusCauseDetector: CdkFocusCauseDetector) {} + constructor(private _focusCauseDetector: FocusOriginMonitor) {} /** Handles focus event on the element. */ _onFocus() { @@ -67,14 +69,14 @@ export class CdkAddFocusClasses { export function FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY( - parentDispatcher: CdkFocusCauseDetector) { - return parentDispatcher || new CdkFocusCauseDetector(); + parentDispatcher: FocusOriginMonitor) { + return parentDispatcher || new FocusOriginMonitor(); } export const FOCUS_CAUSE_DETECTOR_PROVIDER = { - // If there is already a CdkFocusCauseDetector available, use that. Otherwise, provide a new one. - provide: CdkFocusCauseDetector, - deps: [[new Optional(), new SkipSelf(), CdkFocusCauseDetector]], + // If there is already a FocusOriginMonitor available, use that. Otherwise, provide a new one. + provide: FocusOriginMonitor, + deps: [[new Optional(), new SkipSelf(), FocusOriginMonitor]], useFactory: FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY }; diff --git a/src/lib/core/style/perf_results b/src/lib/core/style/perf_results new file mode 100644 index 000000000000..cde9c4565926 --- /dev/null +++ b/src/lib/core/style/perf_results @@ -0,0 +1,26 @@ +results using code in schedule_async_perf.html and timestamp_perf.html, +just holding down the 'd' key: + +schedule async results: +3501.774999999998 +3030.1300000000047 +3032.8799999999974 +3024.5250000000087 +3043.399999999994 +3040.6100000000006 +3035.854999999996 +3033.0149999999994 +3037.0150000000067 +3037.554999999993 + +timestamp results: +3509.304999999993 +3042.4100000000035 +3034.8600000000006 +3032.175000000003 +3030.3900000000067 +3030.520000000004 +3038.3300000000017 +3035.9850000000006 +3033.899999999994 +3041.965000000011 diff --git a/src/lib/core/style/schedule_async_perf.html b/src/lib/core/style/schedule_async_perf.html new file mode 100644 index 000000000000..0cd856fa7c92 --- /dev/null +++ b/src/lib/core/style/schedule_async_perf.html @@ -0,0 +1,26 @@ + + + Schedule Async + + + + + diff --git a/src/lib/core/style/timestamp_perf.html b/src/lib/core/style/timestamp_perf.html new file mode 100644 index 000000000000..0334c71f3f1c --- /dev/null +++ b/src/lib/core/style/timestamp_perf.html @@ -0,0 +1,25 @@ + + + Timestamp + + + + + From a46e5ac00007c2c2a52870c97921280f4bb5079d Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 24 Jan 2017 17:19:54 -0800 Subject: [PATCH 03/12] move all functionality to FocusOriginMonitor and add a method to fake the FocusOrigin. --- src/demo-app/style/style-demo.html | 5 ++ src/demo-app/style/style-demo.ts | 7 +- src/lib/core/style/add-focus-classes.spec.ts | 2 +- src/lib/core/style/add-focus-classes.ts | 91 +++++++++++--------- src/lib/core/style/index.ts | 8 +- 5 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/demo-app/style/style-demo.html b/src/demo-app/style/style-demo.html index 509472ae91a6..96783762aa48 100644 --- a/src/demo-app/style/style-demo.html +++ b/src/demo-app/style/style-demo.html @@ -1,3 +1,8 @@ + + + + +
Active classes: {{b.classList}}
diff --git a/src/demo-app/style/style-demo.ts b/src/demo-app/style/style-demo.ts index 1705c448213c..e1151da131f8 100644 --- a/src/demo-app/style/style-demo.ts +++ b/src/demo-app/style/style-demo.ts @@ -1,4 +1,5 @@ -import {Component} from '@angular/core'; +import {Component, Renderer} from '@angular/core'; +import {FocusOriginMonitor} from '@angular/material'; @Component({ @@ -7,4 +8,6 @@ import {Component} from '@angular/core'; templateUrl: 'style-demo.html', styleUrls: ['style-demo.css'], }) -export class StyleDemo {} +export class StyleDemo { + constructor(public renderer: Renderer, public fom: FocusOriginMonitor) {} +} diff --git a/src/lib/core/style/add-focus-classes.spec.ts b/src/lib/core/style/add-focus-classes.spec.ts index 52597aa7106f..9d5f1b0bbb2c 100644 --- a/src/lib/core/style/add-focus-classes.spec.ts +++ b/src/lib/core/style/add-focus-classes.spec.ts @@ -4,7 +4,7 @@ import {StyleModule} from './index'; import {By} from '@angular/platform-browser'; -describe('MdSlider', () => { +describe('cdkAddFocusClasses', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [StyleModule], diff --git a/src/lib/core/style/add-focus-classes.ts b/src/lib/core/style/add-focus-classes.ts index e60c9607f1df..e27c370fc1d8 100644 --- a/src/lib/core/style/add-focus-classes.ts +++ b/src/lib/core/style/add-focus-classes.ts @@ -1,29 +1,70 @@ -import {Directive, Injectable, Optional, SkipSelf} from '@angular/core'; +import {Directive, Injectable, Optional, SkipSelf, Renderer, ElementRef} from '@angular/core'; -/** Singleton that allows all instances of CdkAddFocusClasses to share document event listeners. */ +export type FocusOrigin = 'mouse' | 'keyboard' | 'programmatic'; + + +/** Monitors mouse and keyboard events to determine the cause of focus events. */ @Injectable() export class FocusOriginMonitor { /** Whether a keydown event has just occurred. */ - get keydownOccurred() { return this._keydownOccurred; } private _keydownOccurred = false; - get mousedownOccurred() { return this._mousedownOccurred; } + /** Whether a mousedown event has just occurred. */ private _mousedownOccurred = false; + /** The focus origin that we're pretending the next focus event is a result of. */ + private _fakeOrigin: FocusOrigin = null; + + /** A function to clear the fake origin. */ + private _clearFakeOrigin = (): void => { + Promise.resolve().then(() => this._fakeOrigin = null); + document.removeEventListener('focus', this._clearFakeOrigin, true); + }; + constructor() { // Listen to keydown and mousedown in the capture phase so we can detect them even if the user // stops propagation. + // TODO(mmalerba): Figure out how to handle touchstart document.addEventListener('keydown', () => { this._keydownOccurred = true; - Promise.resolve().then(() => this._keydownOccurred = false); + setTimeout(() => this._keydownOccurred = false, 0); }, true); document.addEventListener('mousedown', () => { this._mousedownOccurred = true; - Promise.resolve().then(() => this._mousedownOccurred = false); + setTimeout(() => this._mousedownOccurred = false, 0); }, true); } + + /** Register an element to receive focus classes. */ + registerElementForFocusClasses(element: Element, renderer: Renderer) { + renderer.listen(element, 'focus', () => { + let isKeyboard = this._fakeOrigin ? this._fakeOrigin === 'keyboard' : this._keydownOccurred; + let isMouse = this._fakeOrigin ? this._fakeOrigin === 'mouse' : this._mousedownOccurred; + let isProgrammatic = this._fakeOrigin ? + this._fakeOrigin === 'programmatic' : !this._keydownOccurred && !this._mousedownOccurred; + + renderer.setElementClass(element, 'cdk-focused', true); + renderer.setElementClass(element, 'cdk-keyboard-focused', isKeyboard); + renderer.setElementClass(element, 'cdk-mouse-focused', isMouse); + renderer.setElementClass(element, 'cdk-programmatically-focused', isProgrammatic); + }); + + renderer.listen(element, 'blur', () => { + renderer.setElementClass(element, 'cdk-focused', false); + renderer.setElementClass(element, 'cdk-keyboard-focused', false); + renderer.setElementClass(element, 'cdk-mouse-focused', false); + renderer.setElementClass(element, 'cdk-programmatically-focused', false); + }); + } + + /** Focuses the element via the specified focus origin. */ + focusVia(element: Node, renderer: Renderer, focusOrigin: FocusOrigin) { + this._fakeOrigin = focusOrigin; + document.addEventListener('focus', this._clearFakeOrigin, true); + renderer.invokeElementMethod(element, 'focus'); + } } @@ -33,50 +74,22 @@ export class FocusOriginMonitor { */ @Directive({ selector: '[cdkAddFocusClasses]', - host: { - '[class.cdk-focused]': 'keyboardFocused || mouseFocused || programmaticallyFocused', - '[class.cdk-keyboard-focused]': 'keyboardFocused', - '[class.cdk-mouse-focused]': 'mouseFocused', - '[class.cdk-programmatically-focused]': 'programmaticallyFocused', - '(focus)': '_onFocus()', - '(blur)': '_onBlur()', - } }) export class CdkAddFocusClasses { - /** Whether the elmenet is focused due to a keyboard event. */ - keyboardFocused = false; - - /** Whether the element is focused due to a mouse event. */ - mouseFocused = false; - - /** Whether the has been programmatically focused. */ - programmaticallyFocused = false; - - constructor(private _focusCauseDetector: FocusOriginMonitor) {} - - /** Handles focus event on the element. */ - _onFocus() { - this.keyboardFocused = this._focusCauseDetector.keydownOccurred; - this.mouseFocused = this._focusCauseDetector.mousedownOccurred; - this.programmaticallyFocused = !this.keyboardFocused && !this.mouseFocused; - } - - /** Handles blur event on the element. */ - _onBlur() { - this.keyboardFocused = this.mouseFocused = this.programmaticallyFocused = false; + constructor(elementRef: ElementRef, focusOriginMonitor: FocusOriginMonitor, renderer: Renderer) { + focusOriginMonitor.registerElementForFocusClasses(elementRef.nativeElement, renderer); } } -export function FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY( - parentDispatcher: FocusOriginMonitor) { +export function FOCUS_ORIGIN_MONITOR_PROVIDER_FACTORY(parentDispatcher: FocusOriginMonitor) { return parentDispatcher || new FocusOriginMonitor(); } -export const FOCUS_CAUSE_DETECTOR_PROVIDER = { +export const FOCUS_ORIGIN_MONITOR_PROVIDER = { // If there is already a FocusOriginMonitor available, use that. Otherwise, provide a new one. provide: FocusOriginMonitor, deps: [[new Optional(), new SkipSelf(), FocusOriginMonitor]], - useFactory: FOCUS_CAUSE_DETECTOR_PROVIDER_FACTORY + useFactory: FOCUS_ORIGIN_MONITOR_PROVIDER_FACTORY }; diff --git a/src/lib/core/style/index.ts b/src/lib/core/style/index.ts index 38ff2bd72266..a4c6fd06a4bb 100644 --- a/src/lib/core/style/index.ts +++ b/src/lib/core/style/index.ts @@ -1,15 +1,13 @@ import {NgModule} from '@angular/core'; -import {CdkAddFocusClasses, FOCUS_CAUSE_DETECTOR_PROVIDER} from './add-focus-classes'; -import {DefaultStyleCompatibilityModeModule} from '../compatibility/default-mode'; +import {CdkAddFocusClasses, FOCUS_ORIGIN_MONITOR_PROVIDER} from './add-focus-classes'; export * from './add-focus-classes'; export * from './apply-transform'; @NgModule({ - imports: [DefaultStyleCompatibilityModeModule], declarations: [CdkAddFocusClasses], - exports: [CdkAddFocusClasses, DefaultStyleCompatibilityModeModule], - providers: [FOCUS_CAUSE_DETECTOR_PROVIDER], + exports: [CdkAddFocusClasses], + providers: [FOCUS_ORIGIN_MONITOR_PROVIDER], }) export class StyleModule {} From be94341384b423b65bab0aeea6554078e220639d Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 24 Jan 2017 17:22:49 -0800 Subject: [PATCH 04/12] small fix --- src/lib/core/style/add-focus-classes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/core/style/add-focus-classes.ts b/src/lib/core/style/add-focus-classes.ts index e27c370fc1d8..e59715ed9675 100644 --- a/src/lib/core/style/add-focus-classes.ts +++ b/src/lib/core/style/add-focus-classes.ts @@ -18,7 +18,7 @@ export class FocusOriginMonitor { /** A function to clear the fake origin. */ private _clearFakeOrigin = (): void => { - Promise.resolve().then(() => this._fakeOrigin = null); + setTimeout(() => this._fakeOrigin = null, 0); document.removeEventListener('focus', this._clearFakeOrigin, true); }; From 01d4f66fac3b23d0df9844a7ba082d4d2171475e Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 24 Jan 2017 17:29:00 -0800 Subject: [PATCH 05/12] re-did perf test w/ setTimeout just to be sure --- src/lib/core/style/perf_results | 20 ++++++++++---------- src/lib/core/style/schedule_async_perf.html | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/core/style/perf_results b/src/lib/core/style/perf_results index cde9c4565926..3858c00898bb 100644 --- a/src/lib/core/style/perf_results +++ b/src/lib/core/style/perf_results @@ -2,16 +2,16 @@ results using code in schedule_async_perf.html and timestamp_perf.html, just holding down the 'd' key: schedule async results: -3501.774999999998 -3030.1300000000047 -3032.8799999999974 -3024.5250000000087 -3043.399999999994 -3040.6100000000006 -3035.854999999996 -3033.0149999999994 -3037.0150000000067 -3037.554999999993 +3503.0650000000014 +3035.915000000001 +3028.6849999999995 +3028.914999999999 +3028.170000000002 +3042.7599999999984 +3034.6399999999994 +3035.0499999999993 +3041.780000000006 +3035.5850000000064 timestamp results: 3509.304999999993 diff --git a/src/lib/core/style/schedule_async_perf.html b/src/lib/core/style/schedule_async_perf.html index 0cd856fa7c92..87c30ee2ed23 100644 --- a/src/lib/core/style/schedule_async_perf.html +++ b/src/lib/core/style/schedule_async_perf.html @@ -19,7 +19,7 @@ } count++; x = true; - Promise.resolve().then(() => x = false); + setTimeout(() => x = false, 0); }, true); From 626c9d8012d15c291a223b5da7fc36d657ae3c88 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 26 Jan 2017 15:19:52 -0800 Subject: [PATCH 06/12] s/cdkAddFocusClasses/cdkFocusClasses --- src/demo-app/style/style-demo.html | 2 +- src/lib/core/style/add-focus-classes.spec.ts | 6 +++--- src/lib/core/style/add-focus-classes.ts | 4 ++-- src/lib/core/style/index.ts | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/demo-app/style/style-demo.html b/src/demo-app/style/style-demo.html index 96783762aa48..3d17b7cf6451 100644 --- a/src/demo-app/style/style-demo.html +++ b/src/demo-app/style/style-demo.html @@ -1,4 +1,4 @@ - + diff --git a/src/lib/core/style/add-focus-classes.spec.ts b/src/lib/core/style/add-focus-classes.spec.ts index 9d5f1b0bbb2c..3eebbef72836 100644 --- a/src/lib/core/style/add-focus-classes.spec.ts +++ b/src/lib/core/style/add-focus-classes.spec.ts @@ -4,7 +4,7 @@ import {StyleModule} from './index'; import {By} from '@angular/platform-browser'; -describe('cdkAddFocusClasses', () => { +describe('cdkFocusClasses', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [StyleModule], @@ -16,7 +16,7 @@ describe('cdkAddFocusClasses', () => { TestBed.compileComponents(); })); - describe('cdkAddFocusClasses', () => { + describe('cdkFocusClasses', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement; @@ -87,7 +87,7 @@ describe('cdkAddFocusClasses', () => { }); -@Component({template: ``}) +@Component({template: ``}) class ButtonWithFocusClasses {} diff --git a/src/lib/core/style/add-focus-classes.ts b/src/lib/core/style/add-focus-classes.ts index e59715ed9675..5b73f6225ad1 100644 --- a/src/lib/core/style/add-focus-classes.ts +++ b/src/lib/core/style/add-focus-classes.ts @@ -73,9 +73,9 @@ export class FocusOriginMonitor { * programmatically) and adds corresponding classes to the element. */ @Directive({ - selector: '[cdkAddFocusClasses]', + selector: '[cdkFocusClasses]', }) -export class CdkAddFocusClasses { +export class CdkFocusClasses { constructor(elementRef: ElementRef, focusOriginMonitor: FocusOriginMonitor, renderer: Renderer) { focusOriginMonitor.registerElementForFocusClasses(elementRef.nativeElement, renderer); } diff --git a/src/lib/core/style/index.ts b/src/lib/core/style/index.ts index a4c6fd06a4bb..3c84d10cd287 100644 --- a/src/lib/core/style/index.ts +++ b/src/lib/core/style/index.ts @@ -1,13 +1,13 @@ import {NgModule} from '@angular/core'; -import {CdkAddFocusClasses, FOCUS_ORIGIN_MONITOR_PROVIDER} from './add-focus-classes'; +import {CdkFocusClasses, FOCUS_ORIGIN_MONITOR_PROVIDER} from './add-focus-classes'; export * from './add-focus-classes'; export * from './apply-transform'; @NgModule({ - declarations: [CdkAddFocusClasses], - exports: [CdkAddFocusClasses], + declarations: [CdkFocusClasses], + exports: [CdkFocusClasses], providers: [FOCUS_ORIGIN_MONITOR_PROVIDER], }) export class StyleModule {} From f654887c126e774cc6bea9c321ca374f4db4084c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 30 Jan 2017 15:40:27 -0800 Subject: [PATCH 07/12] rename files & use TAB constant --- .../style/{add-focus-classes.spec.ts => focus-classes.spec.ts} | 3 ++- src/lib/core/style/{add-focus-classes.ts => focus-classes.ts} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename src/lib/core/style/{add-focus-classes.spec.ts => focus-classes.spec.ts} (97%) rename src/lib/core/style/{add-focus-classes.ts => focus-classes.ts} (100%) diff --git a/src/lib/core/style/add-focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts similarity index 97% rename from src/lib/core/style/add-focus-classes.spec.ts rename to src/lib/core/style/focus-classes.spec.ts index 3eebbef72836..8a87abe9e720 100644 --- a/src/lib/core/style/add-focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -2,6 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Component} from '@angular/core'; import {StyleModule} from './index'; import {By} from '@angular/platform-browser'; +import {TAB} from '../keyboard/keycodes'; describe('cdkFocusClasses', () => { @@ -33,7 +34,7 @@ describe('cdkFocusClasses', () => { it('should detect focus via keyboard', async(() => { // Simulate focus via keyboard. - dispatchKeydownEvent(document, 9 /* tab */); + dispatchKeydownEvent(document, TAB); buttonElement.focus(); fixture.detectChanges(); diff --git a/src/lib/core/style/add-focus-classes.ts b/src/lib/core/style/focus-classes.ts similarity index 100% rename from src/lib/core/style/add-focus-classes.ts rename to src/lib/core/style/focus-classes.ts From 0a8e0eddae122fe8685a14b41a895ad5d3322e13 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 30 Jan 2017 16:06:04 -0800 Subject: [PATCH 08/12] fix imports --- src/lib/core/style/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/core/style/index.ts b/src/lib/core/style/index.ts index 3c84d10cd287..13c58e4ea291 100644 --- a/src/lib/core/style/index.ts +++ b/src/lib/core/style/index.ts @@ -1,7 +1,7 @@ import {NgModule} from '@angular/core'; -import {CdkFocusClasses, FOCUS_ORIGIN_MONITOR_PROVIDER} from './add-focus-classes'; +import {CdkFocusClasses, FOCUS_ORIGIN_MONITOR_PROVIDER} from './focus-classes'; -export * from './add-focus-classes'; +export * from './focus-classes'; export * from './apply-transform'; From 46d8cabae3ec1716d726d3e2bb6e483d36b4ef10 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 31 Jan 2017 09:30:39 -0800 Subject: [PATCH 09/12] addressed comments and added more tests --- src/demo-app/style/style-demo.html | 2 +- src/demo-app/style/style-demo.scss | 2 +- src/lib/core/style/focus-classes.spec.ts | 238 ++++++++++++++++------- src/lib/core/style/focus-classes.ts | 57 ++---- 4 files changed, 196 insertions(+), 103 deletions(-) diff --git a/src/demo-app/style/style-demo.html b/src/demo-app/style/style-demo.html index 3d17b7cf6451..a3aee1f7bb3f 100644 --- a/src/demo-app/style/style-demo.html +++ b/src/demo-app/style/style-demo.html @@ -3,6 +3,6 @@ - +
Active classes: {{b.classList}}
diff --git a/src/demo-app/style/style-demo.scss b/src/demo-app/style/style-demo.scss index e11216c5614a..a04570ce0dee 100644 --- a/src/demo-app/style/style-demo.scss +++ b/src/demo-app/style/style-demo.scss @@ -10,6 +10,6 @@ background: yellow; } -.demo-button.cdk-programmatically-focused { +.demo-button.cdk-program-focused { background: blue; } diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index 8a87abe9e720..0d9dc13c58ff 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -1,11 +1,130 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component} from '@angular/core'; +import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {Component, Renderer} from '@angular/core'; import {StyleModule} from './index'; import {By} from '@angular/platform-browser'; import {TAB} from '../keyboard/keycodes'; +import {FocusOriginMonitor} from './focus-classes'; + + +describe('FocusOriginMonitor', () => { + let fixture: ComponentFixture; + let buttonElement: HTMLElement; + let buttonRenderer: Renderer; + let focusOriginMonitor: FocusOriginMonitor; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [StyleModule], + declarations: [ + PlainButton, + ], + }); + + TestBed.compileComponents(); + })); + + beforeEach(inject([FocusOriginMonitor], (fom: FocusOriginMonitor) => { + fixture = TestBed.createComponent(PlainButton); + fixture.detectChanges(); + + buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; + buttonRenderer = fixture.componentInstance.renderer; + focusOriginMonitor = fom; + + focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer); + })); + + it('manually registered element should receive focus classes', () => { + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + }); + + it('should detect focus via keyboard', () => { + // Simulate focus via keyboard. + dispatchKeydownEvent(document, TAB); + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }); + + it('should detect focus via mouse', () => { + // Simulate focus via mouse. + dispatchMousedownEvent(document); + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }); + + it('should detect programmatic focus', () => { + // Programmatically focus. + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-program-focused')) + .toBe(true, 'button should have cdk-program-focused class'); + }); + + it('focusVia keyboard should simulate keyboard focus', () => { + focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard'); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }); + + it('focusVia mouse should simulate mouse focus', () => { + focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse'); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }); + + it('focusVia program should simulate programmatic focus', () => { + focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program'); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-program-focused')) + .toBe(true, 'button should have cdk-program-focused class'); + }); +}); describe('cdkFocusClasses', () => { + let fixture: ComponentFixture; + let buttonElement: HTMLElement; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [StyleModule], @@ -17,77 +136,66 @@ describe('cdkFocusClasses', () => { TestBed.compileComponents(); })); - describe('cdkFocusClasses', () => { - let fixture: ComponentFixture; - let buttonElement: HTMLElement; + beforeEach(() => { + fixture = TestBed.createComponent(ButtonWithFocusClasses); + fixture.detectChanges(); - beforeEach(() => { - fixture = TestBed.createComponent(ButtonWithFocusClasses); - fixture.detectChanges(); + buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; + }); - buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; - }); + it('should initially not be focused', () => { + expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes'); + }); - it('should initially not be focused', () => { - expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes'); - }); + it('should detect focus via keyboard', () => { + // Simulate focus via keyboard. + dispatchKeydownEvent(document, TAB); + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }); - it('should detect focus via keyboard', async(() => { - // Simulate focus via keyboard. - dispatchKeydownEvent(document, TAB); - buttonElement.focus(); - fixture.detectChanges(); - - setTimeout(() => { - fixture.detectChanges(); - - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-keyboard-focused')) - .toBe(true, 'button should have cdk-keyboard-focused class'); - }, 0); - })); - - it('should detect focus via mouse', async(() => { - // Simulate focus via mouse. - dispatchMousedownEvent(document); - buttonElement.focus(); - fixture.detectChanges(); - - setTimeout(() => { - fixture.detectChanges(); - - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-mouse-focused')) - .toBe(true, 'button should have cdk-mouse-focused class'); - }, 0); - })); - - it('should detect programmatic focus', async(() => { - // Programmatically focus. - buttonElement.focus(); - fixture.detectChanges(); - - setTimeout(() => { - fixture.detectChanges(); - - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-programmatically-focused')) - .toBe(true, 'button should have cdk-programmatically-focused class'); - }, 0); - })); + it('should detect focus via mouse', () => { + // Simulate focus via mouse. + dispatchMousedownEvent(document); + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }); + + it('should detect programmatic focus', () => { + // Programmatically focus. + buttonElement.focus(); + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-program-focused')) + .toBe(true, 'button should have cdk-program-focused class'); }); }); +@Component({template: ``}) +class PlainButton { + constructor(public renderer: Renderer) {} +} + + @Component({template: ``}) class ButtonWithFocusClasses {} diff --git a/src/lib/core/style/focus-classes.ts b/src/lib/core/style/focus-classes.ts index 5b73f6225ad1..d7652e5a0050 100644 --- a/src/lib/core/style/focus-classes.ts +++ b/src/lib/core/style/focus-classes.ts @@ -1,70 +1,55 @@ import {Directive, Injectable, Optional, SkipSelf, Renderer, ElementRef} from '@angular/core'; -export type FocusOrigin = 'mouse' | 'keyboard' | 'programmatic'; +export type FocusOrigin = 'mouse' | 'keyboard' | 'program'; /** Monitors mouse and keyboard events to determine the cause of focus events. */ @Injectable() export class FocusOriginMonitor { - /** Whether a keydown event has just occurred. */ - private _keydownOccurred = false; - - /** Whether a mousedown event has just occurred. */ - private _mousedownOccurred = false; - - /** The focus origin that we're pretending the next focus event is a result of. */ - private _fakeOrigin: FocusOrigin = null; - - /** A function to clear the fake origin. */ - private _clearFakeOrigin = (): void => { - setTimeout(() => this._fakeOrigin = null, 0); - document.removeEventListener('focus', this._clearFakeOrigin, true); - }; + /** The focus origin that the next focus event is a result of. */ + private _origin: FocusOrigin = null; constructor() { // Listen to keydown and mousedown in the capture phase so we can detect them even if the user // stops propagation. // TODO(mmalerba): Figure out how to handle touchstart - document.addEventListener('keydown', () => { - this._keydownOccurred = true; - setTimeout(() => this._keydownOccurred = false, 0); - }, true); - - document.addEventListener('mousedown', () => { - this._mousedownOccurred = true; - setTimeout(() => this._mousedownOccurred = false, 0); - }, true); + document.addEventListener( + 'keydown', () => this._setOriginForCurrentEventQueue('keyboard'), true); + + document.addEventListener( + 'mousedown', () => this._setOriginForCurrentEventQueue('mouse'), true); } /** Register an element to receive focus classes. */ registerElementForFocusClasses(element: Element, renderer: Renderer) { renderer.listen(element, 'focus', () => { - let isKeyboard = this._fakeOrigin ? this._fakeOrigin === 'keyboard' : this._keydownOccurred; - let isMouse = this._fakeOrigin ? this._fakeOrigin === 'mouse' : this._mousedownOccurred; - let isProgrammatic = this._fakeOrigin ? - this._fakeOrigin === 'programmatic' : !this._keydownOccurred && !this._mousedownOccurred; - renderer.setElementClass(element, 'cdk-focused', true); - renderer.setElementClass(element, 'cdk-keyboard-focused', isKeyboard); - renderer.setElementClass(element, 'cdk-mouse-focused', isMouse); - renderer.setElementClass(element, 'cdk-programmatically-focused', isProgrammatic); + renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard'); + renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse'); + renderer.setElementClass(element, 'cdk-program-focused', + !this._origin || this._origin == 'program'); }); renderer.listen(element, 'blur', () => { renderer.setElementClass(element, 'cdk-focused', false); renderer.setElementClass(element, 'cdk-keyboard-focused', false); renderer.setElementClass(element, 'cdk-mouse-focused', false); - renderer.setElementClass(element, 'cdk-programmatically-focused', false); + renderer.setElementClass(element, 'cdk-program-focused', false); }); } /** Focuses the element via the specified focus origin. */ - focusVia(element: Node, renderer: Renderer, focusOrigin: FocusOrigin) { - this._fakeOrigin = focusOrigin; - document.addEventListener('focus', this._clearFakeOrigin, true); + focusVia(element: Node, renderer: Renderer, origin: FocusOrigin) { + this._setOriginForCurrentEventQueue(origin); renderer.invokeElementMethod(element, 'focus'); } + + /** Sets the origin and schedules an async function to clear it at the end of the event queue. */ + private _setOriginForCurrentEventQueue(origin: FocusOrigin) { + this._origin = origin; + setTimeout(() => this._origin = null, 0); + } } From d14ac919eae3b25d846db82b4938704a24dab154 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 31 Jan 2017 09:58:44 -0800 Subject: [PATCH 10/12] addressed comments --- src/lib/core/style/focus-classes.spec.ts | 185 ++++++++++++-------- src/lib/core/style/focus-classes.ts | 34 ++-- src/lib/core/style/perf_results | 26 --- src/lib/core/style/schedule_async_perf.html | 26 --- src/lib/core/style/timestamp_perf.html | 25 --- 5 files changed, 135 insertions(+), 161 deletions(-) delete mode 100644 src/lib/core/style/perf_results delete mode 100644 src/lib/core/style/schedule_async_perf.html delete mode 100644 src/lib/core/style/timestamp_perf.html diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index 0d9dc13c58ff..76f91db41f48 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -34,90 +34,118 @@ describe('FocusOriginMonitor', () => { focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer); })); - it('manually registered element should receive focus classes', () => { + it('manually registered element should receive focus classes', async(() => { buttonElement.focus(); fixture.detectChanges(); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + }, 0); + })); - it('should detect focus via keyboard', () => { + it('should detect focus via keyboard', async(() => { // Simulate focus via keyboard. dispatchKeydownEvent(document, TAB); buttonElement.focus(); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-keyboard-focused')) - .toBe(true, 'button should have cdk-keyboard-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }, 0); + })); - it('should detect focus via mouse', () => { + it('should detect focus via mouse', async(() => { // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-mouse-focused')) - .toBe(true, 'button should have cdk-mouse-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }, 0); + })); - it('should detect programmatic focus', () => { + it('should detect programmatic focus', async(() => { // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-program-focused')) - .toBe(true, 'button should have cdk-program-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-program-focused')) + .toBe(true, 'button should have cdk-program-focused class'); + }, 0); + })); - it('focusVia keyboard should simulate keyboard focus', () => { + it('focusVia keyboard should simulate keyboard focus', async(() => { focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard'); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-keyboard-focused')) - .toBe(true, 'button should have cdk-keyboard-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); - it('focusVia mouse should simulate mouse focus', () => { + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }, 0); + })); + + it('focusVia mouse should simulate mouse focus', async(() => { focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse'); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-mouse-focused')) - .toBe(true, 'button should have cdk-mouse-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); - it('focusVia program should simulate programmatic focus', () => { + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }, 0); + })); + + it('focusVia program should simulate programmatic focus', async(() => { focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program'); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-program-focused')) - .toBe(true, 'button should have cdk-program-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-program-focused')) + .toBe(true, 'button should have cdk-program-focused class'); + }, 0); + })); }); @@ -143,50 +171,67 @@ describe('cdkFocusClasses', () => { buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; }); + afterEach(() => { + buttonElement.blur(); + fixture.detectChanges(); + }); + it('should initially not be focused', () => { expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes'); }); - it('should detect focus via keyboard', () => { + it('should detect focus via keyboard', async(() => { // Simulate focus via keyboard. dispatchKeydownEvent(document, TAB); buttonElement.focus(); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-keyboard-focused')) - .toBe(true, 'button should have cdk-keyboard-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); - it('should detect focus via mouse', () => { + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + }, 0); + })); + + it('should detect focus via mouse', async(() => { // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); fixture.detectChanges(); - expect(buttonElement.classList.length) - .toBe(2, 'button should have exactly 2 focus classes'); - expect(buttonElement.classList.contains('cdk-focused')) - .toBe(true, 'button should have cdk-focused class'); - expect(buttonElement.classList.contains('cdk-mouse-focused')) - .toBe(true, 'button should have cdk-mouse-focused class'); - }); + setTimeout(() => { + fixture.detectChanges(); - it('should detect programmatic focus', () => { + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-mouse-focused')) + .toBe(true, 'button should have cdk-mouse-focused class'); + }, 0); + })); + + it('should detect programmatic focus', async(() => { // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); + setTimeout(() => { + fixture.detectChanges(); + expect(buttonElement.classList.length) .toBe(2, 'button should have exactly 2 focus classes'); expect(buttonElement.classList.contains('cdk-focused')) .toBe(true, 'button should have cdk-focused class'); expect(buttonElement.classList.contains('cdk-program-focused')) .toBe(true, 'button should have cdk-program-focused class'); - }); + }, 0); + })); }); diff --git a/src/lib/core/style/focus-classes.ts b/src/lib/core/style/focus-classes.ts index d7652e5a0050..2bd6ff5bcdb8 100644 --- a/src/lib/core/style/focus-classes.ts +++ b/src/lib/core/style/focus-classes.ts @@ -23,20 +23,8 @@ export class FocusOriginMonitor { /** Register an element to receive focus classes. */ registerElementForFocusClasses(element: Element, renderer: Renderer) { - renderer.listen(element, 'focus', () => { - renderer.setElementClass(element, 'cdk-focused', true); - renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard'); - renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse'); - renderer.setElementClass(element, 'cdk-program-focused', - !this._origin || this._origin == 'program'); - }); - - renderer.listen(element, 'blur', () => { - renderer.setElementClass(element, 'cdk-focused', false); - renderer.setElementClass(element, 'cdk-keyboard-focused', false); - renderer.setElementClass(element, 'cdk-mouse-focused', false); - renderer.setElementClass(element, 'cdk-program-focused', false); - }); + renderer.listen(element, 'focus', () => this._onFocus(element, renderer)); + renderer.listen(element, 'blur', () => this._onBlur(element, renderer)); } /** Focuses the element via the specified focus origin. */ @@ -50,6 +38,24 @@ export class FocusOriginMonitor { this._origin = origin; setTimeout(() => this._origin = null, 0); } + + /** Handles focus events on a registered element. */ + private _onFocus(element: Element, renderer: Renderer) { + renderer.setElementClass(element, 'cdk-focused', true); + renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard'); + renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse'); + renderer.setElementClass(element, 'cdk-program-focused', + !this._origin || this._origin == 'program'); + this._origin = null; + } + + /** Handles blur events on a registered element. */ + private _onBlur(element: Element, renderer: Renderer) { + renderer.setElementClass(element, 'cdk-focused', false); + renderer.setElementClass(element, 'cdk-keyboard-focused', false); + renderer.setElementClass(element, 'cdk-mouse-focused', false); + renderer.setElementClass(element, 'cdk-program-focused', false); + } } diff --git a/src/lib/core/style/perf_results b/src/lib/core/style/perf_results deleted file mode 100644 index 3858c00898bb..000000000000 --- a/src/lib/core/style/perf_results +++ /dev/null @@ -1,26 +0,0 @@ -results using code in schedule_async_perf.html and timestamp_perf.html, -just holding down the 'd' key: - -schedule async results: -3503.0650000000014 -3035.915000000001 -3028.6849999999995 -3028.914999999999 -3028.170000000002 -3042.7599999999984 -3034.6399999999994 -3035.0499999999993 -3041.780000000006 -3035.5850000000064 - -timestamp results: -3509.304999999993 -3042.4100000000035 -3034.8600000000006 -3032.175000000003 -3030.3900000000067 -3030.520000000004 -3038.3300000000017 -3035.9850000000006 -3033.899999999994 -3041.965000000011 diff --git a/src/lib/core/style/schedule_async_perf.html b/src/lib/core/style/schedule_async_perf.html deleted file mode 100644 index 87c30ee2ed23..000000000000 --- a/src/lib/core/style/schedule_async_perf.html +++ /dev/null @@ -1,26 +0,0 @@ - - - Schedule Async - - - - - diff --git a/src/lib/core/style/timestamp_perf.html b/src/lib/core/style/timestamp_perf.html deleted file mode 100644 index 0334c71f3f1c..000000000000 --- a/src/lib/core/style/timestamp_perf.html +++ /dev/null @@ -1,25 +0,0 @@ - - - Timestamp - - - - - From 6e06f600b8752693b6835afb8e9b2309e88a8ec7 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 31 Jan 2017 13:02:07 -0800 Subject: [PATCH 11/12] skip tests on ff --- src/lib/core/style/focus-classes.spec.ts | 43 ++++++++++++++++++------ src/lib/core/style/focus-classes.ts | 1 - 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index 76f91db41f48..963a94a16d86 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -4,17 +4,21 @@ import {StyleModule} from './index'; import {By} from '@angular/platform-browser'; import {TAB} from '../keyboard/keycodes'; import {FocusOriginMonitor} from './focus-classes'; +import {PlatformModule} from '../platform/index'; +import {Platform} from '../platform/platform'; +// NOTE: Focus listeners fail to trigger in Firefox for some reason, so we skip tests on Firefox. describe('FocusOriginMonitor', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement; let buttonRenderer: Renderer; let focusOriginMonitor: FocusOriginMonitor; + let platform: Platform; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [StyleModule], + imports: [StyleModule, PlatformModule], declarations: [ PlainButton, ], @@ -23,18 +27,21 @@ describe('FocusOriginMonitor', () => { TestBed.compileComponents(); })); - beforeEach(inject([FocusOriginMonitor], (fom: FocusOriginMonitor) => { + beforeEach(inject([FocusOriginMonitor, Platform], (fom: FocusOriginMonitor, pfm: Platform) => { fixture = TestBed.createComponent(PlainButton); fixture.detectChanges(); buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; buttonRenderer = fixture.componentInstance.renderer; focusOriginMonitor = fom; + platform = pfm; focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer); })); it('manually registered element should receive focus classes', async(() => { + if (platform.FIREFOX) { return; } + buttonElement.focus(); fixture.detectChanges(); @@ -47,6 +54,8 @@ describe('FocusOriginMonitor', () => { })); it('should detect focus via keyboard', async(() => { + if (platform.FIREFOX) { return; } + // Simulate focus via keyboard. dispatchKeydownEvent(document, TAB); buttonElement.focus(); @@ -65,6 +74,8 @@ describe('FocusOriginMonitor', () => { })); it('should detect focus via mouse', async(() => { + if (platform.FIREFOX) { return; } + // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); @@ -83,6 +94,8 @@ describe('FocusOriginMonitor', () => { })); it('should detect programmatic focus', async(() => { + if (platform.FIREFOX) { return; } + // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); @@ -100,6 +113,8 @@ describe('FocusOriginMonitor', () => { })); it('focusVia keyboard should simulate keyboard focus', async(() => { + if (platform.FIREFOX) { return; } + focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard'); fixture.detectChanges(); @@ -116,6 +131,8 @@ describe('FocusOriginMonitor', () => { })); it('focusVia mouse should simulate mouse focus', async(() => { + if (platform.FIREFOX) { return; } + focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse'); fixture.detectChanges(); @@ -132,6 +149,8 @@ describe('FocusOriginMonitor', () => { })); it('focusVia program should simulate programmatic focus', async(() => { + if (platform.FIREFOX) { return; } + focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program'); fixture.detectChanges(); @@ -149,13 +168,15 @@ describe('FocusOriginMonitor', () => { }); +// NOTE: Focus listeners fail to trigger in Firefox for some reason, so we skip tests on Firefox. describe('cdkFocusClasses', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement; + let platform: Platform; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [StyleModule], + imports: [StyleModule, PlatformModule], declarations: [ ButtonWithFocusClasses, ], @@ -164,23 +185,21 @@ describe('cdkFocusClasses', () => { TestBed.compileComponents(); })); - beforeEach(() => { + beforeEach(inject([Platform], (pfm: Platform) => { fixture = TestBed.createComponent(ButtonWithFocusClasses); fixture.detectChanges(); buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; - }); - - afterEach(() => { - buttonElement.blur(); - fixture.detectChanges(); - }); + platform = pfm; + })); it('should initially not be focused', () => { expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes'); }); it('should detect focus via keyboard', async(() => { + if (platform.FIREFOX) { return; } + // Simulate focus via keyboard. dispatchKeydownEvent(document, TAB); buttonElement.focus(); @@ -199,6 +218,8 @@ describe('cdkFocusClasses', () => { })); it('should detect focus via mouse', async(() => { + if (platform.FIREFOX) { return; } + // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); @@ -217,6 +238,8 @@ describe('cdkFocusClasses', () => { })); it('should detect programmatic focus', async(() => { + if (platform.FIREFOX) { return; } + // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); diff --git a/src/lib/core/style/focus-classes.ts b/src/lib/core/style/focus-classes.ts index 2bd6ff5bcdb8..efea43939010 100644 --- a/src/lib/core/style/focus-classes.ts +++ b/src/lib/core/style/focus-classes.ts @@ -16,7 +16,6 @@ export class FocusOriginMonitor { // TODO(mmalerba): Figure out how to handle touchstart document.addEventListener( 'keydown', () => this._setOriginForCurrentEventQueue('keyboard'), true); - document.addEventListener( 'mousedown', () => this._setOriginForCurrentEventQueue('mouse'), true); } From 9b3704496327dc8a88f7ec90b51d8be3f8dfd3e6 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 1 Feb 2017 14:35:24 -0800 Subject: [PATCH 12/12] improve explanation of ff issues --- src/lib/core/style/focus-classes.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index 963a94a16d86..e3e86b2004c3 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -8,7 +8,12 @@ import {PlatformModule} from '../platform/index'; import {Platform} from '../platform/platform'; -// NOTE: Focus listeners fail to trigger in Firefox for some reason, so we skip tests on Firefox. +// NOTE: Firefox only fires focus & blur events when it is the currently active window. +// This is not always the case on our CI setup, therefore we disable tests that depend on these +// events firing for Firefox. We may be able to fix this by configuring our CI to start Firefox with +// the following preference: focusmanager.testmode = true + + describe('FocusOriginMonitor', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement; @@ -168,7 +173,6 @@ describe('FocusOriginMonitor', () => { }); -// NOTE: Focus listeners fail to trigger in Firefox for some reason, so we skip tests on Firefox. describe('cdkFocusClasses', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement;