From 6f43cf6c34b39cb36dae91c036077ef6ac1b064d Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Wed, 23 Jan 2019 10:13:13 -0600 Subject: [PATCH] fix(core): align breakpoints with those used in CDK (#1006) @angular/cdk BreakpointObserver will not replace `MediaObserver`. **MediaObserver** is an enhanced version that notifies subscribers of activations for standard AND **overlapping (lt-xxx, gt-xxx)** breakpoints. * Ensure standard breakpoints mediaQueries are aligned with those in the CDK * Update MediaObserver * isActive() enhanced to support list of aliases to determine if any match * properly disconnects subscribers when destroyed > Note: Developers should use MediaObserver (not use MatchMedia) service to observe breakpoint activations! MediaObserver is the recommended service to use for application developers; MatchMedia should be considered a private service. Fixes #685. Refs #1001. --- src/lib/core/breakpoints/data/break-points.ts | 18 +++---- .../data/orientation-break-points.ts | 8 +-- .../media-observer/media-observer.spec.ts | 6 +-- src/lib/core/media-observer/media-observer.ts | 53 +++++++++++++------ src/lib/core/sass/_layout-bp.scss | 18 +++---- src/lib/core/utils/array.ts | 12 +++++ src/lib/core/utils/index.ts | 1 + 7 files changed, 76 insertions(+), 40 deletions(-) create mode 100644 src/lib/core/utils/array.ts diff --git a/src/lib/core/breakpoints/data/break-points.ts b/src/lib/core/breakpoints/data/break-points.ts index f28d029c5..b9064e7af 100644 --- a/src/lib/core/breakpoints/data/break-points.ts +++ b/src/lib/core/breakpoints/data/break-points.ts @@ -13,52 +13,52 @@ import {BreakPoint} from '../break-point'; export const DEFAULT_BREAKPOINTS: BreakPoint[] = [ { alias: 'xs', - mediaQuery: 'screen and (min-width: 0px) and (max-width: 599.9999px)', + mediaQuery: 'screen and (min-width: 0px) and (max-width: 599.99px)', priority: 1000, }, { alias: 'sm', - mediaQuery: 'screen and (min-width: 600px) and (max-width: 959.9999px)', + mediaQuery: 'screen and (min-width: 600px) and (max-width: 959.99px)', priority: 900, }, { alias: 'md', - mediaQuery: 'screen and (min-width: 960px) and (max-width: 1279.9999px)', + mediaQuery: 'screen and (min-width: 960px) and (max-width: 1279.99px)', priority: 800, }, { alias: 'lg', - mediaQuery: 'screen and (min-width: 1280px) and (max-width: 1919.9999px)', + mediaQuery: 'screen and (min-width: 1280px) and (max-width: 1919.99px)', priority: 700, }, { alias: 'xl', - mediaQuery: 'screen and (min-width: 1920px) and (max-width: 4999.9999px)', + mediaQuery: 'screen and (min-width: 1920px) and (max-width: 4999.99px)', priority: 600, }, { alias: 'lt-sm', overlapping: true, - mediaQuery: 'screen and (max-width: 599.9999px)', + mediaQuery: 'screen and (max-width: 599.99px)', priority: 950, }, { alias: 'lt-md', overlapping: true, - mediaQuery: 'screen and (max-width: 959.9999px)', + mediaQuery: 'screen and (max-width: 959.99px)', priority: 850, }, { alias: 'lt-lg', overlapping: true, - mediaQuery: 'screen and (max-width: 1279.9999px)', + mediaQuery: 'screen and (max-width: 1279.99px)', priority: 750, }, { alias: 'lt-xl', overlapping: true, priority: 650, - mediaQuery: 'screen and (max-width: 1919.9999px)', + mediaQuery: 'screen and (max-width: 1919.99px)', }, { alias: 'gt-xs', diff --git a/src/lib/core/breakpoints/data/orientation-break-points.ts b/src/lib/core/breakpoints/data/orientation-break-points.ts index 1d1547259..cf8517aa1 100644 --- a/src/lib/core/breakpoints/data/orientation-break-points.ts +++ b/src/lib/core/breakpoints/data/orientation-break-points.ts @@ -9,11 +9,11 @@ import {BreakPoint} from '../break-point'; /* tslint:disable */ -const HANDSET_PORTRAIT = '(orientation: portrait) and (max-width: 599px)'; -const HANDSET_LANDSCAPE = '(orientation: landscape) and (max-width: 959px)'; +const HANDSET_PORTRAIT = '(orientation: portrait) and (max-width: 599.99px)'; +const HANDSET_LANDSCAPE = '(orientation: landscape) and (max-width: 959.99px)'; -const TABLET_LANDSCAPE = '(orientation: landscape) and (min-width: 960px) and (max-width: 1279px)'; -const TABLET_PORTRAIT = '(orientation: portrait) and (min-width: 600px) and (max-width: 839px)'; +const TABLET_PORTRAIT = '(orientation: portrait) and (min-width: 600px) and (max-width: 839.99px)'; +const TABLET_LANDSCAPE = '(orientation: landscape) and (min-width: 960px) and (max-width: 1279.99px)'; const WEB_PORTRAIT = '(orientation: portrait) and (min-width: 840px)'; const WEB_LANDSCAPE = '(orientation: landscape) and (min-width: 1280px)'; diff --git a/src/lib/core/media-observer/media-observer.spec.ts b/src/lib/core/media-observer/media-observer.spec.ts index 7985c6187..8d2450445 100644 --- a/src/lib/core/media-observer/media-observer.spec.ts +++ b/src/lib/core/media-observer/media-observer.spec.ts @@ -181,7 +181,7 @@ describe('media-observer', () => { describe('with custom BreakPoints', () => { const gtXsMediaQuery = 'screen and (min-width:120px) and (orientation:landscape)'; const superXLQuery = 'screen and (min-width:10000px)'; - const smMediaQuery = 'screen and (min-width: 600px) and (max-width: 959.9999px)'; + const smMediaQuery = 'screen and (min-width: 600px) and (max-width: 959.99px)'; const CUSTOM_BREAKPOINTS = [ {alias: 'slate.xl', priority: 11000, mediaQuery: superXLQuery}, @@ -236,7 +236,7 @@ describe('media-observer', () => { }); describe('with layout "print" configured', () => { - const mdMediaQuery = 'screen and (min-width: 960px) and (max-width: 1279.9999px)'; + const mdMediaQuery = 'screen and (min-width: 960px) and (max-width: 1279.99px)'; beforeEach(() => { // Configure testbed to prepare services @@ -288,7 +288,7 @@ describe('media-observer', () => { }); describe('with layout print NOT configured', () => { - const smMediaQuery = 'screen and (min-width: 600px) and (max-width: 959.9999px)'; + const smMediaQuery = 'screen and (min-width: 600px) and (max-width: 959.99px)'; beforeEach(() => { // Configure testbed to prepare services diff --git a/src/lib/core/media-observer/media-observer.ts b/src/lib/core/media-observer/media-observer.ts index 14e3097c6..b83865a85 100644 --- a/src/lib/core/media-observer/media-observer.ts +++ b/src/lib/core/media-observer/media-observer.ts @@ -5,16 +5,19 @@ * 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} from '@angular/core'; -import {Observable, of} from 'rxjs'; -import {debounceTime, filter, map, switchMap} from 'rxjs/operators'; +import {Injectable, OnDestroy} from '@angular/core'; +import {Subject, asapScheduler, Observable, of} from 'rxjs'; +import {debounceTime, filter, map, switchMap, takeUntil} from 'rxjs/operators'; import {mergeAlias} from '../add-alias'; import {MediaChange} from '../media-change'; import {MatchMedia} from '../match-media/match-media'; import {PrintHook} from '../media-marshaller/print-hook'; import {BreakPointRegistry, OptionalBreakPoint} from '../breakpoints/break-point-registry'; + import {sortDescendingPriority} from '../utils/sort'; +import {coerceArray} from '../utils/array'; + /** * MediaObserver enables applications to listen for 1..n mediaQuery activations and to determine @@ -58,7 +61,7 @@ import {sortDescendingPriority} from '../utils/sort'; * } */ @Injectable({providedIn: 'root'}) -export class MediaObserver { +export class MediaObserver implements OnDestroy { /** * @deprecated Use `asObservable()` instead. @@ -80,6 +83,14 @@ export class MediaObserver { ); } + /** + * Completes the active subject, signalling to all complete for all + * MediaObserver subscribers + */ + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } // ************************************************ // Public Methods @@ -93,18 +104,19 @@ export class MediaObserver { } /** - * Allow programmatic query to determine if specified query/alias is active. + * Allow programmatic query to determine if one or more media query/alias match + * the current viewport size. + * @param value One or more media queries (or aliases) to check. + * @returns Whether any of the media queries match. */ - isActive(alias: string): boolean { - const query = toMediaQuery(alias, this.breakpoints); - return this.matchMedia.isActive(query); + isActive(value: string | string[]): boolean { + const aliases = splitQueries(coerceArray(value)); + return aliases.some(alias => { + const query = toMediaQuery(alias, this.breakpoints); + return this.matchMedia.isActive(query); + }); } - /** - * Subscribers to activation list can use this function to easily exclude overlaps - */ - - // ************************************************ // Internal Methods // ************************************************ @@ -151,10 +163,11 @@ export class MediaObserver { .observe(this.hook.withPrintQuery(mqList)) .pipe( filter((change: MediaChange) => change.matches), - debounceTime(10), + debounceTime(0, asapScheduler), switchMap(_ => of(this.findAllActivations())), map(excludeOverlaps), - filter(hasChanges) + filter(hasChanges), + takeUntil(this.destroyed$) ); } @@ -180,6 +193,7 @@ export class MediaObserver { } private readonly _media$: Observable; + private readonly destroyed$ = new Subject(); } /** @@ -190,3 +204,12 @@ function toMediaQuery(query: string, locator: BreakPointRegistry) { return bp ? bp.mediaQuery : query; } +/** + * Split each query string into separate query strings if two queries are provided as comma + * separated. + */ +function splitQueries(queries: string[]): string[] { + return queries.map((query: string) => query.split(',')) + .reduce((a1: string[], a2: string[]) => a1.concat(a2)) + .map(query => query.trim()); +} diff --git a/src/lib/core/sass/_layout-bp.scss b/src/lib/core/sass/_layout-bp.scss index 7548ee23b..f26866133 100644 --- a/src/lib/core/sass/_layout-bp.scss +++ b/src/lib/core/sass/_layout-bp.scss @@ -5,23 +5,23 @@ $breakpoints: ( xs: ( begin: 0, - end: 599.9999px + end: 599.99px ), sm: ( begin: 600px, - end: 959.9999px + end: 959.99px ), md: ( begin: 960px, - end: 1279.9999px + end: 1279.99px ), lg: ( begin: 1280px, - end: 1919.9999px + end: 1919.99px ), xl: ( begin: 1920px, - end: 4999.9999px + end: 4999.99px ), ) !default; @@ -39,10 +39,10 @@ $overlapping-gt: ( // Material Design breakpoints // @type map $overlapping-lt: ( - lt-sm: 599.9999px, - lt-md: 959.9999px, - lt-lg: 1279.9999px, - lt-xl: 1919.9999px, + lt-sm: 599.99px, + lt-md: 959.99px, + lt-lg: 1279.99px, + lt-xl: 1919.99px, ) !default; diff --git a/src/lib/core/utils/array.ts b/src/lib/core/utils/array.ts new file mode 100644 index 000000000..6ab9c7eb1 --- /dev/null +++ b/src/lib/core/utils/array.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * 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 + */ + +/** Wraps the provided value in an array, unless the provided value is an array. */ +export function coerceArray(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value]; +} diff --git a/src/lib/core/utils/index.ts b/src/lib/core/utils/index.ts index 135393a1c..d0b1b476e 100644 --- a/src/lib/core/utils/index.ts +++ b/src/lib/core/utils/index.ts @@ -6,3 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ export * from './sort'; +export * from './array';