@govuk-one-login/frontend-ui 3.0.0 → 3.1.1

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.
@@ -76,7 +76,8 @@ var skipLink$1 = {
76
76
  var progressButton$1 = {
77
77
  text: "Parhau",
78
78
  waitingText: "Aros",
79
- longWaitingText: "Parhau i aros"
79
+ longWaitingText: "Parhau i aros",
80
+ noJavascriptMessage: "Gall gymryd hyd at 10 eiliad i barhau i'r dudalen nesaf. Ar ôl i chi barhau, peidiwch ag ail-lwytho na chau'r dudalen hon."
80
81
  };
81
82
  var translationCy = {
82
83
  cookieBanner: cookieBanner$1,
@@ -161,7 +162,8 @@ var skipLink = {
161
162
  var progressButton = {
162
163
  text: "Continue",
163
164
  waitingText: "Wait",
164
- longWaitingText: "Keep waiting"
165
+ longWaitingText: "Keep waiting",
166
+ noJavascriptMessage: "It can take up to 10 seconds to continue to the next page. After you continue, do not reload or close this page."
165
167
  };
166
168
  var translationEn = {
167
169
  cookieBanner: cookieBanner,
@@ -95,6 +95,7 @@ export declare const frontendUiTranslationEn: {
95
95
  text: string;
96
96
  waitingText: string;
97
97
  longWaitingText: string;
98
+ noJavascriptMessage: string;
98
99
  };
99
100
  };
100
101
  export declare const frontendUiTranslationCy: {
@@ -158,6 +159,7 @@ export declare const frontendUiTranslationCy: {
158
159
  text: string;
159
160
  waitingText: string;
160
161
  longWaitingText: string;
162
+ noJavascriptMessage: string;
161
163
  };
162
164
  };
163
165
  export {};
@@ -95,6 +95,7 @@ export declare const frontendUiTranslationEn: {
95
95
  text: string;
96
96
  waitingText: string;
97
97
  longWaitingText: string;
98
+ noJavascriptMessage: string;
98
99
  };
99
100
  };
100
101
  export declare const frontendUiTranslationCy: {
@@ -158,6 +159,7 @@ export declare const frontendUiTranslationCy: {
158
159
  text: string;
159
160
  waitingText: string;
160
161
  longWaitingText: string;
162
+ noJavascriptMessage: string;
161
163
  };
162
164
  };
163
165
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQ1D,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,EAAE;YAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC;KAClC,CAAC;CACH;AAED,UAAU,cAAe,SAAQ,OAAO;IACtC,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,eAAgB,SAAQ,QAAQ;IACxC,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAGD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAER,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAaR,eAAO,MAAM,yBAAyB,GAAI,cAAc,OAAO,OAAO,SAerE,CAAC;AAGF,eAAO,MAAM,kCAAkC,GAC7C,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,YAAY,SAUnB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,UAU3D;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAkBhE;AAED,eAAO,MAAM,mBAAmB,GAC9B,cAAc,OAAO,OAAO,EAC5B,WAAW,MAAM,SASlB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,WAAW,MAAM,KAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAuBxB,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC;AACrD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQ1D,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,EAAE;YAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC;KAClC,CAAC;CACH;AAED,UAAU,cAAe,SAAQ,OAAO;IACtC,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,eAAgB,SAAQ,QAAQ;IACxC,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAGD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAER,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAaR,eAAO,MAAM,yBAAyB,GAAI,cAAc,OAAO,OAAO,SAerE,CAAC;AAGF,eAAO,MAAM,kCAAkC,GAC7C,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,YAAY,SAUnB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,UAU3D;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAkBhE;AAED,eAAO,MAAM,mBAAmB,GAC9B,cAAc,OAAO,OAAO,EAC5B,WAAW,MAAM,SASlB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,WAAW,MAAM,KAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAuBxB,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC;AACrD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
@@ -24,6 +24,7 @@ exports.PollResult = void 0;
24
24
  PollResult[PollResult["Success"] = 0] = "Success";
25
25
  PollResult[PollResult["Failure"] = 1] = "Failure";
26
26
  PollResult[PollResult["Pending"] = 2] = "Pending";
27
+ PollResult[PollResult["Backoff"] = 3] = "Backoff";
27
28
  })(exports.PollResult || (exports.PollResult = {}));
28
29
  var SpinnerState;
29
30
  (function (SpinnerState) {
@@ -35,6 +36,7 @@ var SpinnerState;
35
36
  class Spinner {
36
37
  constructor(domContainer, pollingFunction, onSuccess, onError) {
37
38
  this.state = SpinnerState.Waiting;
39
+ this.backOffCount = 0;
38
40
  this.handleAbort = () => {
39
41
  this.abortController.abort();
40
42
  };
@@ -172,6 +174,9 @@ class Spinner {
172
174
  hideSpinnerOnError: element.dataset.hideSpinnerOnError
173
175
  ? (element.dataset.hideSpinnerOnError === 'true')
174
176
  : false,
177
+ maxBackoffTries: element.dataset.maxBackoffTries
178
+ ? parseInt(element.dataset.maxBackoffTries)
179
+ : 3,
175
180
  };
176
181
  }
177
182
  createAbortController() {
@@ -230,12 +235,24 @@ class Spinner {
230
235
  this.reflectError();
231
236
  }
232
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
+ }
233
250
  setTimeout(async () => {
234
251
  if (Date.now() - initTime >= this.config.msBeforeAbort) {
235
252
  return;
236
253
  }
237
254
  await this.callPollingFunction(initTime);
238
- }, this.config.msBetweenRequests);
255
+ }, timeToNextPoll);
239
256
  }
240
257
  })
241
258
  .catch((error) => {
@@ -248,6 +265,10 @@ class Spinner {
248
265
  this.updateDom();
249
266
  });
250
267
  }
268
+ calculateBackoffTime(backOffCount) {
269
+ const extraDelay = Math.pow(2, backOffCount - 2) * this.config.msBetweenRequests;
270
+ return this.config.msBetweenRequests + extraDelay;
271
+ }
251
272
  createAriaLiveContainer() {
252
273
  // For the Aria alert to work reliably we need to create its container once and then update the contents
253
274
  // https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/
@@ -268,4 +289,109 @@ class Spinner {
268
289
  }
269
290
  }
270
291
 
292
+ function initialiseProgressButtons(document = window.document) {
293
+ const progressButtons = Array.prototype.slice.call(document.querySelectorAll('[data-frontendui="di-progress-button"]'));
294
+ function findClosestForm(element) {
295
+ let el = element;
296
+ while (el && el.nodeName !== 'FORM') {
297
+ el = el.parentElement;
298
+ }
299
+ return el;
300
+ }
301
+ progressButtons.forEach(function (button) {
302
+ const form = findClosestForm(button);
303
+ let isSubmitting = false;
304
+ button.addEventListener('click', function (event) {
305
+ if (isSubmitting) {
306
+ event.preventDefault();
307
+ return;
308
+ }
309
+ const waitingText = button.getAttribute('data-waiting-text');
310
+ const longWaitingText = button.getAttribute('data-long-waiting-text');
311
+ const errorPage = button.getAttribute('data-error-page');
312
+ const isInput = button.tagName.toLowerCase() === 'input';
313
+ if (!waitingText || !longWaitingText || !errorPage) {
314
+ console.error('Progress button is missing required data attributes.');
315
+ return;
316
+ }
317
+ isSubmitting = true;
318
+ // Always handle the button click, regardless of form presence
319
+ handleProgressButtonClick(button, waitingText, longWaitingText, errorPage, isInput);
320
+ // If no form, we're done. If there is a form, the form submit handler will take over
321
+ if (!form) {
322
+ return;
323
+ }
324
+ // For form buttons, let the click propagate to trigger form submission
325
+ });
326
+ if (form) {
327
+ form.addEventListener('submit', function (event) {
328
+ // The button click handler has already set isSubmitting and handled the button state
329
+ if (isSubmitting) {
330
+ // Allow the first submission, prevent subsequent ones
331
+ if (event.target === form && event.submitter === button) {
332
+ return; // Allow the first submission to proceed
333
+ }
334
+ event.preventDefault ? event.preventDefault() : (event.returnValue = false);
335
+ return;
336
+ }
337
+ // If the form is submitted through another method (not our button),
338
+ // prevent double submission but don't show progress state
339
+ isSubmitting = true;
340
+ });
341
+ }
342
+ });
343
+ }
344
+ function handleProgressButtonClick(element, waitingText, longWaitingText, errorPage, isInput) {
345
+ var originalText = isInput && element instanceof HTMLInputElement ? element.value : element.innerText;
346
+ if (typeof element.blur === 'function') {
347
+ element.blur();
348
+ }
349
+ element.setAttribute('data-prevent-double-click', 'true');
350
+ var classes = element.className.split(' ');
351
+ if (classes.indexOf('govuk-button--progress-loading') === -1) {
352
+ classes.push('govuk-button--progress-loading');
353
+ element.className = classes.join(' ');
354
+ }
355
+ if (isInput && element instanceof HTMLInputElement) {
356
+ element.value = waitingText;
357
+ }
358
+ else {
359
+ element.innerText = waitingText;
360
+ }
361
+ element.setAttribute('aria-label', waitingText);
362
+ var longWaitTimeout = window.setTimeout(function () {
363
+ if (isInput && element instanceof HTMLInputElement) {
364
+ element.value = longWaitingText;
365
+ }
366
+ else {
367
+ element.innerText = longWaitingText;
368
+ }
369
+ element.setAttribute('aria-label', longWaitingText);
370
+ }, 5000);
371
+ var errorTimeout = window.setTimeout(function () {
372
+ window.location.href = errorPage;
373
+ }, 10000);
374
+ function resetButton() {
375
+ var classes = element.className.split(' ');
376
+ var loadingIndex = classes.indexOf('govuk-button--progress-loading');
377
+ if (loadingIndex !== -1) {
378
+ classes.splice(loadingIndex, 1);
379
+ element.className = classes.join(' ');
380
+ }
381
+ element.setAttribute('data-prevent-double-click', 'false');
382
+ if (isInput && element instanceof HTMLInputElement) {
383
+ element.value = originalText;
384
+ }
385
+ else {
386
+ element.innerText = originalText;
387
+ }
388
+ element.setAttribute('aria-label', originalText);
389
+ window.clearTimeout(errorTimeout);
390
+ window.clearTimeout(longWaitTimeout);
391
+ }
392
+ element.resetProgressButton = resetButton;
393
+ return resetButton;
394
+ }
395
+
396
+ exports.initialiseProgressButtons = initialiseProgressButtons;
271
397
  exports.useSpinner = useSpinner;
@@ -1,3 +1,4 @@
1
1
  export { useSpinner, PollResult } from "./spinner/spinner";
2
2
  export type { PollingFunction } from "./spinner/spinner";
3
+ export { initialiseProgressButtons } from "./progress-button/progress-button";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export { useSpinner, PollResult } from "./spinner/spinner";
2
2
  export type { PollingFunction } from "./spinner/spinner";
3
+ export { initialiseProgressButtons } from "./progress-button/progress-button";
3
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,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,eAAe,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(document?: Document): 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,CAAC,QAAQ,GAAE,QAA0B,QAgE7E"}
@@ -3,7 +3,8 @@ export type PollingFunction = (abortSignal: AbortSignal) => Promise<PollResult>;
3
3
  export declare enum PollResult {
4
4
  Success = 0,
5
5
  Failure = 1,
6
- Pending = 2
6
+ Pending = 2,
7
+ Backoff = 3
7
8
  }
8
9
  declare enum SpinnerState {
9
10
  Waiting = 0,
@@ -18,6 +19,7 @@ type SpinnerConfig = {
18
19
  msBetweenDomUpdate: number;
19
20
  ariaAlertCompletionText?: string;
20
21
  hideSpinnerOnError: boolean;
22
+ maxBackoffTries: number;
21
23
  };
22
24
  export declare class Spinner {
23
25
  container: HTMLDivElement;
@@ -33,6 +35,7 @@ export declare class Spinner {
33
35
  updateDomTimer?: NodeJS.Timeout;
34
36
  abortController: AbortController;
35
37
  config: SpinnerConfig;
38
+ backOffCount: number;
36
39
  pollingFunction: PollingFunction;
37
40
  onSuccess: VoidFunction;
38
41
  onError: VoidFunction;
@@ -53,6 +56,7 @@ export declare class Spinner {
53
56
  private updateDom;
54
57
  private cloneAndAddIfExists;
55
58
  private callPollingFunction;
59
+ private calculateBackoffTime;
56
60
  private createAriaLiveContainer;
57
61
  private updateAriaAlert;
58
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../../../frontend-src/spinner/spinner.ts"],"names":[],"mappings":"AAAA,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,iBAoBjJ;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEhF,oBAAY,UAAU;IACpB,OAAO,IAAA;IACP,OAAO,IAAA;IACP,OAAO,IAAA;CACR;AAED,aAAK,YAAY;IACf,OAAO,IAAA;IACP,WAAW,IAAA;IACX,KAAK,IAAA;IACL,QAAQ,IAAA;CACT;AAED,KAAK,aAAa,GAAG;IACnB,2BAA2B,EAAE,MAAM,CAAC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,qBAAa,OAAO;IAClB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,cAAc,CAAC,EAAE,WAAW,CAAC;IAC7B,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,iBAAiB,EAAE,cAAc,CAAC;IAElC,wBAAwB,EAAE,cAAc,CAAC;IAEzC,KAAK,EAAE,YAAY,CAAwB;IAC3C,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAChC,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,aAAa,CAAC;IAEtB,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;gBAGpB,YAAY,EAAE,cAAc,EAC5B,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY;IA2CjB,IAAI;IAUV,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,SAAS;IAuBjB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,SAAS,CAKf;IAEF,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,cAAc,CAGpB;IAEF,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,iCAAiC,CAWvC;IAEF,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,SAAS,CAkDf;IAEF,OAAO,CAAC,mBAAmB;YASb,mBAAmB;IA8BjC,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,eAAe;CAWxB"}
1
+ {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../../../frontend-src/spinner/spinner.ts"],"names":[],"mappings":"AAAA,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,iBAoBjJ;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEhF,oBAAY,UAAU;IACpB,OAAO,IAAA;IACP,OAAO,IAAA;IACP,OAAO,IAAA;IACP,OAAO,IAAA;CACR;AAED,aAAK,YAAY;IACf,OAAO,IAAA;IACP,WAAW,IAAA;IACX,KAAK,IAAA;IACL,QAAQ,IAAA;CACT;AAED,KAAK,aAAa,GAAG;IACnB,2BAA2B,EAAE,MAAM,CAAC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,qBAAa,OAAO;IAClB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,cAAc,CAAC,EAAE,WAAW,CAAC;IAC7B,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,iBAAiB,EAAE,cAAc,CAAC;IAElC,wBAAwB,EAAE,cAAc,CAAC;IAEzC,KAAK,EAAE,YAAY,CAAwB;IAC3C,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAChC,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,EAAE,MAAM,CAAK;IAEzB,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;gBAGpB,YAAY,EAAE,cAAc,EAC5B,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY;IA2CjB,IAAI;IAUV,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,SAAS;IA0BjB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,SAAS,CAKf;IAEF,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,cAAc,CAGpB;IAEF,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,iCAAiC,CAWvC;IAEF,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,SAAS,CAkDf;IAEF,OAAO,CAAC,mBAAmB;YASb,mBAAmB;IA2CjC,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,eAAe;CAWxB"}
@@ -1,5 +1,15 @@
1
1
  {% set progressButton = params.translations %}
2
-
2
+ <noscript>
3
+ <style>
4
+ .noscript {
5
+ border: #F3F3F3 3px solid;
6
+ padding-top: 28px;
7
+ padding-left: 12px;
8
+ padding-right: 12px;
9
+ margin-bottom: 12px;
10
+ }
11
+ </style>
12
+ </noscript>
3
13
 
4
14
  {# Set classes for this component #}
5
15
  {%- set classNames = "govuk-button govuk-button--progress" -%}
@@ -23,98 +33,83 @@
23
33
 
24
34
  {% if params.isStartButton %}
25
35
  {% set iconHtml %}
26
- {#- The SVG needs `focusable="false"` so that Internet Explorer does not
36
+ {#- The SVG needs `focusable="false"` so that Internet Explorer does not
27
37
  treat it as an interactive element - without this it will be
28
38
  'focusable' when using the keyboard to navigate. #}
29
- <svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
30
- <path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"/>
31
- </svg>
39
+ <svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
40
+ <path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"/>
41
+ </svg>
32
42
  {% endset %}
33
43
  {% set classNames = classNames + " govuk-button--start" %}
34
44
  {% endif %}
35
45
 
36
46
  {#- Define common attributes that we can use across all element types #}
37
47
 
38
- {%- set commonAttributes %} class="{{ classNames }}" data-module="govuk-button"{% for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}{% if params.id %} id="{{ params.id }}"{% endif %}{% endset %}
48
+ {%- set commonAttributes %} data-module="govuk-button"{% for attribute, value in params.attributes %}
49
+ {{attribute}}="{{value}}"{% endfor %}
50
+ {% if params.id %} id="{{ params.id }}"{% endif %}
51
+ {% endset %}
39
52
 
40
53
  {#- Define common attributes we can use for both button and input types #}
41
54
 
42
- {%- set buttonAttributes %}{% if params.name %} name="{{ params.name }}"{% endif %}{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick !== undefined %} data-prevent-double-click="{{params.preventDoubleClick}}"{% endif %}{% endset %}
43
-
55
+ {%- set buttonAttributes %}
56
+ {% if params.name %} name="{{ params.name }}"{% endif %}
57
+ {% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}
58
+ {% if params.preventDoubleClick !== undefined %} data-prevent-double-click="{{params.preventDoubleClick}}"{% endif %}{% endset %}
44
59
 
45
60
  {%- if element == 'a' %}
46
- <a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{- commonAttributes | safe }}
47
- aria-live="assertive"
48
- onclick="
49
- this.blur();
50
- this.dataPreventDoubleClick = 'true';
51
- this.classList.add('govuk-button--progress-loading');
52
- this.innerText = '{{ progressButton.waitingText }}';
53
- this.ariaLabel = '{{ progressButton.waitingText }}';
54
- setTimeout(() => {
55
- this.innerText = '{{ progressButton.longWaitingText }}';
56
- this.ariaLabel = '{{ progressButton.longWaitingText }}';
57
- }, 5000);
58
- setTimeout(() => {
59
- window.location.href = '{{ params.errorPage | default('#') }}';
60
- }, 10000);
61
- ">
62
- {% if params.html %}
63
- {{ params.html | safe }}
64
- {% elseif params.text %}
65
- {{ params.text | safe }}
66
- {% else %}
67
- {{ progressButton.text }}
68
- {% endif %}
69
- {{- iconHtml | safe | trim | indent(2, true) if iconHtml -}}
70
- </a>
61
+ <div class="noscript">
62
+ <noscript>
63
+ <div class="govuk-body" >
64
+ {{ progressButton.noJavascriptMessage }}
65
+ </div>
66
+ </noscript>
67
+
68
+ <a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{- commonAttributes | safe }} class = "{{ classNames }}"
69
+ aria-live="assertive" data-frontendui="di-progress-button" data-waiting-text="{{ progressButton.waitingText }}" data-long-waiting-text="{{ progressButton.longWaitingText }}" data-error-page="{{ params.errorPage | default('#') }}" >
70
+ {% if params.html %}
71
+ {{ params.html | safe }}
72
+ {% elseif params.text %}
73
+ {{ params.text | safe }}
74
+ {% else %}
75
+ {{ progressButton.text }}
76
+ {% endif %}
77
+ {{- iconHtml | safe | trim | indent(2, true) if iconHtml -}}
78
+ </a>
79
+ </div>
71
80
 
72
81
  {%- elseif element == 'button' %}
73
- <button {%- if params.value %} value="{{ params.value }}"{% endif %}{%- if params.type %} type="{{ params.type }}"{% endif %} {{- buttonAttributes | safe }} {{- commonAttributes | safe }}
74
- aria-live="assertive"
75
- onclick="
76
- this.blur();
77
- this.dataPreventDoubleClick = 'true';
78
- this.classList.add('govuk-button--progress-loading');
79
- this.innerText = '{{ progressButton.waitingText }}';
80
- this.ariaLabel = '{{ progressButton.waitingText }}';
81
- setTimeout(() => {
82
- this.innerText = '{{ progressButton.longWaitingText }}';
83
- this.ariaLabel = '{{ progressButton.longWaitingText }}';
84
- }, 5000);
85
- setTimeout(() => {
86
- window.location.href = '{{ params.errorPage | default('#') }}';
87
- }, 10000);
88
- ">
89
- {% if params.html %}
90
- {{ params.html | safe }}
91
- {% elseif params.text %}
92
- {{ params.text | safe }}
93
- {% else %}
94
- {{ progressButton.text }}
95
- {% endif %}
96
- {{- iconHtml | safe | trim | indent(2, true) if iconHtml -}}
82
+ <div class="noscript">
83
+ <noscript>
84
+ <div class="govuk-body" >
85
+ {{ progressButton.noJavascriptMessage }}
86
+ </div>
87
+ </noscript>
88
+
89
+ <button {%- if params.value %} value="{{ params.value }}"{% endif %}{%- if params.type %} type="{{ params.type }}"{% endif %} {{- buttonAttributes | safe }} {{- commonAttributes | safe }} class = "{{ classNames }}"
90
+ aria-live="assertive" data-frontendui="di-progress-button" data-waiting-text="{{ progressButton.waitingText }}" data-long-waiting-text="{{ progressButton.longWaitingText }}" data-error-page="{{ params.errorPage | default('#') }}" >
91
+ {% if params.html %}
92
+ {{ params.html | safe }}
93
+ {% elseif params.text %}
94
+ {{ params.text | safe }}
95
+ {% else %}
96
+ {{ progressButton.text }}
97
+ {% endif %}
98
+ {{- iconHtml | safe | trim | indent(2, true) if iconHtml -}}
97
99
 
98
- </button>
100
+ </button>
101
+ </div>
99
102
 
100
103
  {%- elseif element == 'input' %}
101
- <input value="{{ params.value }}" type="{{ params.type if params.type else 'submit'}}"{{- buttonAttributes | safe }} {{- commonAttributes | safe }}
102
- aria-live="assertive"
103
- onclick="
104
- this.blur();
105
- this.dataPreventDoubleClick = 'true';
106
- this.classList.add('govuk-button--progress-loading');
107
- this.value = '{{ progressButton.waitingText }}';
108
- this.ariaLabel = '{{ progressButton.waitingText }}';
109
- setTimeout(() => {
110
- this.value = '{{ progressButton.longWaitingText }}';
111
- this.ariaLabel = '{{ progressButton.longWaitingText }}';
112
- }, 5000);
113
- setTimeout(() => {
114
- window.location.href = '{{ params.errorPage | default('#') }}';
115
- }, 10000);
116
- return false;
117
- "
118
- >
119
- {{- iconHtml | safe | trim | indent(2, true) if iconHtml -}}
120
- {% endif %}
104
+ <div class="noscript">
105
+ <noscript>
106
+ <div class="govuk-body" >
107
+ {{ progressButton.noJavascriptMessage }}
108
+ </div>
109
+ </noscript>
110
+ <input value="{{ params.value }}" type="{{ params.type if params.type else 'submit'}}"{{- buttonAttributes | safe }} {{- commonAttributes | safe }} class = "{{ classNames }}"
111
+ aria-live="assertive" data-frontendui="di-progress-button" data-waiting-text="{{ progressButton.waitingText }}" data-long-waiting-text="{{ progressButton.longWaitingText }}" data-error-page="{{ params.errorPage | default('#') }}" >
112
+ {{- iconHtml | safe | trim | indent(2, true) if iconHtml -}}
113
+ </div>
114
+
115
+ {% endif %}
@@ -25,6 +25,7 @@ For the spinner to perform correctly it requires the following:
25
25
  - An optional data attribute `data-ms-between-dom-update` to set the amount of time between updates to the spinner UI
26
26
  - An optional data attribute `data-ms-between-requests` to set the amount of time between calls to the `pollingFunction`
27
27
  - An optional data attribute `data-hide-spinner-on-error` to hide the spinner graphic on error (defaults to false)
28
+ - An optional data attribute `data-max-backoff-tries` to set the number of times in a row the polling function can return `backoff` before the error content is displayed (defaults to 3)
28
29
  - An optional data attribute `aria-alert-completion-text`. If supplied this text will be set as an aria alert if the spinner completes successfully
29
30
 
30
31
  For example:
@@ -36,6 +37,7 @@ For example:
36
37
  data-ms-between-dom-update="1000"
37
38
  data-ms-between-requests="2000"
38
39
  data-hide-spinner-on-error="true"
40
+ data-max-backoff-tries="3"
39
41
  data-aria-alert-completion-text="Task completed successfully, you may now continue">
40
42
  <div id="no-js-content"><p class="centre govuk-body">JS is disabled</p></div>
41
43
  <div id="wait-content" style="display:none"><p class="centre govuk-body">Waiting</p></div>
@@ -70,6 +72,9 @@ If a call to the polling function returns `success` the spinner will stop animat
70
72
  If a call to the polling function returns `failure` the spinner will stop animating, display the `error-content` content, and call the `errorFunction` (if specified)
71
73
  If the spinner waits past the `long-wait` duration while the polling function keeps returning `pending` the spinner will display the `long-wait-content` content
72
74
  If the spinner waits past the `abort` duration while the polling function keeps returning `pending` the spinner will stop animating, display the `error-content` content, and call the `errorFunction` (if specified)
75
+ The polling function can optionally return `backoff` if it should be tried again but after a delay (e.g. to account for network instability). On receiving `backoff` the spinner will exponentially increase the time before the
76
+ next call to the polling function. If the polling function returns `backoff` more times than the `max-backoff-tries` value in a row then the final call is treated as if had returned `failure` instead. If a call to the polling
77
+ function returns `pending` then the backoff counter resets.
73
78
 
74
79
  ### Page refreshes
75
80
 
@@ -95,6 +95,7 @@ export declare const frontendUiTranslationEn: {
95
95
  text: string;
96
96
  waitingText: string;
97
97
  longWaitingText: string;
98
+ noJavascriptMessage: string;
98
99
  };
99
100
  };
100
101
  export declare const frontendUiTranslationCy: {
@@ -158,6 +159,7 @@ export declare const frontendUiTranslationCy: {
158
159
  text: string;
159
160
  waitingText: string;
160
161
  longWaitingText: string;
162
+ noJavascriptMessage: string;
161
163
  };
162
164
  };
163
165
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQ1D,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,EAAE;YAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC;KAClC,CAAC;CACH;AAED,UAAU,cAAe,SAAQ,OAAO;IACtC,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,eAAgB,SAAQ,QAAQ;IACxC,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAGD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAER,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAaR,eAAO,MAAM,yBAAyB,GAAI,cAAc,OAAO,OAAO,SAerE,CAAC;AAGF,eAAO,MAAM,kCAAkC,GAC7C,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,YAAY,SAUnB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,UAU3D;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAkBhE;AAED,eAAO,MAAM,mBAAmB,GAC9B,cAAc,OAAO,OAAO,EAC5B,WAAW,MAAM,SASlB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,WAAW,MAAM,KAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAuBxB,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC;AACrD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQ1D,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,EAAE;YAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC;KAClC,CAAC;CACH;AAED,UAAU,cAAe,SAAQ,OAAO;IACtC,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,eAAgB,SAAQ,QAAQ;IACxC,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE;QACN,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAGD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAER,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,YAAY,GACjB,IAAI,CAAC;AAaR,eAAO,MAAM,yBAAyB,GAAI,cAAc,OAAO,OAAO,SAerE,CAAC;AAGF,eAAO,MAAM,kCAAkC,GAC7C,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,YAAY,SAUnB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,UAU3D;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAkBhE;AAED,eAAO,MAAM,mBAAmB,GAC9B,cAAc,OAAO,OAAO,EAC5B,WAAW,MAAM,SASlB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,WAAW,MAAM,KAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAuBxB,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC;AACrD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
@@ -74,7 +74,8 @@ var skipLink$1 = {
74
74
  var progressButton$1 = {
75
75
  text: "Parhau",
76
76
  waitingText: "Aros",
77
- longWaitingText: "Parhau i aros"
77
+ longWaitingText: "Parhau i aros",
78
+ noJavascriptMessage: "Gall gymryd hyd at 10 eiliad i barhau i'r dudalen nesaf. Ar ôl i chi barhau, peidiwch ag ail-lwytho na chau'r dudalen hon."
78
79
  };
79
80
  var translationCy = {
80
81
  cookieBanner: cookieBanner$1,
@@ -159,7 +160,8 @@ var skipLink = {
159
160
  var progressButton = {
160
161
  text: "Continue",
161
162
  waitingText: "Wait",
162
- longWaitingText: "Keep waiting"
163
+ longWaitingText: "Keep waiting",
164
+ noJavascriptMessage: "It can take up to 10 seconds to continue to the next page. After you continue, do not reload or close this page."
163
165
  };
164
166
  var translationEn = {
165
167
  cookieBanner: cookieBanner,
@@ -1,3 +1,4 @@
1
1
  export { useSpinner, PollResult } from "./spinner/spinner";
2
2
  export type { PollingFunction } from "./spinner/spinner";
3
+ export { initialiseProgressButtons } from "./progress-button/progress-button";
3
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,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,eAAe,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"}
@@ -22,6 +22,7 @@ var PollResult;
22
22
  PollResult[PollResult["Success"] = 0] = "Success";
23
23
  PollResult[PollResult["Failure"] = 1] = "Failure";
24
24
  PollResult[PollResult["Pending"] = 2] = "Pending";
25
+ PollResult[PollResult["Backoff"] = 3] = "Backoff";
25
26
  })(PollResult || (PollResult = {}));
26
27
  var SpinnerState;
27
28
  (function (SpinnerState) {
@@ -33,6 +34,7 @@ var SpinnerState;
33
34
  class Spinner {
34
35
  constructor(domContainer, pollingFunction, onSuccess, onError) {
35
36
  this.state = SpinnerState.Waiting;
37
+ this.backOffCount = 0;
36
38
  this.handleAbort = () => {
37
39
  this.abortController.abort();
38
40
  };
@@ -170,6 +172,9 @@ class Spinner {
170
172
  hideSpinnerOnError: element.dataset.hideSpinnerOnError
171
173
  ? (element.dataset.hideSpinnerOnError === 'true')
172
174
  : false,
175
+ maxBackoffTries: element.dataset.maxBackoffTries
176
+ ? parseInt(element.dataset.maxBackoffTries)
177
+ : 3,
173
178
  };
174
179
  }
175
180
  createAbortController() {
@@ -228,12 +233,24 @@ class Spinner {
228
233
  this.reflectError();
229
234
  }
230
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
+ }
231
248
  setTimeout(async () => {
232
249
  if (Date.now() - initTime >= this.config.msBeforeAbort) {
233
250
  return;
234
251
  }
235
252
  await this.callPollingFunction(initTime);
236
- }, this.config.msBetweenRequests);
253
+ }, timeToNextPoll);
237
254
  }
238
255
  })
239
256
  .catch((error) => {
@@ -246,6 +263,10 @@ class Spinner {
246
263
  this.updateDom();
247
264
  });
248
265
  }
266
+ calculateBackoffTime(backOffCount) {
267
+ const extraDelay = Math.pow(2, backOffCount - 2) * this.config.msBetweenRequests;
268
+ return this.config.msBetweenRequests + extraDelay;
269
+ }
249
270
  createAriaLiveContainer() {
250
271
  // For the Aria alert to work reliably we need to create its container once and then update the contents
251
272
  // https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/
@@ -266,4 +287,108 @@ class Spinner {
266
287
  }
267
288
  }
268
289
 
269
- export { PollResult, useSpinner };
290
+ function initialiseProgressButtons(document = window.document) {
291
+ const progressButtons = Array.prototype.slice.call(document.querySelectorAll('[data-frontendui="di-progress-button"]'));
292
+ function findClosestForm(element) {
293
+ let el = element;
294
+ while (el && el.nodeName !== 'FORM') {
295
+ el = el.parentElement;
296
+ }
297
+ return el;
298
+ }
299
+ progressButtons.forEach(function (button) {
300
+ const form = findClosestForm(button);
301
+ let isSubmitting = false;
302
+ button.addEventListener('click', function (event) {
303
+ if (isSubmitting) {
304
+ event.preventDefault();
305
+ return;
306
+ }
307
+ const waitingText = button.getAttribute('data-waiting-text');
308
+ const longWaitingText = button.getAttribute('data-long-waiting-text');
309
+ const errorPage = button.getAttribute('data-error-page');
310
+ const isInput = button.tagName.toLowerCase() === 'input';
311
+ if (!waitingText || !longWaitingText || !errorPage) {
312
+ console.error('Progress button is missing required data attributes.');
313
+ return;
314
+ }
315
+ isSubmitting = true;
316
+ // Always handle the button click, regardless of form presence
317
+ handleProgressButtonClick(button, waitingText, longWaitingText, errorPage, isInput);
318
+ // If no form, we're done. If there is a form, the form submit handler will take over
319
+ if (!form) {
320
+ return;
321
+ }
322
+ // For form buttons, let the click propagate to trigger form submission
323
+ });
324
+ if (form) {
325
+ form.addEventListener('submit', function (event) {
326
+ // The button click handler has already set isSubmitting and handled the button state
327
+ if (isSubmitting) {
328
+ // Allow the first submission, prevent subsequent ones
329
+ if (event.target === form && event.submitter === button) {
330
+ return; // Allow the first submission to proceed
331
+ }
332
+ event.preventDefault ? event.preventDefault() : (event.returnValue = false);
333
+ return;
334
+ }
335
+ // If the form is submitted through another method (not our button),
336
+ // prevent double submission but don't show progress state
337
+ isSubmitting = true;
338
+ });
339
+ }
340
+ });
341
+ }
342
+ function handleProgressButtonClick(element, waitingText, longWaitingText, errorPage, isInput) {
343
+ var originalText = isInput && element instanceof HTMLInputElement ? element.value : element.innerText;
344
+ if (typeof element.blur === 'function') {
345
+ element.blur();
346
+ }
347
+ element.setAttribute('data-prevent-double-click', 'true');
348
+ var classes = element.className.split(' ');
349
+ if (classes.indexOf('govuk-button--progress-loading') === -1) {
350
+ classes.push('govuk-button--progress-loading');
351
+ element.className = classes.join(' ');
352
+ }
353
+ if (isInput && element instanceof HTMLInputElement) {
354
+ element.value = waitingText;
355
+ }
356
+ else {
357
+ element.innerText = waitingText;
358
+ }
359
+ element.setAttribute('aria-label', waitingText);
360
+ var longWaitTimeout = window.setTimeout(function () {
361
+ if (isInput && element instanceof HTMLInputElement) {
362
+ element.value = longWaitingText;
363
+ }
364
+ else {
365
+ element.innerText = longWaitingText;
366
+ }
367
+ element.setAttribute('aria-label', longWaitingText);
368
+ }, 5000);
369
+ var errorTimeout = window.setTimeout(function () {
370
+ window.location.href = errorPage;
371
+ }, 10000);
372
+ function resetButton() {
373
+ var classes = element.className.split(' ');
374
+ var loadingIndex = classes.indexOf('govuk-button--progress-loading');
375
+ if (loadingIndex !== -1) {
376
+ classes.splice(loadingIndex, 1);
377
+ element.className = classes.join(' ');
378
+ }
379
+ element.setAttribute('data-prevent-double-click', 'false');
380
+ if (isInput && element instanceof HTMLInputElement) {
381
+ element.value = originalText;
382
+ }
383
+ else {
384
+ element.innerText = originalText;
385
+ }
386
+ element.setAttribute('aria-label', originalText);
387
+ window.clearTimeout(errorTimeout);
388
+ window.clearTimeout(longWaitTimeout);
389
+ }
390
+ element.resetProgressButton = resetButton;
391
+ return resetButton;
392
+ }
393
+
394
+ export { PollResult, initialiseProgressButtons, useSpinner };
@@ -0,0 +1,2 @@
1
+ export declare function initialiseProgressButtons(document?: Document): 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,CAAC,QAAQ,GAAE,QAA0B,QAgE7E"}
@@ -3,7 +3,8 @@ export type PollingFunction = (abortSignal: AbortSignal) => Promise<PollResult>;
3
3
  export declare enum PollResult {
4
4
  Success = 0,
5
5
  Failure = 1,
6
- Pending = 2
6
+ Pending = 2,
7
+ Backoff = 3
7
8
  }
8
9
  declare enum SpinnerState {
9
10
  Waiting = 0,
@@ -18,6 +19,7 @@ type SpinnerConfig = {
18
19
  msBetweenDomUpdate: number;
19
20
  ariaAlertCompletionText?: string;
20
21
  hideSpinnerOnError: boolean;
22
+ maxBackoffTries: number;
21
23
  };
22
24
  export declare class Spinner {
23
25
  container: HTMLDivElement;
@@ -33,6 +35,7 @@ export declare class Spinner {
33
35
  updateDomTimer?: NodeJS.Timeout;
34
36
  abortController: AbortController;
35
37
  config: SpinnerConfig;
38
+ backOffCount: number;
36
39
  pollingFunction: PollingFunction;
37
40
  onSuccess: VoidFunction;
38
41
  onError: VoidFunction;
@@ -53,6 +56,7 @@ export declare class Spinner {
53
56
  private updateDom;
54
57
  private cloneAndAddIfExists;
55
58
  private callPollingFunction;
59
+ private calculateBackoffTime;
56
60
  private createAriaLiveContainer;
57
61
  private updateAriaAlert;
58
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../../../frontend-src/spinner/spinner.ts"],"names":[],"mappings":"AAAA,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,iBAoBjJ;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEhF,oBAAY,UAAU;IACpB,OAAO,IAAA;IACP,OAAO,IAAA;IACP,OAAO,IAAA;CACR;AAED,aAAK,YAAY;IACf,OAAO,IAAA;IACP,WAAW,IAAA;IACX,KAAK,IAAA;IACL,QAAQ,IAAA;CACT;AAED,KAAK,aAAa,GAAG;IACnB,2BAA2B,EAAE,MAAM,CAAC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,qBAAa,OAAO;IAClB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,cAAc,CAAC,EAAE,WAAW,CAAC;IAC7B,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,iBAAiB,EAAE,cAAc,CAAC;IAElC,wBAAwB,EAAE,cAAc,CAAC;IAEzC,KAAK,EAAE,YAAY,CAAwB;IAC3C,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAChC,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,aAAa,CAAC;IAEtB,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;gBAGpB,YAAY,EAAE,cAAc,EAC5B,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY;IA2CjB,IAAI;IAUV,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,SAAS;IAuBjB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,SAAS,CAKf;IAEF,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,cAAc,CAGpB;IAEF,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,iCAAiC,CAWvC;IAEF,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,SAAS,CAkDf;IAEF,OAAO,CAAC,mBAAmB;YASb,mBAAmB;IA8BjC,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,eAAe;CAWxB"}
1
+ {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../../../frontend-src/spinner/spinner.ts"],"names":[],"mappings":"AAAA,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,iBAoBjJ;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEhF,oBAAY,UAAU;IACpB,OAAO,IAAA;IACP,OAAO,IAAA;IACP,OAAO,IAAA;IACP,OAAO,IAAA;CACR;AAED,aAAK,YAAY;IACf,OAAO,IAAA;IACP,WAAW,IAAA;IACX,KAAK,IAAA;IACL,QAAQ,IAAA;CACT;AAED,KAAK,aAAa,GAAG;IACnB,2BAA2B,EAAE,MAAM,CAAC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,qBAAa,OAAO;IAClB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,cAAc,CAAC,EAAE,WAAW,CAAC;IAC7B,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,iBAAiB,EAAE,cAAc,CAAC;IAElC,wBAAwB,EAAE,cAAc,CAAC;IAEzC,KAAK,EAAE,YAAY,CAAwB;IAC3C,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAChC,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,EAAE,MAAM,CAAK;IAEzB,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;gBAGpB,YAAY,EAAE,cAAc,EAC5B,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY;IA2CjB,IAAI;IAUV,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,SAAS;IA0BjB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,SAAS,CAKf;IAEF,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,cAAc,CAGpB;IAEF,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,iCAAiC,CAWvC;IAEF,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,SAAS,CAkDf;IAEF,OAAO,CAAC,mBAAmB;YASb,mBAAmB;IA2CjC,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,eAAe;CAWxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@govuk-one-login/frontend-ui",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "",
5
5
  "main": "build/cjs/backend/index.cjs",
6
6
  "module": "build/esm/backend/index.js",
@@ -22,7 +22,8 @@
22
22
  ],
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/govuk-one-login/govuk-one-login-frontend.git"
25
+ "url": "git+https://github.com/govuk-one-login/govuk-one-login-frontend.git",
26
+ "directory": "packages/frontend-ui"
26
27
  },
27
28
  "keywords": [
28
29
  "govuk-one-login",