@ministryofjustice/hmpps-connect-dps-shared-items 5.1.0 → 5.2.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 (26) hide show
  1. package/dist/assets/dps/components/address-autosuggest/_address-autosuggest.scss +7 -0
  2. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.d.ts +26 -0
  3. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.js +20 -8
  4. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.ts +76 -0
  5. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.d.ts +18 -0
  6. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.js +27 -13
  7. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.ts +108 -0
  8. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.d.ts +82 -0
  9. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.js +83 -53
  10. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.ts +459 -0
  11. package/dist/assets/dps/components/address-autosuggest/index.d.ts +1 -0
  12. package/dist/assets/dps/components/address-autosuggest/index.js +4 -4
  13. package/dist/assets/dps/components/address-autosuggest/index.ts +9 -0
  14. package/dist/assets/dps/components/alert-flag/_alert-flag.scss +2 -1
  15. package/dist/assets/dps/components/modal/index.d.ts +34 -0
  16. package/dist/assets/dps/components/modal/macro.njk +2 -1
  17. package/dist/assets/dps/components/webcam-capture/_webcam-capture.scss +2 -2
  18. package/dist/assets/dps/components/webcam-capture/index.d.ts +1 -0
  19. package/dist/assets/dps/components/webcam-capture/index.js +14 -8
  20. package/dist/assets/dps/components/webcam-capture/index.ts +165 -0
  21. package/dist/assets/js/all.d.ts +1 -0
  22. package/dist/assets/js/all.ts +10 -0
  23. package/dist/assets/scss/all.css +66 -16
  24. package/dist/index.d.ts +10 -1
  25. package/dist/types/public/addressAutosuggest/addressAutosuggestResponse.d.ts +9 -0
  26. package/package.json +4 -4
@@ -1,9 +1,14 @@
1
1
  @use "govuk-frontend/dist/govuk/helpers/colour";
2
+ @use "govuk-frontend/dist/govuk/helpers/typography";
2
3
 
3
4
  .hmpps-address-autosuggest {
4
5
  position: relative;
5
6
  max-width: 630px;
6
7
 
8
+ .hmpps-display-none {
9
+ display: none;
10
+ }
11
+
7
12
  .govuk-form-group {
8
13
  margin-bottom: 10px;
9
14
  }
@@ -27,6 +32,7 @@
27
32
  background: colour.govuk-colour("black", $variant: "tint-95");
28
33
  border-bottom: 1px solid colour.govuk-functional-colour(border);
29
34
  padding: 0.25rem 0.5rem;
35
+ @include typography.govuk-font($size: 19);
30
36
  }
31
37
 
32
38
  &__listbox {
@@ -37,6 +43,7 @@
37
43
  &:focus {
38
44
  outline: none;
39
45
  }
46
+ @include typography.govuk-font($size: 19);
40
47
  }
41
48
 
42
49
  &__option {
@@ -0,0 +1,26 @@
1
+ export declare const FetchStatus: {
2
+ readonly unsent: "UNSENT";
3
+ readonly loading: "LOADING";
4
+ readonly done: "DONE";
5
+ };
6
+ export type FetchStatus = typeof FetchStatus;
7
+ export declare const abortError: "AbortError";
8
+ export declare const abortTimeout: "AbortTimeout";
9
+ export interface AbortableFetchOptions {
10
+ /** How long to delay the fetch call for; defaults to 0.5s */
11
+ fetchDelay?: number;
12
+ }
13
+ export declare class AbortableFetch {
14
+ private controller;
15
+ status: FetchStatus[keyof FetchStatus];
16
+ private readonly fetchDelay;
17
+ private readonly url;
18
+ constructor(url: string, options?: AbortableFetchOptions);
19
+ send(): Promise<Response>;
20
+ abort(): void;
21
+ }
22
+ export declare class ResponseError extends Error {
23
+ readonly response: Response;
24
+ constructor(message: string, response: Response);
25
+ }
26
+ export declare const abortableFetch: (url: string, options: AbortableFetchOptions) => AbortableFetch;
@@ -1,4 +1,5 @@
1
1
  // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/js/abortable-fetch.js
2
+ /* eslint-disable max-classes-per-file, @stylistic/lines-between-class-members */
2
3
  const FetchStatus = {
3
4
  unsent: 'UNSENT',
4
5
  loading: 'LOADING',
@@ -7,7 +8,11 @@ const FetchStatus = {
7
8
  const abortError = 'AbortError';
8
9
  const abortTimeout = 'AbortTimeout';
9
10
  class AbortableFetch {
10
- constructor(url, options) {
11
+ controller;
12
+ status;
13
+ fetchDelay;
14
+ url;
15
+ constructor(url, options = {}) {
11
16
  this.url = url;
12
17
  this.controller = new window.AbortController();
13
18
  this.status = FetchStatus.unsent;
@@ -17,18 +22,18 @@ class AbortableFetch {
17
22
  this.status = FetchStatus.loading;
18
23
  try {
19
24
  // Abortable delay before querying the API to limit the rate of requests when someone is typing:
20
- await new Promise((resolve) => {
25
+ await new Promise(resolve => {
21
26
  setTimeout(resolve, this.fetchDelay);
22
- this.controller.signal.addEventListener('abort', () => { resolve(); }, { once: true });
27
+ this.controller.signal.addEventListener('abort', () => {
28
+ resolve();
29
+ }, { once: true });
23
30
  });
24
31
  setTimeout(() => {
25
32
  this.controller.abort(abortTimeout);
26
33
  }, 5000);
27
34
  const response = await window.fetch(this.url, { signal: this.controller.signal });
28
- if (!(+response.status === 200)) {
29
- const error = new Error(`API error: ${response?.status ?? 500}`);
30
- error.response = response;
31
- throw error;
35
+ if (!response.ok) {
36
+ throw new ResponseError(`API error: ${response.status ?? 500}`, response);
32
37
  }
33
38
  return response;
34
39
  }
@@ -40,6 +45,13 @@ class AbortableFetch {
40
45
  this.controller.abort();
41
46
  }
42
47
  }
48
+ class ResponseError extends Error {
49
+ response;
50
+ constructor(message, response) {
51
+ super(message);
52
+ this.response = response;
53
+ }
54
+ }
43
55
  const abortableFetch = (url, options) => new AbortableFetch(url, options);
44
56
 
45
- export { FetchStatus, abortError, abortTimeout, abortableFetch };
57
+ export { AbortableFetch, FetchStatus, ResponseError, abortError, abortTimeout, abortableFetch };
@@ -0,0 +1,76 @@
1
+ // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/js/abortable-fetch.js
2
+ /* eslint-disable max-classes-per-file, @stylistic/lines-between-class-members */
3
+
4
+ export const FetchStatus = {
5
+ unsent: 'UNSENT',
6
+ loading: 'LOADING',
7
+ done: 'DONE',
8
+ } as const
9
+ export type FetchStatus = typeof FetchStatus
10
+
11
+ export const abortError = 'AbortError' as const
12
+ export const abortTimeout = 'AbortTimeout' as const
13
+
14
+ export interface AbortableFetchOptions {
15
+ /** How long to delay the fetch call for; defaults to 0.5s */
16
+ fetchDelay?: number
17
+ }
18
+
19
+ export class AbortableFetch {
20
+ private controller: AbortController
21
+ public status: FetchStatus[keyof FetchStatus]
22
+ private readonly fetchDelay: number
23
+ private readonly url: string
24
+
25
+ constructor(url: string, options: AbortableFetchOptions = {}) {
26
+ this.url = url
27
+ this.controller = new window.AbortController()
28
+ this.status = FetchStatus.unsent
29
+ this.fetchDelay = options?.fetchDelay || 500
30
+ }
31
+
32
+ public async send(): Promise<Response> {
33
+ this.status = FetchStatus.loading
34
+ try {
35
+ // Abortable delay before querying the API to limit the rate of requests when someone is typing:
36
+ await new Promise<void>(resolve => {
37
+ setTimeout(resolve, this.fetchDelay)
38
+ this.controller.signal.addEventListener(
39
+ 'abort',
40
+ () => {
41
+ resolve()
42
+ },
43
+ { once: true },
44
+ )
45
+ })
46
+
47
+ setTimeout(() => {
48
+ this.controller.abort(abortTimeout)
49
+ }, 5000)
50
+
51
+ const response = await window.fetch(this.url, { signal: this.controller.signal })
52
+
53
+ if (!response.ok) {
54
+ throw new ResponseError(`API error: ${response.status ?? 500}`, response)
55
+ }
56
+ return response
57
+ } finally {
58
+ this.status = FetchStatus.done
59
+ }
60
+ }
61
+
62
+ public abort(): void {
63
+ this.controller.abort()
64
+ }
65
+ }
66
+
67
+ export class ResponseError extends Error {
68
+ constructor(
69
+ message: string,
70
+ public readonly response: Response,
71
+ ) {
72
+ super(message)
73
+ }
74
+ }
75
+
76
+ export const abortableFetch = (url: string, options: AbortableFetchOptions) => new AbortableFetch(url, options)
@@ -0,0 +1,18 @@
1
+ import { type AddressSuggestion, type AddressSuggestions } from './autosuggest-ui';
2
+ export declare class AutosuggestAddress {
3
+ readonly context: HTMLDivElement;
4
+ readonly input: HTMLInputElement;
5
+ readonly container: HTMLDivElement;
6
+ readonly uprn: HTMLInputElement;
7
+ private readonly findUrl;
8
+ private readonly fetchDelay;
9
+ private fetch;
10
+ private autosuggest;
11
+ constructor(context: HTMLDivElement);
12
+ private checkAPIStatus;
13
+ suggestAddresses(query: string): Promise<AddressSuggestions>;
14
+ private findAddress;
15
+ private mapFindResults;
16
+ private addressMapping;
17
+ onAddressSelect(selectedResult: AddressSuggestion): Promise<void>;
18
+ }
@@ -1,18 +1,30 @@
1
- import { AutosuggestUi, NoResults } from './autosuggest-ui.js';
1
+ import AutosuggestUi, { NoResults } from './autosuggest-ui.js';
2
2
  import { abortableFetch, abortError, FetchStatus } from './abortable-fetch.js';
3
3
 
4
4
  // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/components/address-input/autosuggest.address.js
5
+ /* eslint-disable import/prefer-default-export, @stylistic/lines-between-class-members */
5
6
  const classUPRN = 'hmpps-js-uprn';
6
7
  const classInputContainer = 'hmpps-address-autosuggest';
7
8
  const classInput = 'hmpps-js-autosuggest-input';
8
9
  class AutosuggestAddress {
10
+ context;
11
+ input;
12
+ container;
13
+ uprn;
14
+ findUrl;
15
+ fetchDelay;
16
+ fetch;
17
+ autosuggest;
9
18
  constructor(context) {
10
19
  this.context = context;
11
20
  this.input = context.querySelector(`.${classInput}`);
12
21
  this.container = context.querySelector(`.${classInputContainer}`);
13
22
  this.uprn = context.querySelector(`.${classUPRN}`);
14
- this.findUrl = this.container.getAttribute('data-find-url');
15
- this.fetchDelay = this.container.getAttribute('data-fetch-delay');
23
+ this.findUrl = this.container.dataset.findUrl;
24
+ const fetchDelay = Number.parseInt(this.container.dataset.fetchDelay, 10);
25
+ if (Number.isSafeInteger(fetchDelay)) {
26
+ this.fetchDelay = fetchDelay;
27
+ }
16
28
  // State:
17
29
  this.fetch = null;
18
30
  // Initialise autosuggest:
@@ -34,7 +46,7 @@ class AutosuggestAddress {
34
46
  }
35
47
  }
36
48
  catch (error) {
37
- if (error.name !== abortError) {
49
+ if (error && typeof error === 'object' && 'name' in error && error.name !== abortError) {
38
50
  this.autosuggest.handleNoResults(NoResults.server_error);
39
51
  }
40
52
  }
@@ -51,29 +63,31 @@ class AutosuggestAddress {
51
63
  this.fetch = abortableFetch(`${this.findUrl}/${query}`, { fetchDelay: this.fetchDelay });
52
64
  const data = await this.fetch.send();
53
65
  const response = await data.json();
54
- const status = response.status;
55
- const addresses = response.results;
56
- return this.mapFindResults(addresses, status);
66
+ return this.mapFindResults(response);
57
67
  }
58
- mapFindResults(results, status) {
59
- if (!results)
68
+ mapFindResults(response) {
69
+ const { status } = response;
70
+ if (!('results' in response)) {
60
71
  return { status };
72
+ }
73
+ const { results } = response;
61
74
  return {
62
75
  results: this.addressMapping(results),
63
- status: status,
76
+ status,
64
77
  };
65
78
  }
66
79
  addressMapping(addresses) {
67
- if (!addresses?.length)
80
+ if (!addresses?.length) {
68
81
  return [];
82
+ }
69
83
  return addresses.map(address => ({ uprn: address.uprn, displayText: address.addressString }));
70
84
  }
71
85
  async onAddressSelect(selectedResult) {
72
86
  if (this.fetch && this.fetch.status !== FetchStatus.done) {
73
87
  this.fetch.abort();
74
88
  }
75
- this.autosuggest.input.value = selectedResult.displayText;
76
- this.uprn.value = selectedResult.uprn;
89
+ this.autosuggest.input.value = selectedResult.displayText ? selectedResult.displayText : '';
90
+ this.uprn.value = `${selectedResult.uprn}`;
77
91
  }
78
92
  }
79
93
 
@@ -0,0 +1,108 @@
1
+ // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/components/address-input/autosuggest.address.js
2
+ /* eslint-disable import/prefer-default-export, @stylistic/lines-between-class-members */
3
+
4
+ import type { AddressAutosuggestResponse } from '../../../../types/public/addressAutosuggest/addressAutosuggestResponse'
5
+ import type { OsAddress } from '../../../../types/public/addressAutosuggest/osAddress'
6
+ import AutosuggestUi, { type AddressSuggestion, type AddressSuggestions, NoResults } from './autosuggest-ui'
7
+ import { type AbortableFetch, abortableFetch, abortError, FetchStatus } from './abortable-fetch'
8
+
9
+ const classUPRN = 'hmpps-js-uprn'
10
+ const classInputContainer = 'hmpps-address-autosuggest'
11
+ const classInput = 'hmpps-js-autosuggest-input'
12
+
13
+ export class AutosuggestAddress {
14
+ public readonly input: HTMLInputElement
15
+ public readonly container: HTMLDivElement
16
+ public readonly uprn: HTMLInputElement
17
+ private readonly findUrl: string
18
+ private readonly fetchDelay: number | undefined
19
+ private fetch: AbortableFetch | null
20
+ private autosuggest: AutosuggestUi
21
+
22
+ constructor(public readonly context: HTMLDivElement) {
23
+ this.input = context.querySelector(`.${classInput}`)!
24
+ this.container = context.querySelector(`.${classInputContainer}`)!
25
+ this.uprn = context.querySelector(`.${classUPRN}`)!
26
+ this.findUrl = this.container.dataset.findUrl!
27
+ const fetchDelay = Number.parseInt(this.container.dataset.fetchDelay!, 10)
28
+ if (Number.isSafeInteger(fetchDelay)) {
29
+ this.fetchDelay = fetchDelay
30
+ }
31
+
32
+ // State:
33
+ this.fetch = null
34
+
35
+ // Initialise autosuggest:
36
+ this.autosuggest = new AutosuggestUi({
37
+ context: this.container,
38
+ onSelect: this.onAddressSelect.bind(this),
39
+ suggestionFunction: this.suggestAddresses.bind(this),
40
+ })
41
+
42
+ // Check API status:
43
+ this.checkAPIStatus()
44
+ }
45
+
46
+ private async checkAPIStatus(): Promise<void> {
47
+ this.fetch = abortableFetch(`${this.findUrl}/SW1H9AJ`, { fetchDelay: this.fetchDelay })
48
+
49
+ try {
50
+ const response = await this.fetch.send()
51
+ const status = response && ((await response.json()) as AddressAutosuggestResponse).status
52
+ if (status !== 200) {
53
+ this.autosuggest.handleNoResults(NoResults.server_error)
54
+ }
55
+ } catch (error) {
56
+ if (error && typeof error === 'object' && 'name' in error && error.name !== abortError) {
57
+ this.autosuggest.handleNoResults(NoResults.server_error)
58
+ }
59
+ }
60
+ }
61
+
62
+ public async suggestAddresses(query: string): Promise<AddressSuggestions> {
63
+ if (this.fetch && this.fetch.status !== FetchStatus.done) {
64
+ this.fetch.abort()
65
+ }
66
+
67
+ // Reset any previously selected result:
68
+ this.uprn.value = ''
69
+
70
+ return await this.findAddress(query?.replace(/[^A-Z0-9-.' ]/gi, ''))
71
+ }
72
+
73
+ private async findAddress(query: string): Promise<AddressSuggestions> {
74
+ this.fetch = abortableFetch(`${this.findUrl}/${query}`, { fetchDelay: this.fetchDelay })
75
+ const data = await this.fetch.send()
76
+ const response: AddressAutosuggestResponse = await data.json()
77
+ return this.mapFindResults(response)
78
+ }
79
+
80
+ private mapFindResults(response: AddressAutosuggestResponse): AddressSuggestions {
81
+ const { status } = response
82
+ if (!('results' in response)) {
83
+ return { status }
84
+ }
85
+ const { results } = response
86
+ return {
87
+ results: this.addressMapping(results),
88
+ status,
89
+ }
90
+ }
91
+
92
+ private addressMapping(addresses: OsAddress[]): AddressSuggestion[] {
93
+ if (!addresses?.length) {
94
+ return []
95
+ }
96
+
97
+ return addresses.map(address => ({ uprn: address.uprn, displayText: address.addressString }))
98
+ }
99
+
100
+ public async onAddressSelect(selectedResult: AddressSuggestion): Promise<void> {
101
+ if (this.fetch && this.fetch.status !== FetchStatus.done) {
102
+ this.fetch.abort()
103
+ }
104
+
105
+ this.autosuggest.input.value = selectedResult.displayText ? selectedResult.displayText : ''
106
+ this.uprn.value = `${selectedResult.uprn}`
107
+ }
108
+ }
@@ -0,0 +1,82 @@
1
+ export declare const NoResults: {
2
+ readonly type_more: "TYPE_MORE";
3
+ readonly no_match: "NO_MATCH";
4
+ readonly server_error: "SERVER_ERROR";
5
+ };
6
+ type NoResults = typeof NoResults;
7
+ export interface AddressSuggestion {
8
+ displayText?: string;
9
+ uprn?: number;
10
+ }
11
+ export interface AddressSuggestions {
12
+ status: number;
13
+ results?: AddressSuggestion[];
14
+ }
15
+ type AddressSuggestionFunction = (query: string) => Promise<AddressSuggestions>;
16
+ type AddressSelectedCallback = (selectedResult: AddressSuggestion) => Promise<void>;
17
+ interface AutosuggestUiOptions {
18
+ context: HTMLDivElement;
19
+ suggestionFunction: AddressSuggestionFunction;
20
+ onSelect: AddressSelectedCallback;
21
+ }
22
+ export default class AutosuggestUi {
23
+ private readonly context;
24
+ readonly input: HTMLInputElement;
25
+ private readonly resultsContainer;
26
+ private readonly resultsTitleContainer;
27
+ private readonly listbox;
28
+ private readonly instructions;
29
+ private readonly errorContainer;
30
+ private readonly errorTextContainer;
31
+ private readonly ariaStatus;
32
+ private readonly minChars;
33
+ private readonly ariaYouHaveSelected;
34
+ private readonly ariaMinChars;
35
+ private readonly ariaOneResult;
36
+ private readonly ariaNResults;
37
+ private readonly noResults;
38
+ private readonly typeMore;
39
+ private readonly listboxId;
40
+ private readonly errorMessage;
41
+ private readonly stylingBaseClass;
42
+ private readonly fetchSuggestions;
43
+ private readonly onSelect;
44
+ private query;
45
+ private sanitisedQuery;
46
+ private results;
47
+ private resultOptions;
48
+ private numberOfResults;
49
+ private blurring;
50
+ private blurTimeout;
51
+ private scrolling;
52
+ private scrollTimeout;
53
+ private inputTimeout;
54
+ private highlightedResultIndex;
55
+ constructor({ context, onSelect, suggestionFunction }: AutosuggestUiOptions);
56
+ private initialiseUI;
57
+ private bindEventListeners;
58
+ private handleKeydown;
59
+ private handleKeyup;
60
+ private handleChange;
61
+ private handleBlur;
62
+ private handleFocus;
63
+ private handleScroll;
64
+ private checkCharCount;
65
+ private navigateResults;
66
+ private sanitiseAutosuggestText;
67
+ private getSuggestions;
68
+ private unsetResults;
69
+ private clearListbox;
70
+ private handleResults;
71
+ private createListElement;
72
+ handleNoResults(reason: NoResults[keyof NoResults]): void;
73
+ private displayAPIError;
74
+ private setHighlightedResult;
75
+ private selectResult;
76
+ private setAriaStatus;
77
+ private emboldenMatch;
78
+ private escapeRegExp;
79
+ private isAboveViewport;
80
+ private isBelowViewport;
81
+ }
82
+ export {};