@ministryofjustice/hmpps-connect-dps-shared-items 4.4.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +8 -6
  2. package/dist/alertFlags/alertTypes.d.ts +25 -0
  3. package/dist/alertFlags/alertTypes.test.d.ts +1 -0
  4. package/dist/alertFlags/getAlertFlagLabelsForAlerts.d.ts +4 -2
  5. package/dist/assets/dps/components/address-autosuggest/_address-autosuggest.scss +9 -10
  6. package/dist/assets/dps/components/address-autosuggest/abortable-fetch.js +40 -46
  7. package/dist/assets/dps/components/address-autosuggest/autosuggest-address.js +74 -85
  8. package/dist/assets/dps/components/address-autosuggest/autosuggest-ui.js +343 -383
  9. package/dist/assets/dps/components/address-autosuggest/index.js +9 -8
  10. package/dist/assets/dps/components/alert-flag/_alert-flag.scss +8 -5
  11. package/dist/assets/dps/components/alert-flag/macro.njk +1 -1
  12. package/dist/assets/dps/components/modal/_index.scss +148 -0
  13. package/dist/assets/dps/components/modal/index.js +171 -0
  14. package/dist/assets/dps/components/modal/index.ts +203 -0
  15. package/dist/assets/dps/components/modal/macro.njk +48 -0
  16. package/dist/assets/dps/components/webcam-capture/_webcam-capture.scss +11 -10
  17. package/dist/assets/dps/components/webcam-capture/index.js +126 -149
  18. package/dist/assets/dps/components/webcam-capture/macro.njk +10 -26
  19. package/dist/assets/js/all.js +10 -5
  20. package/dist/assets/scss/all.css +844 -0
  21. package/dist/assets/scss/all.scss +4 -3
  22. package/dist/index.cjs +57 -0
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.ts +29 -1
  25. package/dist/index.esm.js +56 -1
  26. package/dist/index.esm.js.map +1 -1
  27. package/dist/types/public/alertFlags/index.d.ts +1 -0
  28. package/package.json +18 -7
  29. package/dist/assets/dps/components/_common.scss +0 -21
@@ -1,403 +1,363 @@
1
- // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/components/autosuggest/autosuggest.ui.js
2
- import { abortError, abortTimeout } from './abortable-fetch'
3
-
4
- const jsBaseClass = 'hmpps-js-autosuggest'
5
-
6
- const classSuffixInitialised = '--initialised'
7
- const classSuffixOption = '__option'
8
- const classSuffixOptionFocused = `${classSuffixOption}--focused`
9
- const classSuffixOptionNoResults = `${classSuffixOption}--no-results`
10
- const classSuffixHasResults = '--has-results'
11
-
12
- export const NoResults = {
13
- type_more: 'TYPE_MORE',
14
- no_match: 'NO_MATCH',
15
- server_error: 'SERVER_ERROR',
16
- }
17
-
18
- export class AutosuggestUi {
19
- constructor({ context, onSelect, suggestionFunction }) {
20
- // DOM Elements
21
- this.context = context
22
- this.input = context.querySelector(`.${jsBaseClass}-input`)
23
- this.resultsContainer = context.querySelector(`.${jsBaseClass}-results`)
24
- this.resultsTitleContainer = this.resultsContainer.querySelector(`.${jsBaseClass}-results-title`)
25
- this.listbox = this.resultsContainer.querySelector(`.${jsBaseClass}-listbox`)
26
- this.instructions = context.querySelector(`.${jsBaseClass}-instructions`)
27
- this.errorContainer = context.querySelector(`.${jsBaseClass}-error`)
28
- this.errorTextContainer = this.errorContainer.querySelector('.moj-alert__content')
29
- this.ariaStatus = context.querySelector(`.${jsBaseClass}-aria-status`)
30
-
31
- // Settings
32
- this.minChars = 3
33
- this.ariaYouHaveSelected = context.getAttribute('data-aria-you-have-selected')
34
- this.ariaMinChars = context.getAttribute('data-aria-min-chars')
35
- this.ariaOneResult = context.getAttribute('data-aria-one-result')
36
- this.ariaNResults = context.getAttribute('data-aria-n-results')
37
- this.noResults = context.getAttribute('data-no-results')
38
- this.typeMore = context.getAttribute('data-type-more')
39
- this.listboxId = this.listbox.getAttribute('id')
40
- this.errorMessage = context.getAttribute('data-error-api')
41
- this.stylingBaseClass = context.getAttribute('data-styling-base-class')
42
-
43
- // Callbacks
44
- this.onSelect = onSelect
45
- this.fetchSuggestions = suggestionFunction
46
-
47
- // State
48
- this.query = ''
49
- this.sanitisedQuery = ''
50
- this.results = []
51
- this.resultOptions = []
52
- this.data = []
53
- this.numberOfResults = 0
54
- this.highlightedResultIndex = null
55
- this.blurring = false
56
- this.blurTimeout = null
57
- this.scrolling = false
58
- this.scrollTimeout = null
59
-
60
- this.initialiseUI()
61
- }
62
-
63
- initialiseUI() {
64
- this.input.setAttribute('aria-autocomplete', 'list')
65
- this.input.setAttribute('aria-controls', this.listbox.getAttribute('id'))
66
- this.input.setAttribute('aria-describedby', this.instructions.getAttribute('id'))
67
- this.input.setAttribute('aria-haspopup', true)
68
- this.input.setAttribute('aria-owns', this.listbox.getAttribute('id'))
69
- this.input.setAttribute('aria-expanded', false)
70
- this.input.setAttribute('autocomplete', 'off')
71
- this.input.setAttribute('role', 'combobox')
72
-
73
- this.context.classList.add(`${this.stylingBaseClass}${classSuffixInitialised}`)
74
-
75
- this.bindEventListeners()
76
- }
1
+ import { abortError, abortTimeout } from './abortable-fetch.js';
77
2
 
78
- bindEventListeners() {
79
- this.input.addEventListener('keydown', this.handleKeydown.bind(this))
80
- this.input.addEventListener('keyup', this.handleKeyup.bind(this))
81
- this.input.addEventListener('input', this.handleChange.bind(this))
82
- this.input.addEventListener('blur', this.handleBlur.bind(this))
83
- this.input.addEventListener('focus', this.handleFocus.bind(this))
84
- }
85
-
86
- handleKeydown(event) {
87
- switch (event.keyCode) {
88
- case 38: {
89
- // Up
90
- event.preventDefault()
91
- this.navigateResults(-1)
92
- break
93
- }
94
- case 40: {
95
- // Down
96
- event.preventDefault()
97
- this.navigateResults(1)
98
- break
99
- }
100
- case 27: {
101
- // Escape
102
- this.clearListbox()
103
- break
104
- }
105
- case 13: {
106
- // Enter
107
- if (this.highlightedResultIndex !== null) {
108
- event.preventDefault()
3
+ // Based on ONSDigital's https://github.com/ONSdigital/design-system/blob/main/src/components/autosuggest/autosuggest.ui.js
4
+ const jsBaseClass = 'hmpps-js-autosuggest';
5
+ const classSuffixInitialised = '--initialised';
6
+ const classSuffixOption = '__option';
7
+ const classSuffixOptionFocused = `${classSuffixOption}--focused`;
8
+ const classSuffixOptionNoResults = `${classSuffixOption}--no-results`;
9
+ const classSuffixHasResults = '--has-results';
10
+ const NoResults = {
11
+ type_more: 'TYPE_MORE',
12
+ no_match: 'NO_MATCH',
13
+ server_error: 'SERVER_ERROR',
14
+ };
15
+ class AutosuggestUi {
16
+ constructor({ context, onSelect, suggestionFunction }) {
17
+ // DOM Elements
18
+ this.context = context;
19
+ this.input = context.querySelector(`.${jsBaseClass}-input`);
20
+ this.resultsContainer = context.querySelector(`.${jsBaseClass}-results`);
21
+ this.resultsTitleContainer = this.resultsContainer.querySelector(`.${jsBaseClass}-results-title`);
22
+ this.listbox = this.resultsContainer.querySelector(`.${jsBaseClass}-listbox`);
23
+ this.instructions = context.querySelector(`.${jsBaseClass}-instructions`);
24
+ this.errorContainer = context.querySelector(`.${jsBaseClass}-error`);
25
+ this.errorTextContainer = this.errorContainer.querySelector('.moj-alert__content');
26
+ this.ariaStatus = context.querySelector(`.${jsBaseClass}-aria-status`);
27
+ // Settings
28
+ this.minChars = 3;
29
+ this.ariaYouHaveSelected = context.getAttribute('data-aria-you-have-selected');
30
+ this.ariaMinChars = context.getAttribute('data-aria-min-chars');
31
+ this.ariaOneResult = context.getAttribute('data-aria-one-result');
32
+ this.ariaNResults = context.getAttribute('data-aria-n-results');
33
+ this.noResults = context.getAttribute('data-no-results');
34
+ this.typeMore = context.getAttribute('data-type-more');
35
+ this.listboxId = this.listbox.getAttribute('id');
36
+ this.errorMessage = context.getAttribute('data-error-api');
37
+ this.stylingBaseClass = context.getAttribute('data-styling-base-class');
38
+ // Callbacks
39
+ this.onSelect = onSelect;
40
+ this.fetchSuggestions = suggestionFunction;
41
+ // State
42
+ this.query = '';
43
+ this.sanitisedQuery = '';
44
+ this.results = [];
45
+ this.resultOptions = [];
46
+ this.data = [];
47
+ this.numberOfResults = 0;
48
+ this.highlightedResultIndex = null;
49
+ this.blurring = false;
50
+ this.blurTimeout = null;
51
+ this.scrolling = false;
52
+ this.scrollTimeout = null;
53
+ this.initialiseUI();
54
+ }
55
+ initialiseUI() {
56
+ this.input.setAttribute('aria-autocomplete', 'list');
57
+ this.input.setAttribute('aria-controls', this.listbox.getAttribute('id'));
58
+ this.input.setAttribute('aria-describedby', this.instructions.getAttribute('id'));
59
+ this.input.setAttribute('aria-haspopup', true);
60
+ this.input.setAttribute('aria-owns', this.listbox.getAttribute('id'));
61
+ this.input.setAttribute('aria-expanded', false);
62
+ this.input.setAttribute('autocomplete', 'off');
63
+ this.input.setAttribute('role', 'combobox');
64
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixInitialised}`);
65
+ this.bindEventListeners();
66
+ }
67
+ bindEventListeners() {
68
+ this.input.addEventListener('keydown', this.handleKeydown.bind(this));
69
+ this.input.addEventListener('keyup', this.handleKeyup.bind(this));
70
+ this.input.addEventListener('input', this.handleChange.bind(this));
71
+ this.input.addEventListener('blur', this.handleBlur.bind(this));
72
+ this.input.addEventListener('focus', this.handleFocus.bind(this));
73
+ }
74
+ handleKeydown(event) {
75
+ switch (event.keyCode) {
76
+ case 38: {
77
+ // Up
78
+ event.preventDefault();
79
+ this.navigateResults(-1);
80
+ break;
81
+ }
82
+ case 40: {
83
+ // Down
84
+ event.preventDefault();
85
+ this.navigateResults(1);
86
+ break;
87
+ }
88
+ case 27: {
89
+ // Escape
90
+ this.clearListbox();
91
+ break;
92
+ }
93
+ case 13: {
94
+ // Enter
95
+ if (this.highlightedResultIndex !== null) {
96
+ event.preventDefault();
97
+ }
98
+ break;
99
+ }
109
100
  }
110
- break
111
- }
112
101
  }
113
- }
114
-
115
- handleKeyup(event) {
116
- switch (event.keyCode) {
117
- // Up and down
118
- case 40:
119
- case 38: {
120
- event.preventDefault()
121
- break
122
- }
123
- case 13: {
124
- // Enter
125
- if (this.highlightedResultIndex !== null) {
126
- this.selectResult()
102
+ handleKeyup(event) {
103
+ switch (event.keyCode) {
104
+ // Up and down
105
+ case 40:
106
+ case 38: {
107
+ event.preventDefault();
108
+ break;
109
+ }
110
+ case 13: {
111
+ // Enter
112
+ if (this.highlightedResultIndex !== null) {
113
+ this.selectResult();
114
+ }
115
+ break;
116
+ }
127
117
  }
128
- break
129
- }
130
118
  }
131
- }
132
-
133
- handleChange() {
134
- if (!this.blurring && this.input.value.trim()) {
135
- this.getSuggestions()
136
- } else {
137
- this.clearListbox()
119
+ handleChange() {
120
+ if (!this.blurring && this.input.value.trim()) {
121
+ this.getSuggestions();
122
+ }
123
+ else {
124
+ this.clearListbox();
125
+ }
138
126
  }
139
- }
140
-
141
- handleBlur() {
142
- clearTimeout(this.blurTimeout)
143
- this.blurring = true
144
-
145
- // Timeout required to allow user to click an option before clearing the listbox:
146
- this.blurTimeout = setTimeout(() => {
147
- this.blurring = false
148
-
149
- this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`)
150
- this.input.setAttribute('aria-expanded', false)
151
- this.setAriaStatus()
152
- }, 300)
153
- }
154
-
155
- handleFocus() {
156
- if (this.listbox.innerHTML) {
157
- this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`)
158
- this.input.setAttribute('aria-expanded', true)
159
- this.setAriaStatus()
127
+ handleBlur() {
128
+ clearTimeout(this.blurTimeout);
129
+ this.blurring = true;
130
+ // Timeout required to allow user to click an option before clearing the listbox:
131
+ this.blurTimeout = setTimeout(() => {
132
+ this.blurring = false;
133
+ this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`);
134
+ this.input.setAttribute('aria-expanded', false);
135
+ this.setAriaStatus();
136
+ }, 300);
160
137
  }
161
- }
162
-
163
- handleScroll(option, scrollUp) {
164
- clearTimeout(this.scrollTimeout)
165
- this.scrolling = true
166
- option.scrollIntoView(scrollUp)
167
-
168
- // Timeout required to allow user to click an option before clearing the listbox:
169
- this.scrollTimeout = setTimeout(() => {
170
- this.scrolling = false
171
- }, 300)
172
- }
173
-
174
- checkCharCount() {
175
- if (this.input.value.length > 1 && this.input.value.length < this.minChars) {
176
- this.inputTimeout = setTimeout(() => {
177
- this.handleNoResults(NoResults.type_more)
178
- }, 2000)
179
- } else {
180
- clearTimeout(this.inputTimeout)
138
+ handleFocus() {
139
+ if (this.listbox.innerHTML) {
140
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`);
141
+ this.input.setAttribute('aria-expanded', true);
142
+ this.setAriaStatus();
143
+ }
181
144
  }
182
- }
183
-
184
- navigateResults(direction) {
185
- let index = 0
186
- if (this.highlightedResultIndex !== null) {
187
- index = this.highlightedResultIndex + direction
145
+ handleScroll(option, scrollUp) {
146
+ clearTimeout(this.scrollTimeout);
147
+ this.scrolling = true;
148
+ option.scrollIntoView(scrollUp);
149
+ // Timeout required to allow user to click an option before clearing the listbox:
150
+ this.scrollTimeout = setTimeout(() => {
151
+ this.scrolling = false;
152
+ }, 300);
188
153
  }
189
-
190
- if (index < this.numberOfResults) {
191
- if (index < 0) {
192
- index = null
193
- }
194
-
195
- this.setHighlightedResult(index)
154
+ checkCharCount() {
155
+ if (this.input.value.length > 1 && this.input.value.length < this.minChars) {
156
+ this.inputTimeout = setTimeout(() => {
157
+ this.handleNoResults(NoResults.type_more);
158
+ }, 2000);
159
+ }
160
+ else {
161
+ clearTimeout(this.inputTimeout);
162
+ }
196
163
  }
197
- }
198
-
199
- sanitiseAutosuggestText(string) {
200
- let sanitisedString = string.toLowerCase()
201
-
202
- sanitisedString = sanitisedString.trim()
203
- sanitisedString = sanitisedString.replace(/,/g, '')
204
- sanitisedString = sanitisedString.replace(/\s\s+/g, ' ')
205
- sanitisedString = sanitisedString.replace(/[&]/g, '%26')
206
- sanitisedString = sanitisedString.replace(/\d(?=[a-z]{3,})/gi, '$& ')
207
-
208
- return sanitisedString
209
- }
210
-
211
- getSuggestions() {
212
- this.query = this.input.value
213
-
214
- const sanitisedQuery = this.sanitiseAutosuggestText(this.query)
215
-
216
- if (sanitisedQuery !== this.sanitisedQuery) {
217
- this.sanitisedQuery = sanitisedQuery
218
- this.unsetResults()
219
- this.checkCharCount()
220
-
221
- if (this.sanitisedQuery.length >= this.minChars) {
222
- this.fetchSuggestions(this.sanitisedQuery, this.data)
223
- .then(this.handleResults.bind(this))
224
- .catch(error => {
225
- if (error.name !== abortError && error.reason !== abortTimeout) {
226
- console.log('error:', error)
227
- this.handleNoResults(NoResults.server_error)
164
+ navigateResults(direction) {
165
+ let index = 0;
166
+ if (this.highlightedResultIndex !== null) {
167
+ index = this.highlightedResultIndex + direction;
168
+ }
169
+ if (index < this.numberOfResults) {
170
+ if (index < 0) {
171
+ index = null;
228
172
  }
229
- })
230
- } else {
231
- this.clearListbox()
232
- }
173
+ this.setHighlightedResult(index);
174
+ }
233
175
  }
234
- }
235
-
236
- unsetResults() {
237
- this.results = []
238
- this.resultOptions = []
239
- }
240
-
241
- clearListbox() {
242
- this.listbox.innerHTML = ''
243
- this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`)
244
- this.input.removeAttribute('aria-activedescendant')
245
- this.input.setAttribute('aria-expanded', false)
246
- this.setHighlightedResult(null)
247
- this.setAriaStatus()
248
- }
249
-
250
- handleResults(result) {
251
- this.results = result.results
252
- this.numberOfResults = this.results?.length || 0
253
- this.setAriaStatus()
254
- this.listbox.innerHTML = ''
255
-
256
- this.resultOptions =
257
- this.results?.map((result, index) => {
258
- this.resultsTitleContainer?.classList?.remove('hmpps-display-none')
259
- const listElement = this.createListElement(result, index)
260
- this.listbox.appendChild(listElement)
261
- return listElement
262
- }) ?? []
263
-
264
- this.setHighlightedResult(null)
265
- this.input.setAttribute('aria-expanded', this.numberOfResults ? 'true' : 'false')
266
-
267
- if (!!this.numberOfResults && this.sanitisedQuery.length >= this.minChars) {
268
- this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`)
269
- } else {
270
- this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`)
271
- this.clearListbox()
176
+ sanitiseAutosuggestText(string) {
177
+ let sanitisedString = string.toLowerCase();
178
+ sanitisedString = sanitisedString.trim();
179
+ sanitisedString = sanitisedString.replace(/,/g, '');
180
+ sanitisedString = sanitisedString.replace(/\s\s+/g, ' ');
181
+ sanitisedString = sanitisedString.replace(/[&]/g, '%26');
182
+ sanitisedString = sanitisedString.replace(/\d(?=[a-z]{3,})/gi, '$& ');
183
+ return sanitisedString;
272
184
  }
273
-
274
- if (this.numberOfResults === 0 && this.noResults) {
275
- this.handleNoResults(NoResults.no_match)
185
+ getSuggestions() {
186
+ this.query = this.input.value;
187
+ const sanitisedQuery = this.sanitiseAutosuggestText(this.query);
188
+ if (sanitisedQuery !== this.sanitisedQuery) {
189
+ this.sanitisedQuery = sanitisedQuery;
190
+ this.unsetResults();
191
+ this.checkCharCount();
192
+ if (this.sanitisedQuery.length >= this.minChars) {
193
+ this.fetchSuggestions(this.sanitisedQuery, this.data)
194
+ .then(this.handleResults.bind(this))
195
+ .catch(error => {
196
+ if (error.name !== abortError && error.reason !== abortTimeout) {
197
+ console.log('error:', error);
198
+ this.handleNoResults(NoResults.server_error);
199
+ }
200
+ });
201
+ }
202
+ else {
203
+ this.clearListbox();
204
+ }
205
+ }
276
206
  }
277
- }
278
-
279
- createListElement(result, index) {
280
- const innerHTML = this.emboldenMatch(result.displayText, this.query)
281
- const listElement = document.createElement('li')
282
- listElement.className = `${this.stylingBaseClass}${classSuffixOption}`
283
- listElement.setAttribute('id', `${this.listboxId}__option--${index}`)
284
- listElement.setAttribute('role', 'option')
285
- listElement.innerHTML = innerHTML
286
- listElement.addEventListener('click', () => {
287
- this.selectResult(index)
288
- })
289
- listElement.addEventListener('mouseenter', () => !this.scrolling && this.setHighlightedResult(index))
290
- return listElement
291
- }
292
-
293
- handleNoResults(reason) {
294
- if ([NoResults.type_more, NoResults.no_match].includes(reason)) {
295
- this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`)
296
- this.resultsTitleContainer?.classList?.add('hmpps-display-none')
297
- this.input.setAttribute('aria-expanded', true)
298
-
299
- const message = reason === NoResults.type_more ? this.typeMore : this.noResults
300
- this.setAriaStatus(message)
301
- this.listbox.innerHTML = ''
302
- const noResultsItem = document.createElement('li')
303
- noResultsItem.className = `${this.stylingBaseClass}${classSuffixOption} ${this.stylingBaseClass}${classSuffixOptionNoResults}`
304
- noResultsItem.textContent = message
305
- this.listbox.appendChild(noResultsItem)
306
- } else {
307
- this.displayAPIError()
207
+ unsetResults() {
208
+ this.results = [];
209
+ this.resultOptions = [];
308
210
  }
309
- }
310
-
311
- displayAPIError() {
312
- this.input.value = ''
313
- this.input.setAttribute('disabled', true)
314
- this.clearListbox()
315
- this.errorContainer.classList.remove('hmpps-display-none')
316
- this.errorTextContainer.textContent = this.errorMessage
317
- this.ariaStatus.setAttribute('aria-hidden', 'true')
318
- this.setAriaStatus(this.errorMessage)
319
- }
320
-
321
- setHighlightedResult(index) {
322
- this.highlightedResultIndex = index
323
-
324
- if (this.highlightedResultIndex === null) {
325
- this.input.removeAttribute('aria-activedescendant')
211
+ clearListbox() {
212
+ this.listbox.innerHTML = '';
213
+ this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`);
214
+ this.input.removeAttribute('aria-activedescendant');
215
+ this.input.setAttribute('aria-expanded', false);
216
+ this.setHighlightedResult(null);
217
+ this.setAriaStatus();
326
218
  }
327
-
328
- if (this.numberOfResults) {
329
- this.resultOptions.forEach((option, optionIndex) => {
330
- if (index !== null && optionIndex === index) {
331
- if (this.isAboveViewport(option)) {
332
- this.handleScroll(option, true)
333
- }
334
- if (this.isBelowViewport(option)) {
335
- this.handleScroll(option, false)
336
- }
337
- option.classList.add(`${this.stylingBaseClass}${classSuffixOptionFocused}`)
338
- option.setAttribute('aria-selected', true)
339
- this.input.setAttribute('aria-activedescendant', option.getAttribute('id'))
340
- const optionText = option.innerHTML.replace('<b>', '').replace('</b>', '')
341
- this.setAriaStatus(optionText)
342
- } else {
343
- option.classList.remove(`${this.stylingBaseClass}${classSuffixOptionFocused}`)
344
- option.removeAttribute('aria-selected')
219
+ handleResults(result) {
220
+ this.results = result.results;
221
+ this.numberOfResults = this.results?.length || 0;
222
+ this.setAriaStatus();
223
+ this.listbox.innerHTML = '';
224
+ this.resultOptions =
225
+ this.results?.map((result, index) => {
226
+ this.resultsTitleContainer?.classList?.remove('hmpps-display-none');
227
+ const listElement = this.createListElement(result, index);
228
+ this.listbox.appendChild(listElement);
229
+ return listElement;
230
+ }) ?? [];
231
+ this.setHighlightedResult(null);
232
+ this.input.setAttribute('aria-expanded', this.numberOfResults ? 'true' : 'false');
233
+ if (!!this.numberOfResults && this.sanitisedQuery.length >= this.minChars) {
234
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`);
235
+ }
236
+ else {
237
+ this.context.classList.remove(`${this.stylingBaseClass}${classSuffixHasResults}`);
238
+ this.clearListbox();
239
+ }
240
+ if (this.numberOfResults === 0 && this.noResults) {
241
+ this.handleNoResults(NoResults.no_match);
345
242
  }
346
- })
347
243
  }
348
- }
349
-
350
- selectResult(index) {
351
- if (!this.results.length) return
352
-
353
- const selectedIndex = typeof index === 'number' ? index : this.highlightedResultIndex
354
- if (selectedIndex === null || selectedIndex === undefined) return
355
-
356
- const result = this.results[selectedIndex]
357
- if (!result) return
358
-
359
- this.onSelect(result)
360
- this.clearListbox()
361
- this.setAriaStatus(`${this.ariaYouHaveSelected}: ${result.displayText}.`)
362
- }
363
-
364
- setAriaStatus(content) {
365
- if (!content) {
366
- const queryTooShort = this.sanitisedQuery.length < this.minChars
367
- const noResults = this.numberOfResults === 0
368
- if (queryTooShort) {
369
- content = this.ariaMinChars
370
- } else if (noResults) {
371
- content = `${this.noResults}: "${this.query}"`
372
- } else if (this.numberOfResults === 1) {
373
- content = this.ariaOneResult
374
- } else {
375
- content = this.ariaNResults.replace('{n}', this.numberOfResults)
376
- }
244
+ createListElement(result, index) {
245
+ const innerHTML = this.emboldenMatch(result.displayText, this.query);
246
+ const listElement = document.createElement('li');
247
+ listElement.className = `${this.stylingBaseClass}${classSuffixOption}`;
248
+ listElement.setAttribute('id', `${this.listboxId}__option--${index}`);
249
+ listElement.setAttribute('role', 'option');
250
+ listElement.innerHTML = innerHTML;
251
+ listElement.addEventListener('click', () => {
252
+ this.selectResult(index);
253
+ });
254
+ listElement.addEventListener('mouseenter', () => !this.scrolling && this.setHighlightedResult(index));
255
+ return listElement;
256
+ }
257
+ handleNoResults(reason) {
258
+ if ([NoResults.type_more, NoResults.no_match].includes(reason)) {
259
+ this.context.classList.add(`${this.stylingBaseClass}${classSuffixHasResults}`);
260
+ this.resultsTitleContainer?.classList?.add('hmpps-display-none');
261
+ this.input.setAttribute('aria-expanded', true);
262
+ const message = reason === NoResults.type_more ? this.typeMore : this.noResults;
263
+ this.setAriaStatus(message);
264
+ this.listbox.innerHTML = '';
265
+ const noResultsItem = document.createElement('li');
266
+ noResultsItem.className = `${this.stylingBaseClass}${classSuffixOption} ${this.stylingBaseClass}${classSuffixOptionNoResults}`;
267
+ noResultsItem.textContent = message;
268
+ this.listbox.appendChild(noResultsItem);
269
+ }
270
+ else {
271
+ this.displayAPIError();
272
+ }
273
+ }
274
+ displayAPIError() {
275
+ this.input.value = '';
276
+ this.input.setAttribute('disabled', true);
277
+ this.clearListbox();
278
+ this.errorContainer.classList.remove('hmpps-display-none');
279
+ this.errorTextContainer.textContent = this.errorMessage;
280
+ this.ariaStatus.setAttribute('aria-hidden', 'true');
281
+ this.setAriaStatus(this.errorMessage);
282
+ }
283
+ setHighlightedResult(index) {
284
+ this.highlightedResultIndex = index;
285
+ if (this.highlightedResultIndex === null) {
286
+ this.input.removeAttribute('aria-activedescendant');
287
+ }
288
+ if (this.numberOfResults) {
289
+ this.resultOptions.forEach((option, optionIndex) => {
290
+ if (index !== null && optionIndex === index) {
291
+ if (this.isAboveViewport(option)) {
292
+ this.handleScroll(option, true);
293
+ }
294
+ if (this.isBelowViewport(option)) {
295
+ this.handleScroll(option, false);
296
+ }
297
+ option.classList.add(`${this.stylingBaseClass}${classSuffixOptionFocused}`);
298
+ option.setAttribute('aria-selected', true);
299
+ this.input.setAttribute('aria-activedescendant', option.getAttribute('id'));
300
+ const optionText = option.innerHTML.replace('<b>', '').replace('</b>', '');
301
+ this.setAriaStatus(optionText);
302
+ }
303
+ else {
304
+ option.classList.remove(`${this.stylingBaseClass}${classSuffixOptionFocused}`);
305
+ option.removeAttribute('aria-selected');
306
+ }
307
+ });
308
+ }
309
+ }
310
+ selectResult(index) {
311
+ if (!this.results.length)
312
+ return;
313
+ const selectedIndex = typeof index === 'number' ? index : this.highlightedResultIndex;
314
+ if (selectedIndex === null || selectedIndex === undefined)
315
+ return;
316
+ const result = this.results[selectedIndex];
317
+ if (!result)
318
+ return;
319
+ this.onSelect(result);
320
+ this.clearListbox();
321
+ this.setAriaStatus(`${this.ariaYouHaveSelected}: ${result.displayText}.`);
322
+ }
323
+ setAriaStatus(content) {
324
+ if (!content) {
325
+ const queryTooShort = this.sanitisedQuery.length < this.minChars;
326
+ const noResults = this.numberOfResults === 0;
327
+ if (queryTooShort) {
328
+ content = this.ariaMinChars;
329
+ }
330
+ else if (noResults) {
331
+ content = `${this.noResults}: "${this.query}"`;
332
+ }
333
+ else if (this.numberOfResults === 1) {
334
+ content = this.ariaOneResult;
335
+ }
336
+ else {
337
+ content = this.ariaNResults.replace('{n}', this.numberOfResults);
338
+ }
339
+ }
340
+ this.ariaStatus.textContent = content;
341
+ }
342
+ emboldenMatch(string, query) {
343
+ return this.escapeRegExp(query)
344
+ .trim()
345
+ .replace(/\s\s+/g, ' ')
346
+ .split(' ')
347
+ .reduce((accumulator, currentValue) => {
348
+ let reg = new RegExp(`(?<!<)${currentValue}(?![\\w\\s]*>)`, 'gi');
349
+ return accumulator.replace(reg, '<b>$&</b>');
350
+ }, string);
351
+ }
352
+ escapeRegExp(string) {
353
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
354
+ }
355
+ isAboveViewport(element) {
356
+ return element.getBoundingClientRect().top < 0;
357
+ }
358
+ isBelowViewport(element) {
359
+ return element.getBoundingClientRect().bottom > (window.innerHeight || document.documentElement.clientHeight);
377
360
  }
378
- this.ariaStatus.textContent = content
379
- }
380
-
381
- emboldenMatch(string, query) {
382
- return this.escapeRegExp(query)
383
- .trim()
384
- .replace(/\s\s+/g, ' ')
385
- .split(' ')
386
- .reduce((accumulator, currentValue) => {
387
- let reg = new RegExp(`(?<!<)${currentValue}(?![\\w\\s]*>)`, 'gi')
388
- return accumulator.replace(reg, '<b>$&</b>')
389
- }, string)
390
- }
391
-
392
- escapeRegExp(string) {
393
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
394
- }
395
-
396
- isAboveViewport(element) {
397
- return element.getBoundingClientRect().top < 0
398
- }
399
-
400
- isBelowViewport(element) {
401
- return element.getBoundingClientRect().bottom > (window.innerHeight || document.documentElement.clientHeight)
402
- }
403
361
  }
362
+
363
+ export { AutosuggestUi, NoResults };