Skip to content

Commit 2830a64

Browse files
josephperrottjelbourn
authored andcommitted
fix(breakpoints): emit only one event for adjacent breakpoint changes. (#11007)
1 parent 1e754a0 commit 2830a64

File tree

3 files changed

+64
-49
lines changed

3 files changed

+64
-49
lines changed

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

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
89
import {LayoutModule} from './layout-module';
910
import {BreakpointObserver, BreakpointState} from './breakpoints-observer';
1011
import {MediaMatcher} from './media-matcher';
11-
import {async, TestBed, inject} from '@angular/core/testing';
12+
import {fakeAsync, TestBed, inject, flush} from '@angular/core/testing';
1213
import {Injectable} from '@angular/core';
1314

1415
describe('BreakpointObserver', () => {
1516
let breakpointManager: BreakpointObserver;
1617
let mediaMatcher: FakeMediaMatcher;
1718

18-
beforeEach(async(() => {
19+
beforeEach(fakeAsync(() => {
1920
TestBed.configureTestingModule({
2021
imports: [LayoutModule],
2122
providers: [{provide: MediaMatcher, useClass: FakeMediaMatcher}]
@@ -33,12 +34,12 @@ describe('BreakpointObserver', () => {
3334
mediaMatcher.clear();
3435
});
3536

36-
it('retrieves the whether a query is currently matched', () => {
37+
it('retrieves the whether a query is currently matched', fakeAsync(() => {
3738
const query = 'everything starts as true in the FakeMediaMatcher';
3839
expect(breakpointManager.isMatched(query)).toBeTruthy();
39-
});
40+
}));
4041

41-
it('reuses the same MediaQueryList for matching queries', () => {
42+
it('reuses the same MediaQueryList for matching queries', fakeAsync(() => {
4243
expect(mediaMatcher.queryCount).toBe(0);
4344
breakpointManager.observe('query1');
4445
expect(mediaMatcher.queryCount).toBe(1);
@@ -48,79 +49,88 @@ describe('BreakpointObserver', () => {
4849
expect(mediaMatcher.queryCount).toBe(2);
4950
breakpointManager.observe('query1');
5051
expect(mediaMatcher.queryCount).toBe(2);
51-
});
52+
}));
5253

53-
it('splits combined query strings into individual matchMedia listeners', () => {
54+
it('splits combined query strings into individual matchMedia listeners', fakeAsync(() => {
5455
expect(mediaMatcher.queryCount).toBe(0);
5556
breakpointManager.observe('query1, query2');
5657
expect(mediaMatcher.queryCount).toBe(2);
5758
breakpointManager.observe('query1');
5859
expect(mediaMatcher.queryCount).toBe(2);
5960
breakpointManager.observe('query2, query3');
6061
expect(mediaMatcher.queryCount).toBe(3);
61-
});
62+
}));
6263

63-
it('accepts an array of queries', () => {
64+
it('accepts an array of queries', fakeAsync(() => {
6465
const queries = ['1 query', '2 query', 'red query', 'blue query'];
6566
breakpointManager.observe(queries);
6667
expect(mediaMatcher.queryCount).toBe(queries.length);
67-
});
68+
}));
6869

69-
it('completes all events when the breakpoint manager is destroyed', () => {
70+
it('completes all events when the breakpoint manager is destroyed', fakeAsync(() => {
7071
const firstTest = jasmine.createSpy('test1');
7172
breakpointManager.observe('test1').subscribe(undefined, undefined, firstTest);
7273
const secondTest = jasmine.createSpy('test2');
7374
breakpointManager.observe('test2').subscribe(undefined, undefined, secondTest);
7475

76+
flush();
7577
expect(firstTest).not.toHaveBeenCalled();
7678
expect(secondTest).not.toHaveBeenCalled();
7779

7880
breakpointManager.ngOnDestroy();
81+
flush();
7982

8083
expect(firstTest).toHaveBeenCalled();
8184
expect(secondTest).toHaveBeenCalled();
82-
});
85+
}));
8386

84-
it('emits an event on the observable when values change', () => {
87+
it('emits an event on the observable when values change', fakeAsync(() => {
8588
const query = '(width: 999px)';
8689
let queryMatchState = false;
8790
breakpointManager.observe(query).subscribe((state: BreakpointState) => {
8891
queryMatchState = state.matches;
8992
});
9093

94+
flush();
9195
expect(queryMatchState).toBeTruthy();
9296
mediaMatcher.setMatchesQuery(query, false);
97+
flush();
9398
expect(queryMatchState).toBeFalsy();
94-
});
95-
96-
it('emits an event on the observable with the matching state of all queries provided', () => {
97-
const queryOne = '(width: 999px)';
98-
const queryTwo = '(width: 700px)';
99-
let state: BreakpointState = {matches: false, breakpoints: {}};
100-
breakpointManager.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => {
101-
state = breakpoint;
102-
});
103-
104-
mediaMatcher.setMatchesQuery(queryOne, false);
105-
mediaMatcher.setMatchesQuery(queryTwo, false);
106-
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});
99+
}));
107100

108-
mediaMatcher.setMatchesQuery(queryOne, true);
109-
mediaMatcher.setMatchesQuery(queryTwo, false);
110-
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
111-
});
101+
it('emits an event on the observable with the matching state of all queries provided',
102+
fakeAsync(() => {
103+
const queryOne = '(width: 999px)';
104+
const queryTwo = '(width: 700px)';
105+
let state: BreakpointState = {matches: false, breakpoints: {}};
106+
breakpointManager.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => {
107+
state = breakpoint;
108+
});
109+
110+
mediaMatcher.setMatchesQuery(queryOne, false);
111+
mediaMatcher.setMatchesQuery(queryTwo, false);
112+
flush();
113+
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});
114+
115+
mediaMatcher.setMatchesQuery(queryOne, true);
116+
mediaMatcher.setMatchesQuery(queryTwo, false);
117+
flush();
118+
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
119+
}));
112120

113-
it('emits a true matches state when the query is matched', () => {
121+
it('emits a true matches state when the query is matched', fakeAsync(() => {
114122
const query = '(width: 999px)';
123+
breakpointManager.observe(query).subscribe();
115124
mediaMatcher.setMatchesQuery(query, true);
116125
expect(breakpointManager.isMatched(query)).toBeTruthy();
117-
});
126+
}));
118127

119-
it('emits a false matches state when the query is not matched', () => {
128+
it('emits a false matches state when the query is not matched', fakeAsync(() => {
120129
const query = '(width: 999px)';
130+
breakpointManager.observe(query).subscribe();
121131
mediaMatcher.setMatchesQuery(query, false);
122-
expect(breakpointManager.isMatched(query)).toBeTruthy();
123-
});
132+
expect(breakpointManager.isMatched(query)).toBeFalsy();
133+
}));
124134
});
125135

126136
export class FakeMediaQueryList implements MediaQueryList {
@@ -170,6 +180,8 @@ export class FakeMediaMatcher {
170180
setMatchesQuery(query: string, matches: boolean) {
171181
if (this.queries.has(query)) {
172182
this.queries.get(query)!.setMatches(matches);
183+
} else {
184+
throw Error('This query is not being observed.');
173185
}
174186
}
175187
}

src/cdk/layout/breakpoints-observer.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
89
import {Injectable, NgZone, OnDestroy} from '@angular/core';
910
import {MediaMatcher} from './media-matcher';
10-
import {combineLatest, fromEventPattern, Observable, Subject} from 'rxjs';
11-
import {map, startWith, takeUntil} from 'rxjs/operators';
11+
import {asapScheduler, combineLatest, fromEventPattern, Observable, Subject} from 'rxjs';
12+
import {debounceTime, map, startWith, takeUntil} from 'rxjs/operators';
1213
import {coerceArray} from '@angular/cdk/coercion';
1314

1415

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

77-
return combineLatest(observables).pipe(map((breakpointStates: InternalBreakpointState[]) => {
78-
const response: BreakpointState = {
79-
matches: false,
80-
breakpoints: {},
81-
};
82-
breakpointStates.forEach((state: InternalBreakpointState) => {
83-
response.matches = response.matches || state.matches;
84-
response.breakpoints[state.query] = state.matches;
85-
});
86-
return response;
87-
}));
78+
return combineLatest(observables).pipe(
79+
debounceTime(0, asapScheduler),
80+
map((breakpointStates: InternalBreakpointState[]) => {
81+
const response: BreakpointState = {
82+
matches: false,
83+
breakpoints: {},
84+
};
85+
breakpointStates.forEach((state: InternalBreakpointState) => {
86+
response.matches = response.matches || state.matches;
87+
response.breakpoints[state.query] = state.matches;
88+
});
89+
return response;
90+
}));
8891
}
8992

9093
/** 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)