govuk_publishing_components 65.1.0 → 65.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/component_guide/application.scss +13 -15
  3. data/app/assets/stylesheets/govuk_publishing_components/all-components.scss +0 -1
  4. data/app/assets/stylesheets/govuk_publishing_components/components/_contents-list.scss +25 -0
  5. data/app/assets/stylesheets/govuk_publishing_components/components/_details.scss +34 -0
  6. data/app/assets/stylesheets/govuk_publishing_components/components/_heading.scss +1 -0
  7. data/app/assets/stylesheets/govuk_publishing_components/components/helpers/_markdown-typography.scss +4 -0
  8. data/app/assets/stylesheets/govuk_publishing_components/specific-components.scss +0 -1
  9. data/app/controllers/govuk_publishing_components/applications_page_controller.rb +1 -1
  10. data/app/controllers/govuk_publishing_components/audit_controller.rb +0 -1
  11. data/app/models/govuk_publishing_components/component_doc.rb +10 -3
  12. data/app/models/govuk_publishing_components/component_wrapper_helper_options.rb +4 -2
  13. data/app/views/govuk_publishing_components/component_guide/component_doc/_component.html.erb +1 -1
  14. data/app/views/govuk_publishing_components/component_guide/component_doc/_preview.html.erb +2 -2
  15. data/app/views/govuk_publishing_components/component_guide/show.html.erb +47 -33
  16. data/app/views/govuk_publishing_components/components/_details.html.erb +4 -0
  17. data/app/views/govuk_publishing_components/components/_devolved_nations.html.erb +2 -2
  18. data/app/views/govuk_publishing_components/components/_option_select.html.erb +2 -2
  19. data/app/views/govuk_publishing_components/components/docs/details.yml +16 -0
  20. data/lib/govuk_publishing_components/presenters/devolved_nations_helper.rb +13 -7
  21. data/lib/govuk_publishing_components/version.rb +1 -1
  22. data/node_modules/choices.js/README.md +140 -30
  23. data/node_modules/choices.js/package.json +10 -13
  24. data/node_modules/choices.js/public/assets/scripts/choices.js +144 -89
  25. data/node_modules/choices.js/public/assets/scripts/choices.min.js +2 -2
  26. data/node_modules/choices.js/public/assets/scripts/choices.mjs +144 -89
  27. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.js +144 -86
  28. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.min.js +2 -2
  29. data/node_modules/choices.js/public/assets/scripts/choices.search-basic.mjs +144 -86
  30. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.js +139 -77
  31. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.min.js +2 -2
  32. data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.mjs +139 -77
  33. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.js +137 -76
  34. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.min.js +2 -2
  35. data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.mjs +137 -76
  36. data/node_modules/choices.js/public/assets/styles/base.css +39 -9
  37. data/node_modules/choices.js/public/assets/styles/base.css.map +1 -1
  38. data/node_modules/choices.js/public/assets/styles/base.min.css +1 -1
  39. data/node_modules/choices.js/public/assets/styles/choices.css +93 -95
  40. data/node_modules/choices.js/public/assets/styles/choices.css.map +1 -1
  41. data/node_modules/choices.js/public/assets/styles/choices.min.css +1 -1
  42. data/node_modules/choices.js/public/types/src/scripts/choices.d.ts +11 -0
  43. data/node_modules/choices.js/public/types/src/scripts/components/container.d.ts +2 -0
  44. data/node_modules/choices.js/public/types/src/scripts/interfaces/choice-full.d.ts +2 -1
  45. data/node_modules/choices.js/public/types/src/scripts/interfaces/class-names.d.ts +2 -0
  46. data/node_modules/choices.js/public/types/src/scripts/interfaces/input-choice.d.ts +2 -1
  47. data/node_modules/choices.js/public/types/src/scripts/interfaces/options.d.ts +19 -3
  48. data/node_modules/choices.js/public/types/src/scripts/interfaces/store.d.ts +2 -1
  49. data/node_modules/choices.js/public/types/src/scripts/interfaces/types.d.ts +2 -1
  50. data/node_modules/choices.js/public/types/src/scripts/lib/utils.d.ts +3 -1
  51. data/node_modules/choices.js/src/scripts/choices.ts +110 -64
  52. data/node_modules/choices.js/src/scripts/components/container.ts +8 -0
  53. data/node_modules/choices.js/src/scripts/components/wrapped-select.ts +3 -1
  54. data/node_modules/choices.js/src/scripts/defaults.ts +12 -7
  55. data/node_modules/choices.js/src/scripts/interfaces/choice-full.ts +2 -1
  56. data/node_modules/choices.js/src/scripts/interfaces/class-names.ts +2 -0
  57. data/node_modules/choices.js/src/scripts/interfaces/event-choice.ts +1 -0
  58. data/node_modules/choices.js/src/scripts/interfaces/input-choice.ts +4 -2
  59. data/node_modules/choices.js/src/scripts/interfaces/options.ts +21 -3
  60. data/node_modules/choices.js/src/scripts/interfaces/store.ts +2 -1
  61. data/node_modules/choices.js/src/scripts/interfaces/types.ts +3 -1
  62. data/node_modules/choices.js/src/scripts/lib/utils.ts +27 -4
  63. data/node_modules/choices.js/src/scripts/search/kmp.ts +2 -1
  64. data/node_modules/choices.js/src/scripts/store/store.ts +4 -1
  65. data/node_modules/choices.js/src/scripts/templates.ts +6 -3
  66. data/node_modules/choices.js/src/styles/base.scss +42 -9
  67. data/node_modules/choices.js/src/styles/choices.scss +119 -93
  68. metadata +2 -3
  69. data/app/assets/stylesheets/govuk_publishing_components/components/helpers/_contents-list-helper.scss +0 -24
@@ -13,6 +13,7 @@ import {
13
13
  escapeForTemplate,
14
14
  generateId,
15
15
  getAdjacentEl,
16
+ getChoiceForOutput,
16
17
  getClassNames,
17
18
  getClassNamesSelector,
18
19
  isScrolledIntoView,
@@ -259,7 +260,7 @@ class Choices {
259
260
 
260
261
  this._store = new Store(config);
261
262
  this._currentValue = '';
262
- config.searchEnabled = (!isText && config.searchEnabled) || isSelectMultiple;
263
+ config.searchEnabled = !isText && config.searchEnabled;
263
264
  this._canSearch = config.searchEnabled;
264
265
  this._isScrollingOnIe = false;
265
266
  this._highlightPosition = 0;
@@ -303,6 +304,8 @@ class Choices {
303
304
  this._onEscapeKey = this._onEscapeKey.bind(this);
304
305
  this._onDirectionKey = this._onDirectionKey.bind(this);
305
306
  this._onDeleteKey = this._onDeleteKey.bind(this);
307
+ this._onChange = this._onChange.bind(this);
308
+ this._onInvalid = this._onInvalid.bind(this);
306
309
 
307
310
  // If element has already been initialised with Choices, fail silently
308
311
  if (this.passedElement.isActive) {
@@ -415,7 +418,7 @@ class Choices {
415
418
  this._store.dispatch(highlightItem(choice, true));
416
419
 
417
420
  if (runEvent) {
418
- this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(choice));
421
+ this.passedElement.triggerEvent(EventType.highlightItem, getChoiceForOutput(choice));
419
422
  }
420
423
 
421
424
  return this;
@@ -433,7 +436,7 @@ class Choices {
433
436
  this._store.dispatch(highlightItem(choice, false));
434
437
 
435
438
  if (runEvent) {
436
- this.passedElement.triggerEvent(EventType.unhighlightItem, this._getChoiceForOutput(choice));
439
+ this.passedElement.triggerEvent(EventType.unhighlightItem, getChoiceForOutput(choice));
437
440
  }
438
441
 
439
442
  return this;
@@ -445,7 +448,7 @@ class Choices {
445
448
  if (!item.highlighted) {
446
449
  this._store.dispatch(highlightItem(item, true));
447
450
 
448
- this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(item));
451
+ this.passedElement.triggerEvent(EventType.highlightItem, getChoiceForOutput(item));
449
452
  }
450
453
  });
451
454
  });
@@ -459,7 +462,7 @@ class Choices {
459
462
  if (item.highlighted) {
460
463
  this._store.dispatch(highlightItem(item, false));
461
464
 
462
- this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(item));
465
+ this.passedElement.triggerEvent(EventType.highlightItem, getChoiceForOutput(item));
463
466
  }
464
467
  });
465
468
  });
@@ -518,6 +521,15 @@ class Choices {
518
521
  }
519
522
 
520
523
  this.passedElement.triggerEvent(EventType.showDropdown);
524
+
525
+ const activeElement = this.choiceList.element.querySelector<HTMLElement>(
526
+ getClassNamesSelector(this.config.classNames.selectedState),
527
+ );
528
+
529
+ if (activeElement !== null && !isScrolledIntoView(activeElement, this.choiceList.element)) {
530
+ // We use the native scrollIntoView function instead of choiceList.scrollToChildElement to avoid animated scroll.
531
+ activeElement.scrollIntoView();
532
+ }
521
533
  });
522
534
 
523
535
  return this;
@@ -528,6 +540,8 @@ class Choices {
528
540
  return this;
529
541
  }
530
542
 
543
+ this._removeHighlightedChoices();
544
+
531
545
  requestAnimationFrame(() => {
532
546
  this.dropdown.hide();
533
547
  this.containerOuter.close();
@@ -545,7 +559,7 @@ class Choices {
545
559
 
546
560
  getValue<B extends boolean = false>(valueOnly?: B): EventChoiceValueType<B> | EventChoiceValueType<B>[] {
547
561
  const values = this._store.items.map((item) => {
548
- return (valueOnly ? item.value : this._getChoiceForOutput(item)) as EventChoiceValueType<B>;
562
+ return (valueOnly ? item.value : getChoiceForOutput(item)) as EventChoiceValueType<B>;
549
563
  });
550
564
 
551
565
  return this._isSelectOneElement || this.config.singleModeForMultiSelect ? values[0] : values;
@@ -848,7 +862,7 @@ class Choices {
848
862
  this._searcher.reset();
849
863
 
850
864
  if (choice.selected) {
851
- this.passedElement.triggerEvent(EventType.removeItem, this._getChoiceForOutput(choice));
865
+ this.passedElement.triggerEvent(EventType.removeItem, getChoiceForOutput(choice));
852
866
  }
853
867
 
854
868
  return this;
@@ -943,12 +957,7 @@ class Choices {
943
957
  const { config, _isSearching: isSearching } = this;
944
958
  const { activeGroups, activeChoices } = this._store;
945
959
 
946
- let renderLimit = 0;
947
- if (isSearching && config.searchResultLimit > 0) {
948
- renderLimit = config.searchResultLimit;
949
- } else if (config.renderChoiceLimit > 0) {
950
- renderLimit = config.renderChoiceLimit;
951
- }
960
+ const renderLimit = isSearching ? config.searchResultLimit : config.renderChoiceLimit;
952
961
 
953
962
  if (this._isSelectElement) {
954
963
  const backingOptions = activeChoices.filter((choice) => !choice.element);
@@ -961,11 +970,16 @@ class Choices {
961
970
  const renderableChoices = (choices: ChoiceFull[]): ChoiceFull[] =>
962
971
  choices.filter(
963
972
  (choice) =>
964
- !choice.placeholder && (isSearching ? !!choice.rank : config.renderSelectedChoices || !choice.selected),
973
+ !choice.placeholder &&
974
+ (isSearching
975
+ ? (config.searchRenderSelectedChoices || !choice.selected) && !!choice.rank
976
+ : config.renderSelectedChoices || !choice.selected),
965
977
  );
966
978
 
979
+ const showLabel = config.appendGroupInSearch && isSearching;
967
980
  let selectableChoices = false;
968
- const renderChoices = (choices: ChoiceFull[], withinGroup: boolean, groupLabel?: string): void => {
981
+ let highlightedEl: HTMLElement | null = null;
982
+ const renderChoices = (choices: ChoiceFull[], withinGroup: boolean): void => {
969
983
  if (isSearching) {
970
984
  // sortByRank is used to ensure stable sorting, as scores are non-unique
971
985
  // this additionally ensures fuseOptions.sortFn is not ignored
@@ -975,17 +989,25 @@ class Choices {
975
989
  }
976
990
 
977
991
  let choiceLimit = choices.length;
978
- choiceLimit = !withinGroup && renderLimit && choiceLimit > renderLimit ? renderLimit : choiceLimit;
992
+ choiceLimit = !withinGroup && renderLimit > 0 && choiceLimit > renderLimit ? renderLimit : choiceLimit;
979
993
  choiceLimit--;
980
994
 
981
995
  choices.every((choice, index) => {
982
996
  // choiceEl being empty signals the contents has probably significantly changed
983
997
  const dropdownItem =
984
- choice.choiceEl || this._templates.choice(config, choice, config.itemSelectText, groupLabel);
998
+ choice.choiceEl ||
999
+ this._templates.choice(
1000
+ config,
1001
+ choice,
1002
+ config.itemSelectText,
1003
+ showLabel && choice.group ? choice.group.label : undefined,
1004
+ );
985
1005
  choice.choiceEl = dropdownItem;
986
1006
  fragment.appendChild(dropdownItem);
987
1007
  if (isSearching || !choice.selected) {
988
1008
  selectableChoices = true;
1009
+ } else if (!highlightedEl) {
1010
+ highlightedEl = dropdownItem;
989
1011
  }
990
1012
 
991
1013
  return index < choiceLimit;
@@ -1002,7 +1024,6 @@ class Choices {
1002
1024
  renderChoices(
1003
1025
  activeChoices.filter((choice) => choice.placeholder && !choice.group),
1004
1026
  false,
1005
- undefined,
1006
1027
  );
1007
1028
  }
1008
1029
 
@@ -1016,7 +1037,6 @@ class Choices {
1016
1037
  renderChoices(
1017
1038
  activeChoices.filter((choice) => !choice.placeholder && !choice.group),
1018
1039
  false,
1019
- undefined,
1020
1040
  );
1021
1041
 
1022
1042
  activeGroups.forEach((group) => {
@@ -1028,11 +1048,11 @@ class Choices {
1028
1048
  dropdownGroup.remove();
1029
1049
  fragment.appendChild(dropdownGroup);
1030
1050
  }
1031
- renderChoices(groupChoices, true, config.appendGroupInSearch && isSearching ? group.label : undefined);
1051
+ renderChoices(groupChoices, true);
1032
1052
  }
1033
1053
  });
1034
1054
  } else {
1035
- renderChoices(renderableChoices(activeChoices), false, undefined);
1055
+ renderChoices(renderableChoices(activeChoices), false);
1036
1056
  }
1037
1057
  }
1038
1058
 
@@ -1049,9 +1069,7 @@ class Choices {
1049
1069
  this._renderNotice(fragment);
1050
1070
  this.choiceList.element.replaceChildren(fragment);
1051
1071
 
1052
- if (selectableChoices) {
1053
- this._highlightChoice();
1054
- }
1072
+ this._highlightChoice(highlightedEl);
1055
1073
  }
1056
1074
 
1057
1075
  _renderItems(): void {
@@ -1183,23 +1201,12 @@ class Choices {
1183
1201
  }
1184
1202
  }
1185
1203
 
1204
+ /**
1205
+ * @deprecated Use utils.getChoiceForOutput
1206
+ */
1186
1207
  // eslint-disable-next-line class-methods-use-this
1187
1208
  _getChoiceForOutput(choice: ChoiceFull, keyCode?: number): EventChoice {
1188
- return {
1189
- id: choice.id,
1190
- highlighted: choice.highlighted,
1191
- labelClass: choice.labelClass,
1192
- labelDescription: choice.labelDescription,
1193
- customProperties: choice.customProperties,
1194
- disabled: choice.disabled,
1195
- active: choice.active,
1196
- label: choice.label,
1197
- placeholder: choice.placeholder,
1198
- value: choice.value,
1199
- groupValue: choice.group ? choice.group.label : undefined,
1200
- element: choice.element,
1201
- keyCode,
1202
- };
1209
+ return getChoiceForOutput(choice, keyCode);
1203
1210
  }
1204
1211
 
1205
1212
  _triggerChange(value): void {
@@ -1218,7 +1225,7 @@ class Choices {
1218
1225
  return;
1219
1226
  }
1220
1227
 
1221
- const id = element && parseDataSetId(element.parentElement);
1228
+ const id = element && parseDataSetId(element.closest('[data-id]'));
1222
1229
  const itemToRemove = id && items.find((item) => item.id === id);
1223
1230
  if (!itemToRemove) {
1224
1231
  return;
@@ -1428,7 +1435,7 @@ class Choices {
1428
1435
 
1429
1436
  if (canAddItem && typeof config.addItemFilter === 'function' && !config.addItemFilter(value)) {
1430
1437
  canAddItem = false;
1431
- notice = resolveNoticeFunction(config.customAddItemText, value);
1438
+ notice = resolveNoticeFunction(config.customAddItemText, value, undefined);
1432
1439
  }
1433
1440
 
1434
1441
  if (canAddItem) {
@@ -1442,13 +1449,13 @@ class Choices {
1442
1449
  }
1443
1450
  if (!config.duplicateItemsAllowed) {
1444
1451
  canAddItem = false;
1445
- notice = resolveNoticeFunction(config.uniqueItemText, value);
1452
+ notice = resolveNoticeFunction(config.uniqueItemText, value, undefined);
1446
1453
  }
1447
1454
  }
1448
1455
  }
1449
1456
 
1450
1457
  if (canAddItem) {
1451
- notice = resolveNoticeFunction(config.addItemText, value);
1458
+ notice = resolveNoticeFunction(config.addItemText, value, undefined);
1452
1459
  }
1453
1460
 
1454
1461
  if (notice) {
@@ -1510,6 +1517,7 @@ class Choices {
1510
1517
  const documentElement = this._docRoot;
1511
1518
  const outerElement = this.containerOuter.element;
1512
1519
  const inputElement = this.input.element;
1520
+ const passedElement = this.passedElement.element;
1513
1521
 
1514
1522
  // capture events - can cancel event processing or propagation
1515
1523
  documentElement.addEventListener('touchend', this._onTouchEnd, true);
@@ -1554,6 +1562,16 @@ class Choices {
1554
1562
  });
1555
1563
  }
1556
1564
 
1565
+ if (passedElement.hasAttribute('required')) {
1566
+ passedElement.addEventListener('change', this._onChange, {
1567
+ passive: true,
1568
+ });
1569
+
1570
+ passedElement.addEventListener('invalid', this._onInvalid, {
1571
+ passive: true,
1572
+ });
1573
+ }
1574
+
1557
1575
  this.input.addEventListeners();
1558
1576
  }
1559
1577
 
@@ -1561,6 +1579,7 @@ class Choices {
1561
1579
  const documentElement = this._docRoot;
1562
1580
  const outerElement = this.containerOuter.element;
1563
1581
  const inputElement = this.input.element;
1582
+ const passedElement = this.passedElement.element;
1564
1583
 
1565
1584
  documentElement.removeEventListener('touchend', this._onTouchEnd, true);
1566
1585
  outerElement.removeEventListener('keydown', this._onKeyDown, true);
@@ -1584,6 +1603,11 @@ class Choices {
1584
1603
  inputElement.form.removeEventListener('reset', this._onFormReset);
1585
1604
  }
1586
1605
 
1606
+ if (passedElement.hasAttribute('required')) {
1607
+ passedElement.removeEventListener('change', this._onChange);
1608
+ passedElement.removeEventListener('invalid', this._onInvalid);
1609
+ }
1610
+
1587
1611
  this.input.removeEventListeners();
1588
1612
  }
1589
1613
 
@@ -1882,7 +1906,7 @@ class Choices {
1882
1906
  */
1883
1907
  _onMouseDown(event: MouseEvent): void {
1884
1908
  const { target } = event;
1885
- if (!(target instanceof HTMLElement)) {
1909
+ if (!(target instanceof Element)) {
1886
1910
  return;
1887
1911
  }
1888
1912
 
@@ -1994,7 +2018,7 @@ class Choices {
1994
2018
  containerOuter.removeFocusState();
1995
2019
 
1996
2020
  // Also close the dropdown if search is disabled
1997
- if (!this._canSearch) {
2021
+ if (!this.config.searchEnabled) {
1998
2022
  this.hideDropdown(true);
1999
2023
  }
2000
2024
  }
@@ -2018,14 +2042,22 @@ class Choices {
2018
2042
  });
2019
2043
  }
2020
2044
 
2021
- _highlightChoice(el: HTMLElement | null = null): void {
2022
- const choices = Array.from(this.dropdown.element.querySelectorAll<HTMLElement>(selectableChoiceIdentifier));
2023
-
2024
- if (!choices.length) {
2045
+ _onChange(event: Event & { target: HTMLInputElement | HTMLSelectElement }): void {
2046
+ if (!event.target.checkValidity()) {
2025
2047
  return;
2026
2048
  }
2027
2049
 
2028
- let passedEl = el;
2050
+ this.containerOuter.removeInvalidState();
2051
+ }
2052
+
2053
+ _onInvalid(): void {
2054
+ this.containerOuter.addInvalidState();
2055
+ }
2056
+
2057
+ /**
2058
+ * Removes any highlighted choice options
2059
+ */
2060
+ _removeHighlightedChoices(): void {
2029
2061
  const { highlightedState } = this.config.classNames;
2030
2062
  const highlightedChoices = Array.from(
2031
2063
  this.dropdown.element.querySelectorAll<HTMLElement>(getClassNamesSelector(highlightedState)),
@@ -2036,6 +2068,19 @@ class Choices {
2036
2068
  removeClassesFromElement(choice, highlightedState);
2037
2069
  choice.setAttribute('aria-selected', 'false');
2038
2070
  });
2071
+ }
2072
+
2073
+ _highlightChoice(el: HTMLElement | null = null): void {
2074
+ const choices = Array.from(this.dropdown.element.querySelectorAll<HTMLElement>(selectableChoiceIdentifier));
2075
+
2076
+ if (!choices.length) {
2077
+ return;
2078
+ }
2079
+
2080
+ let passedEl = el;
2081
+ const { highlightedState } = this.config.classNames;
2082
+
2083
+ this._removeHighlightedChoices();
2039
2084
 
2040
2085
  if (passedEl) {
2041
2086
  this._highlightPosition = choices.indexOf(passedEl);
@@ -2080,10 +2125,11 @@ class Choices {
2080
2125
  this._store.dispatch(addItem(item));
2081
2126
 
2082
2127
  if (withEvents) {
2083
- this.passedElement.triggerEvent(EventType.addItem, this._getChoiceForOutput(item));
2128
+ const eventChoice = getChoiceForOutput(item);
2129
+ this.passedElement.triggerEvent(EventType.addItem, eventChoice);
2084
2130
 
2085
2131
  if (userTriggered) {
2086
- this.passedElement.triggerEvent(EventType.choice, this._getChoiceForOutput(item));
2132
+ this.passedElement.triggerEvent(EventType.choice, eventChoice);
2087
2133
  }
2088
2134
  }
2089
2135
  }
@@ -2099,7 +2145,7 @@ class Choices {
2099
2145
  this._clearNotice();
2100
2146
  }
2101
2147
 
2102
- this.passedElement.triggerEvent(EventType.removeItem, this._getChoiceForOutput(item));
2148
+ this.passedElement.triggerEvent(EventType.removeItem, getChoiceForOutput(item));
2103
2149
  }
2104
2150
 
2105
2151
  _addChoice(choice: ChoiceFull, withEvents: boolean = true, userTriggered = false): void {
@@ -2242,26 +2288,26 @@ class Choices {
2242
2288
  // Wrapper inner container with outer container
2243
2289
  containerOuter.wrap(containerInner.element);
2244
2290
 
2291
+ containerOuter.element.appendChild(containerInner.element);
2292
+ containerOuter.element.appendChild(dropdownElement);
2293
+ containerInner.element.appendChild(this.itemList.element);
2294
+ dropdownElement.appendChild(this.choiceList.element);
2295
+
2245
2296
  if (this._isSelectOneElement) {
2246
2297
  this.input.placeholder = this.config.searchPlaceholderValue || '';
2298
+ if (this.config.searchEnabled) {
2299
+ dropdownElement.insertBefore(this.input.element, dropdownElement.firstChild);
2300
+ }
2247
2301
  } else {
2302
+ if (!this._isSelectMultipleElement || this.config.searchEnabled) {
2303
+ containerInner.element.appendChild(this.input.element);
2304
+ }
2248
2305
  if (this._placeholderValue) {
2249
2306
  this.input.placeholder = this._placeholderValue;
2250
2307
  }
2251
2308
  this.input.setWidth();
2252
2309
  }
2253
2310
 
2254
- containerOuter.element.appendChild(containerInner.element);
2255
- containerOuter.element.appendChild(dropdownElement);
2256
- containerInner.element.appendChild(this.itemList.element);
2257
- dropdownElement.appendChild(this.choiceList.element);
2258
-
2259
- if (!this._isSelectOneElement) {
2260
- containerInner.element.appendChild(this.input.element);
2261
- } else if (this.config.searchEnabled) {
2262
- dropdownElement.insertBefore(this.input.element, dropdownElement.firstChild);
2263
- }
2264
-
2265
2311
  this._highlightPosition = 0;
2266
2312
  this._isSearching = false;
2267
2313
  }
@@ -100,6 +100,14 @@ export default class Container {
100
100
  removeClassesFromElement(this.element, this.classNames.focusState);
101
101
  }
102
102
 
103
+ addInvalidState(): void {
104
+ addClassesToElement(this.element, this.classNames.invalidState);
105
+ }
106
+
107
+ removeInvalidState(): void {
108
+ removeClassesFromElement(this.element, this.classNames.invalidState);
109
+ }
110
+
103
111
  enable(): void {
104
112
  removeClassesFromElement(this.element, this.classNames.disabledState);
105
113
  this.element.removeAttribute('aria-disabled');
@@ -94,7 +94,9 @@ export default class WrappedSelect extends WrappedElement<HTMLSelectElement> {
94
94
  labelClass:
95
95
  typeof option.dataset.labelClass !== 'undefined' ? stringToHtmlClass(option.dataset.labelClass) : undefined,
96
96
  labelDescription:
97
- typeof option.dataset.labelDescription !== 'undefined' ? option.dataset.labelDescription : undefined,
97
+ typeof option.dataset.labelDescription !== 'undefined'
98
+ ? { trusted: option.dataset.labelDescription }
99
+ : undefined,
98
100
  customProperties: parseCustomProperties(option.dataset.customProperties),
99
101
  };
100
102
  }
@@ -1,6 +1,7 @@
1
1
  import { ClassNames } from './interfaces/class-names';
2
2
  import { Options } from './interfaces/options';
3
- import { sortByAlpha } from './lib/utils';
3
+ import { sanitise, sortByAlpha } from './lib/utils';
4
+ import { EventChoice } from './interfaces';
4
5
 
5
6
  export const DEFAULT_CLASSNAMES: ClassNames = {
6
7
  containerOuter: ['choices'],
@@ -28,6 +29,7 @@ export const DEFAULT_CLASSNAMES: ClassNames = {
28
29
  selectedState: ['is-selected'],
29
30
  flippedState: ['is-flipped'],
30
31
  loadingState: ['is-loading'],
32
+ invalidState: ['is-invalid'],
31
33
  notice: ['choices__notice'],
32
34
  addChoice: ['choices__item--selectable', 'add-choice'],
33
35
  noResults: ['has-no-results'],
@@ -44,7 +46,7 @@ export const DEFAULT_CONFIG: Options = {
44
46
  singleModeForMultiSelect: false,
45
47
  addChoices: false,
46
48
  addItems: true,
47
- addItemFilter: (value) => !!value && value !== '',
49
+ addItemFilter: (value: string): boolean => !!value && value !== '',
48
50
  removeItems: true,
49
51
  removeItemButton: false,
50
52
  removeItemButtonAlignLeft: false,
@@ -56,6 +58,7 @@ export const DEFAULT_CONFIG: Options = {
56
58
  paste: true,
57
59
  searchEnabled: true,
58
60
  searchChoices: true,
61
+ searchDisabledChoices: false,
59
62
  searchFloor: 1,
60
63
  searchResultLimit: 4,
61
64
  searchFields: ['label', 'value'],
@@ -71,17 +74,19 @@ export const DEFAULT_CONFIG: Options = {
71
74
  prependValue: null,
72
75
  appendValue: null,
73
76
  renderSelectedChoices: 'auto',
77
+ searchRenderSelectedChoices: true,
74
78
  loadingText: 'Loading...',
75
79
  noResultsText: 'No results found',
76
80
  noChoicesText: 'No choices to choose from',
77
81
  itemSelectText: 'Press to select',
78
82
  uniqueItemText: 'Only unique values can be added',
79
83
  customAddItemText: 'Only values matching specific conditions can be added',
80
- addItemText: (value) => `Press Enter to add <b>"${value}"</b>`,
81
- removeItemIconText: () => `Remove item`,
82
- removeItemLabelText: (value) => `Remove item: ${value}`,
83
- maxItemText: (maxItemCount) => `Only ${maxItemCount} values can be added`,
84
- valueComparer: (value1, value2) => value1 === value2,
84
+ addItemText: (value: string) => `Press Enter to add <b>"${value}"</b>`,
85
+ removeItemIconText: (): string => `Remove item`,
86
+ removeItemLabelText: (value: string, _valueRaw: string, i?: EventChoice): string =>
87
+ `Remove item: ${i ? sanitise<string>(i.label) : value}`,
88
+ maxItemText: (maxItemCount: number): string => `Only ${maxItemCount} values can be added`,
89
+ valueComparer: (value1: string, value2: string): boolean => value1 === value2,
85
90
  fuseOptions: {
86
91
  includeScore: true,
87
92
  },
@@ -1,4 +1,5 @@
1
1
  import { StringUntrusted } from './string-untrusted';
2
+ import { StringPreEscaped } from './string-pre-escaped';
2
3
  import { Types } from './types';
3
4
  // eslint-disable-next-line import/no-cycle
4
5
  import { GroupFull } from './group-full';
@@ -15,7 +16,7 @@ export interface ChoiceFull {
15
16
  itemEl?: HTMLElement;
16
17
  choiceEl?: HTMLElement;
17
18
  labelClass?: Array<string>;
18
- labelDescription?: string;
19
+ labelDescription?: StringPreEscaped | StringUntrusted | string;
19
20
  customProperties?: Types.CustomProperties;
20
21
  disabled: boolean;
21
22
  active: boolean;
@@ -50,6 +50,8 @@ export interface ClassNames {
50
50
  flippedState: string | Array<string>;
51
51
  /** @default ['is-loading'] */
52
52
  loadingState: string | Array<string>;
53
+ /** @default ['is-invalid'] */
54
+ invalidState: string | Array<string>;
53
55
  /** @default ['choices__notice'] */
54
56
  notice: string | Array<string>;
55
57
  /** @default ['choices__item--selectable', 'add-choice'] */
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line import/no-cycle
1
2
  import { InputChoice } from './input-choice';
2
3
 
3
4
  export type EventChoiceValueType<B extends boolean> = B extends true ? string : EventChoice;
@@ -1,11 +1,13 @@
1
1
  import { StringUntrusted } from './string-untrusted';
2
+ import { StringPreEscaped } from './string-pre-escaped';
3
+ // eslint-disable-next-line
2
4
  import { Types } from './types';
3
5
 
4
6
  export interface InputChoice {
5
7
  id?: number;
6
8
  highlighted?: boolean;
7
9
  labelClass?: string | Array<string>;
8
- labelDescription?: string;
10
+ labelDescription?: StringPreEscaped | StringUntrusted | string;
9
11
  customProperties?: Types.CustomProperties;
10
12
  disabled?: boolean;
11
13
  active?: boolean;
@@ -13,5 +15,5 @@ export interface InputChoice {
13
15
  placeholder?: boolean;
14
16
  selected?: boolean;
15
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- value: any;
18
+ value: any; // string;
17
19
  }
@@ -200,7 +200,7 @@ export interface Options {
200
200
  *
201
201
  * @default
202
202
  * ```
203
- * (value, valueRaw) => `Remove item`;
203
+ * (value, valueRaw, item) => `Remove item`;
204
204
  * ```
205
205
  */
206
206
  removeItemIconText: string | Types.NoticeStringFunction;
@@ -215,7 +215,7 @@ export interface Options {
215
215
  *
216
216
  * @default
217
217
  * ```
218
- * (value, valueRaw) => `Remove item: ${value}`;
218
+ * (value, valueRaw, item) => `Remove item: ${value}`;
219
219
  * ```
220
220
  */
221
221
  removeItemLabelText: string | Types.NoticeStringFunction;
@@ -321,6 +321,15 @@ export interface Options {
321
321
  */
322
322
  searchChoices: boolean;
323
323
 
324
+ /**
325
+ * Whether disabled choices should be included in search results. If `true`, disabled choices will appear in search results but still cannot be selected.
326
+ *
327
+ * **Input types affected:** select-one, select-multiple
328
+ *
329
+ * @default false
330
+ */
331
+ searchDisabledChoices: boolean;
332
+
324
333
  /**
325
334
  * The minimum length a search value should be before choices are searched.
326
335
  *
@@ -471,6 +480,15 @@ export interface Options {
471
480
  */
472
481
  renderSelectedChoices: 'auto' | 'always' | boolean;
473
482
 
483
+ /**
484
+ * Whether selected choices should be removed from the list during search.
485
+ *
486
+ * **Input types affected:** select-multiple
487
+ *
488
+ * @default false;
489
+ */
490
+ searchRenderSelectedChoices: boolean;
491
+
474
492
  /**
475
493
  * The text that is shown whilst choices are being populated via AJAX.
476
494
  *
@@ -615,5 +633,5 @@ export interface Options {
615
633
  */
616
634
  callbackOnCreateTemplates: CallbackOnCreateTemplatesFn | null;
617
635
 
618
- appendGroupInSearch: false;
636
+ appendGroupInSearch: boolean;
619
637
  }
@@ -53,7 +53,8 @@ export interface Store {
53
53
  get activeChoices(): ChoiceFull[];
54
54
 
55
55
  /**
56
- * Get choices that can be searched (excluding placeholders)
56
+ * Get choices that can be searched (excluding placeholders,
57
+ * optionally excluding disabled based on config)
57
58
  */
58
59
  get searchableChoices(): ChoiceFull[];
59
60
 
@@ -1,12 +1,14 @@
1
1
  import { StringUntrusted } from './string-untrusted';
2
2
  import { StringPreEscaped } from './string-pre-escaped';
3
+ // eslint-disable-next-line import/no-cycle
4
+ import { EventChoice } from './event-choice';
3
5
 
4
6
  export namespace Types {
5
7
  export type StrToEl = (str: string) => HTMLElement | HTMLInputElement | HTMLOptionElement;
6
8
  export type EscapeForTemplateFn = (allowHTML: boolean, s: StringUntrusted | StringPreEscaped | string) => string;
7
9
  export type GetClassNamesFn = (s: string | Array<string>) => string;
8
10
  export type StringFunction = () => string;
9
- export type NoticeStringFunction = (value: string, valueRaw: string) => string;
11
+ export type NoticeStringFunction = (value: string, valueRaw: string, item?: EventChoice) => string;
10
12
  export type NoticeLimitFunction = (maxItemCount: number) => string;
11
13
  export type FilterFunction = (value: string) => boolean;
12
14
  export type ValueCompareFunction = (value1: string, value2: string) => boolean;