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