• 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  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