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