feat: add bottom sheet component (#9764)

* Adds an initial implementation of the new `MatBottomSheet` service that allows users to display component-based or template-based bottom sheets.
* Sets up the various boilerplate and infrastructure necessary for a new component.

Note: this is an initial implementation that has the necessary functionality, styles and accessibility. More docs, examples and touch gestures will be added in a follow-up.
This commit is contained in:
Kristiyan Kostadinov
2018-02-06 18:31:11 +01:00
committed by mmalerba
parent 4a5287cd1c
commit bbf62cdd6c
29 changed files with 1430 additions and 0 deletions
+2
View File
@@ -1,6 +1,7 @@
# Angular Material components
/src/lib/* @jelbourn
/src/lib/autocomplete/** @kara @crisbeto
/src/lib/bottom-sheet/** @jelbourn @crisbeto
/src/lib/button-toggle/** @tinayuangao
/src/lib/button/** @tinayuangao
/src/lib/card/** @jelbourn
@@ -88,6 +89,7 @@
/src/demo-app/* @jelbourn
/src/demo-app/a11y/** @tinayuangao
/src/demo-app/autocomplete/** @kara @crisbeto
/src/demo-app/bottom-sheet/** @jelbourn @crisbeto
/src/demo-app/baseline/** @mmalerba
/src/demo-app/button-toggle/** @tinayuangao
/src/demo-app/button/** @tinayuangao
@@ -0,0 +1,45 @@
<h1>Bottom sheet demo</h1>
<button mat-raised-button color="primary" (click)="openComponent()">Open component sheet</button>
<button mat-raised-button color="accent" (click)="openTemplate()">Open template sheet</button>
<mat-card class="demo-dialog-card">
<mat-card-content>
<h2>Options</h2>
<p>
<mat-checkbox [(ngModel)]="config.hasBackdrop">Has backdrop</mat-checkbox>
</p>
<p>
<mat-checkbox [(ngModel)]="config.disableClose">Disable close</mat-checkbox>
</p>
<p>
<mat-form-field>
<input matInput [(ngModel)]="config.backdropClass" placeholder="Backdrop class">
</mat-form-field>
</p>
<p>
<mat-form-field>
<mat-select placeholder="Direction" [(ngModel)]="config.direction">
<mat-option value="ltr">LTR</mat-option>
<mat-option value="rtl">RTL</mat-option>
</mat-select>
</mat-form-field>
</p>
</mat-card-content>
</mat-card>
<ng-template let-bottomSheetRef="bottomSheetRef">
<mat-nav-list>
<mat-list-item (click)="bottomSheetRef.dismiss()" *ngFor="let action of [1, 2, 3]">
<mat-icon mat-list-icon>folder</mat-icon>
<span mat-line>Action {{ link }}</span>
<span mat-line>Description</span>
</mat-list-item>
</mat-nav-list>
</ng-template>
@@ -0,0 +1,8 @@
.demo-dialog-card {
max-width: 405px;
margin: 20px 0;
}
.mat-raised-button {
margin-right: 5px;
}
@@ -0,0 +1,66 @@
/**
* @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
*/
import {Component, ViewEncapsulation, TemplateRef, ViewChild} from '@angular/core';
import {
MatBottomSheet,
MatBottomSheetRef,
MatBottomSheetConfig,
} from '@angular/material/bottom-sheet';
const defaultConfig = new MatBottomSheetConfig();
@Component({
moduleId: module.id,
selector: 'bottom-sheet-demo',
styleUrls: ['bottom-sheet-demo.css'],
templateUrl: 'bottom-sheet-demo.html',
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
})
export class BottomSheetDemo {
config: MatBottomSheetConfig = {
hasBackdrop: defaultConfig.hasBackdrop,
disableClose: defaultConfig.disableClose,
backdropClass: defaultConfig.backdropClass,
direction: 'ltr'
};
@ViewChild(TemplateRef) template: TemplateRef<any>;
constructor(private _bottomSheet: MatBottomSheet) {}
openComponent() {
this._bottomSheet.open(ExampleBottomSheet, this.config);
}
openTemplate() {
this._bottomSheet.open(this.template, this.config);
}
}
@Component({
template: `
<mat-nav-list>
<a href="#" mat-list-item (click)="handleClick($event)" *ngFor="let action of [1, 2, 3]">
<mat-icon mat-list-icon>folder</mat-icon>
<span mat-line>Action {{ link }}</span>
<span mat-line>Description</span>
</a>
</mat-nav-list>
`
})
export class ExampleBottomSheet {
constructor(private sheet: MatBottomSheetRef) {}
handleClick(event: MouseEvent) {
event.preventDefault();
this.sheet.dismiss();
}
}
+1
View File
@@ -51,6 +51,7 @@ export class DemoApp {
dark = false;
navItems = [
{name: 'Autocomplete', route: '/autocomplete'},
{name: 'Bottom sheet', route: '/bottom-sheet'},
{name: 'Button Toggle', route: '/button-toggle'},
{name: 'Button', route: '/button'},
{name: 'Card', route: '/card'},
+4
View File
@@ -12,6 +12,7 @@ import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {BottomSheetDemo, ExampleBottomSheet} from '../bottom-sheet/bottom-sheet-demo';
import {BaselineDemo} from '../baseline/baseline-demo';
import {ButtonToggleDemo} from '../button-toggle/button-toggle-demo';
import {ButtonDemo} from '../button/button-demo';
@@ -71,6 +72,7 @@ import {TableDemoModule} from '../table/table-demo-module';
],
declarations: [
AutocompleteDemo,
BottomSheetDemo,
BaselineDemo,
ButtonDemo,
ButtonToggleDemo,
@@ -120,6 +122,7 @@ import {TableDemoModule} from '../table/table-demo-module';
ToolbarDemo,
TooltipDemo,
TypographyDemo,
ExampleBottomSheet,
],
providers: [
{provide: OverlayContainer, useClass: FullscreenOverlayContainer},
@@ -133,6 +136,7 @@ import {TableDemoModule} from '../table/table-demo-module';
RotiniPanel,
ScienceJoke,
SpagettiPanel,
ExampleBottomSheet,
],
})
export class DemoModule {}
+2
View File
@@ -10,6 +10,7 @@ import {Routes} from '@angular/router';
import {AccessibilityDemo} from '../a11y/a11y';
import {ACCESSIBILITY_DEMO_ROUTES} from '../a11y/routes';
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {BottomSheetDemo} from '../bottom-sheet/bottom-sheet-demo';
import {BaselineDemo} from '../baseline/baseline-demo';
import {ButtonToggleDemo} from '../button-toggle/button-toggle-demo';
import {ButtonDemo} from '../button/button-demo';
@@ -55,6 +56,7 @@ export const DEMO_APP_ROUTES: Routes = [
{path: '', component: DemoApp, children: [
{path: '', component: Home},
{path: 'autocomplete', component: AutocompleteDemo},
{path: 'bottom-sheet', component: BottomSheetDemo},
{path: 'baseline', component: BaselineDemo},
{path: 'button', component: ButtonDemo},
{path: 'button-toggle', component: ButtonToggleDemo},
+2
View File
@@ -17,6 +17,7 @@ import {CdkTableModule} from '@angular/cdk/table';
import {NgModule} from '@angular/core';
import {
MatAutocompleteModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
@@ -57,6 +58,7 @@ import {
@NgModule({
exports: [
MatAutocompleteModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
+1
View File
@@ -59,6 +59,7 @@ System.config({
'@angular/cdk/table': 'dist/packages/cdk/table/index.js',
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
'@angular/material/bottom-sheet': 'dist/packages/material/bottom-sheet/index.js',
'@angular/material/button': 'dist/packages/material/button/index.js',
'@angular/material/button-toggle': 'dist/packages/material/button-toggle/index.js',
'@angular/material/card': 'dist/packages/material/card/index.js',
+1
View File
@@ -51,6 +51,7 @@ System.config({
'@angular/material-examples': 'dist/bundles/material-examples.umd.js',
'@angular/material/autocomplete': 'dist/bundles/material-autocomplete.umd.js',
'@angular/material/bottom-sheet': 'dist/bundles/material-bottom-sheet.umd.js',
'@angular/material/button': 'dist/bundles/material-button.umd.js',
'@angular/material/button-toggle': 'dist/bundles/material-button-toggle.umd.js',
'@angular/material/card': 'dist/bundles/material-card.umd.js',
+38
View File
@@ -0,0 +1,38 @@
package(default_visibility=["//visibility:public"])
load("@angular//:index.bzl", "ng_module")
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_library", "sass_binary")
ng_module(
name = "bottom-sheet",
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
module_name = "@angular/material/bottom_sheet",
assets = [
":bottom_sheet_container_css",
],
deps = [
"//src/lib/core",
"//src/cdk/a11y",
"//src/cdk/overlay",
"//src/cdk/portal",
"//src/cdk/layout",
"@rxjs",
],
tsconfig = ":tsconfig-build.json",
)
sass_binary(
name = "bottom_sheet_container_scss",
src = "bottom-sheet-container.scss",
deps = ["//src/lib/core:core_scss_lib"],
)
# TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir.
# Copy the output of the sass_binary such that the filename and path match what we expect.
genrule(
name = "bottom_sheet_container_css",
srcs = [":bottom_sheet_container_scss"],
outs = ["bottom-sheet-container.css"],
cmd = "cat $(locations :bottom_sheet_container_scss) > $@",
)
+1
View File
@@ -0,0 +1 @@
Please see the official documentation at https://material.angular.io/components/component/bottom-sheet
@@ -0,0 +1,21 @@
@import '../core/typography/typography-utils';
@import '../core/theming/palette';
@mixin mat-bottom-sheet-theme($theme) {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.mat-bottom-sheet-container {
background: mat-color($background, dialog);
color: mat-color($foreground, text);
}
}
@mixin mat-bottom-sheet-typography($config) {
.mat-bottom-sheet-container {
// Note: we don't use the line-height, because it's way too big.
font-family: mat-font-family($config);
font-size: mat-font-size($config, subheading-2);
font-weight: mat-font-weight($config, subheading-2);
}
}
@@ -0,0 +1,31 @@
/**
* @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
*/
import {
animate,
state,
style,
transition,
trigger,
AnimationTriggerMetadata,
} from '@angular/animations';
import {AnimationCurves, AnimationDurations} from '@angular/material/core';
/** Animations used by the Material bottom sheet. */
export const matBottomSheetAnimations: {
readonly bottomSheetState: AnimationTriggerMetadata;
} = {
/** Animation that shows and hides a bottom sheet. */
bottomSheetState: trigger('state', [
state('void, hidden', style({transform: 'translateY(100%)'})),
state('visible', style({transform: 'translateY(0%)'})),
transition('visible => void, visible => hidden',
animate(`${AnimationDurations.COMPLEX} ${AnimationCurves.ACCELERATION_CURVE}`)),
transition('void => visible',
animate(`${AnimationDurations.EXITING} ${AnimationCurves.DECELERATION_CURVE}`)),
])
};
@@ -0,0 +1,42 @@
/**
* @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
*/
import {ViewContainerRef, InjectionToken} from '@angular/core';
import {Direction} from '@angular/cdk/bidi';
/** Injection token that can be used to access the data that was passed in to a bottom sheet. */
export const MAT_BOTTOM_SHEET_DATA = new InjectionToken<any>('MatBottomSheetData');
/**
* Configuration used when opening a bottom sheet.
*/
export class MatBottomSheetConfig<D = any> {
/** The view container to place the overlay for the bottom sheet into. */
viewContainerRef?: ViewContainerRef;
/** Extra CSS classes to be added to the bottom sheet container. */
panelClass?: string | string[];
/** Text layout direction for the bottom sheet. */
direction?: Direction = 'ltr';
/** Data being injected into the child component. */
data?: D | null = null;
/** Whether the bottom sheet has a backdrop. */
hasBackdrop?: boolean = true;
/** Custom class for the backdrop. */
backdropClass?: string;
/** Whether the user can use escape or clicking outside to close the bottom sheet. */
disableClose?: boolean = false;
/** Aria label to assign to the bottom sheet element. */
ariaLabel?: string | null = null;
}
@@ -0,0 +1 @@
<ng-template cdkPortalOutlet></ng-template>
@@ -0,0 +1,31 @@
@import '../core/style/elevation';
// The bottom sheet minimum width on larger screen sizes is based
// on increments of the toolbar, according to the spec. See:
// https://material.io/guidelines/components/bottom-sheets.html#bottom-sheets-specs
$_mat-bottom-sheet-width-increment: 64px;
$mat-bottom-sheet-container-vertical-padding: 8px !default;
$mat-bottom-sheet-container-horizontal-padding: 16px !default;
.mat-bottom-sheet-container {
@include mat-elevation(16);
padding: $mat-bottom-sheet-container-vertical-padding
$mat-bottom-sheet-container-horizontal-padding;
min-width: 100vw;
box-sizing: border-box;
display: block;
outline: 0;
}
.mat-bottom-sheet-container-medium {
min-width: $_mat-bottom-sheet-width-increment * 6;
}
.mat-bottom-sheet-container-large {
min-width: $_mat-bottom-sheet-width-increment * 8;
}
.mat-bottom-sheet-container-xlarge {
min-width: $_mat-bottom-sheet-width-increment * 9;
}
@@ -0,0 +1,205 @@
/**
* @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
*/
import {
Component,
ComponentRef,
EmbeddedViewRef,
ViewChild,
OnDestroy,
ElementRef,
ChangeDetectionStrategy,
ViewEncapsulation,
ChangeDetectorRef,
EventEmitter,
Inject,
Optional,
} from '@angular/core';
import {AnimationEvent} from '@angular/animations';
import {
BasePortalOutlet,
ComponentPortal,
TemplatePortal,
CdkPortalOutlet,
} from '@angular/cdk/portal';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {MatBottomSheetConfig} from './bottom-sheet-config';
import {matBottomSheetAnimations} from './bottom-sheet-animations';
import {Subscription} from 'rxjs/Subscription';
import {DOCUMENT} from '@angular/common';
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
// TODO(crisbeto): consolidate some logic between this, MatDialog and MatSnackBar
/**
* Internal component that wraps user-provided bottom sheet content.
* @docs-private
*/
@Component({
moduleId: module.id,
selector: 'mat-bottom-sheet-container',
templateUrl: 'bottom-sheet-container.html',
styleUrls: ['bottom-sheet-container.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
animations: [matBottomSheetAnimations.bottomSheetState],
host: {
'class': 'mat-bottom-sheet-container',
'tabindex': '-1',
'role': 'dialog',
'[attr.aria-label]': 'bottomSheetConfig?.ariaLabel',
'[@state]': '_animationState',
'(@state.start)': '_onAnimationStart($event)',
'(@state.done)': '_onAnimationDone($event)'
},
})
export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestroy {
private _breakpointSubscription: Subscription;
/** The portal outlet inside of this container into which the content will be loaded. */
@ViewChild(CdkPortalOutlet) _portalOutlet: CdkPortalOutlet;
/** The state of the bottom sheet animations. */
_animationState: 'void' | 'visible' | 'hidden' = 'void';
/** Emits whenever the state of the animation changes. */
_animationStateChanged = new EventEmitter<AnimationEvent>();
/** The bottom sheet configuration. */
bottomSheetConfig: MatBottomSheetConfig;
/** The class that traps and manages focus within the bottom sheet. */
private _focusTrap: FocusTrap;
/** Element that was focused before the bottom sheet was opened. */
private _elementFocusedBeforeOpened: HTMLElement | null = null;
/** Server-side rendering-compatible reference to the global document object. */
private _document: Document;
constructor(
private _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
private _focusTrapFactory: FocusTrapFactory,
breakpointObserver: BreakpointObserver,
@Optional() @Inject(DOCUMENT) document: any) {
super();
this._document = document;
this._breakpointSubscription = breakpointObserver
.observe([Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge])
.subscribe(() => {
this._toggleClass('mat-bottom-sheet-container-medium',
breakpointObserver.isMatched(Breakpoints.Medium));
this._toggleClass('mat-bottom-sheet-container-large',
breakpointObserver.isMatched(Breakpoints.Large));
this._toggleClass('mat-bottom-sheet-container-xlarge',
breakpointObserver.isMatched(Breakpoints.XLarge));
});
}
/** Attach a component portal as content to this bottom sheet container. */
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
this._validatePortalAttached();
this._setPanelClass();
this._savePreviouslyFocusedElement();
return this._portalOutlet.attachComponentPortal(portal);
}
/** Attach a template portal as content to this bottom sheet container. */
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
this._validatePortalAttached();
this._setPanelClass();
this._savePreviouslyFocusedElement();
return this._portalOutlet.attachTemplatePortal(portal);
}
/** Begin animation of bottom sheet entrance into view. */
enter(): void {
this._animationState = 'visible';
this._changeDetectorRef.detectChanges();
}
/** Begin animation of the bottom sheet exiting from view. */
exit(): void {
this._animationState = 'hidden';
this._changeDetectorRef.markForCheck();
}
ngOnDestroy() {
this._breakpointSubscription.unsubscribe();
}
_onAnimationDone(event: AnimationEvent) {
if (event.toState === 'visible') {
this._trapFocus();
} else if (event.toState === 'hidden') {
this._restoreFocus();
}
this._animationStateChanged.emit(event);
}
_onAnimationStart(event: AnimationEvent) {
this._animationStateChanged.emit(event);
}
private _toggleClass(cssClass: string, add: boolean) {
const classList = this._elementRef.nativeElement.classList;
add ? classList.add(cssClass) : classList.remove(cssClass);
}
private _validatePortalAttached() {
if (this._portalOutlet.hasAttached()) {
throw Error('Attempting to attach bottom sheet content after content is already attached');
}
}
private _setPanelClass() {
const element: HTMLElement = this._elementRef.nativeElement;
const panelClass = this.bottomSheetConfig.panelClass;
if (Array.isArray(panelClass)) {
// Note that we can't use a spread here, because IE doesn't support multiple arguments.
panelClass.forEach(cssClass => element.classList.add(cssClass));
} else if (panelClass) {
element.classList.add(panelClass);
}
}
/** Moves the focus inside the focus trap. */
private _trapFocus() {
if (!this._focusTrap) {
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
}
this._focusTrap.focusInitialElementWhenReady();
}
/** Restores focus to the element that was focused before the bottom sheet opened. */
private _restoreFocus() {
const toFocus = this._elementFocusedBeforeOpened;
// We need the extra check, because IE can set the `activeElement` to null in some cases.
if (toFocus && typeof toFocus.focus === 'function') {
toFocus.focus();
}
if (this._focusTrap) {
this._focusTrap.destroy();
}
}
/** Saves a reference to the element that was focused before the bottom sheet was opened. */
private _savePreviouslyFocusedElement() {
this._elementFocusedBeforeOpened = this._document.activeElement as HTMLElement;
Promise.resolve().then(() => this._elementRef.nativeElement.focus());
}
}
@@ -0,0 +1,34 @@
/**
* @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
*/
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MatCommonModule} from '@angular/material/core';
import {A11yModule} from '@angular/cdk/a11y';
import {OverlayModule} from '@angular/cdk/overlay';
import {PortalModule} from '@angular/cdk/portal';
import {LayoutModule} from '@angular/cdk/layout';
import {MatBottomSheetContainer} from './bottom-sheet-container';
import {MatBottomSheet} from './bottom-sheet';
@NgModule({
imports: [
A11yModule,
CommonModule,
OverlayModule,
MatCommonModule,
PortalModule,
LayoutModule,
],
exports: [MatBottomSheetContainer, MatCommonModule],
declarations: [MatBottomSheetContainer],
entryComponents: [MatBottomSheetContainer],
providers: [MatBottomSheet],
})
export class MatBottomSheetModule {}
+105
View File
@@ -0,0 +1,105 @@
/**
* @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
*/
import {OverlayRef} from '@angular/cdk/overlay';
import {ESCAPE} from '@angular/cdk/keycodes';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {merge} from 'rxjs/observable/merge';
import {filter} from 'rxjs/operators/filter';
import {take} from 'rxjs/operators/take';
import {MatBottomSheetContainer} from './bottom-sheet-container';
/**
* Reference to a bottom sheet dispatched from the bottom sheet service.
*/
export class MatBottomSheetRef<T = any> {
/** Instance of the component making up the content of the bottom sheet. */
instance: T;
/**
* Instance of the component into which the bottom sheet content is projected.
* @docs-private
*/
containerInstance: MatBottomSheetContainer;
/** Subject for notifying the user that the bottom sheet has been dismissed. */
private readonly _afterDismissed = new Subject<void>();
/** Subject for notifying the user that the bottom sheet has opened and appeared. */
private readonly _afterOpened = new Subject<void>();
constructor(containerInstance: MatBottomSheetContainer, private _overlayRef: OverlayRef) {
this.containerInstance = containerInstance;
// Emit when opening animation completes
containerInstance._animationStateChanged.pipe(
filter(event => event.phaseName === 'done' && event.toState === 'visible'),
take(1)
)
.subscribe(() => {
this._afterOpened.next();
this._afterOpened.complete();
});
// Dispose overlay when closing animation is complete
containerInstance._animationStateChanged.pipe(
filter(event => event.phaseName === 'done' && event.toState === 'hidden'),
take(1)
)
.subscribe(() => {
this._overlayRef.dispose();
this._afterDismissed.next();
this._afterDismissed.complete();
});
if (!containerInstance.bottomSheetConfig.disableClose) {
merge(
_overlayRef.backdropClick(),
_overlayRef._keydownEvents.pipe(filter(event => event.keyCode === ESCAPE))
).subscribe(() => this.dismiss());
}
}
/** Dismisses the bottom sheet. */
dismiss(): void {
if (!this._afterDismissed.closed) {
// Transition the backdrop in parallel to the bottom sheet.
this.containerInstance._animationStateChanged.pipe(
filter(event => event.phaseName === 'start'),
take(1)
).subscribe(() => this._overlayRef.detachBackdrop());
this.containerInstance.exit();
}
}
/** Gets an observable that is notified when the bottom sheet is finished closing. */
afterDismissed(): Observable<void> {
return this._afterDismissed.asObservable();
}
/** Gets an observable that is notified when the bottom sheet has opened and appeared. */
afterOpened(): Observable<void> {
return this._afterOpened.asObservable();
}
/**
* Gets an observable that emits when the overlay's backdrop has been clicked.
*/
backdropClick(): Observable<MouseEvent> {
return this._overlayRef.backdropClick();
}
/**
* Gets an observable that emits when keydown events are targeted on the overlay.
*/
keydownEvents(): Observable<KeyboardEvent> {
return this._overlayRef.keydownEvents();
}
}
+587
View File
@@ -0,0 +1,587 @@
import {
Directive,
Component,
NgModule,
ViewContainerRef,
ViewChild,
Inject,
Injector,
TemplateRef,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flushMicrotasks,
inject,
TestBed,
tick,
flush,
} from '@angular/core/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Directionality} from '@angular/cdk/bidi';
import {MatBottomSheetModule} from './bottom-sheet-module';
import {MatBottomSheet} from './bottom-sheet';
import {MatBottomSheetRef} from './bottom-sheet-ref';
import {MAT_BOTTOM_SHEET_DATA} from './bottom-sheet-config';
import {MatBottomSheetConfig} from './bottom-sheet-config';
import {OverlayContainer, ViewportRuler} from '@angular/cdk/overlay';
import {A, ESCAPE} from '@angular/cdk/keycodes';
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
describe('MatBottomSheet', () => {
let bottomSheet: MatBottomSheet;
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;
let viewportRuler: ViewportRuler;
let testViewContainerRef: ViewContainerRef;
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
beforeEach(fakeAsync(() => {
TestBed
.configureTestingModule({imports: [MatBottomSheetModule, BottomSheetTestModule]})
.compileComponents();
}));
beforeEach(inject([MatBottomSheet, OverlayContainer, ViewportRuler],
(bs: MatBottomSheet, oc: OverlayContainer, vr: ViewportRuler) => {
bottomSheet = bs;
overlayContainer = oc;
viewportRuler = vr;
overlayContainerElement = oc.getContainerElement();
}));
afterEach(() => {
overlayContainer.ngOnDestroy();
});
beforeEach(() => {
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
viewContainerFixture.detectChanges();
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
});
it('should open a bottom sheet with a component', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(bottomSheetRef.instance instanceof PizzaMsg).toBe(true);
expect(bottomSheetRef.instance.bottomSheetRef).toBe(bottomSheetRef);
});
it('should open a bottom sheet with a template', () => {
const templateRefFixture = TestBed.createComponent(ComponentWithTemplateRef);
templateRefFixture.componentInstance.localValue = 'Bees';
templateRefFixture.detectChanges();
const bottomSheetRef = bottomSheet.open(templateRefFixture.componentInstance.templateRef, {
data: {value: 'Knees'}
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('Cheese Bees Knees');
expect(templateRefFixture.componentInstance.bottomSheetRef).toBe(bottomSheetRef);
});
it('should position the bottom sheet at the bottom center of the screen', () => {
bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
const containerElement = overlayContainerElement.querySelector('mat-bottom-sheet-container')!;
const containerRect = containerElement.getBoundingClientRect();
const viewportSize = viewportRuler.getViewportSize();
expect(Math.floor(containerRect.bottom)).toBe(Math.floor(viewportSize.height));
expect(Math.floor(containerRect.left + containerRect.width / 2))
.toBe(Math.floor(viewportSize.width / 2));
});
it('should emit when the bottom sheet opening animation is complete', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const spy = jasmine.createSpy('afterOpened spy');
bottomSheetRef.afterOpened().subscribe(spy);
viewContainerFixture.detectChanges();
// callback should not be called before animation is complete
expect(spy).not.toHaveBeenCalled();
flushMicrotasks();
expect(spy).toHaveBeenCalled();
}));
it('should use the correct injector', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
const injector = bottomSheetRef.instance.injector;
expect(bottomSheetRef.instance.bottomSheetRef).toBe(bottomSheetRef);
expect(injector.get<DirectiveWithViewContainer>(DirectiveWithViewContainer)).toBeTruthy();
});
it('should open a bottom sheet with a component and no ViewContainerRef', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(bottomSheetRef.instance instanceof PizzaMsg).toBe(true);
expect(bottomSheetRef.instance.bottomSheetRef).toBe(bottomSheetRef);
});
it('should apply the correct role to the container element', () => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
const containerElement = overlayContainerElement.querySelector('mat-bottom-sheet-container')!;
expect(containerElement.getAttribute('role')).toBe('dialog');
});
it('should close a bottom sheet via the escape key', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeNull();
}));
it('should close when clicking on the overlay backdrop', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeFalsy();
}));
it('should emit the backdropClick stream when clicking on the overlay backdrop', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const spy = jasmine.createSpy('backdropClick spy');
bottomSheetRef.backdropClick().subscribe(spy);
viewContainerFixture.detectChanges();
const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
expect(spy).toHaveBeenCalledTimes(1);
viewContainerFixture.detectChanges();
flush();
// Additional clicks after the bottom sheet was closed should not be emitted
backdrop.click();
expect(spy).toHaveBeenCalledTimes(1);
}));
it('should emit the keyboardEvent stream when key events target the overlay', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const spy = jasmine.createSpy('keyboardEvent spy');
bottomSheetRef.keydownEvents().subscribe(spy);
viewContainerFixture.detectChanges();
const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
const container =
overlayContainerElement.querySelector('mat-bottom-sheet-container') as HTMLElement;
dispatchKeyboardEvent(document.body, 'keydown', A);
dispatchKeyboardEvent(document.body, 'keydown', A, backdrop);
dispatchKeyboardEvent(document.body, 'keydown', A, container);
expect(spy).toHaveBeenCalledTimes(3);
}));
it('should allow setting the layout direction', () => {
bottomSheet.open(PizzaMsg, { direction: 'rtl' });
viewContainerFixture.detectChanges();
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
expect(overlayPane.getAttribute('dir')).toBe('rtl');
});
it('should be able to set a custom panel class', () => {
bottomSheet.open(PizzaMsg, {
panelClass: 'custom-panel-class',
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.custom-panel-class')).toBeTruthy();
});
it('should be able to set a custom aria-label', () => {
bottomSheet.open(PizzaMsg, {
ariaLabel: 'Hello there',
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
const container = overlayContainerElement.querySelector('mat-bottom-sheet-container')!;
expect(container.getAttribute('aria-label')).toBe('Hello there');
});
it('should be able to get dismissed through the service', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);
bottomSheet.dismiss();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.childElementCount).toBe(0);
}));
it('should open a new bottom sheet after dismissing a previous sheet', fakeAsync(() => {
let config: MatBottomSheetConfig = {viewContainerRef: testViewContainerRef};
let bottomSheetRef: MatBottomSheetRef<any> = bottomSheet.open(PizzaMsg, config);
viewContainerFixture.detectChanges();
bottomSheetRef.dismiss();
viewContainerFixture.detectChanges();
// Wait for the dismiss animation to finish.
flush();
bottomSheetRef = bottomSheet.open(TacoMsg, config);
viewContainerFixture.detectChanges();
// Wait for the open animation to finish.
flush();
expect(bottomSheetRef.containerInstance._animationState)
.toBe('visible', `Expected the animation state would be 'visible'.`);
}));
it('should remove past bottom sheets when opening new ones', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
bottomSheet.open(TacoMsg);
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.textContent).toContain('Taco');
}));
it('should remove bottom sheet if another is shown while its still animating open',
fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
bottomSheet.open(TacoMsg);
viewContainerFixture.detectChanges();
tick();
expect(overlayContainerElement.textContent).toContain('Taco');
tick(500);
}));
describe('passing in data', () => {
it('should be able to pass in data', () => {
const config = {
data: {
stringParam: 'hello',
dateParam: new Date()
}
};
const instance = bottomSheet.open(BottomSheetWithInjectedData, config).instance;
expect(instance.data.stringParam).toBe(config.data.stringParam);
expect(instance.data.dateParam).toBe(config.data.dateParam);
});
it('should default to null if no data is passed', () => {
expect(() => {
const bottomSheetRef = bottomSheet.open(BottomSheetWithInjectedData);
expect(bottomSheetRef.instance.data).toBeNull();
}).not.toThrow();
});
});
describe('disableClose option', () => {
it('should prevent closing via clicks on the backdrop', () => {
bottomSheet.open(PizzaMsg, {
disableClose: true,
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
});
it('should prevent closing via the escape key', () => {
bottomSheet.open(PizzaMsg, {
disableClose: true,
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
});
});
describe('hasBackdrop option', () => {
it('should have a backdrop', () => {
bottomSheet.open(PizzaMsg, {
hasBackdrop: true,
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy();
});
it('should not have a backdrop', () => {
bottomSheet.open(PizzaMsg, {
hasBackdrop: false,
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
});
});
describe('backdropClass option', () => {
it('should have default backdrop class', () => {
bottomSheet.open(PizzaMsg, {
backdropClass: '',
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-dark-backdrop')).toBeTruthy();
});
it('should have custom backdrop class', () => {
bottomSheet.open(PizzaMsg, {
backdropClass: 'custom-backdrop-class',
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.custom-backdrop-class')).toBeTruthy();
});
});
describe('focus management', () => {
// When testing focus, all of the elements must be in the DOM.
beforeEach(() => document.body.appendChild(overlayContainerElement));
afterEach(() => document.body.removeChild(overlayContainerElement));
it('should focus the first tabbable element of the bottom sheet on open', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef
});
viewContainerFixture.detectChanges();
flushMicrotasks();
expect(document.activeElement.tagName)
.toBe('INPUT', 'Expected first tabbable element (input) in the sheet to be focused.');
}));
it('should re-focus trigger element when bottom sheet closes', fakeAsync(() => {
const button = document.createElement('button');
button.id = 'bottom-sheet-trigger';
document.body.appendChild(button);
button.focus();
const bottomSheetRef = bottomSheet.open(PizzaMsg, { viewContainerRef: testViewContainerRef });
flushMicrotasks();
viewContainerFixture.detectChanges();
flushMicrotasks();
expect(document.activeElement.id)
.not.toBe('bottom-sheet-trigger', 'Expected the focus to change when sheet was opened.');
bottomSheetRef.dismiss();
expect(document.activeElement.id).not.toBe('bottom-sheet-trigger',
'Expcted the focus not to have changed before the animation finishes.');
flushMicrotasks();
viewContainerFixture.detectChanges();
tick(500);
expect(document.activeElement.id).toBe('bottom-sheet-trigger',
'Expected that the trigger was refocused after the sheet is closed.');
document.body.removeChild(button);
}));
});
});
describe('MatBottomSheet with parent MatBottomSheet', () => {
let parentBottomSheet: MatBottomSheet;
let childBottomSheet: MatBottomSheet;
let overlayContainer: OverlayContainer;
let overlayContainerElement: HTMLElement;
let fixture: ComponentFixture<ComponentThatProvidesMatBottomSheet>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatBottomSheetModule, BottomSheetTestModule, NoopAnimationsModule],
declarations: [ComponentThatProvidesMatBottomSheet],
}).compileComponents();
}));
beforeEach(inject([MatBottomSheet, OverlayContainer],
(bs: MatBottomSheet, oc: OverlayContainer) => {
parentBottomSheet = bs;
overlayContainer = oc;
overlayContainerElement = oc.getContainerElement();
fixture = TestBed.createComponent(ComponentThatProvidesMatBottomSheet);
childBottomSheet = fixture.componentInstance.bottomSheet;
fixture.detectChanges();
}));
afterEach(() => {
overlayContainer.ngOnDestroy();
});
it('should close bottom sheets opened by parent when opening from child', fakeAsync(() => {
parentBottomSheet.open(PizzaMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.toContain('Pizza', 'Expected a bottom sheet to be opened');
childBottomSheet.open(TacoMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.toContain('Taco', 'Expected parent bottom sheet to be dismissed by opening from child');
}));
it('should close bottom sheets opened by child when opening from parent', fakeAsync(() => {
childBottomSheet.open(PizzaMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.toContain('Pizza', 'Expected a bottom sheet to be opened');
parentBottomSheet.open(TacoMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.toContain('Taco', 'Expected child bottom sheet to be dismissed by opening from parent');
}));
});
@Directive({selector: 'dir-with-view-container'})
class DirectiveWithViewContainer {
constructor(public viewContainerRef: ViewContainerRef) { }
}
@Component({template: `<dir-with-view-container></dir-with-view-container>`})
class ComponentWithChildViewContainer {
@ViewChild(DirectiveWithViewContainer) childWithViewContainer: DirectiveWithViewContainer;
get childViewContainer() {
return this.childWithViewContainer.viewContainerRef;
}
}
@Component({
selector: 'arbitrary-component-with-template-ref',
template: `<ng-template let-data let-bottomSheetRef="bottomSheetRef">
Cheese {{localValue}} {{data?.value}}{{setRef(bottomSheetRef)}}</ng-template>`,
})
class ComponentWithTemplateRef {
localValue: string;
bottomSheetRef: MatBottomSheetRef<any>;
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
setRef(bottomSheetRef: MatBottomSheetRef<any>): string {
this.bottomSheetRef = bottomSheetRef;
return '';
}
}
@Component({template: '<p>Pizza</p> <input> <button>Close</button>'})
class PizzaMsg {
constructor(public bottomSheetRef: MatBottomSheetRef<PizzaMsg>,
public injector: Injector,
public directionality: Directionality) {}
}
@Component({template: '<p>Taco</p>'})
class TacoMsg {}
@Component({
template: '',
providers: [MatBottomSheet]
})
class ComponentThatProvidesMatBottomSheet {
constructor(public bottomSheet: MatBottomSheet) {}
}
@Component({template: ''})
class BottomSheetWithInjectedData {
constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any) { }
}
// Create a real (non-test) NgModule as a workaround for
// https://github.com/angular/angular/issues/10760
const TEST_DIRECTIVES = [
ComponentWithChildViewContainer,
ComponentWithTemplateRef,
PizzaMsg,
TacoMsg,
DirectiveWithViewContainer,
BottomSheetWithInjectedData,
];
@NgModule({
imports: [MatBottomSheetModule, NoopAnimationsModule],
exports: TEST_DIRECTIVES,
declarations: TEST_DIRECTIVES,
entryComponents: [
ComponentWithChildViewContainer,
ComponentWithTemplateRef,
PizzaMsg,
TacoMsg,
BottomSheetWithInjectedData,
],
})
class BottomSheetTestModule { }
+158
View File
@@ -0,0 +1,158 @@
/**
* @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
*/
import {Overlay, OverlayConfig, OverlayRef} from '@angular/cdk/overlay';
import {ComponentPortal, TemplatePortal, ComponentType, PortalInjector} from '@angular/cdk/portal';
import {ComponentRef, TemplateRef, Injectable, Injector, Optional, SkipSelf} from '@angular/core';
import {MatBottomSheetConfig, MAT_BOTTOM_SHEET_DATA} from './bottom-sheet-config';
import {MatBottomSheetRef} from './bottom-sheet-ref';
import {MatBottomSheetContainer} from './bottom-sheet-container';
/**
* Service to trigger Material Design bottom sheets.
*/
@Injectable()
export class MatBottomSheet {
private _bottomSheetRefAtThisLevel: MatBottomSheetRef<any> | null = null;
/** Reference to the currently opened bottom sheet. */
get _openedBottomSheetRef(): MatBottomSheetRef<any> | null {
const parent = this._parentBottomSheet;
return parent ? parent._openedBottomSheetRef : this._bottomSheetRefAtThisLevel;
}
set _openedBottomSheetRef(value: MatBottomSheetRef<any> | null) {
if (this._parentBottomSheet) {
this._parentBottomSheet._openedBottomSheetRef = value;
} else {
this._bottomSheetRefAtThisLevel = value;
}
}
constructor(
private _overlay: Overlay,
private _injector: Injector,
@Optional() @SkipSelf() private _parentBottomSheet: MatBottomSheet) {}
open<T, D = any>(component: ComponentType<T>,
config?: MatBottomSheetConfig<D>): MatBottomSheetRef<T>;
open<T, D = any>(template: TemplateRef<T>,
config?: MatBottomSheetConfig<D>): MatBottomSheetRef<T>;
open<T, D = any>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
config?: MatBottomSheetConfig<D>): MatBottomSheetRef<T> {
const _config = _applyConfigDefaults(config);
const overlayRef = this._createOverlay(_config);
const container = this._attachContainer(overlayRef, _config);
const ref = new MatBottomSheetRef<T>(container, overlayRef);
if (componentOrTemplateRef instanceof TemplateRef) {
container.attachTemplatePortal(new TemplatePortal<T>(componentOrTemplateRef, null!, {
$implicit: _config.data,
bottomSheetRef: ref
} as any));
} else {
const portal = new ComponentPortal(componentOrTemplateRef, undefined,
this._createInjector(_config, ref));
const contentRef = container.attachComponentPortal(portal);
ref.instance = contentRef.instance;
}
// When the bottom sheet is dismissed, clear the reference to it.
ref.afterDismissed().subscribe(() => {
// Clear the bottom sheet ref if it hasn't already been replaced by a newer one.
if (this._openedBottomSheetRef == ref) {
this._openedBottomSheetRef = null;
}
});
if (this._openedBottomSheetRef) {
// If a bottom sheet is already in view, dismiss it and enter the
// new bottom sheet after exit animation is complete.
this._openedBottomSheetRef.afterDismissed().subscribe(() => ref.containerInstance.enter());
this._openedBottomSheetRef.dismiss();
} else {
// If no bottom sheet is in view, enter the new bottom sheet.
ref.containerInstance.enter();
}
this._openedBottomSheetRef = ref;
return ref;
}
/**
* Dismisses the currently-visible bottom sheet.
*/
dismiss(): void {
if (this._openedBottomSheetRef) {
this._openedBottomSheetRef.dismiss();
}
}
/**
* Attaches the bottom sheet container component to the overlay.
*/
private _attachContainer(overlayRef: OverlayRef,
config: MatBottomSheetConfig): MatBottomSheetContainer {
const containerPortal = new ComponentPortal(MatBottomSheetContainer, config.viewContainerRef);
const containerRef: ComponentRef<MatBottomSheetContainer> = overlayRef.attach(containerPortal);
containerRef.instance.bottomSheetConfig = config;
return containerRef.instance;
}
/**
* Creates a new overlay and places it in the correct location.
* @param config The user-specified bottom sheet config.
*/
private _createOverlay(config: MatBottomSheetConfig): OverlayRef {
const overlayConfig = new OverlayConfig({
direction: config.direction,
hasBackdrop: config.hasBackdrop,
maxWidth: '100%',
scrollStrategy: this._overlay.scrollStrategies.block(),
positionStrategy: this._overlay.position()
.global()
.centerHorizontally()
.bottom('0')
});
if (config.backdropClass) {
overlayConfig.backdropClass = config.backdropClass;
}
return this._overlay.create(overlayConfig);
}
/**
* Creates an injector to be used inside of a bottom sheet component.
* @param config Config that was used to create the bottom sheet.
* @param bottomSheetRef Reference to the bottom sheet.
*/
private _createInjector<T>(config: MatBottomSheetConfig,
bottomSheetRef: MatBottomSheetRef<T>): PortalInjector {
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
const injectionTokens = new WeakMap();
injectionTokens.set(MatBottomSheetRef, bottomSheetRef);
injectionTokens.set(MAT_BOTTOM_SHEET_DATA, config.data);
return new PortalInjector(userInjector || this._injector, injectionTokens);
}
}
/**
* Applies default options to the bottom sheet config.
* @param config The configuration to which the defaults will be applied.
* @returns The new configuration object with defaults applied.
*/
function _applyConfigDefaults(config?: MatBottomSheetConfig): MatBottomSheetConfig {
return {...new MatBottomSheetConfig(), ...config};
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @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
*/
export * from './public-api';
+14
View File
@@ -0,0 +1,14 @@
/**
* @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
*/
export * from './bottom-sheet-module';
export * from './bottom-sheet';
export * from './bottom-sheet-config';
export * from './bottom-sheet-container';
export * from './bottom-sheet-animations';
export * from './bottom-sheet-ref';
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../tsconfig-build",
"files": [
"public-api.ts",
"../typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/material/bottom-sheet",
"skipTemplateCodegen": true,
"fullTemplateTypeCheck": true
}
}
+2
View File
@@ -1,6 +1,7 @@
// Import all the theming functionality.
@import '../core';
@import '../../autocomplete/autocomplete-theme';
@import '../../bottom-sheet/bottom-sheet-theme';
@import '../../button/button-theme';
@import '../../button-toggle/button-toggle-theme';
@import '../../card/card-theme';
@@ -36,6 +37,7 @@
@mixin angular-material-theme($theme) {
@include mat-core-theme($theme);
@include mat-autocomplete-theme($theme);
@include mat-bottom-sheet-theme($theme);
@include mat-button-theme($theme);
@include mat-button-toggle-theme($theme);
@include mat-card-theme($theme);
@@ -1,5 +1,6 @@
@import './typography';
@import '../../autocomplete/autocomplete-theme';
@import '../../bottom-sheet/bottom-sheet-theme';
@import '../../button/button-theme';
@import '../../button-toggle/button-toggle-theme';
@import '../../card/card-theme';
@@ -40,6 +41,7 @@
@include mat-base-typography($config);
@include mat-autocomplete-typography($config);
@include mat-bottom-sheet-typography($config);
@include mat-button-typography($config);
@include mat-button-toggle-typography($config);
@include mat-card-typography($config);
+1
View File
@@ -8,6 +8,7 @@
export * from './version';
export * from '@angular/material/autocomplete';
export * from '@angular/material/bottom-sheet';
export * from '@angular/material/button';
export * from '@angular/material/button-toggle';
export * from '@angular/material/card';
+1
View File
@@ -71,6 +71,7 @@ System.config({
'@angular/cdk/testing': 'dist/packages/cdk/testing/index.js',
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
'@angular/material/bottom-sheet': 'dist/packages/material/bottom-sheet/index.js',
'@angular/material/button': 'dist/packages/material/button/index.js',
'@angular/material/button-toggle': 'dist/packages/material/button-toggle/index.js',
'@angular/material/card': 'dist/packages/material/card/index.js',