GeezerVison is showing github.com — feat(notifications): hold auto-close while the mouse is over a notifi… · phcode-dev/phoenix@a40ab64
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: src/widgets/NotificationUI.js +40-8Lines changed: 40 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -110,6 +110,34 @@ define(function (require, exports, module) { | 110 | 110 | * @type {Object} | 111 | 111 | */ | 112 | 112 |
| | 113 | + /** | | 114 | + * Close after the given seconds, holding off while the mouse is over the notification so a user | | 115 | + * reading it or reaching for one of its actions does not lose it. The countdown restarts in | | 116 | + * full once the mouse leaves. | | 117 | + * @param {jQuery} $notification - The notification element. | | 118 | + * @param {number} seconds - Time to wait before closing. | | 119 | + * @param {function} closeFn - Closes the notification with the timeout reason. | | 120 | + * @private | | 121 | + */ | | 122 | + function _autoCloseUnlessHovered($notification, seconds, closeFn) { | | 123 | + let timer = null; | | 124 | + function stop() { | | 125 | + if (timer) { | | 126 | + clearTimeout(timer); | | 127 | + timer = null; | | 128 | + } | | 129 | + } | | 130 | + function start() { | | 131 | + stop(); | | 132 | + timer = setTimeout(function () { | | 133 | + timer = null; | | 134 | + closeFn(); | | 135 | + }, seconds * 1000); | | 136 | + } | | 137 | + $notification.on("mouseenter", stop).on("mouseleave", start); | | 138 | + start(); | | 139 | + } | | 140 | + | 113 | 141 | /** | 114 | 142 | * @constructor | 115 | 143 | * @private | @@ -211,6 +239,7 @@ define(function (require, exports, module) { | 211 | 239 | * * `allowedPlacements` - Optional String array with values restricting where the notification will be shown. | 212 | 240 | * Values can be a mix of `['top', 'bottom', 'left', 'right']` | 213 | 241 | * * `autoCloseTimeS` - Time in seconds after which the notification should be auto closed. Default is never. | | 242 | + * The countdown pauses while the mouse is over the notification. | 214 | 243 | * * `dismissOnClick` - when clicked, the notification is closed. Default is true(dismiss). | 215 | 244 | * * `toastStyle` - To style the toast notification for error, warning, info etc. Can be | 216 | 245 | * one of `NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.*` or your own css class name. | @@ -229,9 +258,9 @@ define(function (require, exports, module) { | 229 | 258 | let notification = (new Notification($tooltip, NOTIFICATION_TYPE_ARROW)); | 230 | 259 |
| 231 | 260 | if(options.autoCloseTimeS){ | 232 | | - setTimeout(()=>{ | | 261 | + _autoCloseUnlessHovered($tooltip, options.autoCloseTimeS, function () { | 233 | 262 | notification.close(CLOSE_REASON.TIMEOUT); | 234 | | - }, options.autoCloseTimeS * 1000); | | 263 | + }); | 235 | 264 | } | 236 | 265 |
| 237 | 266 | if(options.dismissOnClick){ | @@ -352,6 +381,7 @@ define(function (require, exports, module) { | 352 | 381 | * @param {{dismissOnClick, autoCloseTimeS, toastStyle, instantOpen}} [options] optional, supported | 353 | 382 | * * options are: | 354 | 383 | * * `autoCloseTimeS` - Time in seconds after which the notification should be auto closed. Default is never. | | 384 | + * The countdown pauses while the mouse is over the notification. | 355 | 385 | * * `dismissOnClick` - when clicked, the notification is closed. Default is true(dismiss). | 356 | 386 | * * `toastStyle` - To style the toast notification for error, warning, info etc. Can be | 357 | 387 | * one of `NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.*` or your own css class name. | @@ -387,9 +417,9 @@ define(function (require, exports, module) { | 387 | 417 | }, 0); | 388 | 418 |
| 389 | 419 | if(options.autoCloseTimeS){ | 390 | | - setTimeout(()=>{ | | 420 | + _autoCloseUnlessHovered($NotificationPopup, options.autoCloseTimeS, function () { | 391 | 421 | notification.close(CLOSE_REASON.TIMEOUT); | 392 | | - }, options.autoCloseTimeS * 1000); | | 422 | + }); | 393 | 423 | } | 394 | 424 |
| 395 | 425 | if(options.dismissOnClick){ | @@ -416,6 +446,7 @@ define(function (require, exports, module) { | 416 | 446 | * @param {string|Element} template HTML string or DOM Element for the toast content. | 417 | 447 | * @param {Object} [options] optional, supported options: | 418 | 448 | * * `autoCloseTimeS` - Time in seconds after which the toast auto-closes. Default is 5. | | 449 | + * The countdown pauses while the mouse is over the toast. | 419 | 450 | * * `dismissOnClick` - If true, clicking the toast dismisses it. Default is true. | 420 | 451 | * @return {Notification} Object with a done handler that resolves when the toast closes. | 421 | 452 | * @type {function} | @@ -466,11 +497,11 @@ define(function (require, exports, module) { | 466 | 497 | }; | 467 | 498 |
| 468 | 499 | if (autoCloseTimeS) { | 469 | | - setTimeout(function () { | | 500 | + _autoCloseUnlessHovered($toast, autoCloseTimeS, function () { | 470 | 501 | if (notification.$notification) { | 471 | 502 | notification.close(CLOSE_REASON.TIMEOUT); | 472 | 503 | } | 473 | | - }, autoCloseTimeS * 1000); | | 504 | + }); | 474 | 505 | } | 475 | 506 |
| 476 | 507 | if (dismissOnClick) { | @@ -497,6 +528,7 @@ define(function (require, exports, module) { | 497 | 528 | * @param {string} label Text to display below the icon (e.g. "110%"). | 498 | 529 | * @param {Object} [options] optional, supported options: | 499 | 530 | * * `autoCloseTimeS` - Time in seconds after which the HUD auto-closes. Default is 1. | | 531 | + * The countdown pauses while the mouse is over the HUD. | 500 | 532 | * @return {Notification} Object with a done handler that resolves when the HUD closes. | 501 | 533 | * @type {function} | 502 | 534 | */ | @@ -531,11 +563,11 @@ define(function (require, exports, module) { | 531 | 563 | }; | 532 | 564 |
| 533 | 565 | if (autoCloseTimeS) { | 534 | | - setTimeout(function () { | | 566 | + _autoCloseUnlessHovered($hud, autoCloseTimeS, function () { | 535 | 567 | if (notification.$notification) { | 536 | 568 | notification.close(CLOSE_REASON.TIMEOUT); | 537 | 569 | } | 538 | | - }, autoCloseTimeS * 1000); | | 570 | + }); | 539 | 571 | } | 540 | 572 |
| 541 | 573 | return notification; | |
Collapse file Copy file name to clipboardExpand all lines: test/spec/NotificationUI-test.js +68-1Lines changed: 68 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 19 | 19 | * | 20 | 20 | */ | 21 | 21 |
| 22 | | -/*global describe, it, expect, beforeAll, afterAll, awaitsFor, awaitsForDone, awaits */ | | 22 | +/*global describe, it, expect, beforeAll, afterAll, awaitsFor, awaitsForDone, awaits, jasmine, spyOn */ | 23 | 23 |
| 24 | 24 | define(function (require, exports, module) { | 25 | 25 | let NotificationUI = require("widgets/NotificationUI"); | @@ -106,6 +106,50 @@ define(function (require, exports, module) { | 106 | 106 | }, "waiting for notification to close"); | 107 | 107 | } | 108 | 108 |
| | 109 | + it("Should hold the auto close while the mouse is over the toast", function () { | | 110 | + // Mock time from the start so the auto-close timer itself is under the clock. The close | | 111 | + // itself is observed on the notification, since the removal waits for a CSS transition. | | 112 | + jasmine.clock().install(); | | 113 | + try { | | 114 | + const notification = NotificationUI.createToastFromTemplate("hello", "world", {autoCloseTimeS: 1}); | | 115 | + spyOn(notification, "close").and.callThrough(); | | 116 | + const $popup = $("#toast-notification-container").children().last(); | | 117 | + expect($popup.length).toBe(1); | | 118 | + $popup.trigger("mouseenter"); | | 119 | + jasmine.clock().tick(1500); | | 120 | + expect($popup[0].isConnected).toBe(true); | | 121 | + expect(notification.close).not.toHaveBeenCalled(); | | 122 | + $popup.trigger("mouseleave"); | | 123 | + jasmine.clock().tick(900); | | 124 | + expect(notification.close).not.toHaveBeenCalled(); | | 125 | + jasmine.clock().tick(200); | | 126 | + expect(notification.close).toHaveBeenCalledWith(NotificationUI.CLOSE_REASON.TIMEOUT); | | 127 | + } finally { | | 128 | + jasmine.clock().uninstall(); | | 129 | + } | | 130 | + }); | | 131 | + | | 132 | + it("Should hold the auto close while the mouse is over the HUD", function () { | | 133 | + jasmine.clock().install(); | | 134 | + let closeReason; | | 135 | + try { | | 136 | + const notification = NotificationUI.showHUD("fa-solid fa-magnifying-glass-plus", "110%"); | | 137 | + notification.done(function (reason) { closeReason = reason; }); | | 138 | + const $hud = $("body > .hud-overlay"); | | 139 | + expect($hud.length).toBe(1); | | 140 | + $hud.trigger("mouseenter"); | | 141 | + jasmine.clock().tick(1500); | | 142 | + expect($hud[0].isConnected).toBe(true); | | 143 | + expect(closeReason).toBeUndefined(); | | 144 | + $hud.trigger("mouseleave"); | | 145 | + jasmine.clock().tick(1100); | | 146 | + expect(closeReason).toBe(NotificationUI.CLOSE_REASON.TIMEOUT); | | 147 | + expect($("body > .hud-overlay").length).toBe(0); | | 148 | + } finally { | | 149 | + jasmine.clock().uninstall(); | | 150 | + } | | 151 | + }); | | 152 | + | 109 | 153 | it("Should style toast notification", async function () { | 110 | 154 | await verifyToast(NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.INFO); | 111 | 155 | await verifyToast(NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.WARNING); | @@ -152,6 +196,29 @@ define(function (require, exports, module) { | 152 | 196 | }, "waiting for inline toast to auto-close", 3000); | 153 | 197 | }); | 154 | 198 |
| | 199 | + it("Should hold the auto close while the mouse is over the inline toast", function () { | | 200 | + jasmine.clock().install(); | | 201 | + let closeReason; | | 202 | + try { | | 203 | + const notification = NotificationUI.showToastOn($container[0], "hover me", {autoCloseTimeS: 1}); | | 204 | + notification.done(function (reason) { closeReason = reason; }); | | 205 | + const $toast = $container.find(".inline-toast"); | | 206 | + expect($toast.length).toBe(1); | | 207 | + $toast.trigger("mouseenter"); | | 208 | + jasmine.clock().tick(1500); | | 209 | + expect($toast[0].isConnected).toBe(true); | | 210 | + expect(closeReason).toBeUndefined(); | | 211 | + $toast.trigger("mouseleave"); | | 212 | + jasmine.clock().tick(1100); | | 213 | + // The close falls back to a timer when no transition event arrives. | | 214 | + jasmine.clock().tick(600); | | 215 | + expect(closeReason).toBe(NotificationUI.CLOSE_REASON.TIMEOUT); | | 216 | + expect($container.find(".inline-toast").length).toBe(0); | | 217 | + } finally { | | 218 | + jasmine.clock().uninstall(); | | 219 | + } | | 220 | + }); | | 221 | + | 155 | 222 | it("Should dismiss on click by default", async function () { | 156 | 223 | NotificationUI.showToastOn($container[0], "Click me"); | 157 | 224 | await awaitsFor(function () { | |
|