/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { animate, AnimationTriggerMetadata, state, style, transition, trigger, } from '@angular/animations'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, forwardRef, Inject, Injectable, Input, NgZone, ViewChild, ViewEncapsulation, } from '@angular/core'; import {Subject} from 'rxjs'; import {debounceTime, takeUntil} from 'rxjs/operators'; /** * Animations used by the Material drawers. * @docs-private */ export const matDrawerAnimations: { readonly transformDrawer: AnimationTriggerMetadata; } = { /** Animation that slides a drawer in and out. */ transformDrawer: trigger('transform', [ // We remove the `transform` here completely, rather than setting it to zero, because: // 1. Having a transform can cause elements with ripples or an animated // transform to shift around in Chrome with an RTL layout (see #10023). // 2. 3d transforms causes text to appear blurry on IE and Edge. state( 'open, open-instant', style({ transform: 'none', visibility: 'visible', }), ), state( 'void', style({ // Avoids the shadow showing up when closed in SSR. 'box-shadow': 'none', visibility: 'hidden', }), ), transition('void => open-instant', animate('0ms')), transition( 'void <=> open, open-instant => void', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)'), ), ]), }; /** * This component corresponds to a drawer that can be opened on the drawer container. */ @Injectable() @Component({ selector: 'mat-drawer', exportAs: 'matDrawer', template: `
`, styles: [ ` .mat-drawer.mat-drawer-bottom { left: 0; right: 0; bottom: 0; top: unset; position: fixed; z-index: 5; background-color: #f8f9fa; box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); } `, ], animations: [matDrawerAnimations.transformDrawer], host: { class: 'mat-drawer mat-drawer-bottom', // must prevent the browser from aligning text based on value '[attr.align]': 'null', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class MatDrawer { @Input() mode: 'push' | 'overlay' = 'overlay'; @Input() baseHeight = 0; getBaseHeight() { return this.baseHeight; } } @Component({ selector: 'mat-drawer-content', template: '', styles: [ ` .mat-drawer-content { display: flex; flex-direction: column; position: relative; z-index: 1; height: unset; overflow: unset; width: 100%; flex-grow: 1; } `, ], host: { class: 'mat-drawer-content', '[style.margin-top.px]': 'contentMargins.top', '[style.margin-bottom.px]': 'contentMargins.bottom', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class MatDrawerContent /*extends MatDrawerContentBase*/ { private contentMargins: {top: number | null; bottom: number | null} = { top: null, bottom: null, }; constructor( @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, @Inject(forwardRef(() => MatDrawerContainer)) public container: MatDrawerContainer, ) {} ngAfterContentInit() { this.container.contentMarginChanges.subscribe(() => { this.changeDetectorRef.markForCheck(); }); } setMargins(margins: {top: number | null; bottom: number | null}) { this.contentMargins = margins; } } /** * Container for Material drawers * @docs-private */ @Component({ selector: 'mat-drawer-container', exportAs: 'matDrawerContainer', template: ` `, styles: [ ` .mat-drawer-container { display: flex; flex-direction: column; flex-grow: 1; align-items: center; align-content: center; justify-content: center; } `, ], host: { class: 'mat-drawer-container', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) @Injectable() export class MatDrawerContainer /*extends MatDrawerContainerBase*/ { /** Drawer that belong to this container. */ @ContentChild(MatDrawer) drawer!: MatDrawer; @ContentChild(MatDrawer, {read: ElementRef}) drawerView!: ElementRef; @ContentChild(MatDrawerContent) content!: MatDrawerContent; @ViewChild(MatDrawerContent) userContent!: MatDrawerContent; /** * Margins to be applied to the content. These are used to push / shrink the drawer content when a * drawer is open. We use margin rather than transform even for push mode because transform breaks * fixed position elements inside of the transformed element. */ contentMargins: {top: number | null; bottom: number | null} = { top: null, bottom: null, }; readonly contentMarginChanges = new Subject<{ top: number | null; bottom: number | null; }>(); /** Emits on every ngDoCheck. Used for debouncing reflows. */ private readonly doCheckSubject = new Subject(); /** Emits when the component is destroyed. */ private readonly destroyed = new Subject(); constructor(@Inject(NgZone) private ngZone: NgZone) {} ngAfterContentInit() { this.updateContentMargins(); // Avoid hitting the NgZone through the debounce timeout. this.ngZone.runOutsideAngular(() => { this.doCheckSubject .pipe( debounceTime(10), // Arbitrary debounce time, less than a frame at 60fps takeUntil(this.destroyed), ) .subscribe(() => this.updateContentMargins()); }); } ngOnDestroy() { this.doCheckSubject.complete(); this.destroyed.next(); this.destroyed.complete(); } ngDoCheck() { this.ngZone.runOutsideAngular(() => this.doCheckSubject.next()); } /** * Recalculates and updates the inline styles for the content. Note that this should be used * sparingly, because it causes a reflow. */ updateContentMargins() { // If shift is enabled want to shift the content without resizing it. We do // this by adding to the top or bottom margin and simultaneously subtracting // the same amount of margin from the other side. let top = 0; let bottom = 0; const baseHeight = this.drawer.getBaseHeight(); const height = this.getDrawerHeight(); const shiftAmount = this.drawer.mode === 'push' ? Math.max(0, height - baseHeight) : 0; top -= shiftAmount; bottom += baseHeight + shiftAmount; // If either `top` or `bottom` is zero, don't set a style to the element. This // allows users to specify a custom size via CSS class in SSR scenarios where the // measured widths will always be zero. Note that we reset to `null` here, rather // than below, in order to ensure that the types in the `if` below are consistent. top = top || null!; bottom = bottom || null!; if ( top !== this.contentMargins.top || bottom !== this.contentMargins.bottom ) { this.contentMargins = {top, bottom}; this.content.setMargins(this.contentMargins); // Pull back into the NgZone since in some cases we could be outside. We need to be careful // to do it only when something changed, otherwise we can end up hitting the zone too often. this.ngZone.run(() => this.contentMarginChanges.next(this.contentMargins), ); } } getDrawerHeight(): number { return this.drawerView.nativeElement ? this.drawerView.nativeElement.offsetHeight || 0 : 0; } }