Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit 148351b

Browse files
fix(core): improve use of breakpoint priorities
Use breakpoint priority as the only sorting/scanning mechanism; used to ensure correct MediaChange event notifications. * prioritize breakpoints: non-overlaps hightest, lt- lowest * consistently sort breakpoints ascending by priority * highest priority === smallest range * remove hackery with reverse(), etc. * memoize BreakPointRegistry findBy lookups * fix MatchMedia::observe() to support lazy breakpoint registration * fix fragile logic in MediaMarshaller * fix breakpoint registration usage * clarify update/clear builder function callbacks * fix MediaObserver breakpoint registration usage Fixes #648, Fixes #426
1 parent 5db4ccf commit 148351b

16 files changed

+195
-178
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ node_modules
1717

1818
# misc
1919
.DS_Store
20+
/.firebase
2021
/.sass-cache
2122
/connect.lock
2223
/coverage/*

src/lib/core/breakpoints/break-point-registry.ts

+32-29
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {Injectable, Inject} from '@angular/core';
99

1010
import {BreakPoint} from './break-point';
1111
import {BREAKPOINTS} from './break-points-token';
12+
import {sortAscendingPriority} from './breakpoint-tools';
1213

14+
type OptionalBreakPoint = BreakPoint | null;
1315

1416
/**
1517
* Registry of 1..n MediaQuery breakpoint ranges
@@ -18,55 +20,36 @@ import {BREAKPOINTS} from './break-points-token';
1820
*/
1921
@Injectable({providedIn: 'root'})
2022
export class BreakPointRegistry {
23+
readonly items: BreakPoint[];
2124

22-
constructor(@Inject(BREAKPOINTS) private _registry: BreakPoint[]) {
23-
}
24-
25-
/**
26-
* Accessor to raw list
27-
*/
28-
get items(): BreakPoint[] {
29-
return [...this._registry];
30-
}
31-
32-
/**
33-
* Accessor to sorted list used for registration with matchMedia API
34-
*
35-
* NOTE: During breakpoint registration, we want to register the overlaps FIRST
36-
* so the non-overlaps will trigger the MatchMedia:BehaviorSubject last!
37-
* And the largest, non-overlap, matching breakpoint should be the lastReplay value
38-
*/
39-
get sortedItems(): BreakPoint[] {
40-
let overlaps = this._registry.filter(it => it.overlapping === true);
41-
let nonOverlaps = this._registry.filter(it => it.overlapping !== true);
42-
43-
return [...overlaps, ...nonOverlaps];
25+
constructor(@Inject(BREAKPOINTS) list: BreakPoint[]) {
26+
this.items = [...list].sort(sortAscendingPriority);
4427
}
4528

4629
/**
4730
* Search breakpoints by alias (e.g. gt-xs)
4831
*/
49-
findByAlias(alias: string): BreakPoint | null {
50-
return this._registry.find(bp => bp.alias == alias) || null;
32+
findByAlias(alias: string): OptionalBreakPoint {
33+
return this.findWithPredicate(alias, (bp) => bp.alias == alias);
5134
}
5235

53-
findByQuery(query: string): BreakPoint | null {
54-
return this._registry.find(bp => bp.mediaQuery == query) || null;
36+
findByQuery(query: string): OptionalBreakPoint {
37+
return this.findWithPredicate(query, (bp) => bp.mediaQuery == query);
5538
}
5639

5740
/**
5841
* Get all the breakpoints whose ranges could overlapping `normal` ranges;
5942
* e.g. gt-sm overlaps md, lg, and xl
6043
*/
6144
get overlappings(): BreakPoint[] {
62-
return this._registry.filter(it => it.overlapping == true);
45+
return this.items.filter(it => it.overlapping == true);
6346
}
6447

6548
/**
6649
* Get list of all registered (non-empty) breakpoint aliases
6750
*/
6851
get aliases(): string[] {
69-
return this._registry.map(it => it.alias);
52+
return this.items.map(it => it.alias);
7053
}
7154

7255
/**
@@ -75,6 +58,26 @@ export class BreakPointRegistry {
7558
* for property layoutGtSM.
7659
*/
7760
get suffixes(): string[] {
78-
return this._registry.map(it => !!it.suffix ? it.suffix : '');
61+
return this.items.map(it => !!it.suffix ? it.suffix : '');
7962
}
63+
64+
/**
65+
* Memoized lookup using custom predicate function
66+
*/
67+
private findWithPredicate(key: string,
68+
searchFn: (bp: BreakPoint) => boolean): OptionalBreakPoint {
69+
let response = this.findByMap.get(key);
70+
if (!response) {
71+
response = this.items.find(searchFn) || null;
72+
this.findByMap.set(key, response);
73+
}
74+
return response || null;
75+
76+
}
77+
78+
/**
79+
* Memoized BreakPoint Lookups
80+
*/
81+
private readonly findByMap = new Map<String, OptionalBreakPoint>();
8082
}
83+

src/lib/core/breakpoints/break-point.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export interface BreakPoint {
99
mediaQuery: string;
1010
alias: string;
1111
suffix?: string;
12-
overlapping?: boolean;
13-
// The priority of the individual breakpoint when overlapping another breakpoint
14-
priority?: number;
12+
overlapping?: boolean; // Does this range overlap with any other ranges
13+
priority?: number; // determine order of activation reporting: higher is last reported
1514
}

src/lib/core/breakpoints/breakpoint-tools.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@ export function mergeByAlias(defaults: BreakPoint[], custom: BreakPoint[] = []):
6565
}
6666

6767
/** HOF to sort the breakpoints by priority */
68-
export function prioritySort(a: BreakPoint, b: BreakPoint): number {
68+
export function sortDescendingPriority(a: BreakPoint, b: BreakPoint): number {
6969
const priorityA = a.priority || 0;
7070
const priorityB = b.priority || 0;
7171
return priorityB - priorityA;
7272
}
73+
74+
export function sortAscendingPriority(a: BreakPoint, b: BreakPoint): number {
75+
const pA = a.priority || 0;
76+
const pB = b.priority || 0;
77+
return pA - pB;
78+
}

src/lib/core/breakpoints/data/break-points.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('break-point-provider', () => {
2828
it('has the standard breakpoints', () => {
2929
expect(breakPoints.length).toEqual(DEFAULT_BREAKPOINTS.length);
3030
expect(breakPoints[0].alias).toEqual('xs');
31-
expect(breakPoints[breakPoints.length - 1].alias).toEqual('xl');
31+
expect(breakPoints[breakPoints.length - 1].alias).toEqual('lt-xl');
3232
});
3333
});
3434

src/lib/core/breakpoints/data/break-points.ts

+47-47
Original file line numberDiff line numberDiff line change
@@ -7,83 +7,83 @@
77
*/
88
import {BreakPoint} from '../break-point';
99

10-
export const RESPONSIVE_ALIASES = [
11-
'xs', 'gt-xs', 'sm', 'gt-sm', 'md', 'gt-md', 'lg', 'gt-lg', 'xl'
12-
];
13-
10+
/**
11+
* NOTE: Smaller ranges have HIGHER priority since the match is more specific
12+
*/
1413
export const DEFAULT_BREAKPOINTS: BreakPoint[] = [
1514
{
1615
alias: 'xs',
17-
mediaQuery: '(min-width: 0px) and (max-width: 599px)',
18-
priority: 100,
16+
mediaQuery: 'screen and (min-width: 0px) and (max-width: 599px)',
17+
priority: 10000,
1918
},
2019
{
21-
alias: 'gt-xs',
22-
overlapping: true,
23-
mediaQuery: '(min-width: 600px)',
24-
priority: 7,
20+
alias: 'sm',
21+
mediaQuery: 'screen and (min-width: 600px) and (max-width: 959px)',
22+
priority: 9000,
2523
},
2624
{
27-
alias: 'lt-sm',
28-
overlapping: true,
29-
mediaQuery: '(max-width: 599px)',
30-
priority: 10,
25+
alias: 'md',
26+
mediaQuery: 'screen and (min-width: 960px) and (max-width: 1279px)',
27+
priority: 8000,
3128
},
3229
{
33-
alias: 'sm',
34-
mediaQuery: '(min-width: 600px) and (max-width: 959px)',
35-
priority: 100,
30+
alias: 'lg',
31+
mediaQuery: 'screen and (min-width: 1280px) and (max-width: 1919px)',
32+
priority: 7000,
3633
},
3734
{
38-
alias: 'gt-sm',
39-
overlapping: true,
40-
mediaQuery: '(min-width: 960px)',
41-
priority: 8,
35+
alias: 'xl',
36+
mediaQuery: 'screen and (min-width: 1920px) and (max-width: 5000px)',
37+
priority: 6000,
4238
},
4339
{
44-
alias: 'lt-md',
40+
alias: 'gt-xs',
4541
overlapping: true,
46-
mediaQuery: '(max-width: 959px)',
47-
priority: 9,
48-
},
49-
{
50-
alias: 'md',
51-
mediaQuery: '(min-width: 960px) and (max-width: 1279px)',
52-
priority: 100,
42+
mediaQuery: 'screen and (min-width: 600px)',
43+
priority: 200,
5344
},
5445
{
46+
alias: 'gt-sm',
47+
overlapping: true,
48+
mediaQuery: 'screen and (min-width: 960px)',
49+
priority: 300,
50+
}, {
5551
alias: 'gt-md',
5652
overlapping: true,
57-
mediaQuery: '(min-width: 1280px)',
58-
priority: 9,
53+
mediaQuery: 'screen and (min-width: 1280px)',
54+
priority: 400,
5955
},
6056
{
61-
alias: 'lt-lg',
57+
alias: 'gt-lg',
6258
overlapping: true,
63-
mediaQuery: '(max-width: 1279px)',
64-
priority: 8,
59+
mediaQuery: 'screen and (min-width: 1920px)',
60+
priority: 500,
6561
},
6662
{
67-
alias: 'lg',
68-
mediaQuery: '(min-width: 1280px) and (max-width: 1919px)',
69-
priority: 100,
63+
alias: 'lt-sm',
64+
overlapping: true,
65+
mediaQuery: 'screen and (max-width: 599px)',
66+
priority: 1000,
7067
},
68+
7169
{
72-
alias: 'gt-lg',
70+
alias: 'lt-md',
7371
overlapping: true,
74-
mediaQuery: '(min-width: 1920px)',
75-
priority: 10,
72+
mediaQuery: 'screen and (max-width: 959px)',
73+
priority: 800,
7674
},
75+
7776
{
78-
alias: 'lt-xl',
77+
alias: 'lt-lg',
7978
overlapping: true,
80-
mediaQuery: '(max-width: 1919px)',
81-
priority: 7,
79+
mediaQuery: 'screen and (max-width: 1279px)',
80+
priority: 700,
8281
},
8382
{
84-
alias: 'xl',
85-
mediaQuery: '(min-width: 1920px) and (max-width: 5000px)',
86-
priority: 100,
87-
}
83+
alias: 'lt-xl',
84+
overlapping: true,
85+
priority: 600,
86+
mediaQuery: 'screen and (max-width: 1919px)',
87+
}
8888
];
8989

src/lib/core/breakpoints/data/orientation-break-points.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ export const ScreenTypes = {
3636
* Extended Breakpoints for handset/tablets with landscape or portrait orientations
3737
*/
3838
export const ORIENTATION_BREAKPOINTS : BreakPoint[] = [
39-
{'alias': 'handset', 'mediaQuery': ScreenTypes.HANDSET},
40-
{'alias': 'handset.landscape', 'mediaQuery': ScreenTypes.HANDSET_LANDSCAPE},
41-
{'alias': 'handset.portrait', 'mediaQuery': ScreenTypes.HANDSET_PORTRAIT},
39+
{'alias': 'handset', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET},
40+
{'alias': 'handset.landscape', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET_LANDSCAPE},
41+
{'alias': 'handset.portrait', priority: 10000, 'mediaQuery': ScreenTypes.HANDSET_PORTRAIT},
4242

43-
{'alias': 'tablet', 'mediaQuery': ScreenTypes.TABLET},
44-
{'alias': 'tablet.landscape', 'mediaQuery': ScreenTypes.TABLET},
45-
{'alias': 'tablet.portrait', 'mediaQuery': ScreenTypes.TABLET_PORTRAIT},
43+
{'alias': 'tablet', priority: 8000, 'mediaQuery': ScreenTypes.TABLET},
44+
{'alias': 'tablet.landscape', priority: 8000, 'mediaQuery': ScreenTypes.TABLET},
45+
{'alias': 'tablet.portrait', priority: 8000, 'mediaQuery': ScreenTypes.TABLET_PORTRAIT},
4646

47-
{'alias': 'web', 'mediaQuery': ScreenTypes.WEB, overlapping : true },
48-
{'alias': 'web.landscape', 'mediaQuery': ScreenTypes.WEB_LANDSCAPE, overlapping : true },
49-
{'alias': 'web.portrait', 'mediaQuery': ScreenTypes.WEB_PORTRAIT, overlapping : true }
47+
{'alias': 'web', priority: 9000, 'mediaQuery': ScreenTypes.WEB, overlapping : true },
48+
{'alias': 'web.landscape', priority: 9000, 'mediaQuery': ScreenTypes.WEB_LANDSCAPE, overlapping : true },
49+
{'alias': 'web.portrait', priority: 9000, 'mediaQuery': ScreenTypes.WEB_PORTRAIT, overlapping : true }
5050
];

src/lib/core/breakpoints/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ export * from './data/orientation-break-points';
1212
export * from './break-point';
1313
export * from './break-point-registry';
1414
export * from './break-points-token';
15-
export {prioritySort} from './breakpoint-tools';
15+
16+
export {
17+
sortDescendingPriority,
18+
sortAscendingPriority
19+
} from './breakpoint-tools';

src/lib/core/match-media/match-media.spec.ts

+5-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import {TestBed, inject, async} from '@angular/core/testing';
99

1010
import {MediaChange} from '../media-change';
11-
import {BreakPoint} from '../breakpoints/break-point';
1211
import {MockMatchMedia, MockMatchMediaProvider} from './mock/mock-match-media';
1312
import {BreakPointRegistry} from '../breakpoints/break-point-registry';
1413
import {MatchMedia} from './match-media';
@@ -51,11 +50,8 @@ describe('match-media', () => {
5150
let query1 = 'screen and (min-width: 610px) and (max-width: 620px)';
5251
let query2 = '(min-width: 730px) and (max-width: 950px)';
5352

54-
matchMedia.registerQuery(query1);
55-
matchMedia.registerQuery(query2);
56-
57-
let media$ = matchMedia.observe();
58-
let subscription = media$.subscribe((change: MediaChange) => {
53+
const queries = [query1, query2];
54+
let subscription = matchMedia.observe(queries).subscribe((change: MediaChange) => {
5955
current = change;
6056
});
6157

@@ -83,7 +79,7 @@ describe('match-media', () => {
8379

8480
matchMedia.registerQuery([query1, query2]);
8581

86-
let subscription = matchMedia.observe(query1).subscribe((change: MediaChange) => {
82+
let subscription = matchMedia.observe([query1]).subscribe((change: MediaChange) => {
8783
current = change;
8884
});
8985

@@ -128,11 +124,6 @@ describe('match-media-observable', () => {
128124
matchMedia = _matchMedia; // inject only to manually activate mediaQuery ranges
129125
breakPoints = _breakPoints;
130126
mediaObserver = _mediaObserver;
131-
132-
// Quick register all breakpoint mediaQueries
133-
breakPoints.items.forEach((bp: BreakPoint) => {
134-
matchMedia.observe(bp.mediaQuery);
135-
});
136127
})));
137128
afterEach(() => {
138129
matchMedia.clearAll();
@@ -206,6 +197,8 @@ describe('match-media-observable', () => {
206197
it('ignores mediaQuery de-activations', () => {
207198
let activationCount = 0;
208199
let deactivationCount = 0;
200+
201+
mediaObserver.filterOverlaps = false;
209202
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
210203
if (change.matches) {
211204
++activationCount;

0 commit comments

Comments
 (0)