foundation.js 17 KB

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