@ministryofjustice/hmpps-digital-prison-reporting-frontend 9.2.3 → 9.3.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.
@@ -0,0 +1,2 @@
1
+ import { components } from '../../../../../../src/dpr/types/api';
2
+ export declare const definition: components['schemas']['DashboardDefinition'];
@@ -11,6 +11,7 @@ export declare const scorecardsInvalidVisDefinitionsMock: import("@networkMocks/
11
11
  export declare const scorecardsCompletebadDatasetMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
12
12
  export declare const scorecardGroupCompleteDatasetMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
13
13
  export declare const scorecardGroupCompleteDatasetInvalidMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
14
+ export declare const scorecardGroupCompleteDatasetNoTsMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
14
15
  export declare const matrixCompleteDatasetMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
15
16
  export declare const barCompleteDatasetMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
16
17
  export declare const barPartialDatasetMock: import("@networkMocks/generateNetworkMock").CompleteMockRequest;
package/all.bundle.js CHANGED
@@ -780,8 +780,8 @@ class DprFiltersFormClass extends DprClientClass {
780
780
  updateSingle(params, input) {
781
781
  const name = this.normaliseFilterName(input.name);
782
782
  let value = input.value;
783
- if (input instanceof HTMLInputElement && input.dataset['staticOptionNameValue']) {
784
- value = input.dataset['staticOptionNameValue'];
783
+ if (input instanceof HTMLInputElement && input.dataset['selectedValue']) {
784
+ value = input.dataset['selectedValue'];
785
785
  }
786
786
  const isDateInput = input instanceof HTMLInputElement && input.classList.contains('moj-js-datepicker-input');
787
787
  if (isDateInput) {
@@ -1118,183 +1118,223 @@ class AutoCompleteMulti extends DprClientClass {
1118
1118
  }
1119
1119
  }
1120
1120
 
1121
- /* eslint-disable class-methods-use-this */
1122
1121
  class Autocomplete extends DprClientClass {
1123
- listItemsSelector;
1124
- listParentSelector;
1122
+ activeIndex = -1;
1125
1123
  static getModuleName() {
1126
1124
  return 'autocomplete-text-input';
1127
1125
  }
1128
- constructor(element) {
1129
- super(element);
1130
- const listId = this.getTextInput()?.getAttribute('aria-owns');
1131
- this.listItemsSelector = `#${listId} li`;
1132
- this.listParentSelector = `#${listId} ul`;
1133
- }
1134
1126
  initialise() {
1135
1127
  const textInput = this.getTextInput();
1136
- textInput?.addEventListener('keyup', event => {
1137
- this.onTextInput(event, textInput);
1128
+ if (!textInput) {
1129
+ return;
1130
+ }
1131
+ this.initialiseDefaultValue();
1132
+ textInput.addEventListener('input', () => {
1133
+ this.onInput();
1138
1134
  });
1139
- textInput?.addEventListener('keypress', e => {
1140
- if (e.key === 'Enter') {
1141
- e.stopPropagation();
1142
- e.preventDefault();
1143
- }
1135
+ textInput.addEventListener('keydown', event => {
1136
+ this.onKeyDown(event);
1144
1137
  });
1145
- textInput?.addEventListener('input', () => {
1146
- if (textInput.value !== '') {
1147
- return;
1148
- }
1149
- const hiddenInput = this.getHiddenInput();
1150
- if (hiddenInput) {
1151
- hiddenInput.value = '';
1152
- hiddenInput.disabled = true;
1153
- }
1154
- delete textInput.dataset['staticOptionNameValue'];
1155
- textInput.dispatchEvent(new Event('change', { bubbles: true }));
1138
+ textInput.addEventListener('blur', () => {
1139
+ window.setTimeout(() => {
1140
+ this.closeList();
1141
+ }, 100);
1156
1142
  });
1157
- this.getElement()
1158
- .querySelectorAll('.autocomplete-text-input-list-button')
1159
- .forEach(button => {
1160
- button.addEventListener('mousedown', event => {
1161
- this.onOptionClick(event, textInput, this.getElement());
1143
+ this.getAllOptions().forEach(option => {
1144
+ option.addEventListener('mousedown', event => {
1145
+ event.preventDefault();
1146
+ this.selectOption(option);
1162
1147
  });
1163
1148
  });
1164
- this.initialiseDefaultValue(textInput);
1149
+ textInput.addEventListener('focus', () => {
1150
+ const minimumLength = Number(textInput.dataset['minimumLength'] || 0);
1151
+ if (minimumLength === 0) {
1152
+ this.filterOptions('');
1153
+ }
1154
+ });
1155
+ }
1156
+ getTextInput() {
1157
+ return this.getElement().querySelector('.autocomplete-text-input-box');
1165
1158
  }
1166
- initialiseDefaultValue(textInput) {
1159
+ getHiddenInput() {
1160
+ return this.getElement().querySelector('[data-autocomplete-hidden]');
1161
+ }
1162
+ getAllOptions() {
1163
+ return Array.from(this.getElement().querySelectorAll('.autocomplete-option'));
1164
+ }
1165
+ getVisibleOptions() {
1166
+ return this.getAllOptions().filter(option => !option.classList.contains('autocomplete-text-input-item-hide'));
1167
+ }
1168
+ initialiseDefaultValue() {
1167
1169
  const hiddenInput = this.getHiddenInput();
1168
- if (hiddenInput?.value) {
1169
- hiddenInput.disabled = false;
1170
+ const textInput = this.getTextInput();
1171
+ if (!hiddenInput || !textInput) {
1170
1172
  return;
1171
1173
  }
1172
- if (textInput) {
1173
- textInput.value = '';
1174
- }
1175
- delete textInput?.dataset['staticOptionNameValue'];
1176
- if (hiddenInput) {
1177
- hiddenInput.value = '';
1174
+ const selectedValue = hiddenInput.value;
1175
+ if (!selectedValue) {
1178
1176
  hiddenInput.disabled = true;
1177
+ return;
1179
1178
  }
1179
+ hiddenInput.disabled = false;
1180
+ const matchingOption = this.getAllOptions().find(option => option.dataset['value'] === selectedValue);
1181
+ if (!matchingOption) {
1182
+ return;
1183
+ }
1184
+ textInput.value = matchingOption.dataset['display'] ?? matchingOption.textContent?.trim() ?? '';
1185
+ textInput.dataset['selectedValue'] = selectedValue;
1180
1186
  }
1181
- getTextInput() {
1182
- return this.getElement().querySelector('.autocomplete-text-input-box');
1183
- }
1184
- onTextInput(event, textInput) {
1185
- const minLength = Number(textInput.dataset['minimumLength']);
1186
- const { resourceEndpoint } = textInput.dataset;
1187
- const searchValue = event.target.value.toLowerCase();
1188
- if (resourceEndpoint) {
1189
- if (searchValue.length >= minLength) {
1190
- this.addItem(this.clearListAndRecreateTemplate(), '<i>Searching...</i>');
1191
- this.populateOptionsDynamically(resourceEndpoint, searchValue, textInput, () => this.clearListAndRecreateTemplate());
1192
- }
1193
- else {
1194
- this.clearListAndRecreateTemplate();
1187
+ onInput() {
1188
+ const textInput = this.getTextInput();
1189
+ if (!textInput) {
1190
+ return;
1191
+ }
1192
+ this.clearSelection();
1193
+ const searchValue = textInput.value.trim().toLowerCase();
1194
+ if (!searchValue) {
1195
+ this.closeList();
1196
+ return;
1197
+ }
1198
+ this.filterOptions(searchValue);
1199
+ }
1200
+ filterOptions(searchValue) {
1201
+ const minimumLength = Number(this.getTextInput()?.dataset['minimumLength'] || 0);
1202
+ let visibleCount = 0;
1203
+ this.getAllOptions().forEach(option => {
1204
+ const display = option.dataset['display']?.toLowerCase() ?? '';
1205
+ const value = option.dataset['value']?.toLowerCase() ?? '';
1206
+ const matches = searchValue.length === 0
1207
+ ? true
1208
+ : searchValue.length >= minimumLength && (display.includes(searchValue) || value.includes(searchValue));
1209
+ option.classList.toggle('autocomplete-text-input-item-hide', !matches);
1210
+ option.classList.remove('autocomplete-option-selected');
1211
+ option.setAttribute('aria-selected', 'false');
1212
+ if (matches) {
1213
+ visibleCount += 1;
1195
1214
  }
1215
+ });
1216
+ this.activeIndex = -1;
1217
+ if (visibleCount > 0) {
1218
+ this.openList();
1196
1219
  }
1197
1220
  else {
1198
- this.getElement()
1199
- .querySelectorAll(this.listItemsSelector)
1200
- .forEach(item => {
1201
- if (searchValue.length >= minLength &&
1202
- this.isMatchingStaticOptionNameOrDisplayPrefix(this.getInputListButton(item), searchValue, item)) {
1203
- item.classList.remove('autocomplete-text-input-item-hide');
1204
- }
1205
- else {
1206
- item.classList.add('autocomplete-text-input-item-hide');
1207
- }
1208
- });
1221
+ this.closeList();
1209
1222
  }
1210
- if (searchValue.length === 0) {
1211
- const changeEvent = new Event('change');
1212
- textInput.dispatchEvent(changeEvent);
1213
- }
1214
- }
1215
- getInputListButton(item) {
1216
- return item.querySelector('.autocomplete-text-input-list-button');
1217
1223
  }
1218
- isMatchingStaticOptionNameOrDisplayPrefix(inputListButton, searchValue, item) {
1219
- return (this.isStaticOptionsNamePrefix(inputListButton?.dataset['staticOptionNameValue'], searchValue) ||
1220
- item.innerText.trim().toLowerCase().startsWith(searchValue));
1224
+ onKeyDown(event) {
1225
+ switch (event.key) {
1226
+ case 'ArrowDown':
1227
+ event.preventDefault();
1228
+ this.moveNext();
1229
+ break;
1230
+ case 'ArrowUp':
1231
+ event.preventDefault();
1232
+ this.movePrevious();
1233
+ break;
1234
+ case 'Enter':
1235
+ this.selectActiveOption(event);
1236
+ break;
1237
+ case 'Escape':
1238
+ event.preventDefault();
1239
+ this.closeList();
1240
+ break;
1241
+ }
1221
1242
  }
1222
- isStaticOptionsNamePrefix(staticOptionNameValue, searchValue) {
1223
- return staticOptionNameValue && staticOptionNameValue.trim().toLowerCase().startsWith(searchValue);
1243
+ moveNext() {
1244
+ const options = this.getVisibleOptions();
1245
+ if (!options.length) {
1246
+ return;
1247
+ }
1248
+ const nextIndex = this.activeIndex < options.length - 1 ? this.activeIndex + 1 : 0;
1249
+ this.highlightOption(nextIndex);
1224
1250
  }
1225
- async populateOptionsDynamically(resourceEndpoint, searchValue, textInput, templateProvider) {
1226
- try {
1227
- const response = await fetch(resourceEndpoint.replace('{prefix}', encodeURI(searchValue)));
1228
- const results = await response.json();
1229
- if (searchValue === textInput?.value.toLowerCase()) {
1230
- const template = templateProvider();
1231
- results.forEach((result) => {
1232
- this.addItem(template, result, event => {
1233
- this.onOptionClick(event, textInput, this.getElement());
1234
- });
1235
- });
1236
- }
1251
+ movePrevious() {
1252
+ const options = this.getVisibleOptions();
1253
+ if (!options.length) {
1254
+ return;
1237
1255
  }
1238
- catch (error) {
1239
- this.addItem(templateProvider(), `Failed to retrieve results: ${error}`);
1256
+ const previousIndex = this.activeIndex > 0 ? this.activeIndex - 1 : options.length - 1;
1257
+ this.highlightOption(previousIndex);
1258
+ }
1259
+ highlightOption(index) {
1260
+ const textInput = this.getTextInput();
1261
+ const options = this.getVisibleOptions();
1262
+ if (!textInput || !options.length) {
1263
+ return;
1240
1264
  }
1265
+ this.activeIndex = index;
1266
+ options.forEach((option, optionIndex) => {
1267
+ const selected = optionIndex === index;
1268
+ option.classList.toggle('autocomplete-option-selected', selected);
1269
+ option.setAttribute('aria-selected', String(selected));
1270
+ });
1271
+ const activeOption = options[index];
1272
+ textInput.setAttribute('aria-activedescendant', activeOption.id);
1273
+ activeOption.scrollIntoView({
1274
+ block: 'nearest',
1275
+ });
1241
1276
  }
1242
- onOptionClick(event, textInput, topLevelElement) {
1277
+ selectActiveOption(event) {
1278
+ const options = this.getVisibleOptions();
1279
+ if (this.activeIndex < 0 || !options[this.activeIndex]) {
1280
+ return;
1281
+ }
1243
1282
  event.preventDefault();
1244
- const button = event.currentTarget?.closest('button');
1283
+ this.selectOption(options[this.activeIndex]);
1284
+ }
1285
+ selectOption(option) {
1286
+ const textInput = this.getTextInput();
1245
1287
  const hiddenInput = this.getHiddenInput();
1246
- const displayValue = button?.innerText.trim();
1247
- const actualValue = button?.dataset['staticOptionNameValue'] || '';
1248
- // UI Display Value
1288
+ const display = option.dataset['display'] ?? option.textContent?.trim() ?? '';
1289
+ const value = option.dataset['value'] ?? '';
1249
1290
  if (textInput) {
1250
- this.setValue(textInput, displayValue);
1251
- textInput.dataset['staticOptionNameValue'] = actualValue;
1291
+ textInput.value = display;
1292
+ textInput.dataset['selectedValue'] = value;
1252
1293
  }
1253
- // submission value
1254
1294
  if (hiddenInput) {
1255
- hiddenInput.value = actualValue;
1295
+ hiddenInput.value = value;
1256
1296
  hiddenInput.disabled = false;
1257
1297
  }
1258
- topLevelElement.querySelectorAll('li').forEach(item => {
1259
- item.classList.add('autocomplete-text-input-item-hide');
1260
- });
1298
+ this.closeList();
1299
+ textInput?.dispatchEvent(new Event('change', {
1300
+ bubbles: true,
1301
+ }));
1261
1302
  }
1262
- setValue(textInput, displayValue) {
1263
- if (displayValue) {
1264
- textInput.value = displayValue;
1265
- }
1266
- textInput.focus();
1267
- textInput.dispatchEvent(new Event('change', { bubbles: true }));
1303
+ openList() {
1304
+ const textInput = this.getTextInput();
1305
+ textInput?.setAttribute('aria-expanded', 'true');
1306
+ this.getListbox()?.classList.remove('autocomplete-text-input-list--hidden');
1268
1307
  }
1269
- addItem(template, content, clickEvent) {
1270
- const item = template?.cloneNode(true);
1271
- const button = item?.querySelector('button');
1272
- if (button) {
1273
- button.innerHTML = content;
1308
+ closeList() {
1309
+ const textInput = this.getTextInput();
1310
+ textInput?.setAttribute('aria-expanded', 'false');
1311
+ textInput?.removeAttribute('aria-activedescendant');
1312
+ this.activeIndex = -1;
1313
+ this.getAllOptions().forEach(option => {
1314
+ option.classList.add('autocomplete-text-input-item-hide');
1315
+ option.classList.remove('autocomplete-option-selected');
1316
+ option.setAttribute('aria-selected', 'false');
1317
+ });
1318
+ this.getListbox()?.classList.add('autocomplete-text-input-list--hidden');
1319
+ }
1320
+ clearSelection() {
1321
+ const hiddenInput = this.getHiddenInput();
1322
+ const textInput = this.getTextInput();
1323
+ if (hiddenInput) {
1324
+ hiddenInput.value = '';
1325
+ hiddenInput.disabled = true;
1274
1326
  }
1275
- item.classList.remove('autocomplete-text-input-item-hide');
1276
- this.getElement().querySelector(this.listParentSelector)?.appendChild(item);
1277
- if (clickEvent) {
1278
- item.addEventListener('mousedown', (event) => {
1279
- clickEvent(event);
1280
- });
1327
+ if (textInput) {
1328
+ delete textInput.dataset['selectedValue'];
1281
1329
  }
1282
1330
  }
1283
- clearListAndRecreateTemplate() {
1284
- const template = this.getElement()
1285
- .querySelector(this.listItemsSelector)
1286
- ?.cloneNode(true);
1287
- template?.classList.add('autocomplete-text-input-item-hide');
1288
- this.getElement()
1289
- .querySelectorAll(this.listItemsSelector)
1290
- .forEach(e => e.remove());
1291
- if (template) {
1292
- this.getElement().querySelector(this.listParentSelector)?.append(template);
1331
+ getListbox() {
1332
+ const textInput = this.getTextInput();
1333
+ if (!textInput) {
1334
+ return null;
1293
1335
  }
1294
- return template;
1295
- }
1296
- getHiddenInput() {
1297
- return this.getElement().querySelector('[data-autocomplete-hidden]');
1336
+ const listId = textInput.getAttribute('aria-controls');
1337
+ return listId ? document.getElementById(listId) : null;
1298
1338
  }
1299
1339
  }
1300
1340