Skip to content

Commit cfeca55

Browse files
committed
fix(breakpoints): emit only one event for adjacent breakpoint changes.
1 parent 444fb38 commit cfeca55

File tree

3 files changed

+37
-27
lines changed

3 files changed

+37
-27
lines changed

src/cdk/layout/breakpoints-observer.spec.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import {LayoutModule} from './layout-module';
99
import {BreakpointObserver, BreakpointState} from './breakpoints-observer';
1010
import {MediaMatcher} from './media-matcher';
11-
import {async, TestBed, inject} from '@angular/core/testing';
11+
import {fakeAsync, TestBed, inject, flush} from '@angular/core/testing';
1212
import {Injectable} from '@angular/core';
1313

1414
describe('BreakpointObserver', () => {
1515
let breakpointManager: BreakpointObserver;
1616
let mediaMatcher: FakeMediaMatcher;
1717

18-
beforeEach(async(() => {
18+
beforeEach(fakeAsync(() => {
1919
TestBed.configureTestingModule({
2020
imports: [LayoutModule],
2121
providers: [{provide: MediaMatcher, useClass: FakeMediaMatcher}]
@@ -33,12 +33,12 @@ describe('BreakpointObserver', () => {
3333
mediaMatcher.clear();
3434
});
3535

36-
it('retrieves the whether a query is currently matched', () => {
36+
it('retrieves the whether a query is currently matched', fakeAsync(() => {
3737
let query = 'everything starts as true in the FakeMediaMatcher';
3838
expect(breakpointManager.isMatched(query)).toBeTruthy();
39-
});
39+
}));
4040

41-
it('reuses the same MediaQueryList for matching queries', () => {
41+
it('reuses the same MediaQueryList for matching queries', fakeAsync(() => {
4242
expect(mediaMatcher.queryCount).toBe(0);
4343
breakpointManager.observe('query1');
4444
expect(mediaMatcher.queryCount).toBe(1);
@@ -48,62 +48,68 @@ describe('BreakpointObserver', () => {
4848
expect(mediaMatcher.queryCount).toBe(2);
4949
breakpointManager.observe('query1');
5050
expect(mediaMatcher.queryCount).toBe(2);
51-
});
51+
}));
5252

53-
it('splits combined query strings into individual matchMedia listeners', () => {
53+
it('splits combined query strings into individual matchMedia listeners', fakeAsync(() => {
5454
expect(mediaMatcher.queryCount).toBe(0);
5555
breakpointManager.observe('query1, query2');
5656
expect(mediaMatcher.queryCount).toBe(2);
5757
breakpointManager.observe('query1');
5858
expect(mediaMatcher.queryCount).toBe(2);
5959
breakpointManager.observe('query2, query3');
6060
expect(mediaMatcher.queryCount).toBe(3);
61-
});
61+
}));
6262

63-
it('accepts an array of queries', () => {
63+
it('accepts an array of queries', fakeAsync(() => {
6464
let queries = ['1 query', '2 query', 'red query', 'blue query'];
6565
breakpointManager.observe(queries);
6666
expect(mediaMatcher.queryCount).toBe(queries.length);
67-
});
67+
}));
6868

69-
it('completes all events when the breakpoint manager is destroyed', () => {
69+
it('completes all events when the breakpoint manager is destroyed', fakeAsync(() => {
7070
let firstTest = jasmine.createSpy('test1');
7171
breakpointManager.observe('test1').subscribe(undefined, undefined, firstTest);
7272
let secondTest = jasmine.createSpy('test2');
7373
breakpointManager.observe('test2').subscribe(undefined, undefined, secondTest);
7474

75+
flush();
7576
expect(firstTest).not.toHaveBeenCalled();
7677
expect(secondTest).not.toHaveBeenCalled();
7778

7879
breakpointManager.ngOnDestroy();
80+
flush();
7981

8082
expect(firstTest).toHaveBeenCalled();
8183
expect(secondTest).toHaveBeenCalled();
82-
});
84+
}));
8385

84-
it('emits an event on the observable when values change', () => {
86+
it('emits an event on the observable when values change', fakeAsync(() => {
8587
let query = '(width: 999px)';
8688
let queryMatchState: boolean = false;
8789
breakpointManager.observe(query).subscribe((state: BreakpointState) => {
8890
queryMatchState = state.matches;
8991
});
9092

93+
flush();
9194
expect(queryMatchState).toBeTruthy();
9295
mediaMatcher.setMatchesQuery(query, false);
96+
flush();
9397
expect(queryMatchState).toBeFalsy();
94-
});
98+
}));
9599

96-
it('emits a true matches state when the query is matched', () => {
100+
it('emits a true matches state when the query is matched', fakeAsync(() => {
97101
let query = '(width: 999px)';
102+
breakpointManager.observe(query).subscribe();
98103
mediaMatcher.setMatchesQuery(query, true);
99104
expect(breakpointManager.isMatched(query)).toBeTruthy();
100-
});
105+
}));
101106

102-
it('emits a false matches state when the query is not matched', () => {
107+
it('emits a false matches state when the query is not matched', fakeAsync(() => {
103108
let query = '(width: 999px)';
109+
breakpointManager.observe(query).subscribe();
104110
mediaMatcher.setMatchesQuery(query, false);
105-
expect(breakpointManager.isMatched(query)).toBeTruthy();
106-
});
111+
expect(breakpointManager.isMatched(query)).toBeFalsy();
112+
}));
107113
});
108114

109115
export class FakeMediaQueryList implements MediaQueryList {
@@ -153,6 +159,8 @@ export class FakeMediaMatcher {
153159
setMatchesQuery(query: string, matches: boolean) {
154160
if (this.queries.has(query)) {
155161
this.queries.get(query)!.setMatches(matches);
162+
} else {
163+
throw Error('This query is not being observed.');
156164
}
157165
}
158166
}

src/cdk/layout/breakpoints-observer.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88
import {Injectable, NgZone, OnDestroy} from '@angular/core';
99
import {MediaMatcher} from './media-matcher';
10-
import {combineLatest, fromEventPattern, Observable, Subject} from 'rxjs';
11-
import {map, startWith, takeUntil} from 'rxjs/operators';
10+
import {asapScheduler, combineLatest, fromEventPattern, Observable, Subject} from 'rxjs';
11+
import {debounceTime, map, startWith, takeUntil} from 'rxjs/operators';
1212
import {coerceArray} from '@angular/cdk/coercion';
1313

1414

@@ -59,11 +59,13 @@ export class BreakpointObserver implements OnDestroy {
5959
const queries = splitQueries(coerceArray(value));
6060
const observables = queries.map(query => this._registerQuery(query).observable);
6161

62-
return combineLatest(observables).pipe(map((breakpointStates: BreakpointState[]) => {
63-
return {
64-
matches: breakpointStates.some(state => state && state.matches)
65-
};
66-
}));
62+
return combineLatest(observables).pipe(
63+
debounceTime(0, asapScheduler),
64+
map((breakpointStates: BreakpointState[]) => {
65+
return {
66+
matches: breakpointStates.some(state => state && state.matches)
67+
};
68+
}));
6769
}
6870

6971
/** Registers a specific query to be listened for. */

src/lib/tooltip/tooltip.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="mat-tooltip"
22
[ngClass]="tooltipClass"
3-
[class.mat-tooltip-handset]="(_isHandset | async)!.matches"
3+
[class.mat-tooltip-handset]="(_isHandset | async)?.matches"
44
[@state]="_visibility"
55
(@state.start)="_animationStart()"
66
(@state.done)="_animationDone($event)">{{message}}</div>

0 commit comments

Comments
 (0)