ngx-mat-slide-panel
Version:
469 lines (460 loc) • 24.8 kB
JavaScript
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