Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add visual progress indicators #5186

Merged
merged 3 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"omnibox",
"swiftshader",
"hoge",
"subsubcomain"
"subsubcomain",
"noselect"
],
"ignorePaths": [
"CHANGELOG.md",
Expand Down
15 changes: 15 additions & 0 deletions client-src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { log, logEnabledFeatures, setLogLevel } from "./utils/log.js";
import sendMessage from "./utils/sendMessage.js";
import reloadApp from "./utils/reloadApp.js";
import createSocketURL from "./utils/createSocketURL.js";
import { isProgressSupported, defineProgressElement } from "./progress.js";

defineProgressElement();

/**
* @typedef {Object} OverlayOptions
Expand Down Expand Up @@ -236,6 +239,18 @@ const onSocketMessage = {
);
}

if (isProgressSupported()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move defineProgressElement after this check to avoid extra checks and have customElements.define("wds-progress", WebpackDevServerProgress); only in this function

if (typeof options.progress === "string") {
let progress = document.querySelector("wds-progress");
if (!progress) {
progress = document.createElement("wds-progress");
document.body.appendChild(progress);
}
progress.setAttribute("progress", data.percent);
progress.setAttribute("type", options.progress);
}
}

sendMessage("Progress", data);
},
"still-ok": function stillOk() {
Expand Down
205 changes: 205 additions & 0 deletions client-src/progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
class WebpackDevServerProgress extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.maxDashOffset = -219.99078369140625;
this.animationTimer = null;
}

#reset() {
clearTimeout(this.animationTimer);
this.animationTimer = null;

const typeAttr = this.getAttribute("type")?.toLowerCase();
this.type = typeAttr === "circular" ? "circular" : "linear";

const innerHTML =
this.type === "circular"
? WebpackDevServerProgress.#circularTemplate()
: WebpackDevServerProgress.#linearTemplate();
this.shadowRoot.innerHTML = innerHTML;

this.initialProgress = Number(this.getAttribute("progress")) ?? 0;

this.#update(this.initialProgress);
}

static #circularTemplate() {
return `
<style>
:host {
width: 200px;
height: 200px;
position: fixed;
right: 5%;
top: 5%;
transition: opacity .25s ease-in-out;
z-index: 2147483645;
}

circle {
fill: #282d35;
}

path {
fill: rgba(0, 0, 0, 0);
stroke: rgb(186, 223, 172);
stroke-dasharray: 219.99078369140625;
stroke-dashoffset: -219.99078369140625;
stroke-width: 10;
transform: rotate(90deg) translate(0px, -80px);
}

text {
font-family: 'Open Sans', sans-serif;
font-size: 18px;
fill: #ffffff;
dominant-baseline: middle;
text-anchor: middle;
}

tspan#percent-super {
fill: #bdc3c7;
font-size: 0.45em;
baseline-shift: 10%;
}

@keyframes fade {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0); }
}

.disappear {
animation: fade 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.5s;
}

.hidden {
display: none;
}
</style>
<svg id="progress" class="hidden noselect" viewBox="0 0 80 80">
<circle cx="50%" cy="50%" r="35"></circle>
<path d="M5,40a35,35 0 1,0 70,0a35,35 0 1,0 -70,0"></path>
<text x="50%" y="51%">
<tspan id="percent-value">0</tspan>
<tspan id="percent-super">%</tspan>
</text>
</svg>
`;
}

static #linearTemplate() {
return `
<style>
:host {
position: fixed;
top: 0;
left: 0;
height: 4px;
width: 100vw;
z-index: 2147483645;
}

#bar {
width: 0%;
height: 4px;
background-color: rgb(186, 223, 172);
}

@keyframes fade {
0% { opacity: 1; }
100% { opacity: 0; }
}

.disappear {
animation: fade 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.5s;
}

.hidden {
display: none;
}
</style>
<div id="progress"></div>
`;
}

connectedCallback() {
this.#reset();
}

static get observedAttributes() {
return ["progress", "type"];
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === "progress") {
this.#update(Number(newValue));
} else if (name === "type") {
this.#reset();
}
}

#update(percent) {
const element = this.shadowRoot.querySelector("#progress");
if (this.type === "circular") {
const path = this.shadowRoot.querySelector("path");
const value = this.shadowRoot.querySelector("#percent-value");
const offset = ((100 - percent) / 100) * this.maxDashOffset;

path.style.strokeDashoffset = offset;
value.textContent = percent;
} else {
element.style.width = `${percent}%`;
}

if (percent >= 100) {
this.#hide();
} else if (percent > 0) {
this.#show();
}
}

#show() {
const element = this.shadowRoot.querySelector("#progress");
element.classList.remove("hidden");
}

#hide() {
const element = this.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);
}
}
}

export function isProgressSupported() {
return "customElements" in window && !!HTMLElement.prototype.attachShadow;
}

export function defineProgressElement() {
if (!isProgressSupported() || customElements.get("wds-progress")) {
return;
}

customElements.define("wds-progress", WebpackDevServerProgress);
}
4 changes: 3 additions & 1 deletion examples/client/progress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
// ...
devServer: {
client: {
progress: true,
progress: true | "linear" | "circular",
},
},
};
Expand All @@ -17,6 +17,8 @@ Usage via CLI:

```shell
npx webpack serve --open --client-progress
npx webpack serve --open --client-progress linear
npx webpack serve --open --client-progress circular
```

To disable:
Expand Down
7 changes: 4 additions & 3 deletions lib/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,12 @@
]
},
"ClientProgress": {
"description": "Prints compilation progress in percentage in the browser.",
"description": "Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.",
"link": "https://webpack.js.org/configuration/dev-server/#progress",
"type": "boolean",
"type": ["boolean", "string"],
"enum": [true, false, "linear", "circular"],
"cli": {
"negatedDescription": "Does not print compilation progress in percentage in the browser."
"negatedDescription": "Does not display compilation progress in the browser."
}
},
"ClientReconnect": {
Expand Down
5 changes: 3 additions & 2 deletions test/__snapshots__/validate-options.test.js.snap.webpack5
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ exports[`options validate should throw an error on the "client" option with '{"o

exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = `
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- options.client.progress should be a boolean.
-> Prints compilation progress in percentage in the browser.
- options.client.progress should be one of these:
true | false | "linear" | "circular"
-> Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.
-> Read more at https://webpack.js.org/configuration/dev-server/#progress"
`;

Expand Down
Loading