UNPKG

ngx-mat-slide-panel

Version:
469 lines (460 loc) 24.8 kB
import * as i1$1 from '@angular/cdk/overlay'; import { OverlayModule, OverlayConfig } from '@angular/cdk/overlay'; import * as i3 from '@angular/cdk/portal'; import { CdkPortalOutlet, PortalModule, TemplatePortal, ComponentPortal } from '@angular/cdk/portal'; import * as i0 from '@angular/core'; import { InjectionToken, EventEmitter, DOCUMENT, ViewChild, Optional, Inject, ViewEncapsulation, ChangeDetectionStrategy, Component, NgModule, TemplateRef, Injector, SkipSelf, Injectable } from '@angular/core'; import { trigger, state, transition, style, animate } from '@angular/animations'; import * as i1 from '@angular/cdk/a11y'; import { hasModifierKey } from '@angular/cdk/keycodes'; import { Subject, merge } from 'rxjs'; import { filter, take } from 'rxjs/operators'; import * as i2 from '@angular/common'; const MAT_SLIDE_PANEL_DATA = new InjectionToken('MatSlidePanelData'); class MatSlidePanelConfig { constructor() { /** Data being injected into the child component. */ this.data = null; /** Whether the mat slide panel has a backdrop. */ this.hasBackdrop = true; /** Whether the user can use escape or clicking outside to close the mat slide panel. */ this.disableClose = false; /** Aria label to assign to the mat slide panel element. */ this.ariaLabel = null; /** * Whether the mat slide panel should close when the user goes backwards/forwards in history. * Note that this usually doesn't include clicking on links (unless the user is using * the `HashLocationStrategy`). */ this.closeOnNavigation = true; // Note that this is disabled by default, because while the a11y recommendations are to focus // the first focusable element, doing so prevents screen readers from reading out the // rest of the mat slide panel content. /** Whether the mat slide panel should focus the first focusable element on open. */ this.autoFocus = false; /** * Whether the mat slide panel should restore focus to the * previously-focused element, after it's closed. */ this.restoreFocus = true; /** Slide from which side of viewport. */ this.slideFrom = 'right'; } } // Animation constants (previously from @angular/material/core) const ANIMATION_DURATION_COMPLEX = '375ms'; const ANIMATION_DURATION_EXITING = '195ms'; const ANIMATION_CURVE_ACCELERATION = 'cubic-bezier(0.4, 0.0, 1, 1)'; const ANIMATION_CURVE_DECELERATION = 'cubic-bezier(0.0, 0.0, 0.2, 1)'; /** Animations used by the Material bottom sheet. */ const slideFromLeftAnimations = { /** Animation that shows and hides a bottom sheet. */ slideFromLeftAnimationsState: trigger('left', [ state('void, hidden', style({ transform: 'translateX(-100%)' })), state('visible', style({ transform: 'translateY(0%)' })), transition('visible => void, visible => hidden', animate(`${ANIMATION_DURATION_COMPLEX} ${ANIMATION_CURVE_ACCELERATION}`)), transition('void => visible', animate(`${ANIMATION_DURATION_EXITING} ${ANIMATION_CURVE_DECELERATION}`)), ]) }; /** Animations used by the Material bottom sheet. */ const slideFromRightAnimations = { /** Animation that shows and hides a bottom sheet. */ slideFromLeftAnimationsState: trigger('right', [ state('void, hidden', style({ transform: 'translateX(100%)' })), state('visible', style({ transform: 'translateY(0%)' })), transition('visible => void, visible => hidden', animate(`${ANIMATION_DURATION_COMPLEX} ${ANIMATION_CURVE_ACCELERATION}`)), transition('void => visible', animate(`${ANIMATION_DURATION_EXITING} ${ANIMATION_CURVE_DECELERATION}`)), ]) }; class MatSlidePanelContainer { constructor(_elementRef, _changeDetectorRef, _focusTrapFactory, document, matSlidePanelConfig) { this._elementRef = _elementRef; this._changeDetectorRef = _changeDetectorRef; this._focusTrapFactory = _focusTrapFactory; this.matSlidePanelConfig = matSlidePanelConfig; this._animationState = 'void'; /** Emits whenever the state of the animation changes. */ this._animationStateChanged = new EventEmitter(); this._elementFocusedBeforeOpened = null; this._document = document; // this._animationState = this.matSlidePanelConfig.slideFrom; } /** Attach a component portal as content to this bottom sheet container. */ attachComponentPortal(portal) { this._validatePortalAttached(); this._setPanelClass(); this._savePreviouslyFocusedElement(); return this._portalOutlet.attachComponentPortal(portal); } /** Attach a template portal as content to this bottom sheet container. */ attachTemplatePortal(portal) { this._validatePortalAttached(); this._setPanelClass(); this._savePreviouslyFocusedElement(); return this._portalOutlet.attachTemplatePortal(portal); } /** Attach a portal to this outlet. */ attach(portal) { this._validatePortalAttached(); this._setPanelClass(); this._savePreviouslyFocusedElement(); return this._portalOutlet.attach(portal); } /** Detach the currently attached portal from this outlet. */ detach() { if (this._portalOutlet) { this._portalOutlet.detach(); } } /** Performs cleanup before the outlet is destroyed. */ dispose() { if (this._portalOutlet) { this._portalOutlet.dispose(); } } /** Whether this outlet has an attached portal. */ hasAttached() { return this._portalOutlet.hasAttached(); } enter() { if (!this._destroyed) { this._animationState = 'visible'; this._changeDetectorRef.detectChanges(); } } exit() { if (!this._destroyed) { // this._animationState = this.matSlidePanelConfig.slideFrom; this._animationState = 'hidden'; this._changeDetectorRef.markForCheck(); } } ngOnDestroy() { this._destroyed = true; } _onAnimationDone(event) { // if (event.toState === `hidden-${this.matSlidePanelConfig.slideFrom}`) { if (event.toState === 'hidden') { this._restoreFocus(); } else if (event.toState === 'visible') { this._trapFocus(); } this._animationStateChanged.emit(event); } _onAnimationStart(event) { this._animationStateChanged.emit(event); } _validatePortalAttached() { if (this._portalOutlet.hasAttached()) { throw Error('Attempting to attach slide panel content after content is already attached'); } } _setPanelClass() { const element = this._elementRef.nativeElement; const panelClass = this.matSlidePanelConfig.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. */ _trapFocus() { const element = this._elementRef.nativeElement; if (!this._focusTrap) { this._focusTrap = this._focusTrapFactory.create(element); } if (this.matSlidePanelConfig.autoFocus) { this._focusTrap.focusInitialElementWhenReady(); } else { const activeElement = this._document.activeElement; if (activeElement !== element && !element.contains(activeElement)) { element.focus(); } } } _restoreFocus() { const toFocus = this._elementFocusedBeforeOpened; // We need the extra check, because IE can set the `activeElement` to null in some cases. if (this.matSlidePanelConfig.restoreFocus && toFocus && typeof toFocus.focus === 'function') { const activeElement = this._document.activeElement; const element = this._elementRef.nativeElement; if (!activeElement || activeElement === this._document.body || activeElement === element || element.contains(activeElement)) { toFocus.focus(); } } if (this._focusTrap) { this._focusTrap.destroy(); } } _savePreviouslyFocusedElement() { this._elementFocusedBeforeOpened = this._document.activeElement; // The `focus` method isn't available during server-side rendering. if (this._elementRef.nativeElement.focus) { Promise.resolve().then(() => this._elementRef.nativeElement.focus()); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanelContainer, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i1.FocusTrapFactory }, { token: DOCUMENT, optional: true }, { token: MatSlidePanelConfig }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: MatSlidePanelContainer, isStandalone: false, selector: "mat-slide-panel-container", host: { attributes: { "tabindex": "-1", "role": "dialog", "aria-modal": "true" }, listeners: { "@right.start": "_onAnimationStart($event)", "@right.done": "_onAnimationDone($event)", "@left.start": "_onAnimationStart($event)", "@left.done": "_onAnimationDone($event)" }, properties: { "attr.aria-label": "matSlidePanelConfig?.ariaLabel", "@right": "{value: matSlidePanelConfig.slideFrom === \"right\" ? _animationState : null}", "@left": "{value: matSlidePanelConfig.slideFrom === \"left\" ? _animationState : null}" }, classAttribute: "mat-slide-panel-container" }, viewQueries: [{ propertyName: "_portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], ngImport: i0, template: "<ng-template cdkPortalOutlet></ng-template>\n", styles: [".mat-slide-panel-container{padding:16px;box-sizing:border-box;display:block;outline:0;height:100vh;overflow:auto;background:var(--mat-sys-surface, --mat-sys-surface-container);color:var(--mat-sys-on-surface, --mat-sys-surface-container)}@media(forced-colors:active){.mat-slide-panel-container{outline:1px solid}}.mat-slide-panel-container-xlarge,.mat-slide-panel-container-large,.mat-slide-panel-container-medium{border-top-left-radius:4px;border-top-right-radius:4px}.mat-slide-panel-container-medium{min-width:384px;max-width:calc(100vw - 128px)}.mat-slide-panel-container-large{min-width:512px;max-width:calc(100vw - 256px)}.mat-slide-panel-container-xlarge{min-width:576px;max-width:calc(100vw - 384px)}\n"], dependencies: [{ kind: "directive", type: i3.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], animations: [slideFromLeftAnimations.slideFromLeftAnimationsState, slideFromRightAnimations.slideFromLeftAnimationsState], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanelContainer, decorators: [{ type: Component, args: [{ selector: 'mat-slide-panel-container', changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None, animations: [slideFromLeftAnimations.slideFromLeftAnimationsState, slideFromRightAnimations.slideFromLeftAnimationsState], host: { 'class': 'mat-slide-panel-container', 'tabindex': '-1', 'role': 'dialog', 'aria-modal': 'true', '[attr.aria-label]': 'matSlidePanelConfig?.ariaLabel', '[@right]': '{value: matSlidePanelConfig.slideFrom === "right" ? _animationState : null}', '(@right.start)': '_onAnimationStart($event)', '(@right.done)': '_onAnimationDone($event)', '[@left]': '{value: matSlidePanelConfig.slideFrom === "left" ? _animationState : null}', '(@left.start)': '_onAnimationStart($event)', '(@left.done)': '_onAnimationDone($event)', }, standalone: false, template: "<ng-template cdkPortalOutlet></ng-template>\n", styles: [".mat-slide-panel-container{padding:16px;box-sizing:border-box;display:block;outline:0;height:100vh;overflow:auto;background:var(--mat-sys-surface, --mat-sys-surface-container);color:var(--mat-sys-on-surface, --mat-sys-surface-container)}@media(forced-colors:active){.mat-slide-panel-container{outline:1px solid}}.mat-slide-panel-container-xlarge,.mat-slide-panel-container-large,.mat-slide-panel-container-medium{border-top-left-radius:4px;border-top-right-radius:4px}.mat-slide-panel-container-medium{min-width:384px;max-width:calc(100vw - 128px)}.mat-slide-panel-container-large{min-width:512px;max-width:calc(100vw - 256px)}.mat-slide-panel-container-xlarge{min-width:576px;max-width:calc(100vw - 384px)}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i1.FocusTrapFactory }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DOCUMENT] }] }, { type: MatSlidePanelConfig }], propDecorators: { _portalOutlet: [{ type: ViewChild, args: [CdkPortalOutlet, { static: true }] }] } }); class MatSlidePanelModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanelModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanelModule, declarations: [MatSlidePanelContainer], imports: [OverlayModule, PortalModule], exports: [MatSlidePanelContainer] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanelModule, imports: [OverlayModule, PortalModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanelModule, decorators: [{ type: NgModule, args: [{ declarations: [MatSlidePanelContainer], imports: [ OverlayModule, PortalModule ], exports: [MatSlidePanelContainer] }] }] }); class MatSlidePanelRef { constructor(containerInstance, _overlayRef, _location) { this._overlayRef = _overlayRef; this._afterDismissed = new Subject(); this._afterOpened = new Subject(); this.containerInstance = containerInstance; this.disableClose = containerInstance.matSlidePanelConfig.disableClose; containerInstance._animationStateChanged.pipe(filter(event => event.phaseName === 'done' && event.toState === 'visible'), take(1)) .subscribe(() => { this._afterOpened.next(); this._afterOpened.complete(); }); containerInstance._animationStateChanged .pipe(filter(event => event.phaseName === 'done' && event.toState === 'hidden'), take(1)) .subscribe(() => { clearTimeout(this._closeFallbackTimeout); _overlayRef.dispose(); }); _overlayRef.detachments().pipe(take(1)).subscribe(() => { this._afterDismissed.next(this._result); this._afterDismissed.complete(); }); merge(_overlayRef.backdropClick(), _overlayRef.keydownEvents().pipe(filter(event => event.key === 'Escape'))).subscribe(event => { if (!this.disableClose && (event.type !== 'keydown' || !hasModifierKey(event))) { event.preventDefault(); this.dismiss(); } }); } dismiss(result) { if (!this._afterDismissed.closed) { this.containerInstance._animationStateChanged.pipe(filter(event => event.phaseName === 'start'), take(1)).subscribe(event => { this._closeFallbackTimeout = setTimeout(() => { this._overlayRef.dispose(); }, event.totalTime + 100); this._overlayRef.detachBackdrop(); }); this._result = result; this.containerInstance.exit(); } } afterDismissed() { return this._afterDismissed.asObservable(); } afterOpened() { return this._afterOpened.asObservable(); } backdropClick() { return this._overlayRef.backdropClick(); } keydownEvents() { return this._overlayRef.keydownEvents(); } } const MAT_SLIDE_PANEL_DEFAULT_OPTIONS = new InjectionToken('mat-Slide-panel-default-options'); class MatSlidePanel { /** Reference to the currently opened mat slide panel. */ get _openedMatSlidePanelRef() { const parent = this._parentMatSlidePanel; return parent ? parent._openedMatSlidePanelRef : this._matSlidePanelAtThisLevel; } set _openedMatSlidePanelRef(value) { if (this._parentMatSlidePanel) { this._parentMatSlidePanel._openedMatSlidePanelRef = value; } else { this._matSlidePanelAtThisLevel = value; } } constructor(_overlay, _injector, _parentMatSlidePanel, _location, _defaultOptions) { this._overlay = _overlay; this._injector = _injector; this._parentMatSlidePanel = _parentMatSlidePanel; this._location = _location; this._defaultOptions = _defaultOptions; this._matSlidePanelAtThisLevel = null; } open(componentOrTemplateRef, config) { const _config = _applyConfigDefaults(this._defaultOptions || new MatSlidePanelConfig(), config); const overlayRef = this._createOverlay(_config); const container = this._attachContainer(overlayRef, _config); const ref = new MatSlidePanelRef(container, overlayRef, this._location); if (componentOrTemplateRef instanceof TemplateRef) { container.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null, { $implicit: _config.data, bottomSheetRef: ref })); } else { const portal = new ComponentPortal(componentOrTemplateRef, undefined, this._createInjector(_config, ref)); const contentRef = container.attachComponentPortal(portal); ref.instance = contentRef.instance; } // When the mat slide panel is dismissed, clear the reference to it. ref.afterDismissed().subscribe(() => { // Clear the mat slide panel ref if it hasn't already been replaced by a newer one. if (this._openedMatSlidePanelRef == ref) { this._openedMatSlidePanelRef = null; } }); if (this._openedMatSlidePanelRef) { // If a mat slide panel is already in view, dismiss it and enter the // new mat slide panel after exit animation is complete. this._openedMatSlidePanelRef.afterDismissed().subscribe(() => ref.containerInstance.enter()); this._openedMatSlidePanelRef.dismiss(); } else { // If no mat slide panel is in view, enter the new mat slide panel. ref.containerInstance.enter(); } this._openedMatSlidePanelRef = ref; return ref; } /** * Dismisses the currently-visible mat slide panel. */ dismiss() { if (this._openedMatSlidePanelRef) { this._openedMatSlidePanelRef.dismiss(); } } ngOnDestroy() { if (this._matSlidePanelAtThisLevel) { this._matSlidePanelAtThisLevel.dismiss(); } } /** * Attaches the mat slide panel container component to the overlay. */ _attachContainer(overlayRef, config) { const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; const injector = Injector.create({ parent: userInjector || this._injector, providers: [ { provide: MatSlidePanelConfig, useValue: config } ] }); const containerPortal = new ComponentPortal(MatSlidePanelContainer, config.viewContainerRef, injector); const containerRef = overlayRef.attach(containerPortal); return containerRef.instance; } /** * Creates a new overlay and places it in the correct location. * @param config The user-specified mat slide panel config. */ _createOverlay(config) { const overlayConfig = new OverlayConfig({ direction: config.direction, hasBackdrop: config.hasBackdrop, disposeOnNavigation: config.closeOnNavigation, maxWidth: '100%', scrollStrategy: config.scrollStrategy || this._overlay.scrollStrategies.block(), positionStrategy: config.slideFrom === 'right' ? this._overlay.position().global().centerHorizontally().right('0') : this._overlay.position().global().centerHorizontally().left('0') }); if (config.backdropClass) { overlayConfig.backdropClass = config.backdropClass; } return this._overlay.create(overlayConfig); } /** * Creates an injector to be used inside of a mat slide panel component. * @param config Config that was used to create the mat slide panel. * @param bottomSheetRef Reference to the mat slide panel. */ _createInjector(config, bottomSheetRef) { const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; const providers = [ { provide: MatSlidePanelRef, useValue: bottomSheetRef }, { provide: MAT_SLIDE_PANEL_DATA, useValue: config.data } ]; // If a direction is set in the config, add it to the overlay config if (config.direction) { // No need to provide Directionality directly, as it's already handled by the overlay } return Injector.create({ parent: userInjector || this._injector, providers }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanel, deps: [{ token: i1$1.Overlay }, { token: i0.Injector }, { token: MatSlidePanel, optional: true, skipSelf: true }, { token: i2.Location, optional: true }, { token: MAT_SLIDE_PANEL_DEFAULT_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanel, providedIn: MatSlidePanelModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MatSlidePanel, decorators: [{ type: Injectable, args: [{ providedIn: MatSlidePanelModule }] }], ctorParameters: () => [{ type: i1$1.Overlay }, { type: i0.Injector }, { type: MatSlidePanel, decorators: [{ type: Optional }, { type: SkipSelf }] }, { type: i2.Location, decorators: [{ type: Optional }] }, { type: MatSlidePanelConfig, decorators: [{ type: Optional }, { type: Inject, args: [MAT_SLIDE_PANEL_DEFAULT_OPTIONS] }] }] }); /** * Applies default options to the mat slide panel config. * @param defaults Object containing the default values to which to fall back. * @param config The configuration to which the defaults will be applied. * @returns The new configuration object with defaults applied. */ function _applyConfigDefaults(defaults, config) { return { ...defaults, ...config }; } /** * Generated bundle index. Do not edit. */ export { MAT_SLIDE_PANEL_DATA, MAT_SLIDE_PANEL_DEFAULT_OPTIONS, MatSlidePanel, MatSlidePanelConfig, MatSlidePanelContainer, MatSlidePanelModule, MatSlidePanelRef, slideFromLeftAnimations, slideFromRightAnimations }; //# sourceMappingURL=ngx-mat-slide-panel.mjs.map