UNPKG

17.2 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2017 Google Inc.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 * THE SOFTWARE.
22 */
23import { __assign, __extends } from "tslib";
24import { AnimationFrame } from '@material/animation/animationframe';
25import { MDCFoundation } from '@material/base/foundation';
26import { cssClasses, numbers, strings } from './constants';
27var AnimationKeys;
28(function (AnimationKeys) {
29 AnimationKeys["POLL_SCROLL_POS"] = "poll_scroll_position";
30 AnimationKeys["POLL_LAYOUT_CHANGE"] = "poll_layout_change";
31})(AnimationKeys || (AnimationKeys = {}));
32var MDCDialogFoundation = /** @class */ (function (_super) {
33 __extends(MDCDialogFoundation, _super);
34 function MDCDialogFoundation(adapter) {
35 var _this = _super.call(this, __assign(__assign({}, MDCDialogFoundation.defaultAdapter), adapter)) || this;
36 _this.dialogOpen = false;
37 _this.isFullscreen = false;
38 _this.animationFrame = 0;
39 _this.animationTimer = 0;
40 _this.escapeKeyAction = strings.CLOSE_ACTION;
41 _this.scrimClickAction = strings.CLOSE_ACTION;
42 _this.autoStackButtons = true;
43 _this.areButtonsStacked = false;
44 _this.suppressDefaultPressSelector = strings.SUPPRESS_DEFAULT_PRESS_SELECTOR;
45 _this.animFrame = new AnimationFrame();
46 _this.contentScrollHandler = function () {
47 _this.handleScrollEvent();
48 };
49 _this.windowResizeHandler = function () {
50 _this.layout();
51 };
52 _this.windowOrientationChangeHandler = function () {
53 _this.layout();
54 };
55 return _this;
56 }
57 Object.defineProperty(MDCDialogFoundation, "cssClasses", {
58 get: function () {
59 return cssClasses;
60 },
61 enumerable: false,
62 configurable: true
63 });
64 Object.defineProperty(MDCDialogFoundation, "strings", {
65 get: function () {
66 return strings;
67 },
68 enumerable: false,
69 configurable: true
70 });
71 Object.defineProperty(MDCDialogFoundation, "numbers", {
72 get: function () {
73 return numbers;
74 },
75 enumerable: false,
76 configurable: true
77 });
78 Object.defineProperty(MDCDialogFoundation, "defaultAdapter", {
79 get: function () {
80 return {
81 addBodyClass: function () { return undefined; },
82 addClass: function () { return undefined; },
83 areButtonsStacked: function () { return false; },
84 clickDefaultButton: function () { return undefined; },
85 eventTargetMatches: function () { return false; },
86 getActionFromEvent: function () { return ''; },
87 getInitialFocusEl: function () { return null; },
88 hasClass: function () { return false; },
89 isContentScrollable: function () { return false; },
90 notifyClosed: function () { return undefined; },
91 notifyClosing: function () { return undefined; },
92 notifyOpened: function () { return undefined; },
93 notifyOpening: function () { return undefined; },
94 releaseFocus: function () { return undefined; },
95 removeBodyClass: function () { return undefined; },
96 removeClass: function () { return undefined; },
97 reverseButtons: function () { return undefined; },
98 trapFocus: function () { return undefined; },
99 registerContentEventHandler: function () { return undefined; },
100 deregisterContentEventHandler: function () { return undefined; },
101 isScrollableContentAtTop: function () { return false; },
102 isScrollableContentAtBottom: function () { return false; },
103 registerWindowEventHandler: function () { return undefined; },
104 deregisterWindowEventHandler: function () { return undefined; },
105 };
106 },
107 enumerable: false,
108 configurable: true
109 });
110 MDCDialogFoundation.prototype.init = function () {
111 if (this.adapter.hasClass(cssClasses.STACKED)) {
112 this.setAutoStackButtons(false);
113 }
114 this.isFullscreen = this.adapter.hasClass(cssClasses.FULLSCREEN);
115 };
116 MDCDialogFoundation.prototype.destroy = function () {
117 if (this.animationTimer) {
118 clearTimeout(this.animationTimer);
119 this.handleAnimationTimerEnd();
120 }
121 if (this.isFullscreen) {
122 this.adapter.deregisterContentEventHandler('scroll', this.contentScrollHandler);
123 }
124 this.animFrame.cancelAll();
125 this.adapter.deregisterWindowEventHandler('resize', this.windowResizeHandler);
126 this.adapter.deregisterWindowEventHandler('orientationchange', this.windowOrientationChangeHandler);
127 };
128 MDCDialogFoundation.prototype.open = function (dialogOptions) {
129 var _this = this;
130 this.dialogOpen = true;
131 this.adapter.notifyOpening();
132 this.adapter.addClass(cssClasses.OPENING);
133 if (this.isFullscreen) {
134 // A scroll event listener is registered even if the dialog is not
135 // scrollable on open, since the window resize event, or orientation
136 // change may make the dialog scrollable after it is opened.
137 this.adapter.registerContentEventHandler('scroll', this.contentScrollHandler);
138 }
139 if (dialogOptions && dialogOptions.isAboveFullscreenDialog) {
140 this.adapter.addClass(cssClasses.SCRIM_HIDDEN);
141 }
142 this.adapter.registerWindowEventHandler('resize', this.windowResizeHandler);
143 this.adapter.registerWindowEventHandler('orientationchange', this.windowOrientationChangeHandler);
144 // Wait a frame once display is no longer "none", to establish basis for
145 // animation
146 this.runNextAnimationFrame(function () {
147 _this.adapter.addClass(cssClasses.OPEN);
148 _this.adapter.addBodyClass(cssClasses.SCROLL_LOCK);
149 _this.layout();
150 _this.animationTimer = setTimeout(function () {
151 _this.handleAnimationTimerEnd();
152 _this.adapter.trapFocus(_this.adapter.getInitialFocusEl());
153 _this.adapter.notifyOpened();
154 }, numbers.DIALOG_ANIMATION_OPEN_TIME_MS);
155 });
156 };
157 MDCDialogFoundation.prototype.close = function (action) {
158 var _this = this;
159 if (action === void 0) { action = ''; }
160 if (!this.dialogOpen) {
161 // Avoid redundant close calls (and events), e.g. from keydown on elements
162 // that inherently emit click
163 return;
164 }
165 this.dialogOpen = false;
166 this.adapter.notifyClosing(action);
167 this.adapter.addClass(cssClasses.CLOSING);
168 this.adapter.removeClass(cssClasses.OPEN);
169 this.adapter.removeBodyClass(cssClasses.SCROLL_LOCK);
170 if (this.isFullscreen) {
171 this.adapter.deregisterContentEventHandler('scroll', this.contentScrollHandler);
172 }
173 this.adapter.deregisterWindowEventHandler('resize', this.windowResizeHandler);
174 this.adapter.deregisterWindowEventHandler('orientationchange', this.windowOrientationChangeHandler);
175 cancelAnimationFrame(this.animationFrame);
176 this.animationFrame = 0;
177 clearTimeout(this.animationTimer);
178 this.animationTimer = setTimeout(function () {
179 _this.adapter.releaseFocus();
180 _this.handleAnimationTimerEnd();
181 _this.adapter.notifyClosed(action);
182 }, numbers.DIALOG_ANIMATION_CLOSE_TIME_MS);
183 };
184 /**
185 * Used only in instances of showing a secondary dialog over a full-screen
186 * dialog. Shows the "surface scrim" displayed over the full-screen dialog.
187 */
188 MDCDialogFoundation.prototype.showSurfaceScrim = function () {
189 var _this = this;
190 this.adapter.addClass(cssClasses.SURFACE_SCRIM_SHOWING);
191 this.runNextAnimationFrame(function () {
192 _this.adapter.addClass(cssClasses.SURFACE_SCRIM_SHOWN);
193 });
194 };
195 /**
196 * Used only in instances of showing a secondary dialog over a full-screen
197 * dialog. Hides the "surface scrim" displayed over the full-screen dialog.
198 */
199 MDCDialogFoundation.prototype.hideSurfaceScrim = function () {
200 this.adapter.removeClass(cssClasses.SURFACE_SCRIM_SHOWN);
201 this.adapter.addClass(cssClasses.SURFACE_SCRIM_HIDING);
202 };
203 /**
204 * Handles `transitionend` event triggered when surface scrim animation is
205 * finished.
206 */
207 MDCDialogFoundation.prototype.handleSurfaceScrimTransitionEnd = function () {
208 this.adapter.removeClass(cssClasses.SURFACE_SCRIM_HIDING);
209 this.adapter.removeClass(cssClasses.SURFACE_SCRIM_SHOWING);
210 };
211 MDCDialogFoundation.prototype.isOpen = function () {
212 return this.dialogOpen;
213 };
214 MDCDialogFoundation.prototype.getEscapeKeyAction = function () {
215 return this.escapeKeyAction;
216 };
217 MDCDialogFoundation.prototype.setEscapeKeyAction = function (action) {
218 this.escapeKeyAction = action;
219 };
220 MDCDialogFoundation.prototype.getScrimClickAction = function () {
221 return this.scrimClickAction;
222 };
223 MDCDialogFoundation.prototype.setScrimClickAction = function (action) {
224 this.scrimClickAction = action;
225 };
226 MDCDialogFoundation.prototype.getAutoStackButtons = function () {
227 return this.autoStackButtons;
228 };
229 MDCDialogFoundation.prototype.setAutoStackButtons = function (autoStack) {
230 this.autoStackButtons = autoStack;
231 };
232 MDCDialogFoundation.prototype.getSuppressDefaultPressSelector = function () {
233 return this.suppressDefaultPressSelector;
234 };
235 MDCDialogFoundation.prototype.setSuppressDefaultPressSelector = function (selector) {
236 this.suppressDefaultPressSelector = selector;
237 };
238 MDCDialogFoundation.prototype.layout = function () {
239 var _this = this;
240 this.animFrame.request(AnimationKeys.POLL_LAYOUT_CHANGE, function () {
241 _this.layoutInternal();
242 });
243 };
244 /** Handles click on the dialog root element. */
245 MDCDialogFoundation.prototype.handleClick = function (evt) {
246 var isScrim = this.adapter.eventTargetMatches(evt.target, strings.SCRIM_SELECTOR);
247 // Check for scrim click first since it doesn't require querying ancestors.
248 if (isScrim && this.scrimClickAction !== '') {
249 this.close(this.scrimClickAction);
250 }
251 else {
252 var action = this.adapter.getActionFromEvent(evt);
253 if (action) {
254 this.close(action);
255 }
256 }
257 };
258 /** Handles keydown on the dialog root element. */
259 MDCDialogFoundation.prototype.handleKeydown = function (evt) {
260 var isEnter = evt.key === 'Enter' || evt.keyCode === 13;
261 if (!isEnter) {
262 return;
263 }
264 var action = this.adapter.getActionFromEvent(evt);
265 if (action) {
266 // Action button callback is handled in `handleClick`,
267 // since space/enter keydowns on buttons trigger click events.
268 return;
269 }
270 // `composedPath` is used here, when available, to account for use cases
271 // where a target meant to suppress the default press behaviour
272 // may exist in a shadow root.
273 // For example, a textarea inside a web component:
274 // <mwc-dialog>
275 // <horizontal-layout>
276 // #shadow-root (open)
277 // <mwc-textarea>
278 // #shadow-root (open)
279 // <textarea></textarea>
280 // </mwc-textarea>
281 // </horizontal-layout>
282 // </mwc-dialog>
283 var target = evt.composedPath ? evt.composedPath()[0] : evt.target;
284 var isDefault = this.suppressDefaultPressSelector ?
285 !this.adapter.eventTargetMatches(target, this.suppressDefaultPressSelector) :
286 true;
287 if (isEnter && isDefault) {
288 this.adapter.clickDefaultButton();
289 }
290 };
291 /** Handles keydown on the document. */
292 MDCDialogFoundation.prototype.handleDocumentKeydown = function (evt) {
293 var isEscape = evt.key === 'Escape' || evt.keyCode === 27;
294 if (isEscape && this.escapeKeyAction !== '') {
295 this.close(this.escapeKeyAction);
296 }
297 };
298 /**
299 * Handles scroll event on the dialog's content element -- showing a scroll
300 * divider on the header or footer based on the scroll position. This handler
301 * should only be registered on full-screen dialogs with scrollable content.
302 */
303 MDCDialogFoundation.prototype.handleScrollEvent = function () {
304 var _this = this;
305 // Since scroll events can fire at a high rate, we throttle these events by
306 // using requestAnimationFrame.
307 this.animFrame.request(AnimationKeys.POLL_SCROLL_POS, function () {
308 _this.toggleScrollDividerHeader();
309 _this.toggleScrollDividerFooter();
310 });
311 };
312 MDCDialogFoundation.prototype.layoutInternal = function () {
313 if (this.autoStackButtons) {
314 this.detectStackedButtons();
315 }
316 this.toggleScrollableClasses();
317 };
318 MDCDialogFoundation.prototype.handleAnimationTimerEnd = function () {
319 this.animationTimer = 0;
320 this.adapter.removeClass(cssClasses.OPENING);
321 this.adapter.removeClass(cssClasses.CLOSING);
322 };
323 /**
324 * Runs the given logic on the next animation frame, using setTimeout to
325 * factor in Firefox reflow behavior.
326 */
327 MDCDialogFoundation.prototype.runNextAnimationFrame = function (callback) {
328 var _this = this;
329 cancelAnimationFrame(this.animationFrame);
330 this.animationFrame = requestAnimationFrame(function () {
331 _this.animationFrame = 0;
332 clearTimeout(_this.animationTimer);
333 _this.animationTimer = setTimeout(callback, 0);
334 });
335 };
336 MDCDialogFoundation.prototype.detectStackedButtons = function () {
337 // Remove the class first to let us measure the buttons' natural positions.
338 this.adapter.removeClass(cssClasses.STACKED);
339 var areButtonsStacked = this.adapter.areButtonsStacked();
340 if (areButtonsStacked) {
341 this.adapter.addClass(cssClasses.STACKED);
342 }
343 if (areButtonsStacked !== this.areButtonsStacked) {
344 this.adapter.reverseButtons();
345 this.areButtonsStacked = areButtonsStacked;
346 }
347 };
348 MDCDialogFoundation.prototype.toggleScrollableClasses = function () {
349 // Remove the class first to let us measure the natural height of the
350 // content.
351 this.adapter.removeClass(cssClasses.SCROLLABLE);
352 if (this.adapter.isContentScrollable()) {
353 this.adapter.addClass(cssClasses.SCROLLABLE);
354 if (this.isFullscreen) {
355 // If dialog is full-screen and scrollable, check if a scroll divider
356 // should be shown.
357 this.toggleScrollDividerHeader();
358 this.toggleScrollDividerFooter();
359 }
360 }
361 };
362 MDCDialogFoundation.prototype.toggleScrollDividerHeader = function () {
363 if (!this.adapter.isScrollableContentAtTop()) {
364 this.adapter.addClass(cssClasses.SCROLL_DIVIDER_HEADER);
365 }
366 else if (this.adapter.hasClass(cssClasses.SCROLL_DIVIDER_HEADER)) {
367 this.adapter.removeClass(cssClasses.SCROLL_DIVIDER_HEADER);
368 }
369 };
370 MDCDialogFoundation.prototype.toggleScrollDividerFooter = function () {
371 if (!this.adapter.isScrollableContentAtBottom()) {
372 this.adapter.addClass(cssClasses.SCROLL_DIVIDER_FOOTER);
373 }
374 else if (this.adapter.hasClass(cssClasses.SCROLL_DIVIDER_FOOTER)) {
375 this.adapter.removeClass(cssClasses.SCROLL_DIVIDER_FOOTER);
376 }
377 };
378 return MDCDialogFoundation;
379}(MDCFoundation));
380export { MDCDialogFoundation };
381// tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
382export default MDCDialogFoundation;
383//# sourceMappingURL=foundation.js.map
\No newline at end of file