From 19f3f3739ef0e3f894d739921531da51e19cf67e Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Wed, 1 Jul 2026 15:00:20 +0530 Subject: [PATCH] feat(widget): show loading state on the reload button during retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking "Reload" on the widget loading-error panel tore the panel down immediately and showed a blank iframe, so there was no indication the retry was running until the widget finished loading (or the 30s timeout re-fired). Keep the panel mounted and put it into a loading state instead: disable the button (the existing :disabled style already sets cursor: progress) and swap the copy to "Loading…". The panel is cleared by onWidgetReady on success — which now also re-reveals the iframe — or replaced by onLoadingError on another failure. onLoadingError also removes any existing panel first so repeated failures don't stack panels. --- .../src/widgetIframeLoading.test.ts | 35 +++++++++++++++++++ libs/widget-lib/src/widgetIframeLoading.ts | 21 ++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/libs/widget-lib/src/widgetIframeLoading.test.ts b/libs/widget-lib/src/widgetIframeLoading.test.ts index 004dcdace2..2b48501565 100644 --- a/libs/widget-lib/src/widgetIframeLoading.test.ts +++ b/libs/widget-lib/src/widgetIframeLoading.test.ts @@ -54,6 +54,41 @@ describe('widgetIframeLoading error UI styling', () => { expect(document.head.querySelectorAll(`style[data-${ERROR_CLASS}]`).length).toBeLessThanOrEqual(1) }) + + it('shows a loading state on the reload button while the retry is in progress', () => { + const { container, iframe } = setup() + failIframe(iframe) + + const reloadBtn = container.querySelector(`.${ERROR_CLASS} button`) + expect(reloadBtn?.disabled).toBe(false) + + reloadBtn?.click() + + // The panel stays mounted and the button is disabled (cursor:progress) while the retry runs. + expect(container.querySelector(`.${ERROR_CLASS}`)).not.toBeNull() + expect(reloadBtn?.disabled).toBe(true) + }) + + it('removes the error panel and reveals the iframe once the widget is ready', () => { + const { container, iframe, state } = setup() + failIframe(iframe) + expect(container.querySelector(`.${ERROR_CLASS}`)).not.toBeNull() + expect(iframe.style.display).toBe('none') + + state.onWidgetReady() + + expect(container.querySelector(`.${ERROR_CLASS}`)).toBeNull() + expect(iframe.style.display).toBe('') + }) + + it('keeps a single error panel across repeated failures on the same widget', () => { + const { container, iframe } = setup() + + failIframe(iframe) + failIframe(iframe) + + expect(container.querySelectorAll(`.${ERROR_CLASS}`).length).toBe(1) + }) }) function setup(): { container: HTMLElement; iframe: HTMLIFrameElement; state: LoadingState } { diff --git a/libs/widget-lib/src/widgetIframeLoading.ts b/libs/widget-lib/src/widgetIframeLoading.ts index ebb7a70ee7..88b64b4c79 100644 --- a/libs/widget-lib/src/widgetIframeLoading.ts +++ b/libs/widget-lib/src/widgetIframeLoading.ts @@ -19,22 +19,34 @@ export function widgetIframeLoading( let cancelled = false let timeout: number | null = null + function removeLoadingErrorContainer(): void { + const existing = container.getElementsByClassName(WIDGET_LOADING_ERROR_CLASS)[0] + if (existing) container.removeChild(existing) + } + function onLoadingError(): void { onWidgetLoadingError?.() iframe.style.display = 'none' ensureLoadingErrorStyles() + // Replace any previous error/loading panel so a repeated failure doesn't stack panels. + removeLoadingErrorContainer() + const loadingContainer = document.createElement('div') const reloadBtn = document.createElement('button') const errorText = document.createElement('p') reloadBtn.innerText = 'Reload' reloadBtn.addEventListener('click', () => { - container.removeChild(loadingContainer) + // Keep the panel mounted and switch it to a loading state so the retry is visibly in + // progress; it's cleared by onWidgetReady on success or replaced by onLoadingError on + // another failure. + reloadBtn.disabled = true + errorText.innerText = 'Loading…' + destroy(true) iframe.src = 'about:blank' - iframe.style.display = '' setTimeout(() => { iframe.src = originalSrc @@ -69,8 +81,9 @@ export function widgetIframeLoading( onWidgetReady() { if (timeout) clearTimeout(timeout) - const loadingContainer = document.getElementsByClassName(WIDGET_LOADING_ERROR_CLASS)[0] - if (loadingContainer) container.removeChild(loadingContainer) + // Reveal the iframe again — it is hidden while an error/retry panel is shown. + iframe.style.display = '' + removeLoadingErrorContainer() }, cancelWidgetLoading() { cancelled = true