1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import { 18 animate, 19 AnimationTriggerMetadata, 20 state, 21 style, 22 transition, 23 trigger, 24} from '@angular/animations'; 25import { 26 ChangeDetectionStrategy, 27 ChangeDetectorRef, 28 Component, 29 ContentChild, 30 ElementRef, 31 forwardRef, 32 Inject, 33 Injectable, 34 Input, 35 NgZone, 36 ViewEncapsulation, 37} from '@angular/core'; 38import {assertDefined} from 'common/assert_utils'; 39import {Subject} from 'rxjs'; 40import {debounceTime, takeUntil} from 'rxjs/operators'; 41 42/** 43 * Animation used by the Material drawers. 44 * @docs-private 45 */ 46const transformDrawer: AnimationTriggerMetadata = 47 /** Animation that slides a drawer in and out. */ 48 trigger('transform', [ 49 // We remove the `transform` here completely, rather than setting it to zero, because: 50 // 1. Having a transform can cause elements with ripples or an animated 51 // transform to shift around in Chrome with an RTL layout (see #10023). 52 // 2. 3d transforms causes text to appear blurry on IE and Edge. 53 state( 54 'open, open-instant', 55 style({ 56 transform: 'none', 57 visibility: 'visible', 58 }), 59 ), 60 state( 61 'void', 62 style({ 63 // Avoids the shadow showing up when closed in SSR. 64 'box-shadow': 'none', 65 visibility: 'hidden', 66 }), 67 ), 68 transition('void => open-instant', animate('0ms')), 69 transition( 70 'void <=> open, open-instant => void', 71 animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)'), 72 ), 73 ]); 74 75/** 76 * This component corresponds to a drawer that can be opened on the drawer container. 77 */ 78@Injectable() 79@Component({ 80 selector: 'mat-drawer', 81 exportAs: 'matDrawer', 82 template: ` 83 <div class="mat-drawer-inner-container" #content> 84 <ng-content></ng-content> 85 </div> 86 `, 87 styles: [ 88 ` 89 .mat-drawer.mat-drawer-bottom { 90 left: 0; 91 right: 0; 92 bottom: 0; 93 top: unset; 94 position: fixed; 95 z-index: 5; 96 background-color: #f8f9fa; 97 box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); 98 } 99 `, 100 ], 101 animations: [transformDrawer], 102 host: { 103 class: 'mat-drawer mat-drawer-bottom', 104 // must prevent the browser from aligning text based on value 105 '[attr.align]': 'null', 106 }, 107 changeDetection: ChangeDetectionStrategy.OnPush, 108 encapsulation: ViewEncapsulation.None, 109}) 110export class MatDrawer { 111 @Input() mode: 'push' | 'overlay' = 'overlay'; 112 @Input() baseHeight = 0; 113 114 getBaseHeight() { 115 return this.baseHeight; 116 } 117} 118 119@Component({ 120 selector: 'mat-drawer-content', 121 template: '<ng-content></ng-content>', 122 styles: [ 123 ` 124 .mat-drawer-content { 125 display: flex; 126 flex-direction: column; 127 position: relative; 128 z-index: 1; 129 height: unset; 130 overflow: unset; 131 width: 100%; 132 flex-grow: 1; 133 } 134 `, 135 ], 136 host: { 137 class: 'mat-drawer-content', 138 '[style.margin-top.px]': 'contentMargins.top', 139 '[style.margin-bottom.px]': 'contentMargins.bottom', 140 }, 141 changeDetection: ChangeDetectionStrategy.OnPush, 142 encapsulation: ViewEncapsulation.None, 143}) 144export class MatDrawerContent /*extends MatDrawerContentBase*/ { 145 contentMargins: Margins = { 146 top: undefined, 147 bottom: undefined, 148 }; 149 150 constructor( 151 @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, 152 @Inject(forwardRef(() => MatDrawerContainer)) 153 public container: MatDrawerContainer, 154 ) {} 155 156 ngAfterContentInit() { 157 this.container.contentMarginChanges.subscribe(() => { 158 this.changeDetectorRef.markForCheck(); 159 }); 160 } 161 162 setMargins(margins: Margins) { 163 this.contentMargins = margins; 164 } 165} 166 167/** 168 * Container for Material drawers 169 * @docs-private 170 */ 171@Component({ 172 selector: 'mat-drawer-container', 173 exportAs: 'matDrawerContainer', 174 template: ` 175 <ng-content select="mat-drawer-content"> </ng-content> 176 177 <ng-content select="mat-drawer"></ng-content> 178 `, 179 styles: [ 180 ` 181 .mat-drawer-container { 182 display: flex; 183 flex-direction: column; 184 flex-grow: 1; 185 align-items: center; 186 align-content: center; 187 justify-content: center; 188 } 189 `, 190 ], 191 host: { 192 class: 'mat-drawer-container', 193 }, 194 changeDetection: ChangeDetectionStrategy.OnPush, 195 encapsulation: ViewEncapsulation.None, 196}) 197@Injectable() 198export class MatDrawerContainer /*extends MatDrawerContainerBase*/ { 199 /** Drawer that belong to this container. */ 200 @ContentChild(MatDrawer) drawer: MatDrawer | undefined; 201 @ContentChild(MatDrawer, {read: ElementRef}) drawerView: 202 | ElementRef 203 | undefined; 204 205 @ContentChild(MatDrawerContent) content: MatDrawerContent | undefined; 206 207 readonly contentMarginChanges = new Subject<Margins>(); 208 209 /** 210 * Margins to be applied to the content. These are used to push / shrink the drawer content when a 211 * drawer is open. We use margin rather than transform even for push mode because transform breaks 212 * fixed position elements inside of the transformed element. 213 */ 214 private contentMargins: Margins = { 215 top: undefined, 216 bottom: undefined, 217 }; 218 219 /** Emits on every ngDoCheck. Used for debouncing reflows. */ 220 private readonly doCheckSubject = new Subject<void>(); 221 222 /** Emits when the component is destroyed. */ 223 private readonly destroyed = new Subject<void>(); 224 225 constructor(@Inject(NgZone) private ngZone: NgZone) {} 226 227 ngAfterContentInit() { 228 this.updateContentMargins(); 229 // Avoid hitting the NgZone through the debounce timeout. 230 this.ngZone.runOutsideAngular(() => { 231 this.doCheckSubject 232 .pipe( 233 debounceTime(10), // Arbitrary debounce time, less than a frame at 60fps 234 takeUntil(this.destroyed), 235 ) 236 .subscribe(() => this.updateContentMargins()); 237 }); 238 } 239 240 ngOnDestroy() { 241 this.doCheckSubject.complete(); 242 this.destroyed.next(); 243 this.destroyed.complete(); 244 } 245 246 ngDoCheck() { 247 this.ngZone.runOutsideAngular(() => this.doCheckSubject.next()); 248 } 249 250 /** 251 * Recalculates and updates the inline styles for the content. Note that this should be used 252 * sparingly, because it causes a reflow. 253 */ 254 updateContentMargins() { 255 // If shift is enabled want to shift the content without resizing it. We do 256 // this by adding to the top or bottom margin and simultaneously subtracting 257 // the same amount of margin from the other side. 258 let top: number | undefined = 0; 259 let bottom: number | undefined = 0; 260 261 const drawer = assertDefined(this.drawer); 262 const baseHeight = drawer.getBaseHeight(); 263 const height = this.getDrawerHeight(); 264 const shiftAmount = 265 drawer.mode === 'push' ? Math.max(0, height - baseHeight) : 0; 266 267 top -= shiftAmount; 268 bottom += baseHeight + shiftAmount; 269 270 // If either `top` or `bottom` is zero, don't set a style to the element. This 271 // allows users to specify a custom size via CSS class in SSR scenarios where the 272 // measured widths will always be zero. Note that we reset to `null` here, rather 273 // than below, in order to ensure that the types in the `if` below are consistent. 274 top = top || undefined; 275 bottom = bottom || undefined; 276 277 // Pull back into the NgZone since in some cases we could be outside. We need to be careful 278 // to do it only when something changed, otherwise we can end up hitting the zone too often. 279 this.ngZone.run(() => { 280 if ( 281 top !== this.contentMargins.top || 282 bottom !== this.contentMargins.bottom 283 ) { 284 this.contentMargins = {top, bottom}; 285 assertDefined(this.content).setMargins({top, bottom}); 286 this.contentMarginChanges.next({top, bottom}); 287 } 288 }); 289 } 290 291 getDrawerHeight(): number { 292 return this.drawerView?.nativeElement 293 ? this.drawerView.nativeElement.offsetHeight || 0 294 : 0; 295 } 296} 297 298interface Margins { 299 top: number | undefined; 300 bottom: number | undefined; 301} 302