@lion/ui 0.4.1 → 0.5.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 (158) hide show
  1. package/components/calendar/src/LionCalendar.js +5 -3
  2. package/components/calendar/test-helpers/DayObject.js +2 -2
  3. package/components/combobox/src/LionCombobox.js +82 -29
  4. package/components/combobox/test/lion-combobox-integrations.test.js +5 -0
  5. package/components/combobox/test/lion-combobox.test.js +318 -23
  6. package/components/combobox/test-helpers/combobox-helpers.js +19 -22
  7. package/components/drawer/src/LionDrawer.js +1 -0
  8. package/components/form-core/src/FocusMixin.js +2 -0
  9. package/components/form-core/src/FormControlMixin.js +1 -1
  10. package/components/form-core/src/choice-group/ChoiceGroupMixin.js +1 -0
  11. package/components/form-core/src/choice-group/CustomChoiceGroupMixin.js +173 -0
  12. package/components/form-core/src/utils/getAriaElementsInRightDomOrder.js +7 -14
  13. package/components/form-core/src/validate/validators/Required.js +2 -2
  14. package/components/form-core/test/choice-group/CustomChoiceGroupMixin.integrations.test.js +55 -0
  15. package/components/form-core/test/choice-group/CustomChoiceGroupMixin.test.js +16 -0
  16. package/components/form-core/test/utils/getAriaElementsInRightDomOrder.test.js +0 -25
  17. package/components/form-core/test/validate/StringValidators.test.js +1 -1
  18. package/components/form-core/test-helpers/ExampleValidators.js +1 -3
  19. package/components/form-core/test-suites/FormRegistrationMixins.suite.js +2 -0
  20. package/components/form-core/test-suites/NativeTextFieldMixin.suite.js +4 -4
  21. package/components/form-core/test-suites/ValidateMixin.suite.js +27 -0
  22. package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +11 -2
  23. package/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js +1 -0
  24. package/components/form-core/test-suites/choice-group/CustomChoiceGroupMixin.suite.js +265 -0
  25. package/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts +1 -1
  26. package/components/form-core/types/choice-group/CustomChoiceGroupMixinTypes.ts +30 -0
  27. package/components/form-core/types/choice-group/index.ts +1 -0
  28. package/components/helpers/src/SbActionLogger.js +1 -0
  29. package/components/icon/src/LionIcon.js +1 -9
  30. package/components/icon/test/lion-icon.test.js +2 -0
  31. package/components/input-amount/src/LionInputAmount.js +5 -2
  32. package/components/input-amount/test/lion-input-amount.test.js +21 -0
  33. package/components/input-range/test/lion-input-range.test.js +12 -6
  34. package/components/listbox/src/LionOptions.js +5 -5
  35. package/components/listbox/src/ListboxMixin.js +1 -16
  36. package/components/listbox/test/lion-options.test.js +1 -1
  37. package/components/listbox/test-suites/ListboxMixin.suite.js +1 -1
  38. package/components/localize/src/date/getMonthNames.js +3 -0
  39. package/components/localize/src/date/utils/forceDotsForNlnl.js +11 -0
  40. package/components/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.js +3 -1
  41. package/components/localize/src/number/utils/normalize-format-number-to-parts/forceSymbols.js +2 -0
  42. package/components/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.js +1 -0
  43. package/components/localize/test/date/getMonthNames.test.js +1 -0
  44. package/components/localize/test-helpers/fake-imports.js +1 -0
  45. package/components/overlays/src/utils/adopt-styles.js +1 -1
  46. package/components/radio-group/src/LionRadioGroup.js +1 -1
  47. package/components/radio-group/test/lion-radio-group.test.js +29 -0
  48. package/components/select/test/lion-select.test.js +1 -1
  49. package/components/switch/src/LionSwitchButton.js +1 -5
  50. package/components/tabs/src/LionTabs.js +7 -7
  51. package/components/textarea/src/LionTextarea.js +1 -1
  52. package/custom-elements.json +103314 -61972
  53. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  54. package/dist-types/components/combobox/src/LionCombobox.d.ts +13 -8
  55. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  56. package/dist-types/components/combobox/test-helpers/combobox-helpers.d.ts.map +1 -1
  57. package/dist-types/components/drawer/src/LionDrawer.d.ts.map +1 -1
  58. package/dist-types/components/form-core/src/FocusMixin.d.ts.map +1 -1
  59. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
  60. package/dist-types/components/form-core/src/choice-group/CustomChoiceGroupMixin.d.ts +4 -0
  61. package/dist-types/components/form-core/src/choice-group/CustomChoiceGroupMixin.d.ts.map +1 -0
  62. package/dist-types/components/form-core/src/utils/getAriaElementsInRightDomOrder.d.ts +5 -5
  63. package/dist-types/components/form-core/src/utils/getAriaElementsInRightDomOrder.d.ts.map +1 -1
  64. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.integrations.test.d.ts +2 -0
  65. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.integrations.test.d.ts.map +1 -0
  66. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.test.d.ts +6 -0
  67. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.test.d.ts.map +1 -0
  68. package/dist-types/components/form-core/test-helpers/ExampleValidators.d.ts +0 -1
  69. package/dist-types/components/form-core/test-helpers/ExampleValidators.d.ts.map +1 -1
  70. package/dist-types/components/form-core/test-suites/FormRegistrationMixins.suite.d.ts.map +1 -1
  71. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  72. package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
  73. package/dist-types/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.d.ts.map +1 -1
  74. package/dist-types/components/form-core/test-suites/choice-group/CustomChoiceGroupMixin.suite.d.ts +14 -0
  75. package/dist-types/components/form-core/test-suites/choice-group/CustomChoiceGroupMixin.suite.d.ts.map +1 -0
  76. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts +1 -1
  77. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts.map +1 -1
  78. package/dist-types/components/form-core/types/choice-group/CustomChoiceGroupMixinTypes.d.ts +18 -0
  79. package/dist-types/components/form-core/types/choice-group/CustomChoiceGroupMixinTypes.d.ts.map +1 -0
  80. package/dist-types/components/form-core/types/choice-group/index.d.ts +1 -0
  81. package/dist-types/components/form-core/types/choice-group/index.d.ts.map +1 -1
  82. package/dist-types/components/helpers/src/SbActionLogger.d.ts.map +1 -1
  83. package/dist-types/components/icon/src/LionIcon.d.ts +0 -9
  84. package/dist-types/components/icon/src/LionIcon.d.ts.map +1 -1
  85. package/dist-types/components/input-amount/src/LionInputAmount.d.ts +2 -2
  86. package/dist-types/components/input-amount/src/LionInputAmount.d.ts.map +1 -1
  87. package/dist-types/components/listbox/src/LionOptions.d.ts +0 -5
  88. package/dist-types/components/listbox/src/LionOptions.d.ts.map +1 -1
  89. package/dist-types/components/listbox/src/ListboxMixin.d.ts.map +1 -1
  90. package/dist-types/components/localize/src/date/getMonthNames.d.ts.map +1 -1
  91. package/dist-types/components/localize/src/date/utils/forceDotsForNlnl.d.ts +5 -0
  92. package/dist-types/components/localize/src/date/utils/forceDotsForNlnl.d.ts.map +1 -0
  93. package/dist-types/components/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.d.ts.map +1 -1
  94. package/dist-types/components/localize/src/number/utils/normalize-format-number-to-parts/forceSymbols.d.ts.map +1 -1
  95. package/dist-types/components/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.d.ts.map +1 -1
  96. package/dist-types/components/localize/test-helpers/fake-imports.d.ts.map +1 -1
  97. package/dist-types/components/switch/src/LionSwitchButton.d.ts +0 -5
  98. package/dist-types/components/switch/src/LionSwitchButton.d.ts.map +1 -1
  99. package/dist-types/components/tabs/src/LionTabs.d.ts.map +1 -1
  100. package/dist-types/exports/types/form-core.d.ts +1 -1
  101. package/dist-types/exports/types/form-core.d.ts.map +1 -1
  102. package/dist-types/tsconfig.tsbuildinfo +1 -1
  103. package/docs/components/checkbox-group/overview.md +2 -2
  104. package/docs/components/collapsible/assets/CustomCollapsible.js +4 -0
  105. package/docs/components/collapsible/overview.md +1 -1
  106. package/docs/components/combobox/overview.md +1 -1
  107. package/docs/components/combobox/src/demo-selection-display.js +8 -35
  108. package/docs/components/combobox/use-cases.md +24 -1
  109. package/docs/components/dialog/overview.md +1 -1
  110. package/docs/components/fieldset/overview.md +5 -5
  111. package/docs/components/form/overview.md +3 -3
  112. package/docs/components/icon/use-cases.md +1 -1
  113. package/docs/components/input/overview.md +1 -1
  114. package/docs/components/input-amount/overview.md +3 -3
  115. package/docs/components/input-date/overview.md +3 -3
  116. package/docs/components/input-datepicker/overview.md +5 -5
  117. package/docs/components/input-email/overview.md +2 -2
  118. package/docs/components/input-file/overview.md +1 -1
  119. package/docs/components/input-iban/overview.md +2 -2
  120. package/docs/components/input-range/overview.md +2 -2
  121. package/docs/components/input-stepper/overview.md +1 -1
  122. package/docs/components/input-tel/overview.md +1 -1
  123. package/docs/components/input-tel/use-cases.md +6 -6
  124. package/docs/components/input-tel-dropdown/overview.md +1 -1
  125. package/docs/components/radio-group/overview.md +3 -3
  126. package/docs/components/select-rich/use-cases.md +1 -1
  127. package/docs/components/steps/overview.md +2 -4
  128. package/docs/components/tabs/examples.md +1 -1
  129. package/docs/fundamentals/index.md +4 -4
  130. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  131. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  132. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  133. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  134. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  135. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  136. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  137. package/docs/fundamentals/rationales/accessibility.md +3 -3
  138. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  139. package/docs/fundamentals/systems/core/overview.md +1 -1
  140. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  141. package/docs/fundamentals/systems/form/model-value.md +1 -1
  142. package/docs/fundamentals/systems/form/overview.md +28 -28
  143. package/docs/fundamentals/systems/form/validate.md +18 -2
  144. package/docs/fundamentals/systems/localize/overview.md +4 -4
  145. package/docs/fundamentals/systems/overlays/configuration.md +3 -2
  146. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  147. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  148. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  149. package/docs/fundamentals/tools/singleton-manager/example-complex/overlayCompatibility.js +1 -2
  150. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  151. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  152. package/docs/guides/how-to/create-a-form.md +4 -4
  153. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  154. package/docs/guides/index.md +8 -8
  155. package/docs/guides/principles/index.md +5 -5
  156. package/docs/guides/principles/styling.md +1 -1
  157. package/exports/types/form-core.ts +5 -1
  158. package/package.json +1 -1
@@ -799,7 +799,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
799
799
  * @private
800
800
  */
801
801
  __clickDateDelegation(ev) {
802
- const el = /** @type {HTMLElement & { date: Date }} */ (ev.target);
802
+ const el = /** @type {HTMLElement & { date: Date }} */ (ev.composedPath()[0]);
803
803
  if (isDayButton(el) && !isDisabledDayButton(el)) {
804
804
  this.__dateSelectedByUser(el.date);
805
805
  }
@@ -815,7 +815,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
815
815
  ) {
816
816
  this.__focusedDate = /** @type {HTMLButtonElement & { date: Date }} */ (
817
817
  this.shadowRoot?.activeElement
818
- ).date;
818
+ )?.date;
819
819
  }
820
820
  }
821
821
 
@@ -866,7 +866,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
866
866
  switch (ev.key) {
867
867
  case ' ':
868
868
  case 'Enter':
869
- this.__dayButtonSelection(/** @type {HTMLElement & { date: Date }} */ (ev.target));
869
+ this.__dayButtonSelection(
870
+ /** @type {HTMLElement & { date: Date }} */ (ev.composedPath()[0]),
871
+ );
870
872
  break;
871
873
  case 'ArrowUp':
872
874
  this.__modifyDate(-7, { dateType: '__focusedDate', type: 'Date' });
@@ -63,7 +63,7 @@ export class DayObject {
63
63
 
64
64
  get weekdayNameShort() {
65
65
  const weekdayEls = Array.from(
66
- /** @type {HTMLElement} */ (this.el.parentElement?.parentElement).querySelectorAll(
66
+ /** @type {HTMLElement} */ (this.el.parentElement?.parentElement)?.querySelectorAll(
67
67
  '.calendar__day-cell',
68
68
  ),
69
69
  );
@@ -73,7 +73,7 @@ export class DayObject {
73
73
 
74
74
  get weekdayNameLong() {
75
75
  const weekdayEls = Array.from(
76
- /** @type {HTMLElement} */ (this.el.parentElement?.parentElement).querySelectorAll(
76
+ /** @type {HTMLElement} */ (this.el.parentElement?.parentElement)?.querySelectorAll(
77
77
  '.calendar__day-cell',
78
78
  ),
79
79
  );
@@ -6,6 +6,7 @@ import { OverlayMixin, withDropdownConfig } from '@lion/ui/overlays.js';
6
6
  import { css, html } from 'lit';
7
7
  import { makeMatchingTextBold, unmakeMatchingTextBold } from './utils/makeMatchingTextBold.js';
8
8
  import { MatchesOption } from './validators.js';
9
+ import { CustomChoiceGroupMixin } from '../../form-core/src/choice-group/CustomChoiceGroupMixin.js';
9
10
 
10
11
  const matchA11ySpanReverseFns = new WeakMap();
11
12
 
@@ -27,7 +28,7 @@ const matchA11ySpanReverseFns = new WeakMap();
27
28
  * LionCombobox: implements the wai-aria combobox design pattern and integrates it as a Lion
28
29
  * FormControl
29
30
  */
30
- export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
31
+ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMixin(LionListbox))) {
31
32
  /** @type {any} */
32
33
  static get properties() {
33
34
  return {
@@ -43,6 +44,10 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
43
44
  requireOptionMatch: {
44
45
  type: Boolean,
45
46
  },
47
+ allowCustomChoice: {
48
+ type: Boolean,
49
+ attribute: 'allow-custom-choice',
50
+ },
46
51
  __shouldAutocompleteNextUpdate: Boolean,
47
52
  };
48
53
  }
@@ -316,7 +321,9 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
316
321
  */
317
322
  get _inputNode() {
318
323
  if (this._ariaVersion === '1.1' && this._comboboxNode) {
319
- return /** @type {HTMLInputElement} */ (this._comboboxNode.querySelector('input'));
324
+ return /** @type {HTMLInputElement} */ (
325
+ this._comboboxNode.querySelector('input') || this._comboboxNode
326
+ );
320
327
  }
321
328
  return /** @type {HTMLInputElement} */ (this._comboboxNode);
322
329
  }
@@ -364,6 +371,20 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
364
371
  return this._inputNode;
365
372
  }
366
373
 
374
+ /**
375
+ * @returns {boolean}
376
+ */
377
+ get requireOptionMatch() {
378
+ return !this.allowCustomChoice;
379
+ }
380
+
381
+ /**
382
+ * @param {boolean} value
383
+ */
384
+ set requireOptionMatch(value) {
385
+ this.allowCustomChoice = !value;
386
+ }
387
+
367
388
  constructor() {
368
389
  super();
369
390
  /**
@@ -486,14 +507,20 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
486
507
 
487
508
  /**
488
509
  * Converts viewValue to modelValue
489
- * @param {string} value - viewValue: the formatted value inside <input>
510
+ * @override CustomChoiceGroupMixin
511
+ * @param {string|string[]} value - viewValue: the formatted value inside <input>
490
512
  * @returns {*} modelValue
491
513
  */
492
514
  parser(value) {
493
- if (this.requireOptionMatch && this.checkedIndex === -1 && value !== '') {
515
+ if (
516
+ this.requireOptionMatch &&
517
+ this.checkedIndex === -1 &&
518
+ value !== '' &&
519
+ !Array.isArray(value)
520
+ ) {
494
521
  return new Unparseable(value);
495
522
  }
496
- return value;
523
+ return super.parser(value);
497
524
  }
498
525
 
499
526
  /**
@@ -554,15 +581,6 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
554
581
  if (typeof this._selectionDisplayNode?.onComboboxElementUpdated === 'function') {
555
582
  this._selectionDisplayNode.onComboboxElementUpdated(changedProperties);
556
583
  }
557
-
558
- if (changedProperties.has('requireOptionMatch') || changedProperties.has('multipleChoice')) {
559
- if (!this.requireOptionMatch && this.multipleChoice) {
560
- // TODO implement !requireOptionMatch and multipleChoice flow
561
- throw new Error(
562
- "multipleChoice and requireOptionMatch=false can't be used at the same time (yet).",
563
- );
564
- }
565
- }
566
584
  }
567
585
 
568
586
  /**
@@ -697,8 +715,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
697
715
  * @protected
698
716
  */
699
717
  _setTextboxValue(v) {
700
- // Make sure that we don't loose inputNode.selectionStart and inputNode.selectionEnd
701
- if (this._inputNode.value !== v) {
718
+ // Make sure that we don't lose inputNode.selectionStart and inputNode.selectionEnd
719
+ if (this._inputNode && this._inputNode.value !== v) {
702
720
  this._inputNode.value = v;
703
721
  }
704
722
  }
@@ -849,7 +867,9 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
849
867
  * @protected
850
868
  */
851
869
  _handleAutocompletion() {
852
- const hasSelection = this._inputNode.value.length !== this._inputNode.selectionStart;
870
+ const isSelectionEmpty = this._inputNode.selectionStart === this._inputNode.selectionEnd;
871
+ const hasSelection =
872
+ !isSelectionEmpty && this._inputNode.value.length !== this._inputNode.selectionStart;
853
873
 
854
874
  const inputValue = this._inputNode.value;
855
875
  const inputSelectionStart = this._inputNode.selectionStart;
@@ -953,6 +973,9 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
953
973
  if (autoselect && !hasAutoFilled && !this.multipleChoice) {
954
974
  // This means there is no match for checkedIndex
955
975
  this.setCheckedIndex(-1);
976
+ if (prevValue !== curValue) {
977
+ this.activeIndex = -1;
978
+ }
956
979
  this.modelValue = this.parser(inputValue);
957
980
  }
958
981
 
@@ -1063,25 +1086,52 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1063
1086
  * @protected
1064
1087
  */
1065
1088
  _listboxOnKeyDown(ev) {
1066
- super._listboxOnKeyDown(ev);
1067
1089
  const { key } = ev;
1068
1090
  switch (key) {
1069
1091
  case 'Escape':
1070
1092
  this.opened = false;
1093
+ super._listboxOnKeyDown(ev);
1071
1094
  this._setTextboxValue('');
1072
1095
  break;
1096
+ case 'Backspace':
1097
+ case 'Delete':
1098
+ if (this.requireOptionMatch) {
1099
+ super._listboxOnKeyDown(ev);
1100
+ } else {
1101
+ this.opened = false;
1102
+ }
1103
+ break;
1073
1104
  case 'Enter':
1074
1105
  if (this.multipleChoice && this.opened) {
1075
1106
  ev.preventDefault();
1076
1107
  }
1077
- if (!this.formElements[this.activeIndex]) {
1078
- return;
1108
+
1109
+ if (
1110
+ !this.requireOptionMatch &&
1111
+ this.multipleChoice &&
1112
+ (!this.formElements[this.activeIndex] ||
1113
+ this.formElements[this.activeIndex].hasAttribute('aria-hidden') ||
1114
+ !this.opened)
1115
+ ) {
1116
+ ev.preventDefault();
1117
+
1118
+ this.modelValue = this.parser([...this.modelValue, this._inputNode.value]);
1119
+
1120
+ this._inputNode.value = '';
1121
+ this.opened = false;
1122
+ } else {
1123
+ super._listboxOnKeyDown(ev);
1124
+ // TODO: should we clear the input value here when allowCustomChoice is false?
1125
+ // For now, we don't...
1079
1126
  }
1080
1127
  if (!this.multipleChoice) {
1081
1128
  this.opened = false;
1082
1129
  }
1083
1130
  break;
1084
- /* no default */
1131
+ default: {
1132
+ super._listboxOnKeyDown(ev);
1133
+ break;
1134
+ }
1085
1135
  }
1086
1136
  }
1087
1137
 
@@ -1108,12 +1158,14 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1108
1158
  */
1109
1159
  // eslint-disable-next-line no-unused-vars
1110
1160
  _syncToTextboxMultiple(modelValue, oldModelValue = []) {
1111
- const diff = modelValue.filter(x => !oldModelValue.includes(x));
1112
- const newValue = this.formElements
1113
- .filter(option => diff.includes(option.choiceValue))
1114
- .map(option => this._getTextboxValueFromOption(option))
1115
- .join(' ');
1116
- this._setTextboxValue(newValue); // or last selected value?
1161
+ if (this.requireOptionMatch) {
1162
+ const diff = modelValue.filter(x => !oldModelValue.includes(x));
1163
+ const newValue = this.formElements
1164
+ .filter(option => diff.includes(option.choiceValue))
1165
+ .map(option => this._getTextboxValueFromOption(option))
1166
+ .join(' ');
1167
+ this._setTextboxValue(newValue); // or last selected value?
1168
+ }
1117
1169
  }
1118
1170
 
1119
1171
  /**
@@ -1145,11 +1197,12 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1145
1197
 
1146
1198
  this._comboboxNode.setAttribute('role', 'combobox');
1147
1199
  this._comboboxNode.setAttribute('aria-haspopup', 'listbox');
1200
+
1148
1201
  this._inputNode.setAttribute('aria-autocomplete', this.autocomplete);
1202
+ this._comboboxNode.setAttribute('aria-controls', this._listboxNode.id);
1149
1203
 
1150
1204
  if (this._ariaVersion === '1.1') {
1151
1205
  this._comboboxNode.setAttribute('aria-owns', this._listboxNode.id);
1152
- this._inputNode.setAttribute('aria-controls', this._listboxNode.id);
1153
1206
  } else {
1154
1207
  this._inputNode.setAttribute('aria-owns', this._listboxNode.id);
1155
1208
  }
@@ -1183,8 +1236,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1183
1236
  }
1184
1237
 
1185
1238
  clear() {
1186
- super.clear();
1187
1239
  this.value = '';
1240
+ super.clear();
1188
1241
  this.__shouldAutocompleteNextUpdate = true;
1189
1242
  }
1190
1243
  }
@@ -1,4 +1,9 @@
1
1
  import { runListboxMixinSuite } from '@lion/ui/listbox-test-suites.js';
2
2
  import '@lion/ui/define/lion-combobox.js';
3
+ import { runCustomChoiceGroupMixinSuite } from '../../form-core/test-suites/choice-group/CustomChoiceGroupMixin.suite.js';
3
4
 
4
5
  runListboxMixinSuite({ tagString: 'lion-combobox' });
6
+ runCustomChoiceGroupMixinSuite({
7
+ parentTagString: 'lion-combobox',
8
+ childTagString: 'lion-option',
9
+ });
@@ -395,29 +395,6 @@ describe('lion-combobox', () => {
395
395
  expect(el.formElements[0].checked).to.be.false;
396
396
  });
397
397
 
398
- it('multiple choice and requireOptionMatch is false do not work together yet', async () => {
399
- const errorMessage = `multipleChoice and requireOptionMatch=false can't be used at the same time (yet).`;
400
- let error;
401
- const el = /** @type {LionCombobox} */ (
402
- await fixture(html`
403
- <lion-combobox name="foo" multiple-choice>
404
- <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
405
- <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
406
- <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
407
- <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
408
- </lion-combobox>
409
- `)
410
- );
411
- try {
412
- el.requireOptionMatch = false;
413
- await el.updateComplete;
414
- } catch (err) {
415
- error = err;
416
- }
417
- expect(error).to.be.instanceOf(Error);
418
- expect(/** @type {Error} */ (error).message).to.equal(errorMessage);
419
- });
420
-
421
398
  it('clears modelValue and textbox value on clear()', async () => {
422
399
  const el = /** @type {LionCombobox} */ (
423
400
  await fixture(html`
@@ -453,6 +430,32 @@ describe('lion-combobox', () => {
453
430
  expect(el2._inputNode.value).to.equal('');
454
431
  });
455
432
 
433
+ it('correctly emits event with an empty value when clear() is called', async () => {
434
+ const el = /** @type {LionCombobox} */ (
435
+ await fixture(
436
+ html`<lion-combobox>
437
+ <lion-option .choiceValue=${'red'}>Red</lion-option>
438
+ <lion-option .choiceValue=${'green'}>Green</lion-option>
439
+ <lion-option .choiceValue=${'blue'}>Blue</lion-option>
440
+ </lion-combobox>`,
441
+ )
442
+ );
443
+
444
+ el.modelValue = 'red';
445
+ await el.updateComplete;
446
+
447
+ el.addEventListener('model-value-changed', ({ target }) => {
448
+ expect(target).to.not.be.null;
449
+
450
+ const { modelValue, value } = /** @type {LionCombobox} */ (target);
451
+ expect(value).to.equal('');
452
+ expect(modelValue).to.equal('');
453
+ });
454
+
455
+ el.clear();
456
+ await el.updateComplete;
457
+ });
458
+
456
459
  it('updates option list after clear()', async () => {
457
460
  const el = /** @type {LionCombobox} */ (
458
461
  await fixture(html`
@@ -565,6 +568,33 @@ describe('lion-combobox', () => {
565
568
  expect(el.showsFeedbackFor).to.include('error', 'showsFeedbackFor');
566
569
  });
567
570
 
571
+ it('ignores empty string modelValue inputs', async () => {
572
+ const el = /** @type {LionCombobox} */ (
573
+ await fixture(html`
574
+ <lion-combobox name="foo" multiple-choice autocomplete="none">
575
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
576
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
577
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
578
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
579
+ </lion-combobox>
580
+ `)
581
+ );
582
+
583
+ el.requireOptionMatch = false;
584
+ await el.updateComplete;
585
+ const { _inputNode } = getComboboxMembers(el);
586
+
587
+ mimicKeyPress(_inputNode, 'Enter');
588
+ await el.updateComplete;
589
+ expect(el.modelValue).to.eql([]);
590
+
591
+ mimicUserTyping(el, ' ');
592
+ await el.updateComplete;
593
+ mimicKeyPress(_inputNode, 'Enter');
594
+ await el.updateComplete;
595
+ expect(el.modelValue).to.eql([]);
596
+ });
597
+
568
598
  it('allows a value outside of the option list when requireOptionMatch is false', async () => {
569
599
  const el = /** @type {LionCombobox} */ (
570
600
  await fixture(html`
@@ -589,6 +619,35 @@ describe('lion-combobox', () => {
589
619
  expect(_inputNode.value).to.equal('Foo');
590
620
  });
591
621
 
622
+ it("doesn't select any similar options after using delete when requireOptionMatch is false", async () => {
623
+ const el = /** @type {LionCombobox} */ (
624
+ await fixture(html`
625
+ <lion-combobox name="foo" .validators=${[new Required()]}>
626
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
627
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
628
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
629
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
630
+ </lion-combobox>
631
+ `)
632
+ );
633
+ el.requireOptionMatch = false;
634
+ const { _inputNode } = getComboboxMembers(el);
635
+
636
+ mimicUserTyping(el, 'Art');
637
+ await el.updateComplete;
638
+
639
+ await mimicUserTypingAdvanced(el, ['Delete']);
640
+ await el.updateComplete;
641
+ await el.updateComplete;
642
+
643
+ mimicKeyPress(_inputNode, 'Enter');
644
+ await el.updateComplete;
645
+
646
+ expect(el.checkedIndex).to.equal(-1);
647
+ expect(el.modelValue).to.equal('Art');
648
+ expect(_inputNode.value).to.equal('Art');
649
+ });
650
+
592
651
  it("when removing a letter it won't select the option", async () => {
593
652
  // We don't autocomplete when characters are removed
594
653
  const el = /** @type {LionCombobox} */ (
@@ -641,6 +700,173 @@ describe('lion-combobox', () => {
641
700
  expect(el.modelValue).to.equal('Foo');
642
701
  expect(el.formElements[0].checked).to.be.false;
643
702
  });
703
+
704
+ it('allows custom selections when multi-choice when requireOptionMatch is false', async () => {
705
+ const el = /** @type {LionCombobox} */ (
706
+ await fixture(html`
707
+ <lion-combobox
708
+ name="foo"
709
+ multiple-choice
710
+ .validators=${[new Required()]}
711
+ autocomplete="none"
712
+ >
713
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
714
+ <lion-option checked .choiceValue="${'Chard'}">Chard</lion-option>
715
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
716
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
717
+ </lion-combobox>
718
+ `)
719
+ );
720
+
721
+ el.requireOptionMatch = false;
722
+ await el.updateComplete;
723
+
724
+ const { _inputNode } = getComboboxMembers(el);
725
+ expect(el.modelValue).to.eql(['Chard']);
726
+ expect(el.checkedIndex).to.eql([1]);
727
+
728
+ mimicUserTyping(el, 'Foo');
729
+ await el.updateComplete;
730
+ mimicKeyPress(_inputNode, 'Enter');
731
+ await el.updateComplete;
732
+
733
+ expect(el.modelValue).to.eql(['Chard', 'Foo']);
734
+ expect(el.checkedIndex).to.eql([1]);
735
+
736
+ mimicUserTyping(el, 'Bar');
737
+ await el.updateComplete;
738
+ mimicKeyPress(_inputNode, 'Enter');
739
+ await el.updateComplete;
740
+ });
741
+
742
+ it('allows manyu custom selections when multi-choice when requireOptionMatch is false', async () => {
743
+ const el = /** @type {LionCombobox} */ (
744
+ await fixture(html`
745
+ <lion-combobox
746
+ name="foo"
747
+ multiple-choice
748
+ .validators=${[new Required()]}
749
+ autocomplete="none"
750
+ >
751
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
752
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
753
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
754
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
755
+ </lion-combobox>
756
+ `)
757
+ );
758
+
759
+ el.requireOptionMatch = false;
760
+ await el.updateComplete;
761
+
762
+ const { _inputNode } = getComboboxMembers(el);
763
+
764
+ mimicUserTyping(el, 'Foo');
765
+ await el.updateComplete;
766
+ mimicKeyPress(_inputNode, 'Enter');
767
+ await el.updateComplete;
768
+
769
+ expect(el.modelValue).to.eql(['Foo']);
770
+ expect(el.checkedIndex).to.eql([]);
771
+
772
+ mimicUserTyping(el, 'Bar');
773
+ await el.updateComplete;
774
+ mimicKeyPress(_inputNode, 'Enter');
775
+ await el.updateComplete;
776
+
777
+ expect(el.modelValue).to.eql(['Foo', 'Bar']);
778
+ expect(el.checkedIndex).to.eql([]);
779
+ });
780
+
781
+ it('allows new options when multi-choice when requireOptionMatch=false and autocomplete="both", without selecting similar values', async () => {
782
+ const el = /** @type {LionCombobox} */ (
783
+ await fixture(html`
784
+ <lion-combobox
785
+ name="foo"
786
+ multiple-choice
787
+ .requireOptionMatch=${false}
788
+ autocomplete="both"
789
+ >
790
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
791
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
792
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
793
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
794
+ </lion-combobox>
795
+ `)
796
+ );
797
+
798
+ await el.updateComplete;
799
+
800
+ const { _inputNode } = getComboboxMembers(el);
801
+
802
+ mimicUserTyping(el, 'Artist');
803
+ await el.updateComplete;
804
+ mimicKeyPress(_inputNode, 'Enter');
805
+ await el.updateComplete;
806
+
807
+ expect(el.modelValue).to.eql(['Artist']);
808
+ });
809
+
810
+ it('allows new options when multi-choice when requireOptionMatch=false and autocomplete="both", when deleting autocomplete values using Backspace', async () => {
811
+ const el = /** @type {LionCombobox} */ (
812
+ await fixture(html`
813
+ <lion-combobox
814
+ name="foo"
815
+ multiple-choice
816
+ .requireOptionMatch=${false}
817
+ autocomplete="both"
818
+ >
819
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
820
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
821
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
822
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
823
+ </lion-combobox>
824
+ `)
825
+ );
826
+
827
+ await el.updateComplete;
828
+
829
+ const { _inputNode } = getComboboxMembers(el);
830
+ mimicUserTyping(el, 'Art');
831
+ await el.updateComplete;
832
+ await mimicUserTypingAdvanced(el, ['Backspace']);
833
+ await el.updateComplete;
834
+ mimicKeyPress(_inputNode, 'Enter');
835
+ await el.updateComplete;
836
+
837
+ expect(el.modelValue).to.eql(['Art']);
838
+ });
839
+
840
+ it('allows new custom options when multi-choice when requireOptionMatch=false and autocomplete="both", when deleting autocompleted values using Delete', async () => {
841
+ const el = /** @type {LionCombobox} */ (
842
+ await fixture(html`
843
+ <lion-combobox
844
+ name="foo"
845
+ multiple-choice
846
+ .requireOptionMatch=${false}
847
+ autocomplete="both"
848
+ >
849
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
850
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
851
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
852
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
853
+ </lion-combobox>
854
+ `)
855
+ );
856
+
857
+ await el.updateComplete;
858
+
859
+ const { _inputNode } = getComboboxMembers(el);
860
+ el.modelValue = [];
861
+
862
+ mimicUserTyping(el, 'Art');
863
+ await el.updateComplete;
864
+ await mimicUserTypingAdvanced(el, ['Delete']);
865
+ await el.updateComplete;
866
+ mimicKeyPress(_inputNode, 'Enter');
867
+ await el.updateComplete;
868
+ expect(el.modelValue).to.eql(['Art']);
869
+ });
644
870
  });
645
871
 
646
872
  describe('Overlay visibility', () => {
@@ -1304,6 +1530,47 @@ describe('lion-combobox', () => {
1304
1530
  ]);
1305
1531
  });
1306
1532
 
1533
+ it('does not autocomplete on [Enter] when textbox content does not match options', async () => {
1534
+ const el = /** @type {LionCombobox} */ (
1535
+ await fixture(html`
1536
+ <lion-combobox name="foo">
1537
+ <lion-option .choiceValue="${'Mango'}">Mango</lion-option>
1538
+ <lion-option .choiceValue="${'Lemon'}">Lemon</lion-option>
1539
+ <lion-option .choiceValue="${'Apple'}">Apple</lion-option>
1540
+ </lion-combobox>
1541
+ `)
1542
+ );
1543
+ const { _inputNode } = getComboboxMembers(el);
1544
+ mimicUserTypingAdvanced(el, ['m', 'a', 'k']);
1545
+ await el.updateComplete;
1546
+ mimicKeyPress(_inputNode, 'Enter');
1547
+ await el.updateComplete;
1548
+ await el.updateComplete;
1549
+ expect(_inputNode.value).to.equal('Mak');
1550
+ });
1551
+
1552
+ it('does not autocomplete on [Enter] when textbox content does not match options and content was cleared via [Backspace]', async () => {
1553
+ const el = /** @type {LionCombobox} */ (
1554
+ await fixture(html`
1555
+ <lion-combobox name="foo">
1556
+ <lion-option .choiceValue="${'Mango'}">Mango</lion-option>
1557
+ <lion-option .choiceValue="${'Lemon'}">Lemon</lion-option>
1558
+ <lion-option .choiceValue="${'Apple'}">Apple</lion-option>
1559
+ </lion-combobox>
1560
+ `)
1561
+ );
1562
+ const { _inputNode } = getComboboxMembers(el);
1563
+ mimicUserTypingAdvanced(el, ['m', 'o', 'Backspace', 'Backspace', 'm', 'o']);
1564
+ await el.updateComplete;
1565
+ await el.updateComplete;
1566
+ await el.updateComplete;
1567
+ await el.updateComplete;
1568
+ mimicKeyPress(_inputNode, 'Enter');
1569
+ await el.updateComplete;
1570
+ await el.updateComplete;
1571
+ expect(_inputNode.value).to.equal('Mo');
1572
+ });
1573
+
1307
1574
  it('does not filter options when autocomplete is "inline"', async () => {
1308
1575
  const el = /** @type {LionCombobox} */ (
1309
1576
  await fixture(html`
@@ -1610,6 +1877,34 @@ describe('lion-combobox', () => {
1610
1877
  expect(el.checkedIndex).to.equal(0);
1611
1878
  });
1612
1879
 
1880
+ it('filters options correctly when changing the middle of the word', async () => {
1881
+ const el = /** @type {LionCombobox} */ (
1882
+ await fixture(html`
1883
+ <lion-combobox name="foo" autocomplete="list" match-mode="all">
1884
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
1885
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
1886
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
1887
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
1888
+ </lion-combobox>
1889
+ `)
1890
+ );
1891
+
1892
+ const { _inputNode } = getComboboxMembers(el);
1893
+
1894
+ mimicUserTyping(el, 'char');
1895
+ expect(_inputNode.value).to.equal('char');
1896
+ await el.updateComplete; // Char
1897
+
1898
+ expect(getFilteredOptionValues(el)).to.eql(['Chard']);
1899
+
1900
+ _inputNode.setSelectionRange(3, 3);
1901
+ await mimicUserTypingAdvanced(el, ['Backspace', 'i', 'c', 'o']);
1902
+ await el.updateComplete; // Chicor
1903
+
1904
+ expect(_inputNode.value).to.equal('chicor');
1905
+ expect(getFilteredOptionValues(el)).to.eql(['Chicory']);
1906
+ });
1907
+
1613
1908
  it('computation of "user intends autofill" works correctly afer autofill', async () => {
1614
1909
  const el = /** @type {LionCombobox} */ (
1615
1910
  await fixture(html`