@material/dialog
Version:
The Material Components Web dialog component
383 lines • 17.2 kB
JavaScript
/**
* @license
* Copyright 2017 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { __assign, __extends } from "tslib";
import { AnimationFrame } from '@material/animation/animationframe';
import { MDCFoundation } from '@material/base/foundation';
import { cssClasses, numbers, strings } from './constants';
var AnimationKeys;
(function (AnimationKeys) {
AnimationKeys["POLL_SCROLL_POS"] = "poll_scroll_position";
AnimationKeys["POLL_LAYOUT_CHANGE"] = "poll_layout_change";
})(AnimationKeys || (AnimationKeys = {}));
var MDCDialogFoundation = /** @class */ (function (_super) {
__extends(MDCDialogFoundation, _super);
function MDCDialogFoundation(adapter) {
var _this = _super.call(this, __assign(__assign({}, MDCDialogFoundation.defaultAdapter), adapter)) || this;
_this.dialogOpen = false;
_this.isFullscreen = false;
_this.animationFrame = 0;
_this.animationTimer = 0;
_this.escapeKeyAction = strings.CLOSE_ACTION;
_this.scrimClickAction = strings.CLOSE_ACTION;
_this.autoStackButtons = true;
_this.areButtonsStacked = false;
_this.suppressDefaultPressSelector = strings.SUPPRESS_DEFAULT_PRESS_SELECTOR;
_this.animFrame = new AnimationFrame();
_this.contentScrollHandler = function () {
_this.handleScrollEvent();
};
_this.windowResizeHandler = function () {
_this.layout();
};
_this.windowOrientationChangeHandler = function () {
_this.layout();
};
return _this;
}
Object.defineProperty(MDCDialogFoundation, "cssClasses", {
get: function () {
return cssClasses;
},
enumerable: false,
configurable: true
});
Object.defineProperty(MDCDialogFoundation, "strings", {
get: function () {
return strings;
},
enumerable: false,
configurable: true
});
Object.defineProperty(MDCDialogFoundation, "numbers", {
get: function () {
return numbers;
},
enumerable: false,
configurable: true
});
Object.defineProperty(MDCDialogFoundation, "defaultAdapter", {
get: function () {
return {
addBodyClass: function () { return undefined; },
addClass: function () { return undefined; },
areButtonsStacked: function () { return false; },
clickDefaultButton: function () { return undefined; },
eventTargetMatches: function () { return false; },
getActionFromEvent: function () { return ''; },
getInitialFocusEl: function () { return null; },
hasClass: function () { return false; },
isContentScrollable: function () { return false; },
notifyClosed: function () { return undefined; },
notifyClosing: function () { return undefined; },
notifyOpened: function () { return undefined; },
notifyOpening: function () { return undefined; },
releaseFocus: function () { return undefined; },
removeBodyClass: function () { return undefined; },
removeClass: function () { return undefined; },
reverseButtons: function () { return undefined; },
trapFocus: function () { return undefined; },
registerContentEventHandler: function () { return undefined; },
deregisterContentEventHandler: function () { return undefined; },
isScrollableContentAtTop: function () { return false; },
isScrollableContentAtBottom: function () { return false; },
registerWindowEventHandler: function () { return undefined; },
deregisterWindowEventHandler: function () { return undefined; },
};
},
enumerable: false,
configurable: true
});
MDCDialogFoundation.prototype.init = function () {
if (this.adapter.hasClass(cssClasses.STACKED)) {
this.setAutoStackButtons(false);
}
this.isFullscreen = this.adapter.hasClass(cssClasses.FULLSCREEN);
};
MDCDialogFoundation.prototype.destroy = function () {
if (this.animationTimer) {
clearTimeout(this.animationTimer);
this.handleAnimationTimerEnd();
}
if (this.isFullscreen) {
this.adapter.deregisterContentEventHandler('scroll', this.contentScrollHandler);
}
this.animFrame.cancelAll();
this.adapter.deregisterWindowEventHandler('resize', this.windowResizeHandler);
this.adapter.deregisterWindowEventHandler('orientationchange', this.windowOrientationChangeHandler);
};
MDCDialogFoundation.prototype.open = function (dialogOptions) {
var _this = this;
this.dialogOpen = true;
this.adapter.notifyOpening();
this.adapter.addClass(cssClasses.OPENING);
if (this.isFullscreen) {
// A scroll event listener is registered even if the dialog is not
// scrollable on open, since the window resize event, or orientation
// change may make the dialog scrollable after it is opened.
this.adapter.registerContentEventHandler('scroll', this.contentScrollHandler);
}
if (dialogOptions && dialogOptions.isAboveFullscreenDialog) {
this.adapter.addClass(cssClasses.SCRIM_HIDDEN);
}
this.adapter.registerWindowEventHandler('resize', this.windowResizeHandler);
this.adapter.registerWindowEventHandler('orientationchange', this.windowOrientationChangeHandler);
// Wait a frame once display is no longer "none", to establish basis for
// animation
this.runNextAnimationFrame(function () {
_this.adapter.addClass(cssClasses.OPEN);
_this.adapter.addBodyClass(cssClasses.SCROLL_LOCK);
_this.layout();
_this.animationTimer = setTimeout(function () {
_this.handleAnimationTimerEnd();
_this.adapter.trapFocus(_this.adapter.getInitialFocusEl());
_this.adapter.notifyOpened();
}, numbers.DIALOG_ANIMATION_OPEN_TIME_MS);
});
};
MDCDialogFoundation.prototype.close = function (action) {
var _this = this;
if (action === void 0) { action = ''; }
if (!this.dialogOpen) {
// Avoid redundant close calls (and events), e.g. from keydown on elements
// that inherently emit click
return;
}
this.dialogOpen = false;
this.adapter.notifyClosing(action);
this.adapter.addClass(cssClasses.CLOSING);
this.adapter.removeClass(cssClasses.OPEN);
this.adapter.removeBodyClass(cssClasses.SCROLL_LOCK);
if (this.isFullscreen) {
this.adapter.deregisterContentEventHandler('scroll', this.contentScrollHandler);
}
this.adapter.deregisterWindowEventHandler('resize', this.windowResizeHandler);
this.adapter.deregisterWindowEventHandler('orientationchange', this.windowOrientationChangeHandler);
cancelAnimationFrame(this.animationFrame);
this.animationFrame = 0;
clearTimeout(this.animationTimer);
this.animationTimer = setTimeout(function () {
_this.adapter.releaseFocus();
_this.handleAnimationTimerEnd();
_this.adapter.notifyClosed(action);
}, numbers.DIALOG_ANIMATION_CLOSE_TIME_MS);
};
/**
* Used only in instances of showing a secondary dialog over a full-screen
* dialog. Shows the "surface scrim" displayed over the full-screen dialog.
*/
MDCDialogFoundation.prototype.showSurfaceScrim = function () {
var _this = this;
this.adapter.addClass(cssClasses.SURFACE_SCRIM_SHOWING);
this.runNextAnimationFrame(function () {
_this.adapter.addClass(cssClasses.SURFACE_SCRIM_SHOWN);
});
};
/**
* Used only in instances of showing a secondary dialog over a full-screen
* dialog. Hides the "surface scrim" displayed over the full-screen dialog.
*/
MDCDialogFoundation.prototype.hideSurfaceScrim = function () {
this.adapter.removeClass(cssClasses.SURFACE_SCRIM_SHOWN);
this.adapter.addClass(cssClasses.SURFACE_SCRIM_HIDING);
};
/**
* Handles `transitionend` event triggered when surface scrim animation is
* finished.
*/
MDCDialogFoundation.prototype.handleSurfaceScrimTransitionEnd = function () {
this.adapter.removeClass(cssClasses.SURFACE_SCRIM_HIDING);
this.adapter.removeClass(cssClasses.SURFACE_SCRIM_SHOWING);
};
MDCDialogFoundation.prototype.isOpen = function () {
return this.dialogOpen;
};
MDCDialogFoundation.prototype.getEscapeKeyAction = function () {
return this.escapeKeyAction;
};
MDCDialogFoundation.prototype.setEscapeKeyAction = function (action) {
this.escapeKeyAction = action;
};
MDCDialogFoundation.prototype.getScrimClickAction = function () {
return this.scrimClickAction;
};
MDCDialogFoundation.prototype.setScrimClickAction = function (action) {
this.scrimClickAction = action;
};
MDCDialogFoundation.prototype.getAutoStackButtons = function () {
return this.autoStackButtons;
};
MDCDialogFoundation.prototype.setAutoStackButtons = function (autoStack) {
this.autoStackButtons = autoStack;
};
MDCDialogFoundation.prototype.getSuppressDefaultPressSelector = function () {
return this.suppressDefaultPressSelector;
};
MDCDialogFoundation.prototype.setSuppressDefaultPressSelector = function (selector) {
this.suppressDefaultPressSelector = selector;
};
MDCDialogFoundation.prototype.layout = function () {
var _this = this;
this.animFrame.request(AnimationKeys.POLL_LAYOUT_CHANGE, function () {
_this.layoutInternal();
});
};
/** Handles click on the dialog root element. */
MDCDialogFoundation.prototype.handleClick = function (evt) {
var isScrim = this.adapter.eventTargetMatches(evt.target, strings.SCRIM_SELECTOR);
// Check for scrim click first since it doesn't require querying ancestors.
if (isScrim && this.scrimClickAction !== '') {
this.close(this.scrimClickAction);
}
else {
var action = this.adapter.getActionFromEvent(evt);
if (action) {
this.close(action);
}
}
};
/** Handles keydown on the dialog root element. */
MDCDialogFoundation.prototype.handleKeydown = function (evt) {
var isEnter = evt.key === 'Enter' || evt.keyCode === 13;
if (!isEnter) {
return;
}
var action = this.adapter.getActionFromEvent(evt);
if (action) {
// Action button callback is handled in `handleClick`,
// since space/enter keydowns on buttons trigger click events.
return;
}
// `composedPath` is used here, when available, to account for use cases
// where a target meant to suppress the default press behaviour
// may exist in a shadow root.
// For example, a textarea inside a web component:
// <mwc-dialog>
// <horizontal-layout>
// #shadow-root (open)
// <mwc-textarea>
// #shadow-root (open)
// <textarea></textarea>
// </mwc-textarea>
// </horizontal-layout>
// </mwc-dialog>
var target = evt.composedPath ? evt.composedPath()[0] : evt.target;
var isDefault = this.suppressDefaultPressSelector ?
!this.adapter.eventTargetMatches(target, this.suppressDefaultPressSelector) :
true;
if (isEnter && isDefault) {
this.adapter.clickDefaultButton();
}
};
/** Handles keydown on the document. */
MDCDialogFoundation.prototype.handleDocumentKeydown = function (evt) {
var isEscape = evt.key === 'Escape' || evt.keyCode === 27;
if (isEscape && this.escapeKeyAction !== '') {
this.close(this.escapeKeyAction);
}
};
/**
* Handles scroll event on the dialog's content element -- showing a scroll
* divider on the header or footer based on the scroll position. This handler
* should only be registered on full-screen dialogs with scrollable content.
*/
MDCDialogFoundation.prototype.handleScrollEvent = function () {
var _this = this;
// Since scroll events can fire at a high rate, we throttle these events by
// using requestAnimationFrame.
this.animFrame.request(AnimationKeys.POLL_SCROLL_POS, function () {
_this.toggleScrollDividerHeader();
_this.toggleScrollDividerFooter();
});
};
MDCDialogFoundation.prototype.layoutInternal = function () {
if (this.autoStackButtons) {
this.detectStackedButtons();
}
this.toggleScrollableClasses();
};
MDCDialogFoundation.prototype.handleAnimationTimerEnd = function () {
this.animationTimer = 0;
this.adapter.removeClass(cssClasses.OPENING);
this.adapter.removeClass(cssClasses.CLOSING);
};
/**
* Runs the given logic on the next animation frame, using setTimeout to
* factor in Firefox reflow behavior.
*/
MDCDialogFoundation.prototype.runNextAnimationFrame = function (callback) {
var _this = this;
cancelAnimationFrame(this.animationFrame);
this.animationFrame = requestAnimationFrame(function () {
_this.animationFrame = 0;
clearTimeout(_this.animationTimer);
_this.animationTimer = setTimeout(callback, 0);
});
};
MDCDialogFoundation.prototype.detectStackedButtons = function () {
// Remove the class first to let us measure the buttons' natural positions.
this.adapter.removeClass(cssClasses.STACKED);
var areButtonsStacked = this.adapter.areButtonsStacked();
if (areButtonsStacked) {
this.adapter.addClass(cssClasses.STACKED);
}
if (areButtonsStacked !== this.areButtonsStacked) {
this.adapter.reverseButtons();
this.areButtonsStacked = areButtonsStacked;
}
};
MDCDialogFoundation.prototype.toggleScrollableClasses = function () {
// Remove the class first to let us measure the natural height of the
// content.
this.adapter.removeClass(cssClasses.SCROLLABLE);
if (this.adapter.isContentScrollable()) {
this.adapter.addClass(cssClasses.SCROLLABLE);
if (this.isFullscreen) {
// If dialog is full-screen and scrollable, check if a scroll divider
// should be shown.
this.toggleScrollDividerHeader();
this.toggleScrollDividerFooter();
}
}
};
MDCDialogFoundation.prototype.toggleScrollDividerHeader = function () {
if (!this.adapter.isScrollableContentAtTop()) {
this.adapter.addClass(cssClasses.SCROLL_DIVIDER_HEADER);
}
else if (this.adapter.hasClass(cssClasses.SCROLL_DIVIDER_HEADER)) {
this.adapter.removeClass(cssClasses.SCROLL_DIVIDER_HEADER);
}
};
MDCDialogFoundation.prototype.toggleScrollDividerFooter = function () {
if (!this.adapter.isScrollableContentAtBottom()) {
this.adapter.addClass(cssClasses.SCROLL_DIVIDER_FOOTER);
}
else if (this.adapter.hasClass(cssClasses.SCROLL_DIVIDER_FOOTER)) {
this.adapter.removeClass(cssClasses.SCROLL_DIVIDER_FOOTER);
}
};
return MDCDialogFoundation;
}(MDCFoundation));
export { MDCDialogFoundation };
// tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
export default MDCDialogFoundation;
//# sourceMappingURL=foundation.js.map