@govuk-one-login/frontend-ui 2.0.0 → 3.0.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.
- package/README.md +37 -0
- package/build/all.css +1 -1
- package/build/cjs/backend/index.cjs +14 -2
- package/build/cjs/backend/index.d.cts +10 -0
- package/build/cjs/backend/index.d.ts +10 -0
- package/build/cjs/backend/index.d.ts.map +1 -1
- package/build/cjs/frontend/index.cjs +225 -268
- package/build/cjs/frontend/index.d.cts +2 -1
- package/build/cjs/frontend/index.d.ts +2 -1
- package/build/cjs/frontend/index.d.ts.map +1 -1
- package/build/cjs/frontend/spinner/__tests__/spinner.test.d.ts +0 -4
- package/build/cjs/frontend/spinner/__tests__/spinner.test.d.ts.map +1 -1
- package/build/cjs/frontend/spinner/spinner.d.ts +54 -84
- package/build/cjs/frontend/spinner/spinner.d.ts.map +1 -1
- package/build/components/_all.scss +1 -0
- package/build/components/progress-button/_index.scss +44 -0
- package/build/components/progress-button/macro.njk +2 -0
- package/build/components/progress-button/progress-button.yaml +13 -0
- package/build/components/progress-button/template.njk +120 -0
- package/build/components/spinner/README.md +70 -52
- package/build/components/spinner/_index.scss +2 -11
- package/build/components/spinner/template.njk +15 -8
- package/build/esm/backend/index.d.ts +10 -0
- package/build/esm/backend/index.d.ts.map +1 -1
- package/build/esm/backend/index.js +14 -2
- package/build/esm/frontend/index.d.ts +2 -1
- package/build/esm/frontend/index.d.ts.map +1 -1
- package/build/esm/frontend/index.js +226 -269
- package/build/esm/frontend/spinner/__tests__/spinner.test.d.ts +0 -4
- package/build/esm/frontend/spinner/__tests__/spinner.test.d.ts.map +1 -1
- package/build/esm/frontend/spinner/spinner.d.ts +54 -84
- package/build/esm/frontend/spinner/spinner.d.ts.map +1 -1
- package/package.json +1 -1
- package/build/components/spinner/api.njk +0 -27
|
@@ -1,173 +1,246 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
function useSpinner() {
|
|
4
|
-
const element = document.getElementById(
|
|
5
|
-
if (element) {
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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
|
+
})(exports.PollResult || (exports.PollResult = {}));
|
|
28
|
+
var SpinnerState;
|
|
29
|
+
(function (SpinnerState) {
|
|
30
|
+
SpinnerState[SpinnerState["Waiting"] = 0] = "Waiting";
|
|
31
|
+
SpinnerState[SpinnerState["LongWaiting"] = 1] = "LongWaiting";
|
|
32
|
+
SpinnerState[SpinnerState["Error"] = 2] = "Error";
|
|
33
|
+
SpinnerState[SpinnerState["Complete"] = 3] = "Complete";
|
|
34
|
+
})(SpinnerState || (SpinnerState = {}));
|
|
13
35
|
class Spinner {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
constructor(domContainer, pollingFunction, onSuccess, onError) {
|
|
37
|
+
this.state = SpinnerState.Waiting;
|
|
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");
|
|
17
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");
|
|
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();
|
|
18
140
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
141
|
+
async init() {
|
|
142
|
+
const initTime = this.getInitTime();
|
|
143
|
+
this.updateStateAccordingToTimeElapsed(initTime);
|
|
144
|
+
this.updateDom();
|
|
145
|
+
this.initTimer(initTime);
|
|
146
|
+
await this.callPollingFunction(initTime);
|
|
147
|
+
}
|
|
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`);
|
|
31
152
|
}
|
|
153
|
+
return element;
|
|
32
154
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
};
|
|
176
|
+
}
|
|
177
|
+
createAbortController() {
|
|
178
|
+
const abortController = new AbortController();
|
|
179
|
+
window.removeEventListener("beforeunload", this.handleAbort);
|
|
180
|
+
window.addEventListener("beforeunload", this.handleAbort);
|
|
181
|
+
return abortController;
|
|
182
|
+
}
|
|
183
|
+
getInitTime() {
|
|
184
|
+
const storedSpinnerInitTime = sessionStorage.getItem("spinnerInitTime");
|
|
185
|
+
let spinnerInitTime;
|
|
186
|
+
if (storedSpinnerInitTime === null) {
|
|
187
|
+
spinnerInitTime = Date.now();
|
|
188
|
+
sessionStorage.setItem("spinnerInitTime", spinnerInitTime.toString());
|
|
40
189
|
}
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
|
190
|
+
else {
|
|
191
|
+
spinnerInitTime = parseInt(storedSpinnerInitTime, 10);
|
|
90
192
|
}
|
|
91
|
-
|
|
92
|
-
|
|
193
|
+
return spinnerInitTime;
|
|
194
|
+
}
|
|
195
|
+
hasCompleted() {
|
|
196
|
+
return (this.state === SpinnerState.Error || this.state === SpinnerState.Complete);
|
|
197
|
+
}
|
|
198
|
+
reflectLongWait() {
|
|
199
|
+
if (!this.hasCompleted()) {
|
|
200
|
+
this.state = SpinnerState.LongWaiting;
|
|
93
201
|
}
|
|
94
202
|
}
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
203
|
+
createSpinnerElement(spinnerStateClass) {
|
|
204
|
+
const spinner = document.createElement("div");
|
|
205
|
+
spinner.id = "spinner";
|
|
206
|
+
spinner.classList.add("spinner", "centre");
|
|
207
|
+
if (spinnerStateClass) {
|
|
208
|
+
spinner.classList.add(spinnerStateClass);
|
|
209
|
+
}
|
|
210
|
+
return spinner;
|
|
147
211
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
212
|
+
cloneAndAddIfExists(list, element) {
|
|
213
|
+
if (element) {
|
|
214
|
+
const cloned = element.cloneNode(true);
|
|
215
|
+
cloned.style.display = "";
|
|
216
|
+
cloned.removeAttribute("id");
|
|
217
|
+
list.push(cloned);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async callPollingFunction(initTime) {
|
|
221
|
+
if (Date.now() - initTime >= this.config.msBeforeAbort) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await this.pollingFunction(this.abortController.signal)
|
|
225
|
+
.then((response) => {
|
|
226
|
+
if (response === exports.PollResult.Success) {
|
|
227
|
+
this.reflectSuccess();
|
|
155
228
|
}
|
|
156
|
-
else if (
|
|
229
|
+
else if (response === exports.PollResult.Failure) {
|
|
157
230
|
this.reflectError();
|
|
158
231
|
}
|
|
159
|
-
else if (this.
|
|
232
|
+
else if (!this.hasCompleted()) {
|
|
160
233
|
setTimeout(async () => {
|
|
161
234
|
if (Date.now() - initTime >= this.config.msBeforeAbort) {
|
|
162
235
|
return;
|
|
163
236
|
}
|
|
164
|
-
await this.
|
|
237
|
+
await this.callPollingFunction(initTime);
|
|
165
238
|
}, this.config.msBetweenRequests);
|
|
166
239
|
}
|
|
167
240
|
})
|
|
168
241
|
.catch((error) => {
|
|
169
242
|
if (error.name !== "AbortError") {
|
|
170
|
-
console.error("Error in
|
|
243
|
+
console.error("Error in polling function: ", error);
|
|
171
244
|
this.reflectError();
|
|
172
245
|
}
|
|
173
246
|
})
|
|
@@ -175,139 +248,23 @@ class Spinner {
|
|
|
175
248
|
this.updateDom();
|
|
176
249
|
});
|
|
177
250
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
else {
|
|
186
|
-
spinnerInitTime = parseInt(storedSpinnerInitTime, 10);
|
|
187
|
-
}
|
|
188
|
-
return spinnerInitTime;
|
|
189
|
-
}
|
|
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
|
-
}
|
|
251
|
+
createAriaLiveContainer() {
|
|
252
|
+
// For the Aria alert to work reliably we need to create its container once and then update the contents
|
|
253
|
+
// https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/
|
|
254
|
+
const container = document.createElement("div");
|
|
255
|
+
container.setAttribute("aria-live", "assertive");
|
|
256
|
+
container.classList.add("govuk-visually-hidden");
|
|
257
|
+
return container;
|
|
198
258
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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) => {
|
|
259
|
+
updateAriaAlert(messageText) {
|
|
260
|
+
if (messageText) {
|
|
255
261
|
while (this.ariaLiveContainer.firstChild) {
|
|
256
262
|
this.ariaLiveContainer.removeChild(this.ariaLiveContainer.firstChild);
|
|
257
263
|
}
|
|
258
264
|
/* Create new message and append it to the live region */
|
|
259
265
|
const messageNode = document.createTextNode(messageText);
|
|
260
266
|
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();
|
|
267
|
+
}
|
|
311
268
|
}
|
|
312
269
|
}
|
|
313
270
|
|
|
@@ -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"}
|
|
@@ -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":"
|
|
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"}
|