Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(breakpoints): emit only one event for adjacent breakpoint changes. #11007

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

Filter by extension

Filter by extension

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