-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create screen type service to determine screen type: Web, Tablet, Han…
…dset (#6772)
- Loading branch information
1 parent
f43bf23
commit 811e504
Showing
23 changed files
with
622 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import {coerceArray} from './array'; | ||
|
||
describe('coerceArray', () => { | ||
|
||
it('should wrap a string in an array', () => { | ||
let stringVal = 'just a string'; | ||
expect(coerceArray(stringVal)).toEqual([stringVal]); | ||
}); | ||
|
||
it('should wrap a number in an array', () => { | ||
let numberVal = 42; | ||
expect(coerceArray(numberVal)).toEqual([numberVal]); | ||
}); | ||
|
||
it('should wrap an object in an array', () => { | ||
let objectVal = { something: 'clever' }; | ||
expect(coerceArray(objectVal)).toEqual([objectVal]); | ||
}); | ||
|
||
it('should wrap a null vall in an array', () => { | ||
let nullVal = null; | ||
expect(coerceArray(nullVal)).toEqual([nullVal]); | ||
}); | ||
|
||
it('should wrap an undefined value in an array', () => { | ||
let undefinedVal = undefined; | ||
expect(coerceArray(undefinedVal)).toEqual([undefinedVal]); | ||
}); | ||
|
||
it('should not wrap an array in an array', () => { | ||
let arrayVal = [1, 2, 3]; | ||
expect(coerceArray(arrayVal)).toBe(arrayVal); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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<T>(value: T | T[]): T[] { | ||
return Array.isArray(value) ? value : [value]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ | |
|
||
export * from './boolean-property'; | ||
export * from './number-property'; | ||
export * from './array'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
### BreakpointsModule | ||
|
||
When including the CDK's `LayoutModule`, components can inject `BreakpointsObserver` to request | ||
the matching state of a CSS Media Query. | ||
|
||
A set of breakpoints is provided based on the Material Design | ||
[breakpoint system](https://material.io/guidelines/layout/responsive-ui.html#responsive-ui-breakpoints). | ||
|
||
#### Example | ||
```ts | ||
@Component({ ... }) | ||
export class MyWidget { | ||
isHandset: Observable<BreakpointState>; | ||
|
||
constructor(bm: BreakpointObserver) { | ||
bm.observe(Handset).subscribe((state: BreakpointState) => { | ||
if (state.matches) { | ||
this.makeEverythingFitOnSmallScreen(); | ||
} else { | ||
this.expandEverythingToFillTheScreen(); | ||
} | ||
}); | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 | ||
*/ | ||
import {LayoutModule, BreakpointObserver, BreakpointState} from './index'; | ||
import {MediaMatcher} from './media-matcher'; | ||
import {async, TestBed, inject} from '@angular/core/testing'; | ||
import {Injectable} from '@angular/core'; | ||
|
||
describe('BreakpointObserver', () => { | ||
let breakpointManager: BreakpointObserver; | ||
let mediaMatcher: FakeMediaMatcher; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [LayoutModule], | ||
providers: [{provide: MediaMatcher, useClass: FakeMediaMatcher}] | ||
}); | ||
})); | ||
|
||
beforeEach(inject( | ||
[BreakpointObserver, MediaMatcher], | ||
(bm: BreakpointObserver, mm: FakeMediaMatcher) => { | ||
breakpointManager = bm; | ||
mediaMatcher = mm; | ||
})); | ||
|
||
afterEach(() => { | ||
mediaMatcher.clear(); | ||
}); | ||
|
||
it('retrieves the whether a query is currently matched', () => { | ||
let query = 'everything starts as true in the FakeMediaMatcher'; | ||
expect(breakpointManager.isMatched(query)).toBeTruthy(); | ||
}); | ||
|
||
it('reuses the same MediaQueryList for matching queries', () => { | ||
expect(mediaMatcher.queryCount).toBe(0); | ||
breakpointManager.observe('query1'); | ||
expect(mediaMatcher.queryCount).toBe(1); | ||
breakpointManager.observe('query1'); | ||
expect(mediaMatcher.queryCount).toBe(1); | ||
breakpointManager.observe('query2'); | ||
expect(mediaMatcher.queryCount).toBe(2); | ||
breakpointManager.observe('query1'); | ||
expect(mediaMatcher.queryCount).toBe(2); | ||
}); | ||
|
||
it('accepts an array of queries', () => { | ||
let queries = ['1 query', '2 query', 'red query', 'blue query']; | ||
breakpointManager.observe(queries); | ||
expect(mediaMatcher.queryCount).toBe(queries.length); | ||
}); | ||
|
||
it('completes all events when the breakpoint manager is destroyed', () => { | ||
let firstTest = jasmine.createSpy('test1'); | ||
breakpointManager.observe('test1').subscribe(undefined, undefined, firstTest); | ||
let secondTest = jasmine.createSpy('test2'); | ||
breakpointManager.observe('test2').subscribe(undefined, undefined, secondTest); | ||
|
||
expect(firstTest).not.toHaveBeenCalled(); | ||
expect(secondTest).not.toHaveBeenCalled(); | ||
|
||
breakpointManager.ngOnDestroy(); | ||
|
||
expect(firstTest).toHaveBeenCalled(); | ||
expect(secondTest).toHaveBeenCalled(); | ||
}); | ||
|
||
it('emits an event on the observable when values change', () => { | ||
let query = '(width: 999px)'; | ||
let queryMatchState: boolean = false; | ||
breakpointManager.observe(query).subscribe((state: BreakpointState) => { | ||
queryMatchState = state.matches; | ||
}); | ||
|
||
async(() => { | ||
expect(queryMatchState).toBeTruthy(); | ||
mediaMatcher.setMatchesQuery(query, false); | ||
expect(queryMatchState).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
it('emits a true matches state when the query is matched', () => { | ||
let query = '(width: 999px)'; | ||
mediaMatcher.setMatchesQuery(query, true); | ||
expect(breakpointManager.isMatched(query)).toBeTruthy(); | ||
}); | ||
|
||
it('emits a false matches state when the query is not matched', () => { | ||
let query = '(width: 999px)'; | ||
mediaMatcher.setMatchesQuery(query, false); | ||
expect(breakpointManager.isMatched(query)).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
export class FakeMediaQueryList implements MediaQueryList { | ||
/** The callback for change events. */ | ||
addListenerCallback?: (mql: MediaQueryList) => void; | ||
|
||
constructor(public matches, public media) {} | ||
|
||
/** Toggles the matches state and "emits" a change event. */ | ||
setMatches(matches: boolean) { | ||
this.matches = matches; | ||
this.addListenerCallback!(this); | ||
} | ||
|
||
/** Registers the callback method for change events. */ | ||
addListener(callback: (mql: MediaQueryList) => void) { | ||
this.addListenerCallback = callback; | ||
} | ||
|
||
/** Noop, but required for implementing MediaQueryList. */ | ||
removeListener() {} | ||
} | ||
|
||
@Injectable() | ||
export class FakeMediaMatcher { | ||
/** A map of match media queries. */ | ||
private queries: Map<string, FakeMediaQueryList> = new Map(); | ||
|
||
/** The number of distinct queries created in the media matcher during a test. */ | ||
get queryCount(): number { | ||
return this.queries.size; | ||
} | ||
|
||
/** Fakes the match media response to be controlled in tests. */ | ||
matchMedia(query: string): FakeMediaQueryList { | ||
let mql = new FakeMediaQueryList(true, query); | ||
this.queries.set(query, mql); | ||
return mql; | ||
} | ||
|
||
/** Clears all queries from the map of queries. */ | ||
clear() { | ||
this.queries.clear(); | ||
} | ||
|
||
/** Toggles the matching state of the provided query. */ | ||
setMatchesQuery(query: string, matches: boolean) { | ||
if (this.queries.has(query)) { | ||
this.queries.get(query)!.setMatches(matches); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 | ||
*/ | ||
import {Injectable, NgZone, OnDestroy} from '@angular/core'; | ||
import {MediaMatcher} from './media-matcher'; | ||
import {Observable} from 'rxjs/Observable'; | ||
import {Subject} from 'rxjs/Subject'; | ||
import {RxChain, map, startWith, takeUntil} from '@angular/cdk/rxjs'; | ||
import {coerceArray} from '@angular/cdk/coercion'; | ||
import {combineLatest} from 'rxjs/observable/combineLatest'; | ||
import {fromEventPattern} from 'rxjs/observable/fromEventPattern'; | ||
|
||
/** The current state of a layout breakpoint. */ | ||
export interface BreakpointState { | ||
matches: boolean; | ||
} | ||
|
||
interface Query { | ||
observable: Observable<BreakpointState>; | ||
mql: MediaQueryList; | ||
} | ||
|
||
/** | ||
* Utility for checking the matching state of @media queries. | ||
*/ | ||
@Injectable() | ||
export class BreakpointObserver implements OnDestroy { | ||
/** A map of all media queries currently being listened for. */ | ||
private _queries: Map<string, Query> = new Map(); | ||
/** A subject for all other observables to takeUntil based on. */ | ||
private _destroySubject: Subject<{}> = new Subject(); | ||
|
||
constructor(private mediaMatcher: MediaMatcher, private zone: NgZone) {} | ||
|
||
/** Completes the active subject, signalling to all other observables to complete. */ | ||
ngOnDestroy() { | ||
this._destroySubject.next(); | ||
this._destroySubject.complete(); | ||
} | ||
|
||
/** Whether the query currently is matched. */ | ||
isMatched(value: string | string[]): boolean { | ||
let queries = coerceArray(value); | ||
return queries.some(mediaQuery => this._registerQuery(mediaQuery).mql.matches); | ||
} | ||
|
||
/** | ||
* Gets an observable of results for the given queries that will emit new results for any changes | ||
* in matching of the given queries. | ||
*/ | ||
observe(value: string | string[]): Observable<BreakpointState> { | ||
let queries = coerceArray(value); | ||
let observables = queries.map(query => this._registerQuery(query).observable); | ||
|
||
return combineLatest(observables, (a: BreakpointState, b: BreakpointState) => { | ||
return { | ||
matches: !!((a && a.matches) || (b && b.matches)), | ||
}; | ||
}); | ||
} | ||
|
||
/** Registers a specific query to be listened for. */ | ||
private _registerQuery(query: string): Query { | ||
// Only set up a new MediaQueryList if it is not already being listened for. | ||
if (this._queries.has(query)) { | ||
return this._queries.get(query)!; | ||
} | ||
|
||
let mql: MediaQueryList = this.mediaMatcher.matchMedia(query); | ||
// Create callback for match changes and add it is as a listener. | ||
let queryObservable = RxChain.from(fromEventPattern( | ||
// Listener callback methods are wrapped to be placed back in ngZone. Callbacks must be placed | ||
// back into the zone because matchMedia is only included in Zone.js by loading the | ||
// webapis-media-query.js file alongside the zone.js file. Additionally, some browsers do not | ||
// have MediaQueryList inherit from EventTarget, which causes inconsistencies in how Zone.js | ||
// patches it. | ||
(listener: MediaQueryListListener) => { | ||
mql.addListener((e: MediaQueryList) => this.zone.run(() => listener(e))); | ||
}, | ||
(listener: MediaQueryListListener) => { | ||
mql.removeListener((e: MediaQueryList) => this.zone.run(() => listener(e))); | ||
})) | ||
.call(takeUntil, this._destroySubject) | ||
.call(startWith, mql) | ||
.call(map, (nextMql: MediaQueryList) => ({matches: nextMql.matches})) | ||
.result(); | ||
|
||
// Add the MediaQueryList to the set of queries. | ||
let output = {observable: queryObservable, mql: mql}; | ||
this._queries.set(query, output); | ||
return output; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 | ||
*/ | ||
// PascalCase is being used as Breakpoints is used like an enum. | ||
// tslint:disable-next-line:variable-name | ||
export const Breakpoints = { | ||
Handset: '(max-width: 599px) and (orientation: portrait), ' + | ||
'(max-width: 959px) and (orientation: landscape)', | ||
Tablet: '(min-width: 600px) and (max-width: 839px) and (orientation: portrait), ' + | ||
'(min-width: 960px) and (max-width: 1279px) and (orientation: landscape)', | ||
Web: '(min-width: 840px) and (orientation: portrait), ' + | ||
'(min-width: 1280px) and (orientation: landscape)', | ||
|
||
HandsetPortrait: '(max-width: 599px) and (orientation: portrait)', | ||
TabletPortrait: '(min-width: 600px) and (max-width: 839px) and (orientation: portrait)', | ||
WebPortrait: '(min-width: 840px) and (orientation: portrait)', | ||
|
||
HandsetLandscape: '(max-width: 959px) and (orientation: landscape)', | ||
TabletLandscape: '(min-width: 960px) and (max-width: 1279px) and (orientation: landscape)', | ||
WebLandscape: '(min-width: 1280px) and (orientation: landscape)', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 | ||
*/ | ||
export * from './public_api'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
### MediaMatcher | ||
|
||
When including the CDK's `LayoutModule`, components can inject `MediaMatcher` to access the | ||
matchMedia method, if available on the platform. | ||
|
||
#### Example | ||
```ts | ||
@Component({ ... }) | ||
export class MyWidget { | ||
constructor(mm: MediaMatcher) { | ||
mm.matchMedia('(orientation: landscape)').matches ? | ||
this.setPortraitMode() : | ||
This comment has been minimized.
Sorry, something went wrong. |
||
this.setPortraitMode(); | ||
} | ||
} | ||
``` | ||
|
Oops, something went wrong.
Should this not be setLandscapeMode()?