@govuk-one-login/frontend-ui 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +37 -0
  2. package/build/all.css +1 -1
  3. package/build/cjs/backend/index.cjs +16 -2
  4. package/build/cjs/backend/index.d.cts +12 -0
  5. package/build/cjs/backend/index.d.ts +12 -0
  6. package/build/cjs/backend/index.d.ts.map +1 -1
  7. package/build/cjs/frontend/index.cjs +288 -268
  8. package/build/cjs/frontend/index.d.cts +3 -1
  9. package/build/cjs/frontend/index.d.ts +3 -1
  10. package/build/cjs/frontend/index.d.ts.map +1 -1
  11. package/build/cjs/frontend/progress-button/progress-button.d.ts +2 -0
  12. package/build/cjs/frontend/progress-button/progress-button.d.ts.map +1 -0
  13. package/build/cjs/frontend/spinner/__tests__/spinner.test.d.ts +0 -4
  14. package/build/cjs/frontend/spinner/__tests__/spinner.test.d.ts.map +1 -1
  15. package/build/cjs/frontend/spinner/spinner.d.ts +58 -84
  16. package/build/cjs/frontend/spinner/spinner.d.ts.map +1 -1
  17. package/build/components/_all.scss +1 -0
  18. package/build/components/progress-button/_index.scss +44 -0
  19. package/build/components/progress-button/macro.njk +2 -0
  20. package/build/components/progress-button/progress-button.yaml +13 -0
  21. package/build/components/progress-button/template.njk +115 -0
  22. package/build/components/spinner/README.md +75 -52
  23. package/build/components/spinner/_index.scss +2 -11
  24. package/build/components/spinner/template.njk +15 -8
  25. package/build/esm/backend/index.d.ts +12 -0
  26. package/build/esm/backend/index.d.ts.map +1 -1
  27. package/build/esm/backend/index.js +16 -2
  28. package/build/esm/frontend/index.d.ts +3 -1
  29. package/build/esm/frontend/index.d.ts.map +1 -1
  30. package/build/esm/frontend/index.js +288 -269
  31. package/build/esm/frontend/progress-button/progress-button.d.ts +2 -0
  32. package/build/esm/frontend/progress-button/progress-button.d.ts.map +1 -0
  33. package/build/esm/frontend/spinner/__tests__/spinner.test.d.ts +0 -4
  34. package/build/esm/frontend/spinner/__tests__/spinner.test.d.ts.map +1 -1
  35. package/build/esm/frontend/spinner/spinner.d.ts +58 -84
  36. package/build/esm/frontend/spinner/spinner.d.ts.map +1 -1
  37. package/package.json +3 -2
  38. package/build/components/spinner/api.njk +0 -27
@@ -1,2 +1,4 @@
1
- export { useSpinner } from "./spinner/spinner";
1
+ export { useSpinner, PollResult } from "./spinner/spinner";
2
+ export type { PollingFunction } from "./spinner/spinner";
3
+ export { initialiseProgressButtons } from "./progress-button/progress-button";
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../frontend-src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../frontend-src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC"}
@@ -1,171 +1,261 @@
1
- function useSpinner() {
2
- const element = document.getElementById("spinner-container");
3
- if (element) {
4
- const spinner = new Spinner(element);
5
- spinner.init();
1
+ async function useSpinner(containerId, pollingFunction, successFunction, errorFunction) {
2
+ const element = document.getElementById(containerId);
3
+ if (element && element instanceof HTMLDivElement) {
4
+ let spinner;
5
+ try {
6
+ spinner = new Spinner(element, pollingFunction, successFunction, errorFunction);
7
+ }
8
+ catch (e) {
9
+ const errorText = document.createElement("p");
10
+ errorText.textContent = "Error configuring spinner: " + e;
11
+ element.replaceChildren(errorText);
12
+ return;
13
+ }
14
+ await spinner.init();
6
15
  }
7
16
  else {
8
- console.warn("Attempting to initiate a spinner on a page with no '#spinner-container' element.");
17
+ console.error(`Attempting to initiate a spinner on a page with no '#${containerId}' div element.`);
9
18
  }
10
19
  }
20
+ var PollResult;
21
+ (function (PollResult) {
22
+ PollResult[PollResult["Success"] = 0] = "Success";
23
+ PollResult[PollResult["Failure"] = 1] = "Failure";
24
+ PollResult[PollResult["Pending"] = 2] = "Pending";
25
+ PollResult[PollResult["Backoff"] = 3] = "Backoff";
26
+ })(PollResult || (PollResult = {}));
27
+ var SpinnerState;
28
+ (function (SpinnerState) {
29
+ SpinnerState[SpinnerState["Waiting"] = 0] = "Waiting";
30
+ SpinnerState[SpinnerState["LongWaiting"] = 1] = "LongWaiting";
31
+ SpinnerState[SpinnerState["Error"] = 2] = "Error";
32
+ SpinnerState[SpinnerState["Complete"] = 3] = "Complete";
33
+ })(SpinnerState || (SpinnerState = {}));
11
34
  class Spinner {
12
- reflectLongWait() {
13
- if (this.state.spinnerState !== "ready") {
14
- this.state.spinnerStateText = this.content.longWait.spinnerStateText;
35
+ constructor(domContainer, pollingFunction, onSuccess, onError) {
36
+ this.state = SpinnerState.Waiting;
37
+ this.backOffCount = 0;
38
+ this.handleAbort = () => {
39
+ this.abortController.abort();
40
+ };
41
+ this.initTimer = (initTime) => {
42
+ this.updateDomTimer = setInterval(() => {
43
+ this.updateStateAccordingToTimeElapsed(initTime);
44
+ this.updateDom();
45
+ }, this.config.msBetweenDomUpdate);
46
+ };
47
+ this.reflectSuccess = () => {
48
+ this.state = SpinnerState.Complete;
49
+ sessionStorage.removeItem("spinnerInitTime");
50
+ };
51
+ this.reflectError = () => {
52
+ this.state = SpinnerState.Error;
53
+ sessionStorage.removeItem("spinnerInitTime");
54
+ this.abortController.abort();
55
+ };
56
+ this.updateStateAccordingToTimeElapsed = (initTime) => {
57
+ const elapsedMilliseconds = Date.now() - initTime;
58
+ if (this.hasCompleted()) {
59
+ // If we've already finished waiting then there's no need to update again.
60
+ return;
61
+ }
62
+ if (elapsedMilliseconds >= this.config.msBeforeAbort) {
63
+ this.reflectError();
64
+ }
65
+ else if (elapsedMilliseconds >= this.config.msBeforeInformingOfLongWait) {
66
+ this.reflectLongWait();
67
+ }
68
+ };
69
+ this.updateDom = () => {
70
+ if (this.hasCompleted()) {
71
+ // We've reached an end state so stop updating the DOM after this
72
+ clearInterval(this.updateDomTimer);
73
+ }
74
+ if (this.displayState === this.state) {
75
+ // No need to update anything
76
+ return;
77
+ }
78
+ this.displayState = this.state;
79
+ const newElementsToDisplay = [];
80
+ switch (this.displayState) {
81
+ case SpinnerState.Waiting:
82
+ newElementsToDisplay.push(this.createSpinnerElement(""));
83
+ this.cloneAndAddIfExists(newElementsToDisplay, this.waitContent);
84
+ break;
85
+ case SpinnerState.LongWaiting:
86
+ newElementsToDisplay.push(this.createSpinnerElement(""));
87
+ this.cloneAndAddIfExists(newElementsToDisplay, this.longWaitContent);
88
+ break;
89
+ case SpinnerState.Complete:
90
+ newElementsToDisplay.push(this.createSpinnerElement("spinner__finished"));
91
+ this.cloneAndAddIfExists(newElementsToDisplay, this.successContent);
92
+ break;
93
+ case SpinnerState.Error:
94
+ if (!this.config.hideSpinnerOnError) {
95
+ newElementsToDisplay.push(this.createSpinnerElement("spinner__finished"));
96
+ }
97
+ this.cloneAndAddIfExists(newElementsToDisplay, this.errorContent);
98
+ break;
99
+ }
100
+ this.visibleElementsContainer.replaceChildren(...newElementsToDisplay);
101
+ if (this.displayState === SpinnerState.Complete) {
102
+ if (this.onSuccess) {
103
+ this.onSuccess();
104
+ }
105
+ this.updateAriaAlert(this.config.ariaAlertCompletionText);
106
+ }
107
+ if (this.displayState === SpinnerState.Error && !!this.onError) {
108
+ this.onError();
109
+ }
110
+ };
111
+ this.container = domContainer;
112
+ this.pollingFunction = pollingFunction;
113
+ if (!pollingFunction) {
114
+ throw new Error("Polling function must be provided");
115
+ }
116
+ this.onSuccess = onSuccess;
117
+ this.onError = onError;
118
+ this.noJsContent = this.getElementOrThrow("no-js-content");
119
+ this.noJsContent.style.display = "none";
120
+ this.waitContent =
121
+ this.container.querySelector("#wait-content") || undefined;
122
+ this.longWaitContent =
123
+ this.container.querySelector("#long-wait-content") || undefined;
124
+ this.successContent =
125
+ this.container.querySelector("#success-content") || undefined;
126
+ this.errorContent =
127
+ this.container.querySelector("#error-content") || undefined;
128
+ if (!this.successContent && !onSuccess) {
129
+ throw new Error("One of success-content or successFunction must be provided");
15
130
  }
131
+ if (!this.errorContent && !onError) {
132
+ throw new Error("One of error-content or errorFunction must be provided");
133
+ }
134
+ this.config = this.getConfig(this.container);
135
+ this.visibleElementsContainer = document.createElement("div");
136
+ this.container.appendChild(this.visibleElementsContainer);
137
+ this.ariaLiveContainer = this.createAriaLiveContainer();
138
+ this.container.appendChild(this.ariaLiveContainer);
139
+ this.abortController = this.createAbortController();
140
+ }
141
+ async init() {
142
+ const initTime = this.getInitTime();
143
+ this.updateStateAccordingToTimeElapsed(initTime);
144
+ this.updateDom();
145
+ this.initTimer(initTime);
146
+ await this.callPollingFunction(initTime);
16
147
  }
17
- initialiseState() {
18
- if (this.domRequirementsMet) {
19
- this.state = {
20
- heading: this.content.initial.heading,
21
- spinnerStateText: this.content.initial.spinnerStateText,
22
- spinnerState: this.content.initial.spinnerState,
23
- buttonDisabled: true,
24
- ariaButtonEnabledMessage: "",
25
- done: false,
26
- error: false,
27
- virtualDom: [],
28
- };
148
+ getElementOrThrow(elementId) {
149
+ const element = this.container.querySelector(`#${elementId}`);
150
+ if (element === null || !(element instanceof HTMLElement)) {
151
+ throw new Error(`HTML Element with id ${elementId} must be provided`);
29
152
  }
153
+ return element;
30
154
  }
31
- initialiseContent(element) {
32
- var _a;
33
- function throwIfMissing(data) {
34
- if (data === undefined || data === null) {
35
- throw new Error("Missing required data");
36
- }
37
- return data;
155
+ getConfig(element) {
156
+ return {
157
+ msBeforeInformingOfLongWait: element.dataset.msBeforeInformingOfLongWait
158
+ ? parseInt(element.dataset.msBeforeInformingOfLongWait)
159
+ : 5000,
160
+ msBeforeAbort: element.dataset.msBeforeAbort
161
+ ? parseInt(element.dataset.msBeforeAbort)
162
+ : 30000,
163
+ msBetweenRequests: element.dataset.msBetweenRequests
164
+ ? parseInt(element.dataset.msBetweenRequests)
165
+ : 2000,
166
+ msBetweenDomUpdate: element.dataset.msBetweenDomUpdate
167
+ ? parseInt(element.dataset.msBetweenDomUpdate)
168
+ : 1000,
169
+ ariaAlertCompletionText: element.dataset.ariaAlertCompletionText
170
+ ? element.dataset.ariaAlertCompletionText
171
+ : undefined,
172
+ hideSpinnerOnError: element.dataset.hideSpinnerOnError
173
+ ? (element.dataset.hideSpinnerOnError === 'true')
174
+ : false,
175
+ maxBackoffTries: element.dataset.maxBackoffTries
176
+ ? parseInt(element.dataset.maxBackoffTries)
177
+ : 3,
178
+ };
179
+ }
180
+ createAbortController() {
181
+ const abortController = new AbortController();
182
+ window.removeEventListener("beforeunload", this.handleAbort);
183
+ window.addEventListener("beforeunload", this.handleAbort);
184
+ return abortController;
185
+ }
186
+ getInitTime() {
187
+ const storedSpinnerInitTime = sessionStorage.getItem("spinnerInitTime");
188
+ let spinnerInitTime;
189
+ if (storedSpinnerInitTime === null) {
190
+ spinnerInitTime = Date.now();
191
+ sessionStorage.setItem("spinnerInitTime", spinnerInitTime.toString());
38
192
  }
39
- try {
40
- this.content = {
41
- initial: {
42
- heading: throwIfMissing(element.dataset.initialHeading),
43
- spinnerStateText: throwIfMissing(element.dataset.initialSpinnerstatetext),
44
- spinnerState: throwIfMissing(element.dataset.initialSpinnerstate),
45
- },
46
- error: {
47
- heading: throwIfMissing(element.dataset.errorHeading),
48
- messageText: throwIfMissing(element.dataset.errorMessagetext),
49
- whatYouCanDo: {
50
- heading: throwIfMissing(element.dataset.errorWhatyoucandoHeading),
51
- message: {
52
- text1: throwIfMissing(element.dataset.errorWhatyoucandoMessageText1),
53
- link: {
54
- href: throwIfMissing(element.dataset.errorWhatyoucandoMessageLinkHref),
55
- text: throwIfMissing(element.dataset.errorWhatyoucandoMessageLinkText),
56
- },
57
- text2: throwIfMissing(element.dataset.errorWhatyoucandoMessageText2),
58
- },
59
- },
60
- },
61
- complete: {
62
- spinnerState: throwIfMissing(element.dataset.completeSpinnerstate),
63
- },
64
- longWait: {
65
- spinnerStateText: throwIfMissing(element.dataset.longwaitSpinnerstatetext),
66
- },
67
- continueButton: {
68
- text: (_a = element.dataset.continuebuttonText) !== null && _a !== void 0 ? _a : "Continue",
69
- },
70
- };
71
- this.config = {
72
- apiUrl: element.dataset.apiUrl || this.config.apiUrl,
73
- ariaButtonEnabledMessage: throwIfMissing(element.dataset.ariaButtonEnabledMessage),
74
- msBeforeInformingOfLongWait: element.dataset.msBeforeInformingOfLongWait
75
- ? parseInt(element.dataset.msBeforeInformingOfLongWait)
76
- : this.config.msBeforeInformingOfLongWait,
77
- msBeforeAbort: element.dataset.msBeforeAbort
78
- ? parseInt(element.dataset.msBeforeAbort)
79
- : this.config.msBeforeAbort,
80
- msBetweenRequests: element.dataset.msBetweenRequests
81
- ? parseInt(element.dataset.msBetweenRequests)
82
- : this.config.msBetweenRequests,
83
- msBetweenDomUpdate: element.dataset.msBetweenDomUpdate
84
- ? parseInt(element.dataset.msBetweenDomUpdate)
85
- : this.config.msBetweenDomUpdate,
86
- };
87
- this.domRequirementsMet = true;
193
+ else {
194
+ spinnerInitTime = parseInt(storedSpinnerInitTime, 10);
88
195
  }
89
- catch (e) {
90
- this.domRequirementsMet = false;
196
+ return spinnerInitTime;
197
+ }
198
+ hasCompleted() {
199
+ return (this.state === SpinnerState.Error || this.state === SpinnerState.Complete);
200
+ }
201
+ reflectLongWait() {
202
+ if (!this.hasCompleted()) {
203
+ this.state = SpinnerState.LongWaiting;
204
+ }
205
+ }
206
+ createSpinnerElement(spinnerStateClass) {
207
+ const spinner = document.createElement("div");
208
+ spinner.id = "spinner";
209
+ spinner.classList.add("spinner", "centre");
210
+ if (spinnerStateClass) {
211
+ spinner.classList.add(spinnerStateClass);
91
212
  }
213
+ return spinner;
92
214
  }
93
- createVirtualDom() {
94
- const domInitialState = [
95
- {
96
- nodeName: "h1",
97
- text: this.state.heading,
98
- classes: ["govuk-heading-l"],
99
- },
100
- {
101
- nodeName: "div",
102
- id: "spinner",
103
- classes: [
104
- "spinner",
105
- "spinner__pending",
106
- "centre",
107
- this.state.spinnerState,
108
- ],
109
- },
110
- {
111
- nodeName: "p",
112
- text: this.state.spinnerStateText,
113
- classes: ["centre", "spinner-state-text", "govuk-body"],
114
- },
115
- {
116
- nodeName: "button",
117
- text: this.content.continueButton.text,
118
- buttonDisabled: this.state.buttonDisabled,
119
- classes: ["govuk-button", "govuk-!-margin-top-4"],
120
- },
121
- ];
122
- const domErrorState = [
123
- {
124
- nodeName: "h1",
125
- text: this.state.heading,
126
- classes: ["govuk-heading-l"],
127
- },
128
- {
129
- nodeName: "p",
130
- text: this.state.messageText,
131
- classes: ["govuk-body"],
132
- },
133
- {
134
- nodeName: "h2",
135
- text: this.content.error.whatYouCanDo.heading,
136
- classes: ["govuk-heading-m"],
137
- },
138
- {
139
- nodeName: "p",
140
- innerHTML: `${this.content.error.whatYouCanDo.message.text1}<a href="${this.content.error.whatYouCanDo.message.link.href}">${this.content.error.whatYouCanDo.message.link.text}</a>${this.content.error.whatYouCanDo.message.text2}`,
141
- classes: ["govuk-body"],
142
- },
143
- ];
144
- return this.state.error ? domErrorState : domInitialState;
215
+ cloneAndAddIfExists(list, element) {
216
+ if (element) {
217
+ const cloned = element.cloneNode(true);
218
+ cloned.style.display = "";
219
+ cloned.removeAttribute("id");
220
+ list.push(cloned);
221
+ }
145
222
  }
146
- async requestIDProcessingStatus(initTime) {
147
- const signal = this.abortController.signal;
148
- await fetch(this.config.apiUrl, { signal })
149
- .then((response) => response.json())
150
- .then((data) => {
151
- if (data.status === "COMPLETED" || data.status === "INTERVENTION") {
152
- this.reflectCompletion();
223
+ async callPollingFunction(initTime) {
224
+ if (Date.now() - initTime >= this.config.msBeforeAbort) {
225
+ return;
226
+ }
227
+ await this.pollingFunction(this.abortController.signal)
228
+ .then((response) => {
229
+ if (response === PollResult.Success) {
230
+ this.reflectSuccess();
153
231
  }
154
- else if (data.status === "ERROR") {
232
+ else if (response === PollResult.Failure) {
155
233
  this.reflectError();
156
234
  }
157
- else if (this.notInErrorOrDoneState()) {
235
+ else if (!this.hasCompleted()) {
236
+ let timeToNextPoll = this.config.msBetweenRequests;
237
+ if (response === PollResult.Backoff) {
238
+ this.backOffCount++;
239
+ if (this.backOffCount > this.config.maxBackoffTries) {
240
+ this.reflectError();
241
+ return;
242
+ }
243
+ timeToNextPoll = this.calculateBackoffTime(this.backOffCount);
244
+ }
245
+ else {
246
+ this.backOffCount = 0;
247
+ }
158
248
  setTimeout(async () => {
159
249
  if (Date.now() - initTime >= this.config.msBeforeAbort) {
160
250
  return;
161
251
  }
162
- await this.requestIDProcessingStatus(initTime);
163
- }, this.config.msBetweenRequests);
252
+ await this.callPollingFunction(initTime);
253
+ }, timeToNextPoll);
164
254
  }
165
255
  })
166
256
  .catch((error) => {
167
257
  if (error.name !== "AbortError") {
168
- console.error("Error in requestIDProcessingStatus:", error);
258
+ console.error("Error in polling function: ", error);
169
259
  this.reflectError();
170
260
  }
171
261
  })
@@ -173,140 +263,69 @@ class Spinner {
173
263
  this.updateDom();
174
264
  });
175
265
  }
176
- getInitTime() {
177
- const storedSpinnerInitTime = sessionStorage.getItem("spinnerInitTime");
178
- let spinnerInitTime;
179
- if (storedSpinnerInitTime === null) {
180
- spinnerInitTime = Date.now();
181
- sessionStorage.setItem("spinnerInitTime", spinnerInitTime.toString());
182
- }
183
- else {
184
- spinnerInitTime = parseInt(storedSpinnerInitTime, 10);
185
- }
186
- return spinnerInitTime;
266
+ calculateBackoffTime(backOffCount) {
267
+ const extraDelay = Math.pow(2, backOffCount - 2) * this.config.msBetweenRequests;
268
+ return this.config.msBetweenRequests + extraDelay;
187
269
  }
188
- init() {
189
- if (this.domRequirementsMet) {
190
- const initTime = this.getInitTime();
191
- this.initTimer(initTime);
192
- this.initialiseContainers();
193
- this.updateDom();
194
- this.requestIDProcessingStatus(initTime);
195
- }
270
+ createAriaLiveContainer() {
271
+ // For the Aria alert to work reliably we need to create its container once and then update the contents
272
+ // https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/
273
+ const container = document.createElement("div");
274
+ container.setAttribute("aria-live", "assertive");
275
+ container.classList.add("govuk-visually-hidden");
276
+ return container;
196
277
  }
197
- constructor(domContainer) {
198
- this.config = {
199
- apiUrl: "/prove-identity-status",
200
- msBeforeInformingOfLongWait: 5000,
201
- msBeforeAbort: 25000,
202
- msBetweenRequests: 1000,
203
- msBetweenDomUpdate: 2000,
204
- };
205
- this.notInErrorOrDoneState = () => {
206
- return !(this.state.done || this.state.error);
207
- };
208
- this.reflectCompletion = () => {
209
- this.state.spinnerState = "spinner__ready";
210
- this.state.spinnerStateText = this.content.complete.spinnerState;
211
- this.state.buttonDisabled = false;
212
- this.state.ariaButtonEnabledMessage =
213
- this.content.complete.ariaButtonEnabledMessage;
214
- this.state.done = true;
215
- sessionStorage.removeItem("spinnerInitTime");
216
- };
217
- this.reflectError = () => {
218
- this.state.heading = this.content.error.heading;
219
- this.state.messageText = this.content.error.messageText;
220
- this.state.spinnerState = "spinner__failed";
221
- this.state.done = true;
222
- this.state.error = true;
223
- sessionStorage.removeItem("spinnerInitTime");
224
- this.abortController.abort();
225
- };
226
- this.updateAccordingToTimeElapsed = (initTime) => {
227
- const elapsedMilliseconds = Date.now() - initTime;
228
- if (elapsedMilliseconds >= this.config.msBeforeAbort) {
229
- this.reflectError();
230
- }
231
- else if (elapsedMilliseconds >= this.config.msBeforeInformingOfLongWait) {
232
- this.reflectLongWait();
233
- }
234
- };
235
- this.vDomHasChanged = (currentVDom, nextVDom) => {
236
- return JSON.stringify(currentVDom) !== JSON.stringify(nextVDom);
237
- };
238
- this.convert = (node) => {
239
- const el = document.createElement(node.nodeName);
240
- if (node.text)
241
- el.textContent = node.text;
242
- if (node.innerHTML)
243
- el.innerHTML = node.innerHTML;
244
- if (node.id)
245
- el.id = node.id;
246
- if (node.classes)
247
- el.classList.add(...node.classes);
248
- if (node.buttonDisabled)
249
- el.setAttribute("disabled", `${node.buttonDisabled}`);
250
- return el;
251
- };
252
- this.updateAriaAlert = (messageText) => {
278
+ updateAriaAlert(messageText) {
279
+ if (messageText) {
253
280
  while (this.ariaLiveContainer.firstChild) {
254
281
  this.ariaLiveContainer.removeChild(this.ariaLiveContainer.firstChild);
255
282
  }
256
283
  /* Create new message and append it to the live region */
257
284
  const messageNode = document.createTextNode(messageText);
258
285
  this.ariaLiveContainer.appendChild(messageNode);
259
- };
260
- this.updateDom = () => {
261
- const vDomChanged = this.vDomHasChanged(this.state.virtualDom, this.createVirtualDom());
262
- if (vDomChanged) {
263
- document.title = this.state.heading;
264
- this.state.virtualDom = this.createVirtualDom();
265
- const elements = this.state.virtualDom.map(this.convert);
266
- this.spinnerContainer.replaceChildren(...elements);
267
- }
268
- if (this.state.error) {
269
- this.spinnerContainer.classList.add("spinner-container__error");
270
- }
271
- if (this.state.done) {
272
- clearInterval(this.updateDomTimer);
273
- }
274
- if (this.config.ariaButtonEnabledMessage &&
275
- this.state.ariaButtonEnabledMessage !== "") {
276
- this.updateAriaAlert(this.config.ariaButtonEnabledMessage);
277
- }
278
- };
279
- // For the Aria alert to work reliably we need to create its container once and then update the contents
280
- // https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/
281
- // So here we create a separate DOM element for the Aria live text that won't be touched when the spinner updates.
282
- this.initialiseContainers = () => {
283
- this.spinnerContainer = document.createElement("div");
284
- this.ariaLiveContainer = document.createElement("div");
285
- this.ariaLiveContainer.setAttribute("aria-live", "assertive");
286
- this.ariaLiveContainer.classList.add("govuk-visually-hidden");
287
- this.ariaLiveContainer.appendChild(document.createTextNode(""));
288
- this.container.replaceChildren(this.spinnerContainer, this.ariaLiveContainer);
289
- };
290
- this.initTimer = (initTime) => {
291
- this.updateAccordingToTimeElapsed(initTime);
292
- this.updateDomTimer = setInterval(() => {
293
- this.updateAccordingToTimeElapsed(initTime);
294
- this.updateDom();
295
- }, this.config.msBetweenDomUpdate);
296
- };
297
- this.handleAbort = () => {
298
- this.abortController.abort();
299
- };
300
- this.initialiseAbortController = () => {
301
- this.abortController = new AbortController();
302
- window.removeEventListener("beforeunload", this.handleAbort);
303
- window.addEventListener("beforeunload", this.handleAbort);
304
- };
305
- this.container = domContainer;
306
- this.initialiseContent(this.container);
307
- this.initialiseState();
308
- this.initialiseAbortController();
286
+ }
309
287
  }
310
288
  }
311
289
 
312
- export { useSpinner };
290
+ function initialiseProgressButtons() {
291
+ const progressButtons = document.querySelectorAll('[data-frontendui="di-progress-button"]');
292
+ progressButtons.forEach((button) => {
293
+ button.addEventListener('click', (event) => {
294
+ const waitingText = button.getAttribute('data-waiting-text');
295
+ const longWaitingText = button.getAttribute('data-long-waiting-text');
296
+ const errorPage = button.getAttribute('data-error-page');
297
+ const isInput = button.tagName.toLowerCase() === 'input';
298
+ if (!waitingText || !longWaitingText || !errorPage) {
299
+ console.error('Progress button is missing required data attributes.');
300
+ return;
301
+ }
302
+ handleProgressButtonClick(button, waitingText, longWaitingText, errorPage, isInput);
303
+ });
304
+ });
305
+ }
306
+ const handleProgressButtonClick = (element, waitingText, longWaitingText, errorPage, isInput = false) => {
307
+ element.blur();
308
+ element.setAttribute('data-prevent-double-click', 'true');
309
+ element.classList.add('govuk-button--progress-loading');
310
+ if (isInput && element instanceof HTMLInputElement) {
311
+ element.value = waitingText;
312
+ }
313
+ else {
314
+ element.innerText = waitingText;
315
+ }
316
+ element.setAttribute('aria-label', waitingText);
317
+ setTimeout(() => {
318
+ if (isInput && element instanceof HTMLInputElement) {
319
+ element.value = longWaitingText;
320
+ }
321
+ else {
322
+ element.innerText = longWaitingText;
323
+ }
324
+ element.setAttribute('aria-label', longWaitingText);
325
+ }, 5000);
326
+ setTimeout(() => {
327
+ window.location.href = errorPage;
328
+ }, 10000);
329
+ };
330
+
331
+ export { PollResult, initialiseProgressButtons, useSpinner };
@@ -0,0 +1,2 @@
1
+ export declare function initialiseProgressButtons(): void;
2
+ //# sourceMappingURL=progress-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress-button.d.ts","sourceRoot":"","sources":["../../../../frontend-src/progress-button/progress-button.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,SAgBxC"}
@@ -1,6 +1,2 @@
1
1
  export declare function wait(ms: number): Promise<unknown>;
2
- export declare function waitUntil(conditionFn: Function, { timeout, interval }?: {
3
- timeout?: number | undefined;
4
- interval?: number | undefined;
5
- }): Promise<void>;
6
2
  //# sourceMappingURL=spinner.test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.test.d.ts","sourceRoot":"","sources":["../../../../../frontend-src/spinner/__tests__/spinner.test.ts"],"names":[],"mappings":"AAEA,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,oBAE9B;AAED,wBAAsB,SAAS,CAC7B,WAAW,EAAE,QAAQ,EACrB,EAAE,OAAc,EAAE,QAAa,EAAE;;;CAAK,iBAcvC"}
1
+ {"version":3,"file":"spinner.test.d.ts","sourceRoot":"","sources":["../../../../../frontend-src/spinner/__tests__/spinner.test.ts"],"names":[],"mappings":"AAGA,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,oBAE9B"}