diff --git a/assets/js/phoenix_live_view/constants.js b/assets/js/phoenix_live_view/constants.js index e0feb4575..35ed909fc 100644 --- a/assets/js/phoenix_live_view/constants.js +++ b/assets/js/phoenix_live_view/constants.js @@ -67,6 +67,7 @@ export const PHX_RELOAD_STATUS = "__phoenix_reload_status__" export const LOADER_TIMEOUT = 1 export const MAX_CHILD_JOIN_ATTEMPTS = 3 export const BEFORE_UNLOAD_LOADER_TIMEOUT = 200 +export const DISCONNECTED_TIMEOUT = 500 export const BINDING_PREFIX = "phx-" export const PUSH_TIMEOUT = 30000 export const LINK_HEADER = "x-requested-with" diff --git a/assets/js/phoenix_live_view/live_socket.js b/assets/js/phoenix_live_view/live_socket.js index 136e2f630..f6376f5ad 100644 --- a/assets/js/phoenix_live_view/live_socket.js +++ b/assets/js/phoenix_live_view/live_socket.js @@ -28,6 +28,8 @@ * @param {Object} [opts.uploaders] - The optional object for referencing LiveView uploader callbacks. * @param {integer} [opts.loaderTimeout] - The optional delay in milliseconds to wait before apply * loading states. + * @param {integer} [opts.disconnectedTimeout] - The delay in milliseconds to wait before + * executing phx-disconnected commands. Defaults to 500. * @param {integer} [opts.maxReloads] - The maximum reloads before entering failsafe mode. * @param {integer} [opts.reloadJitterMin] - The minimum time between normal reload attempts. * @param {integer} [opts.reloadJitterMax] - The maximum time between normal reload attempts. @@ -78,6 +80,7 @@ import { DEFAULTS, FAILSAFE_JITTER, LOADER_TIMEOUT, + DISCONNECTED_TIMEOUT, MAX_RELOADS, PHX_DEBOUNCE, PHX_DROP_TARGET, @@ -152,6 +155,7 @@ export default class LiveSocket { this.hooks = opts.hooks || {} this.uploaders = opts.uploaders || {} this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT + this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT this.reloadWithJitterTimer = null this.maxReloads = opts.maxReloads || MAX_RELOADS this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN diff --git a/assets/js/phoenix_live_view/view.js b/assets/js/phoenix_live_view/view.js index 577523be2..3b319d82d 100644 --- a/assets/js/phoenix_live_view/view.js +++ b/assets/js/phoenix_live_view/view.js @@ -143,6 +143,7 @@ export default class View { this.lastAckRef = null this.childJoins = 0 this.loaderTimer = null + this.disconnectedTimer = null this.pendingDiffs = [] this.pendingForms = new Set() this.redirect = false @@ -254,6 +255,7 @@ export default class View { hideLoader(){ clearTimeout(this.loaderTimer) + clearTimeout(this.disconnectedTimer) this.setContainerClasses(PHX_CONNECTED_CLASS) this.execAll(this.binding("connected")) } @@ -891,7 +893,13 @@ export default class View { if(this.isMain()){ DOM.dispatchEvent(window, "phx:page-loading-start", {detail: {to: this.href, kind: "error"}}) } this.showLoader() this.setContainerClasses(...classes) - this.execAll(this.binding("disconnected")) + this.delayedDisconnected() + } + + delayedDisconnected(){ + this.disconnectedTimer = setTimeout(() => { + this.execAll(this.binding("disconnected")) + }, this.liveSocket.disconnectedTimeout) } wrapPush(callerPush, receives){ diff --git a/assets/test/view_test.js b/assets/test/view_test.js index eb912de51..5f4c578ba 100644 --- a/assets/test/view_test.js +++ b/assets/test/view_test.js @@ -636,6 +636,7 @@ describe("View", function(){ afterEach(() => { HTMLFormElement.prototype.submit = submitBefore + jest.useRealTimers() }) afterAll(() => { @@ -696,6 +697,7 @@ describe("View", function(){ }) test("displayError and hideLoader", done => { + jest.useFakeTimers() let liveSocket = new LiveSocket("/live", Socket) let loader = document.createElement("span") let phxView = document.querySelector("[data-phx-session]") @@ -711,15 +713,13 @@ describe("View", function(){ expect(el.classList.contains("phx-error")).toBeTruthy() expect(el.classList.contains("phx-connected")).toBeFalsy() expect(el.classList.contains("user-implemented-class")).toBeTruthy() - window.requestAnimationFrame(() => { - expect(status.style.display).toBe("block") - simulateVisibility(status) - view.hideLoader() - window.requestAnimationFrame(() => { - expect(status.style.display).toBe("none") - done() - }) - }) + jest.runAllTimers() + expect(status.style.display).toBe("block") + simulateVisibility(status) + view.hideLoader() + jest.runAllTimers() + expect(status.style.display).toBe("none") + done() }) test("join", async () => {