From c1e5eaa6e1d9c609917a19fbbbdde519653cc26d Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:02:56 +0300 Subject: [PATCH] fix: make progress indicators interruption-safe --- .changeset/progress-lifecycle.md | 5 +++ client-src/progress.js | 65 ++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 .changeset/progress-lifecycle.md diff --git a/.changeset/progress-lifecycle.md b/.changeset/progress-lifecycle.md new file mode 100644 index 0000000000..13d3c68e9d --- /dev/null +++ b/.changeset/progress-lifecycle.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-server": patch +--- + +Repair linear progress styling and make progress completion interruption-safe, with reusable circular indicators, cleaned-up timers, accessible values, and reduced-motion support. diff --git a/client-src/progress.js b/client-src/progress.js index acac9ed77a..e643537cf5 100644 --- a/client-src/progress.js +++ b/client-src/progress.js @@ -38,9 +38,7 @@ export function defineProgressElement() { (this.shadowRoot).innerHTML = innerHTML; const progressValue = this.getAttribute("progress"); - this.initialProgress = progressValue ? Number(progressValue) : 0; - - this.#update(this.initialProgress); + this.#update(progressValue ? Number(progressValue) : 0); } static #circularTemplate() { @@ -98,6 +96,11 @@ export function defineProgressElement() { .hidden { display: none; } + + @media (prefers-reduced-motion: reduce) { + :host { transition: none; } + .disappear { animation: none; } + }
`; } connectedCallback() { + this.setAttribute("role", "progressbar"); + this.setAttribute("aria-label", "Compilation progress"); + this.setAttribute("aria-valuemin", "0"); + this.setAttribute("aria-valuemax", "100"); this.#reset(); } + disconnectedCallback() { + clearTimeout(this.animationTimer); + this.animationTimer = null; + } + static get observedAttributes() { return ["progress", "type"]; } @@ -162,6 +178,10 @@ export function defineProgressElement() { * @param {string} newValue new value */ attributeChangedCallback(name, oldValue, newValue) { + if (!this.isConnected || oldValue === newValue) { + return; + } + if (name === "progress") { this.#update(Number(newValue)); } else if (name === "type") { @@ -173,6 +193,8 @@ export function defineProgressElement() { * @param {number} percent percent */ #update(percent) { + // cspell:ignore valuenow + this.setAttribute("aria-valuenow", String(percent)); const shadowRoot = /** @type {ShadowRoot} */ (this.shadowRoot); const element = /** @type {HTMLElement} */ @@ -194,43 +216,36 @@ export function defineProgressElement() { if (percent >= 100) { this.#hide(); - } else if (percent > 0) { + } else { this.#show(); } } #show() { + clearTimeout(this.animationTimer); + this.animationTimer = null; const shadowRoot = /** @type {ShadowRoot} */ (this.shadowRoot); const element = /** @type {HTMLElement} */ (shadowRoot.querySelector("#progress")); - element.classList.remove("hidden"); + element.classList.remove("hidden", "disappear"); } #hide() { + if (this.animationTimer !== null) { + return; + } + const shadowRoot = /** @type {ShadowRoot} */ (this.shadowRoot); const element = /** @type {HTMLElement} */ (shadowRoot.querySelector("#progress")); - if (this.type === "circular") { - element.classList.add("disappear"); - element.addEventListener( - "animationend", - () => { - element.classList.add("hidden"); - this.#update(0); - }, - { once: true }, - ); - } else if (this.type === "linear") { - element.classList.add("disappear"); - this.animationTimer = setTimeout(() => { - element.classList.remove("disappear"); - element.classList.add("hidden"); - element.style.width = "0%"; - this.animationTimer = null; - }, 800); - } + element.classList.add("disappear"); + this.animationTimer = setTimeout(() => { + element.classList.remove("disappear"); + element.classList.add("hidden"); + this.animationTimer = null; + }, 800); } }