Skip to content

Commit

Permalink
fix(breakpoints): emit only one event for adjacent breakpoint changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
josephperrott committed Aug 9, 2018
1 parent ea10d94 commit 55607dc
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 49 deletions.
82 changes: 47 additions & 35 deletions src/cdk/layout/breakpoints-observer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {LayoutModule} from './layout-module';
import {BreakpointObserver, BreakpointState} from './breakpoints-observer';
import {MediaMatcher} from './media-matcher';
import {async, TestBed, inject} from '@angular/core/testing';
import {fakeAsync, TestBed, inject, flush} from '@angular/core/testing';
import {Injectable} from '@angular/core';

describe('BreakpointObserver', () => {
let breakpointManager: BreakpointObserver;
let mediaMatcher: FakeMediaMatcher;

beforeEach(async(() => {
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [LayoutModule],
providers: [{provide: MediaMatcher, useClass: FakeMediaMatcher}]
Expand All @@ -33,12 +34,12 @@ describe('BreakpointObserver', () => {
mediaMatcher.clear();
});

it('retrieves the whether a query is currently matched', () => {
it('retrieves the whether a query is currently matched', fakeAsync(() => {
const query = 'everything starts as true in the FakeMediaMatcher';
expect(breakpointManager.isMatched(query)).toBeTruthy();
});
}));

it('reuses the same MediaQueryList for matching queries', () => {
it('reuses the same MediaQueryList for matching queries', fakeAsync(() => {
expect(mediaMatcher.queryCount).toBe(0);
breakpointManager.observe('query1');
expect(mediaMatcher.queryCount).toBe(1);
Expand All @@ -48,79 +49,88 @@ describe('BreakpointObserver', () => {
expect(mediaMatcher.queryCount).toBe(2);
breakpointManager.observe('query1');
expect(mediaMatcher.queryCount).toBe(2);
});
}));

it('splits combined query strings into individual matchMedia listeners', () => {
it('splits combined query strings into individual matchMedia listeners', fakeAsync(() => {
expect(mediaMatcher.queryCount).toBe(0);
breakpointManager.observe('query1, query2');
expect(mediaMatcher.queryCount).toBe(2);
breakpointManager.observe('query1');
expect(mediaMatcher.queryCount).toBe(2);
breakpointManager.observe('query2, query3');
expect(mediaMatcher.queryCount).toBe(3);
});
}));

it('accepts an array of queries', () => {
it('accepts an array of queries', fakeAsync(() => {
const queries = ['1 query', '2 query', 'red query', 'blue query'];
breakpointManager.observe(queries);
expect(mediaMatcher.queryCount).toBe(queries.length);
});
}));

it('completes all events when the breakpoint manager is destroyed', () => {
it('completes all events when the breakpoint manager is destroyed', fakeAsync(() => {
const firstTest = jasmine.createSpy('test1');
breakpointManager.observe('test1').subscribe(undefined, undefined, firstTest);
const secondTest = jasmine.createSpy('test2');
breakpointManager.observe('test2').subscribe(undefined, undefined, secondTest);

flush();
expect(firstTest).not.toHaveBeenCalled();
expect(secondTest).not.toHaveBeenCalled();

breakpointManager.ngOnDestroy();
flush();

expect(firstTest).toHaveBeenCalled();
expect(secondTest).toHaveBeenCalled();
});
}));

it('emits an event on the observable when values change', () => {
it('emits an event on the observable when values change', fakeAsync(() => {
const query = '(width: 999px)';
let queryMatchState = false;
breakpointManager.observe(query).subscribe((state: BreakpointState) => {
queryMatchState = state.matches;
});

flush();
expect(queryMatchState).toBeTruthy();
mediaMatcher.setMatchesQuery(query, false);
flush();
expect(queryMatchState).toBeFalsy();
});

it('emits an event on the observable with the matching state of all queries provided', () => {
const queryOne = '(width: 999px)';
const queryTwo = '(width: 700px)';
let state: BreakpointState = {matches: false, breakpoints: {}};
breakpointManager.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => {
state = breakpoint;
});

mediaMatcher.setMatchesQuery(queryOne, false);
mediaMatcher.setMatchesQuery(queryTwo, false);
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});
}));

mediaMatcher.setMatchesQuery(queryOne, true);
mediaMatcher.setMatchesQuery(queryTwo, false);
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
});
it('emits an event on the observable with the matching state of all queries provided',
fakeAsync(() => {
const queryOne = '(width: 999px)';
const queryTwo = '(width: 700px)';
let state: BreakpointState = {matches: false, breakpoints: {}};
breakpointManager.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => {
state = breakpoint;
});

mediaMatcher.setMatchesQuery(queryOne, false);
mediaMatcher.setMatchesQuery(queryTwo, false);
flush();
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});

mediaMatcher.setMatchesQuery(queryOne, true);
mediaMatcher.setMatchesQuery(queryTwo, false);
flush();
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
}));

it('emits a true matches state when the query is matched', () => {
it('emits a true matches state when the query is matched', fakeAsync(() => {
const query = '(width: 999px)';
breakpointManager.observe(query).subscribe();
mediaMatcher.setMatchesQuery(query, true);
expect(breakpointManager.isMatched(query)).toBeTruthy();
});
}));

it('emits a false matches state when the query is not matched', () => {
it('emits a false matches state when the query is not matched', fakeAsync(() => {
const query = '(width: 999px)';
breakpointManager.observe(query).subscribe();
mediaMatcher.setMatchesQuery(query, false);
expect(breakpointManager.isMatched(query)).toBeTruthy();
});
expect(breakpointManager.isMatched(query)).toBeFalsy();
}));
});

export class FakeMediaQueryList implements MediaQueryList {
Expand Down Expand Up @@ -170,6 +180,8 @@ export class FakeMediaMatcher {
setMatchesQuery(query: string, matches: boolean) {
if (this.queries.has(query)) {
this.queries.get(query)!.setMatches(matches);
} else {
throw Error('This query is not being observed.');
}
}
}
29 changes: 16 additions & 13 deletions src/cdk/layout/breakpoints-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Injectable, NgZone, OnDestroy} from '@angular/core';
import {MediaMatcher} from './media-matcher';
import {combineLatest, fromEventPattern, Observable, Subject} from 'rxjs';
import {map, startWith, takeUntil} from 'rxjs/operators';
import {asapScheduler, combineLatest, fromEventPattern, Observable, Subject} from 'rxjs';
import {debounceTime, map, startWith, takeUntil} from 'rxjs/operators';
import {coerceArray} from '@angular/cdk/coercion';


Expand Down Expand Up @@ -74,17 +75,19 @@ export class BreakpointObserver implements OnDestroy {
const queries = splitQueries(coerceArray(value));
const observables = queries.map(query => this._registerQuery(query).observable);

return combineLatest(observables).pipe(map((breakpointStates: InternalBreakpointState[]) => {
const response: BreakpointState = {
matches: false,
breakpoints: {},
};
breakpointStates.forEach((state: InternalBreakpointState) => {
response.matches = response.matches || state.matches;
response.breakpoints[state.query] = state.matches;
});
return response;
}));
return combineLatest(observables).pipe(
debounceTime(0, asapScheduler),
map((breakpointStates: InternalBreakpointState[]) => {
const response: BreakpointState = {
matches: false,
breakpoints: {},
};
breakpointStates.forEach((state: InternalBreakpointState) => {
response.matches = response.matches || state.matches;
response.breakpoints[state.query] = state.matches;
});
return response;
}));
}

/** Registers a specific query to be listened for. */
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tooltip/tooltip.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="mat-tooltip"
[ngClass]="tooltipClass"
[class.mat-tooltip-handset]="(_isHandset | async)!.matches"
[class.mat-tooltip-handset]="(_isHandset | async)?.matches"
[@state]="_visibility"
(@state.start)="_animationStart()"
(@state.done)="_animationDone($event)">{{message}}</div>

0 comments on commit 55607dc

Please sign in to comment.