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

fix(core): improve use of breakpoint priorities #946

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ node_modules

# misc
.DS_Store
/.firebase
/.sass-cache
/connect.lock
/coverage/*
Expand Down
61 changes: 32 additions & 29 deletions src/lib/core/breakpoints/break-point-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {Injectable, Inject} from '@angular/core';

import {BreakPoint} from './break-point';
import {BREAKPOINTS} from './break-points-token';
import {sortAscendingPriority} from './breakpoint-tools';

type OptionalBreakPoint = BreakPoint | null;

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

constructor(@Inject(BREAKPOINTS) private _registry: BreakPoint[]) {
}

/**
* Accessor to raw list
*/
get items(): BreakPoint[] {
return [...this._registry];
}

/**
* Accessor to sorted list used for registration with matchMedia API
*
* NOTE: During breakpoint registration, we want to register the overlaps FIRST
* so the non-overlaps will trigger the MatchMedia:BehaviorSubject last!
* And the largest, non-overlap, matching breakpoint should be the lastReplay value
*/
get sortedItems(): BreakPoint[] {
let overlaps = this._registry.filter(it => it.overlapping === true);
let nonOverlaps = this._registry.filter(it => it.overlapping !== true);

return [...overlaps, ...nonOverlaps];
constructor(@Inject(BREAKPOINTS) list: BreakPoint[]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a thought: the items remain static, so instead of memoizing, why not just prime it at startup by creating two maps on init, one for aliases, one for mediaqueries?

this.items = [...list].sort(sortAscendingPriority);
}

/**
* Search breakpoints by alias (e.g. gt-xs)
*/
findByAlias(alias: string): BreakPoint | null {
return this._registry.find(bp => bp.alias == alias) || null;
findByAlias(alias: string): OptionalBreakPoint {
return this.findWithPredicate(alias, (bp) => bp.alias == alias);
}

findByQuery(query: string): BreakPoint | null {
return this._registry.find(bp => bp.mediaQuery == query) || null;
findByQuery(query: string): OptionalBreakPoint {
return this.findWithPredicate(query, (bp) => bp.mediaQuery == query);
}

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

/**
* Get list of all registered (non-empty) breakpoint aliases
*/
get aliases(): string[] {
return this._registry.map(it => it.alias);
return this.items.map(it => it.alias);
}

/**
Expand All @@ -75,6 +58,26 @@ export class BreakPointRegistry {
* for property layoutGtSM.
*/
get suffixes(): string[] {
return this._registry.map(it => !!it.suffix ? it.suffix : '');
return this.items.map(it => !!it.suffix ? it.suffix : '');
}

/**
* Memoized lookup using custom predicate function
*/
private findWithPredicate(key: string,
searchFn: (bp: BreakPoint) => boolean): OptionalBreakPoint {
let response = this.findByMap.get(key);
if (!response) {
response = this.items.find(searchFn) || null;
this.findByMap.set(key, response);
}
return response || null;

}

/**
* Memoized BreakPoint Lookups
*/
private readonly findByMap = new Map<String, OptionalBreakPoint>();
}

5 changes: 2 additions & 3 deletions src/lib/core/breakpoints/break-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export interface BreakPoint {
mediaQuery: string;
alias: string;
suffix?: string;
overlapping?: boolean;
// The priority of the individual breakpoint when overlapping another breakpoint
priority?: number;
overlapping?: boolean; // Does this range overlap with any other ranges
priority?: number; // determine order of activation reporting: higher is last reported
}
8 changes: 7 additions & 1 deletion src/lib/core/breakpoints/breakpoint-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ export function mergeByAlias(defaults: BreakPoint[], custom: BreakPoint[] = []):
}

/** HOF to sort the breakpoints by priority */
export function prioritySort(a: BreakPoint, b: BreakPoint): number {
export function sortDescendingPriority(a: BreakPoint, b: BreakPoint): number {
const priorityA = a.priority || 0;
const priorityB = b.priority || 0;
return priorityB - priorityA;
}

export function sortAscendingPriority(a: BreakPoint, b: BreakPoint): number {
const pA = a.priority || 0;
const pB = b.priority || 0;
return pA - pB;
}
2 changes: 1 addition & 1 deletion src/lib/core/breakpoints/data/break-points.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('break-point-provider', () => {
it('has the standard breakpoints', () => {
expect(breakPoints.length).toEqual(DEFAULT_BREAKPOINTS.length);
expect(breakPoints[0].alias).toEqual('xs');
expect(breakPoints[breakPoints.length - 1].alias).toEqual('xl');
expect(breakPoints[breakPoints.length - 1].alias).toEqual('lt-xl');
});
});

Expand Down
94 changes: 47 additions & 47 deletions src/lib/core/breakpoints/data/break-points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,83 +7,83 @@
*/
import {BreakPoint} from '../break-point';

export const RESPONSIVE_ALIASES = [
'xs', 'gt-xs', 'sm', 'gt-sm', 'md', 'gt-md', 'lg', 'gt-lg', 'xl'
];

/**
* NOTE: Smaller ranges have HIGHER priority since the match is more specific
*/
export const DEFAULT_BREAKPOINTS: BreakPoint[] = [
{
alias: 'xs',
mediaQuery: '(min-width: 0px) and (max-width: 599px)',
priority: 100,
mediaQuery: 'screen and (min-width: 0px) and (max-width: 599px)',
priority: 10000,
},
{
alias: 'gt-xs',
overlapping: true,
mediaQuery: '(min-width: 600px)',
priority: 7,
alias: 'sm',
mediaQuery: 'screen and (min-width: 600px) and (max-width: 959px)',
priority: 9000,
},
{
alias: 'lt-sm',
overlapping: true,
mediaQuery: '(max-width: 599px)',
priority: 10,
alias: 'md',
mediaQuery: 'screen and (min-width: 960px) and (max-width: 1279px)',
priority: 8000,
},
{
alias: 'sm',
mediaQuery: '(min-width: 600px) and (max-width: 959px)',
priority: 100,
alias: 'lg',
mediaQuery: 'screen and (min-width: 1280px) and (max-width: 1919px)',
priority: 7000,
},
{
alias: 'gt-sm',
overlapping: true,
mediaQuery: '(min-width: 960px)',
priority: 8,
alias: 'xl',
mediaQuery: 'screen and (min-width: 1920px) and (max-width: 5000px)',
priority: 6000,
},
{
alias: 'lt-md',
alias: 'gt-xs',
overlapping: true,
mediaQuery: '(max-width: 959px)',
priority: 9,
},
{
alias: 'md',
mediaQuery: '(min-width: 960px) and (max-width: 1279px)',
priority: 100,
mediaQuery: 'screen and (min-width: 600px)',
priority: 200,
},
{
alias: 'gt-sm',
overlapping: true,
mediaQuery: 'screen and (min-width: 960px)',
priority: 300,
}, {
alias: 'gt-md',
overlapping: true,
mediaQuery: '(min-width: 1280px)',
priority: 9,
mediaQuery: 'screen and (min-width: 1280px)',
priority: 400,
},
{
alias: 'lt-lg',
alias: 'gt-lg',
overlapping: true,
mediaQuery: '(max-width: 1279px)',
priority: 8,
mediaQuery: 'screen and (min-width: 1920px)',
priority: 500,
},
{
alias: 'lg',
mediaQuery: '(min-width: 1280px) and (max-width: 1919px)',
priority: 100,
alias: 'lt-sm',
overlapping: true,
mediaQuery: 'screen and (max-width: 599px)',
priority: 1000,
},

{
alias: 'gt-lg',
alias: 'lt-md',
overlapping: true,
mediaQuery: '(min-width: 1920px)',
priority: 10,
mediaQuery: 'screen and (max-width: 959px)',
priority: 800,
},

{
alias: 'lt-xl',
alias: 'lt-lg',
overlapping: true,
mediaQuery: '(max-width: 1919px)',
priority: 7,
mediaQuery: 'screen and (max-width: 1279px)',
priority: 700,
},
{
alias: 'xl',
mediaQuery: '(min-width: 1920px) and (max-width: 5000px)',
priority: 100,
}
alias: 'lt-xl',
overlapping: true,
priority: 600,
mediaQuery: 'screen and (max-width: 1919px)',
}
];

6 changes: 5 additions & 1 deletion src/lib/core/breakpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export * from './data/orientation-break-points';
export * from './break-point';
export * from './break-point-registry';
export * from './break-points-token';
export {prioritySort} from './breakpoint-tools';

export {
sortDescendingPriority,
sortAscendingPriority
} from './breakpoint-tools';
17 changes: 5 additions & 12 deletions src/lib/core/match-media/match-media.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import {TestBed, inject, async} from '@angular/core/testing';

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

matchMedia.registerQuery(query1);
matchMedia.registerQuery(query2);

let media$ = matchMedia.observe();
let subscription = media$.subscribe((change: MediaChange) => {
const queries = [query1, query2];
let subscription = matchMedia.observe(queries).subscribe((change: MediaChange) => {
current = change;
});

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

matchMedia.registerQuery([query1, query2]);

let subscription = matchMedia.observe(query1).subscribe((change: MediaChange) => {
let subscription = matchMedia.observe([query1]).subscribe((change: MediaChange) => {
current = change;
});

Expand Down Expand Up @@ -128,11 +124,6 @@ describe('match-media-observable', () => {
matchMedia = _matchMedia; // inject only to manually activate mediaQuery ranges
breakPoints = _breakPoints;
mediaObserver = _mediaObserver;

// Quick register all breakpoint mediaQueries
breakPoints.items.forEach((bp: BreakPoint) => {
matchMedia.observe(bp.mediaQuery);
});
})));
afterEach(() => {
matchMedia.clearAll();
Expand Down Expand Up @@ -206,6 +197,8 @@ describe('match-media-observable', () => {
it('ignores mediaQuery de-activations', () => {
let activationCount = 0;
let deactivationCount = 0;

mediaObserver.filterOverlaps = false;
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
if (change.matches) {
++activationCount;
Expand Down
Loading