@ministryofjustice/hmpps-connect-dps-shared-items 5.0.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 (34) hide show
  1. package/dist/alertFlags/alertTypes.d.ts +25 -0
  2. package/dist/alertFlags/alertTypes.test.d.ts +1 -0
  3. package/dist/alertFlags/getAlertFlagLabelsForAlerts.d.ts +4 -2
  4. package/dist/assets/dps/components/address-autosuggest/_address-autosuggest.scss +7 -0
  5. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.d.ts +26 -0
  6. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.js +20 -8
  7. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.ts +76 -0
  8. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.d.ts +18 -0
  9. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.js +27 -13
  10. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.ts +108 -0
  11. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.d.ts +82 -0
  12. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.js +83 -53
  13. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.ts +459 -0
  14. package/dist/assets/dps/components/address-autosuggest/index.d.ts +1 -0
  15. package/dist/assets/dps/components/address-autosuggest/index.js +4 -4
  16. package/dist/assets/dps/components/address-autosuggest/index.ts +9 -0
  17. package/dist/assets/dps/components/alert-flag/_alert-flag.scss +5 -2
  18. package/dist/assets/dps/components/modal/index.d.ts +34 -0
  19. package/dist/assets/dps/components/modal/macro.njk +2 -1
  20. package/dist/assets/dps/components/webcam-capture/_webcam-capture.scss +2 -2
  21. package/dist/assets/dps/components/webcam-capture/index.d.ts +1 -0
  22. package/dist/assets/dps/components/webcam-capture/index.js +14 -8
  23. package/dist/assets/dps/components/webcam-capture/index.ts +165 -0
  24. package/dist/assets/js/all.d.ts +1 -0
  25. package/dist/assets/js/all.ts +10 -0
  26. package/dist/assets/scss/all.css +66 -16
  27. package/dist/index.cjs +57 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +39 -2
  30. package/dist/index.esm.js +56 -1
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/types/public/addressAutosuggest/addressAutosuggestResponse.d.ts +9 -0
  33. package/dist/types/public/alertFlags/index.d.ts +1 -0
  34. package/package.json +4 -4
@@ -0,0 +1,459 @@
1
+ // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/components/autosuggest/autosuggest.ui.js
2
+ /* eslint-disable no-param-reassign, @stylistic/lines-between-class-members */
3
+
4
+ import { abortError, abortTimeout } from './abortable-fetch'
5
+
6
+ const jsBaseClass = 'hmpps-js-autosuggest'
7
+
8
+ const classSuffixInitialised = '--initialised'
9
+ const classSuffixOption = '__option'
10
+ const classSuffixOptionFocused = `${classSuffixOption}--focused`
11
+ const classSuffixOptionNoResults = `${classSuffixOption}--no-results`
12
+ const classSuffixHasResults = '--has-results'
13
+
14
+ export const NoResults = {
15
+ type_more: 'TYPE_MORE',
16
+ no_match: 'NO_MATCH',
17
+ server_error: 'SERVER_ERROR',
18
+ } as const
19
+ type NoResults = typeof NoResults
20
+
21
+ export interface AddressSuggestion {
22
+ displayText?: string
23
+ uprn?: number
24
+ }
25
+
26
+ export interface AddressSuggestions {
27
+ status: number
28
+ results?: AddressSuggestion[]
29
+ }
30
+
31
+ type AddressSuggestionFunction = (query: string) => Promise<AddressSuggestions>
32
+ type AddressSelectedCallback = (selectedResult: AddressSuggestion) => Promise<void>
33
+
34
+ interface AutosuggestUiOptions {
35
+ context: HTMLDivElement
36
+ suggestionFunction: AddressSuggestionFunction
37
+ onSelect: AddressSelectedCallback
38
+ }
39
+
40
+ export default class AutosuggestUi {
41
+ private readonly context: HTMLDivElement
42
+ public readonly input: HTMLInputElement
43
+ private readonly resultsContainer: HTMLDivElement
44
+ private readonly resultsTitleContainer: HTMLDivElement
45
+ private readonly listbox: HTMLUListElement
46
+ private readonly instructions: HTMLDivElement
47
+ private readonly errorContainer: HTMLDivElement
48
+ private readonly errorTextContainer: HTMLDivElement
49
+ private readonly ariaStatus: HTMLDivElement
50
+
51
+ private readonly minChars: number
52
+ private readonly ariaYouHaveSelected: string
53
+ private readonly ariaMinChars: string
54
+ private readonly ariaOneResult: string
55
+ private readonly ariaNResults: string
56
+ private readonly noResults: string
57
+ private readonly typeMore: string
58
+ private readonly listboxId: string
59
+ private readonly errorMessage: string
60
+ private readonly stylingBaseClass: string
61
+
62
+ private readonly fetchSuggestions: AddressSuggestionFunction
63
+ private readonly onSelect: AddressSelectedCallback
64
+
65
+ private query: string
66
+ private sanitisedQuery: string
67
+ private results: AddressSuggestion[] | undefined
68
+ private resultOptions: HTMLLIElement[]
69
+ private numberOfResults: number
70
+
71
+ private blurring: boolean
72
+ private blurTimeout: number
73
+ private scrolling: boolean
74
+ private scrollTimeout: number
75
+ private inputTimeout: number
76
+ private highlightedResultIndex: number | null
77
+
78
+ constructor({ context, onSelect, suggestionFunction }: AutosuggestUiOptions) {
79
+ // DOM Elements
80
+ this.context = context
81
+ this.input = context.querySelector<HTMLInputElement>(`.${jsBaseClass}-input`)!
82
+ this.resultsContainer = context.querySelector<HTMLDivElement>(`.${jsBaseClass}-results`)!
83
+ this.resultsTitleContainer = this.resultsContainer.querySelector<HTMLDivElement>(`.${jsBaseClass}-results-title`)!
84
+ this.listbox = this.resultsContainer.querySelector<HTMLUListElement>(`.${jsBaseClass}-listbox`)!
85
+ this.instructions = context.querySelector<HTMLDivElement>(`.${jsBaseClass}-instructions`)!
86
+ this.errorContainer = context.querySelector<HTMLDivElement>(`.${jsBaseClass}-error`)!
87
+ this.errorTextContainer = this.errorContainer.querySelector<HTMLDivElement>('.moj-alert__content')!
88
+ this.ariaStatus = context.querySelector<HTMLDivElement>(`.${jsBaseClass}-aria-status`)!
89
+
90
+ // Settings
91
+ this.minChars = 3
92
+ this.ariaYouHaveSelected = context.dataset.ariaYouHaveSelected!
93
+ this.ariaMinChars = context.dataset.ariaMinChars!
94
+ this.ariaOneResult = context.dataset.ariaOneResult!
95
+ this.ariaNResults = context.dataset.ariaNResults!
96
+ this.noResults = context.dataset.noResults!
97
+ this.typeMore = context.dataset.typeMore!
98
+ this.errorMessage = context.dataset.errorApi!
99
+ this.stylingBaseClass = context.dataset.stylingBaseClass!
100
+ this.listboxId = this.listbox.id
101
+
102
+ // Callbacks
103
+ this.onSelect = onSelect
104
+ this.fetchSuggestions = suggestionFunction
105
+
106
+ // State
107
+ this.query = ''
108
+ this.sanitisedQuery = ''
109
+ this.results = []
110
+ this.resultOptions = []
111
+ this.numberOfResults = 0
112
+ this.highlightedResultIndex = null
113
+ this.blurring = false
114
+ this.blurTimeout = 0
115
+ this.scrolling = false
116
+ this.scrollTimeout = 0
117
+ this.inputTimeout = 0
118
+
119
+ this.initialiseUI()
120
+ }
121
+
122
+ private initialiseUI(): void {
123
+ this.input.setAttribute('aria-autocomplete', 'list')
124
+ this.input.setAttribute('aria-controls', this.listbox.id)
125
+ this.input.setAttribute('aria-describedby', this.instructions.id)
126
+ this.input.setAttribute('aria-haspopup', 'true')
127
+ this.input.setAttribute('aria-owns', this.listbox.id)
128
+ this.input.setAttribute('aria-expanded', 'false')
129
+ this.input.setAttribute('autocomplete', 'off')
130
+ this.input.setAttribute('role', 'combobox')
131
+
132
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixInitialised}`)
133
+
134
+ this.bindEventListeners()
135
+ }
136
+
137
+ private bindEventListeners(): void {
138
+ this.input.addEventListener('keydown', this.handleKeydown.bind(this))
139
+ this.input.addEventListener('keyup', this.handleKeyup.bind(this))
140
+ this.input.addEventListener('input', this.handleChange.bind(this))
141
+ this.input.addEventListener('blur', this.handleBlur.bind(this))
142
+ this.input.addEventListener('focus', this.handleFocus.bind(this))
143
+ }
144
+
145
+ private handleKeydown(event: KeyboardEvent): void {
146
+ // eslint-disable-next-line default-case
147
+ switch (event.key) {
148
+ case 'ArrowUp': {
149
+ event.preventDefault()
150
+ this.navigateResults(-1)
151
+ break
152
+ }
153
+ case 'ArrowDown': {
154
+ event.preventDefault()
155
+ this.navigateResults(1)
156
+ break
157
+ }
158
+ case 'Escape': {
159
+ this.clearListbox()
160
+ break
161
+ }
162
+ case 'Enter': {
163
+ if (this.highlightedResultIndex !== null) {
164
+ event.preventDefault()
165
+ }
166
+ break
167
+ }
168
+ }
169
+ }
170
+
171
+ private handleKeyup(event: KeyboardEvent): void {
172
+ // eslint-disable-next-line default-case
173
+ switch (event.key) {
174
+ case 'ArrowUp':
175
+ case 'ArrowDown': {
176
+ event.preventDefault()
177
+ break
178
+ }
179
+ case 'Enter': {
180
+ if (this.highlightedResultIndex !== null) {
181
+ this.selectResult()
182
+ }
183
+ break
184
+ }
185
+ }
186
+ }
187
+
188
+ private handleChange(_event: InputEvent): void {
189
+ if (!this.blurring && this.input.value.trim()) {
190
+ this.getSuggestions()
191
+ } else {
192
+ this.clearListbox()
193
+ }
194
+ }
195
+
196
+ private handleBlur(_event: FocusEvent): void {
197
+ clearTimeout(this.blurTimeout)
198
+ this.blurring = true
199
+
200
+ // Timeout required to allow user to click an option before clearing the listbox:
201
+ this.blurTimeout = setTimeout(() => {
202
+ this.blurring = false
203
+
204
+ this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`)
205
+ this.input.setAttribute('aria-expanded', 'false')
206
+ this.setAriaStatus()
207
+ }, 300)
208
+ }
209
+
210
+ private handleFocus(_event: FocusEvent): void {
211
+ if (this.listbox.innerHTML) {
212
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`)
213
+ this.input.setAttribute('aria-expanded', 'true')
214
+ this.setAriaStatus()
215
+ }
216
+ }
217
+
218
+ private handleScroll(option: HTMLLIElement, scrollUp: boolean): void {
219
+ clearTimeout(this.scrollTimeout)
220
+ this.scrolling = true
221
+ option.scrollIntoView(scrollUp)
222
+
223
+ // Timeout required to allow user to click an option before clearing the listbox:
224
+ this.scrollTimeout = setTimeout(() => {
225
+ this.scrolling = false
226
+ }, 300)
227
+ }
228
+
229
+ private checkCharCount(): void {
230
+ if (this.input.value.length > 1 && this.input.value.length < this.minChars) {
231
+ this.inputTimeout = setTimeout(() => {
232
+ this.handleNoResults(NoResults.type_more)
233
+ }, 2000)
234
+ } else {
235
+ clearTimeout(this.inputTimeout)
236
+ }
237
+ }
238
+
239
+ private navigateResults(direction: number): void {
240
+ let index: number | null = 0
241
+ if (this.highlightedResultIndex !== null) {
242
+ index = this.highlightedResultIndex + direction
243
+ }
244
+
245
+ if (index < this.numberOfResults) {
246
+ if (index < 0) {
247
+ index = null
248
+ }
249
+
250
+ this.setHighlightedResult(index)
251
+ }
252
+ }
253
+
254
+ private sanitiseAutosuggestText(string: string): string {
255
+ let sanitisedString = string.toLowerCase()
256
+
257
+ sanitisedString = sanitisedString.trim()
258
+ sanitisedString = sanitisedString.replace(/,/g, '')
259
+ sanitisedString = sanitisedString.replace(/\s\s+/g, ' ')
260
+ sanitisedString = sanitisedString.replace(/&/g, '%26')
261
+ sanitisedString = sanitisedString.replace(/\d(?=[a-z]{3,})/gi, '$& ')
262
+
263
+ return sanitisedString
264
+ }
265
+
266
+ private getSuggestions(): void {
267
+ this.query = this.input.value
268
+
269
+ const sanitisedQuery = this.sanitiseAutosuggestText(this.query)
270
+
271
+ if (sanitisedQuery !== this.sanitisedQuery) {
272
+ this.sanitisedQuery = sanitisedQuery
273
+ this.unsetResults()
274
+ this.checkCharCount()
275
+
276
+ if (this.sanitisedQuery.length >= this.minChars) {
277
+ this.fetchSuggestions(this.sanitisedQuery)
278
+ .then(this.handleResults.bind(this))
279
+ .catch(error => {
280
+ if (error.name !== abortError && error.reason !== abortTimeout) {
281
+ // eslint-disable-next-line no-console
282
+ console.log('error:', error)
283
+ this.handleNoResults(NoResults.server_error)
284
+ }
285
+ })
286
+ } else {
287
+ this.clearListbox()
288
+ }
289
+ }
290
+ }
291
+
292
+ private unsetResults(): void {
293
+ this.results = []
294
+ this.resultOptions = []
295
+ }
296
+
297
+ private clearListbox(): void {
298
+ this.listbox.innerHTML = ''
299
+ this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`)
300
+ this.input.removeAttribute('aria-activedescendant')
301
+ this.input.setAttribute('aria-expanded', 'false')
302
+ this.setHighlightedResult(null)
303
+ this.setAriaStatus()
304
+ }
305
+
306
+ private handleResults(response: AddressSuggestions): void {
307
+ this.results = response?.results
308
+ this.numberOfResults = this.results?.length || 0
309
+ this.setAriaStatus()
310
+ this.listbox.innerHTML = ''
311
+
312
+ this.resultOptions =
313
+ this.results?.map((result, index) => {
314
+ this.resultsTitleContainer?.classList?.remove('hmpps-display-none')
315
+ const listElement = this.createListElement(result, index)
316
+ this.listbox.appendChild(listElement)
317
+ return listElement
318
+ }) ?? []
319
+
320
+ this.setHighlightedResult(null)
321
+ this.input.setAttribute('aria-expanded', this.numberOfResults ? 'true' : 'false')
322
+
323
+ if (!!this.numberOfResults && this.sanitisedQuery.length >= this.minChars) {
324
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`)
325
+ } else {
326
+ this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`)
327
+ this.clearListbox()
328
+ }
329
+
330
+ if (this.numberOfResults === 0 && this.noResults) {
331
+ this.handleNoResults(NoResults.no_match)
332
+ }
333
+ }
334
+
335
+ private createListElement(result: AddressSuggestion, index: number): HTMLLIElement {
336
+ const innerHTML = this.emboldenMatch(result.displayText ?? '', this.query)
337
+ const listElement: HTMLLIElement = document.createElement('li')
338
+ listElement.className = `${this.stylingBaseClass}${classSuffixOption}`
339
+ listElement.setAttribute('id', `${this.listboxId}__option--${index}`)
340
+ listElement.setAttribute('role', 'option')
341
+ listElement.innerHTML = innerHTML
342
+ listElement.addEventListener('click', () => {
343
+ this.selectResult(index)
344
+ })
345
+ listElement.addEventListener('mouseenter', () => !this.scrolling && this.setHighlightedResult(index))
346
+ return listElement
347
+ }
348
+
349
+ public handleNoResults(reason: NoResults[keyof NoResults]): void {
350
+ if (reason === NoResults.type_more || reason === NoResults.no_match) {
351
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`)
352
+ this.resultsTitleContainer?.classList?.add('hmpps-display-none')
353
+ this.input.setAttribute('aria-expanded', 'true')
354
+
355
+ const message = reason === NoResults.type_more ? this.typeMore : this.noResults
356
+ this.setAriaStatus(message)
357
+ this.listbox.innerHTML = ''
358
+ const noResultsItem = document.createElement('li')
359
+ noResultsItem.className = `${this.stylingBaseClass}${classSuffixOption} ${this.stylingBaseClass}${classSuffixOptionNoResults}`
360
+ noResultsItem.textContent = message
361
+ this.listbox.appendChild(noResultsItem)
362
+ } else {
363
+ this.displayAPIError()
364
+ }
365
+ }
366
+
367
+ private displayAPIError(): void {
368
+ this.input.value = ''
369
+ this.input.setAttribute('disabled', 'true')
370
+ this.clearListbox()
371
+ this.errorContainer.classList.remove('hmpps-display-none')
372
+ this.errorTextContainer.textContent = this.errorMessage
373
+ this.ariaStatus.setAttribute('aria-hidden', 'true')
374
+ this.setAriaStatus(this.errorMessage)
375
+ }
376
+
377
+ private setHighlightedResult(index: number | null): void {
378
+ this.highlightedResultIndex = index
379
+
380
+ if (this.highlightedResultIndex === null) {
381
+ this.input.removeAttribute('aria-activedescendant')
382
+ }
383
+
384
+ if (this.numberOfResults) {
385
+ this.resultOptions.forEach((option, optionIndex) => {
386
+ if (index !== null && optionIndex === index) {
387
+ if (this.isAboveViewport(option)) {
388
+ this.handleScroll(option, true)
389
+ }
390
+ if (this.isBelowViewport(option)) {
391
+ this.handleScroll(option, false)
392
+ }
393
+ option.classList.add(`${this.stylingBaseClass}${classSuffixOptionFocused}`)
394
+ option.setAttribute('aria-selected', 'true')
395
+ this.input.setAttribute('aria-activedescendant', option.id)
396
+ const optionText = option.innerHTML.replace('<b>', '').replace('</b>', '')
397
+ this.setAriaStatus(optionText)
398
+ } else {
399
+ option.classList.remove(`${this.stylingBaseClass}${classSuffixOptionFocused}`)
400
+ option.removeAttribute('aria-selected')
401
+ }
402
+ })
403
+ }
404
+ }
405
+
406
+ private selectResult(index?: number): void {
407
+ if (!this.results?.length) return
408
+
409
+ const selectedIndex = typeof index === 'number' ? index : this.highlightedResultIndex
410
+ if (selectedIndex === null || selectedIndex === undefined) return
411
+
412
+ const result = this.results?.[selectedIndex]
413
+ if (!result) return
414
+
415
+ this.onSelect(result)
416
+ this.clearListbox()
417
+ this.setAriaStatus(`${this.ariaYouHaveSelected}: ${result.displayText}.`)
418
+ }
419
+
420
+ private setAriaStatus(content?: string): void {
421
+ if (!content) {
422
+ const queryTooShort = this.sanitisedQuery.length < this.minChars
423
+ const noResults = this.numberOfResults === 0
424
+ if (queryTooShort) {
425
+ content = this.ariaMinChars
426
+ } else if (noResults) {
427
+ content = `${this.noResults}: "${this.query}"`
428
+ } else if (this.numberOfResults === 1) {
429
+ content = this.ariaOneResult
430
+ } else {
431
+ content = this.ariaNResults.replace('{n}', `${this.numberOfResults}`)
432
+ }
433
+ }
434
+ this.ariaStatus.textContent = content
435
+ }
436
+
437
+ private emboldenMatch(string: string, query: string): string {
438
+ return this.escapeRegExp(query)
439
+ .trim()
440
+ .replace(/\s\s+/g, ' ')
441
+ .split(' ')
442
+ .reduce((accumulator, currentValue) => {
443
+ const reg = new RegExp(`(?<!<)${currentValue}(?![\\w\\s]*>)`, 'gi')
444
+ return accumulator.replace(reg, '<b>$&</b>')
445
+ }, string)
446
+ }
447
+
448
+ private escapeRegExp(string: string): string {
449
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
450
+ }
451
+
452
+ private isAboveViewport(element: HTMLLIElement): boolean {
453
+ return element.getBoundingClientRect().top < 0
454
+ }
455
+
456
+ private isBelowViewport(element: HTMLLIElement): boolean {
457
+ return element.getBoundingClientRect().bottom > (window.innerHeight || document.documentElement.clientHeight)
458
+ }
459
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): void;
@@ -1,11 +1,11 @@
1
1
  import { AutosuggestAddress } from './autosuggest-address.js';
2
2
 
3
+ // eslint-disable-next-line import/prefer-default-export
3
4
  function init() {
4
5
  // Setup all address autosuggest instances:
5
- const addressAutosuggests = [...document.querySelectorAll('.hmpps-js-address-autosuggest')];
6
- if (addressAutosuggests.length) {
7
- addressAutosuggests.forEach(addressAutosuggest => new AutosuggestAddress(addressAutosuggest));
8
- }
6
+ document
7
+ .querySelectorAll('.hmpps-js-address-autosuggest')
8
+ .forEach(addressAutosuggest => new AutosuggestAddress(addressAutosuggest));
9
9
  }
10
10
 
11
11
  export { init };
@@ -0,0 +1,9 @@
1
+ import { AutosuggestAddress } from './autosuggest-address'
2
+
3
+ // eslint-disable-next-line import/prefer-default-export
4
+ export function init() {
5
+ // Setup all address autosuggest instances:
6
+ document
7
+ .querySelectorAll<HTMLDivElement>('.hmpps-js-address-autosuggest')
8
+ .forEach(addressAutosuggest => new AutosuggestAddress(addressAutosuggest))
9
+ }
@@ -1,6 +1,7 @@
1
1
  @use "sass:color";
2
2
  @use "sass:selector";
3
3
  @use "govuk-frontend/dist/govuk/helpers/colour";
4
+ @use "govuk-frontend/dist/govuk/helpers/typography";
4
5
 
5
6
  @mixin unify-parent($child) {
6
7
  @at-root #{selector.unify(&, $child)} {
@@ -14,11 +15,11 @@
14
15
  border: 3px solid;
15
16
  padding: 5px 10px;
16
17
  height: initial;
17
- font-size: 16px;
18
18
  text-align: center;
19
19
  line-height: 1;
20
20
  user-select: none;
21
21
  border-radius: 5px;
22
+ @include typography.govuk-font($size: 16);
22
23
  }
23
24
 
24
25
  @media print {
@@ -60,6 +61,8 @@
60
61
  color: black;
61
62
  background-color: white;
62
63
 
64
+ // NB: styles for alert types should have a matching entry in AlertType enumeration
65
+
63
66
  &--care-leaver {
64
67
  @include alertType(#00703c, #03532e);
65
68
  }
@@ -96,7 +99,7 @@
96
99
  @include alertType(#2ea01c, #26711a);
97
100
  }
98
101
 
99
-
102
+ // NB: styles below are deprecated
100
103
 
101
104
  &--acct {
102
105
  @include alertType(#d34700);
@@ -0,0 +1,34 @@
1
+ /** Modal dialogue box component */
2
+ export declare class Modal {
3
+ private readonly root;
4
+ static readonly showTriggerClassName = "hmpps-modal__trigger-show";
5
+ static readonly hideTriggerClassName = "hmpps-modal__trigger-hide";
6
+ private static readonly modals;
7
+ /** Initialise all modal components rendered in the document; none will automatically open */
8
+ static init(): void;
9
+ /** Find an instance of the `Modal` class to programmatically control it */
10
+ static getById(modalId: string): Modal | undefined;
11
+ /** Hide all showing modals */
12
+ static hideAll(): void;
13
+ private readonly dialog;
14
+ private readonly content;
15
+ private showing;
16
+ constructor(root: HTMLDivElement);
17
+ get id(): string;
18
+ get isShowing(): boolean;
19
+ private attachHideTriggers;
20
+ private handleKeyDown;
21
+ private previouslyFocused;
22
+ private takeFocus;
23
+ private maintainFocus;
24
+ private revertFocus;
25
+ private trapFocus;
26
+ private getFocusableChildren;
27
+ /** Show the modal dialog box */
28
+ show(): void;
29
+ /** Hide the modal dialog box */
30
+ hide(): void;
31
+ /** Show modal dialog box with loading graphic while loading HTML content from `url` */
32
+ load(url: string | URL): Promise<void>;
33
+ private replaceContentWithTemplate;
34
+ }
@@ -28,7 +28,8 @@ Parameters as JSDoc:
28
28
  </dialog>
29
29
 
30
30
  <template class="hmpps-modal__loading-graphic-template">
31
- <svg class="hmpps-modal__loading-graphic" width="50" height="50" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
31
+ <span class="govuk-visually-hidden">Content is loading…</span>
32
+ <svg class="hmpps-modal__loading-graphic" aria-hidden="true" width="50" height="50" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
32
33
  <g>
33
34
  <rect x="11" y="1" width="2" height="5" opacity=".14"/>
34
35
  <rect x="11" y="1" width="2" height="5" transform="rotate(30 12 12)" opacity=".29"/>
@@ -15,13 +15,13 @@ $hmpps-webcam-capture-height: 276px;
15
15
  }
16
16
 
17
17
  &__error {
18
- border: 5px solid #eb611cff;
18
+ border: 5px solid #eb611c;
19
19
 
20
20
  .hmpps-webcam-error-icon {
21
21
  display: block;
22
22
  width: 1.875rem;
23
23
  height: 1.875rem;
24
- color: #eb611cff;
24
+ color: #eb611c;
25
25
  }
26
26
  }
27
27
 
@@ -0,0 +1 @@
1
+ export declare function init(): void;
@@ -13,19 +13,20 @@ function hmppsWebcamCapture(component) {
13
13
  // Permissions
14
14
  const permissionRequested = document.getElementById('hmpps-webcam__permission-requested');
15
15
  const permissionGranted = document.getElementById('hmpps-webcam__permission-granted');
16
- const webcamError = document.getElementById('hmpps-webcam__webcam-error');
17
16
  // Preview elements
18
17
  const photoPreviewContainer = document.getElementById('hmpps-webcam__photo-preview-container');
19
18
  const photoCaptureContainer = document.getElementById('hmpps-webcam__photo-capture-container');
20
19
  const photoCaptureErrorContainer = document.getElementById('hmpps-webcam__photo-capture-container__error');
21
20
  const mimetype = 'image/jpeg';
22
21
  let stream = null;
23
- function updateWebcamList({ activeDeviceId }) {
22
+ function updateWebcamList(activeDeviceId) {
24
23
  webcamSelect.querySelectorAll('option').forEach(option => {
25
24
  if (option.value === activeDeviceId && option.label.indexOf('- Active') === -1) {
25
+ // eslint-disable-next-line no-param-reassign
26
26
  option.label = `${option.label} - Active`;
27
27
  }
28
28
  else if (option.value !== activeDeviceId && option.label.indexOf('- Active') > -1) {
29
+ // eslint-disable-next-line no-param-reassign
29
30
  option.label = option.label.replace(' - Active', '');
30
31
  }
31
32
  });
@@ -55,13 +56,16 @@ function hmppsWebcamCapture(component) {
55
56
  photoCaptureContainer.style.display = 'none';
56
57
  photoCaptureErrorContainer.style.display = 'block';
57
58
  if (enableReporting) {
58
- await fetch(`/api/report-error?pageUrl=${encodeURIComponent(location.href)}&error=${e.name}`, {
59
+ // @ts-expect-error error type is not known
60
+ const error = 'name' in e ? e.name : e.message;
61
+ // eslint-disable-next-line no-restricted-globals
62
+ await fetch(`/api/report-error?pageUrl=${encodeURIComponent(location.href)}&error=${error}`, {
59
63
  method: 'GET',
60
64
  });
61
65
  }
62
66
  }
63
67
  }
64
- async function stopWebcam() {
68
+ function stopWebcam() {
65
69
  if (stream) {
66
70
  stream.getTracks().forEach(track => track.stop());
67
71
  }
@@ -80,11 +84,13 @@ function hmppsWebcamCapture(component) {
80
84
  webcamPlaceholder.style.display = 'none';
81
85
  video.style.display = 'block';
82
86
  captureBtn.disabled = false;
83
- updateWebcamList({ activeDeviceId: deviceId });
87
+ updateWebcamList(deviceId);
84
88
  }
85
89
  catch (error) {
86
90
  permissionRequested.style.display = 'none';
87
- webcamError.style.display = 'block';
91
+ photoCaptureContainer.style.display = 'none';
92
+ photoCaptureErrorContainer.style.display = 'block';
93
+ // eslint-disable-next-line no-console
88
94
  console.error('Webcam error:', error);
89
95
  }
90
96
  }
@@ -103,8 +109,7 @@ function hmppsWebcamCapture(component) {
103
109
  canvas.height = video.videoHeight;
104
110
  const ctx = canvas.getContext('2d');
105
111
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
106
- const imageDataUrl = canvas.toDataURL(mimetype);
107
- snapshot.src = imageDataUrl;
112
+ snapshot.src = canvas.toDataURL(mimetype);
108
113
  canvas.toBlob(blob => {
109
114
  if (!blob)
110
115
  return;
@@ -124,6 +129,7 @@ function hmppsWebcamCapture(component) {
124
129
  }
125
130
  return pageInit();
126
131
  }
132
+ // eslint-disable-next-line import/prefer-default-export
127
133
  function init() {
128
134
  const webcamContainer = document.getElementById('hmpps-webcam-capture-component');
129
135
  if (webcamContainer) {