@lion/ui 0.4.2 → 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 (151) hide show
  1. package/components/calendar/src/LionCalendar.js +1 -1
  2. package/components/calendar/test-helpers/DayObject.js +2 -2
  3. package/components/combobox/src/LionCombobox.js +79 -28
  4. package/components/combobox/test/lion-combobox-integrations.test.js +5 -0
  5. package/components/combobox/test/lion-combobox.test.js +290 -23
  6. package/components/combobox/test-helpers/combobox-helpers.js +5 -2
  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-range/test/lion-input-range.test.js +12 -6
  32. package/components/listbox/src/LionOptions.js +5 -5
  33. package/components/listbox/src/ListboxMixin.js +1 -16
  34. package/components/listbox/test/lion-options.test.js +1 -1
  35. package/components/listbox/test-suites/ListboxMixin.suite.js +1 -1
  36. package/components/localize/src/date/getMonthNames.js +3 -0
  37. package/components/localize/src/date/utils/forceDotsForNlnl.js +11 -0
  38. package/components/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.js +3 -1
  39. package/components/localize/src/number/utils/normalize-format-number-to-parts/forceSymbols.js +2 -0
  40. package/components/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.js +1 -0
  41. package/components/localize/test/date/getMonthNames.test.js +1 -0
  42. package/components/localize/test-helpers/fake-imports.js +1 -0
  43. package/components/radio-group/src/LionRadioGroup.js +1 -1
  44. package/components/radio-group/test/lion-radio-group.test.js +29 -0
  45. package/components/select/test/lion-select.test.js +1 -1
  46. package/components/switch/src/LionSwitchButton.js +1 -5
  47. package/components/tabs/src/LionTabs.js +7 -7
  48. package/components/textarea/src/LionTextarea.js +1 -1
  49. package/custom-elements.json +102923 -61581
  50. package/dist-types/components/combobox/src/LionCombobox.d.ts +13 -8
  51. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  52. package/dist-types/components/combobox/test-helpers/combobox-helpers.d.ts.map +1 -1
  53. package/dist-types/components/drawer/src/LionDrawer.d.ts.map +1 -1
  54. package/dist-types/components/form-core/src/FocusMixin.d.ts.map +1 -1
  55. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
  56. package/dist-types/components/form-core/src/choice-group/CustomChoiceGroupMixin.d.ts +4 -0
  57. package/dist-types/components/form-core/src/choice-group/CustomChoiceGroupMixin.d.ts.map +1 -0
  58. package/dist-types/components/form-core/src/utils/getAriaElementsInRightDomOrder.d.ts +5 -5
  59. package/dist-types/components/form-core/src/utils/getAriaElementsInRightDomOrder.d.ts.map +1 -1
  60. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.integrations.test.d.ts +2 -0
  61. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.integrations.test.d.ts.map +1 -0
  62. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.test.d.ts +6 -0
  63. package/dist-types/components/form-core/test/choice-group/CustomChoiceGroupMixin.test.d.ts.map +1 -0
  64. package/dist-types/components/form-core/test-helpers/ExampleValidators.d.ts +0 -1
  65. package/dist-types/components/form-core/test-helpers/ExampleValidators.d.ts.map +1 -1
  66. package/dist-types/components/form-core/test-suites/FormRegistrationMixins.suite.d.ts.map +1 -1
  67. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  68. package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
  69. package/dist-types/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.d.ts.map +1 -1
  70. package/dist-types/components/form-core/test-suites/choice-group/CustomChoiceGroupMixin.suite.d.ts +14 -0
  71. package/dist-types/components/form-core/test-suites/choice-group/CustomChoiceGroupMixin.suite.d.ts.map +1 -0
  72. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts +1 -1
  73. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts.map +1 -1
  74. package/dist-types/components/form-core/types/choice-group/CustomChoiceGroupMixinTypes.d.ts +18 -0
  75. package/dist-types/components/form-core/types/choice-group/CustomChoiceGroupMixinTypes.d.ts.map +1 -0
  76. package/dist-types/components/form-core/types/choice-group/index.d.ts +1 -0
  77. package/dist-types/components/form-core/types/choice-group/index.d.ts.map +1 -1
  78. package/dist-types/components/helpers/src/SbActionLogger.d.ts.map +1 -1
  79. package/dist-types/components/icon/src/LionIcon.d.ts +0 -9
  80. package/dist-types/components/icon/src/LionIcon.d.ts.map +1 -1
  81. package/dist-types/components/listbox/src/LionOptions.d.ts +0 -5
  82. package/dist-types/components/listbox/src/LionOptions.d.ts.map +1 -1
  83. package/dist-types/components/listbox/src/ListboxMixin.d.ts.map +1 -1
  84. package/dist-types/components/localize/src/date/getMonthNames.d.ts.map +1 -1
  85. package/dist-types/components/localize/src/date/utils/forceDotsForNlnl.d.ts +5 -0
  86. package/dist-types/components/localize/src/date/utils/forceDotsForNlnl.d.ts.map +1 -0
  87. package/dist-types/components/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.d.ts.map +1 -1
  88. package/dist-types/components/localize/src/number/utils/normalize-format-number-to-parts/forceSymbols.d.ts.map +1 -1
  89. package/dist-types/components/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.d.ts.map +1 -1
  90. package/dist-types/components/localize/test-helpers/fake-imports.d.ts.map +1 -1
  91. package/dist-types/components/switch/src/LionSwitchButton.d.ts +0 -5
  92. package/dist-types/components/switch/src/LionSwitchButton.d.ts.map +1 -1
  93. package/dist-types/components/tabs/src/LionTabs.d.ts.map +1 -1
  94. package/dist-types/exports/types/form-core.d.ts +1 -1
  95. package/dist-types/exports/types/form-core.d.ts.map +1 -1
  96. package/dist-types/tsconfig.tsbuildinfo +1 -1
  97. package/docs/components/checkbox-group/overview.md +2 -2
  98. package/docs/components/collapsible/assets/CustomCollapsible.js +4 -0
  99. package/docs/components/collapsible/overview.md +1 -1
  100. package/docs/components/combobox/overview.md +1 -1
  101. package/docs/components/combobox/src/demo-selection-display.js +8 -35
  102. package/docs/components/combobox/use-cases.md +24 -1
  103. package/docs/components/dialog/overview.md +1 -1
  104. package/docs/components/fieldset/overview.md +5 -5
  105. package/docs/components/form/overview.md +3 -3
  106. package/docs/components/icon/use-cases.md +1 -1
  107. package/docs/components/input/overview.md +1 -1
  108. package/docs/components/input-amount/overview.md +3 -3
  109. package/docs/components/input-date/overview.md +3 -3
  110. package/docs/components/input-datepicker/overview.md +5 -5
  111. package/docs/components/input-email/overview.md +2 -2
  112. package/docs/components/input-file/overview.md +1 -1
  113. package/docs/components/input-iban/overview.md +2 -2
  114. package/docs/components/input-range/overview.md +2 -2
  115. package/docs/components/input-stepper/overview.md +1 -1
  116. package/docs/components/input-tel/overview.md +1 -1
  117. package/docs/components/input-tel/use-cases.md +6 -6
  118. package/docs/components/input-tel-dropdown/overview.md +1 -1
  119. package/docs/components/radio-group/overview.md +3 -3
  120. package/docs/components/select-rich/use-cases.md +1 -1
  121. package/docs/components/tabs/examples.md +1 -1
  122. package/docs/fundamentals/index.md +4 -4
  123. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  124. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  125. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  126. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  127. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  128. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  129. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  130. package/docs/fundamentals/rationales/accessibility.md +3 -3
  131. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  132. package/docs/fundamentals/systems/core/overview.md +1 -1
  133. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  134. package/docs/fundamentals/systems/form/model-value.md +1 -1
  135. package/docs/fundamentals/systems/form/overview.md +28 -28
  136. package/docs/fundamentals/systems/form/validate.md +18 -2
  137. package/docs/fundamentals/systems/localize/overview.md +4 -4
  138. package/docs/fundamentals/systems/overlays/configuration.md +3 -2
  139. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  140. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  141. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  142. package/docs/fundamentals/tools/singleton-manager/example-complex/overlayCompatibility.js +1 -2
  143. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  144. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  145. package/docs/guides/how-to/create-a-form.md +4 -4
  146. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  147. package/docs/guides/index.md +8 -8
  148. package/docs/guides/principles/index.md +5 -5
  149. package/docs/guides/principles/styling.md +1 -1
  150. package/exports/types/form-core.ts +5 -1
  151. package/package.json +1 -1
@@ -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
 
@@ -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
  }
@@ -955,6 +973,9 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
955
973
  if (autoselect && !hasAutoFilled && !this.multipleChoice) {
956
974
  // This means there is no match for checkedIndex
957
975
  this.setCheckedIndex(-1);
976
+ if (prevValue !== curValue) {
977
+ this.activeIndex = -1;
978
+ }
958
979
  this.modelValue = this.parser(inputValue);
959
980
  }
960
981
 
@@ -1065,25 +1086,52 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1065
1086
  * @protected
1066
1087
  */
1067
1088
  _listboxOnKeyDown(ev) {
1068
- super._listboxOnKeyDown(ev);
1069
1089
  const { key } = ev;
1070
1090
  switch (key) {
1071
1091
  case 'Escape':
1072
1092
  this.opened = false;
1093
+ super._listboxOnKeyDown(ev);
1073
1094
  this._setTextboxValue('');
1074
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;
1075
1104
  case 'Enter':
1076
1105
  if (this.multipleChoice && this.opened) {
1077
1106
  ev.preventDefault();
1078
1107
  }
1079
- if (!this.formElements[this.activeIndex]) {
1080
- 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...
1081
1126
  }
1082
1127
  if (!this.multipleChoice) {
1083
1128
  this.opened = false;
1084
1129
  }
1085
1130
  break;
1086
- /* no default */
1131
+ default: {
1132
+ super._listboxOnKeyDown(ev);
1133
+ break;
1134
+ }
1087
1135
  }
1088
1136
  }
1089
1137
 
@@ -1110,12 +1158,14 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1110
1158
  */
1111
1159
  // eslint-disable-next-line no-unused-vars
1112
1160
  _syncToTextboxMultiple(modelValue, oldModelValue = []) {
1113
- const diff = modelValue.filter(x => !oldModelValue.includes(x));
1114
- const newValue = this.formElements
1115
- .filter(option => diff.includes(option.choiceValue))
1116
- .map(option => this._getTextboxValueFromOption(option))
1117
- .join(' ');
1118
- 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
+ }
1119
1169
  }
1120
1170
 
1121
1171
  /**
@@ -1147,11 +1197,12 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1147
1197
 
1148
1198
  this._comboboxNode.setAttribute('role', 'combobox');
1149
1199
  this._comboboxNode.setAttribute('aria-haspopup', 'listbox');
1200
+
1150
1201
  this._inputNode.setAttribute('aria-autocomplete', this.autocomplete);
1202
+ this._comboboxNode.setAttribute('aria-controls', this._listboxNode.id);
1151
1203
 
1152
1204
  if (this._ariaVersion === '1.1') {
1153
1205
  this._comboboxNode.setAttribute('aria-owns', this._listboxNode.id);
1154
- this._inputNode.setAttribute('aria-controls', this._listboxNode.id);
1155
1206
  } else {
1156
1207
  this._inputNode.setAttribute('aria-owns', this._listboxNode.id);
1157
1208
  }
@@ -1185,8 +1236,8 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(LionListbox)) {
1185
1236
  }
1186
1237
 
1187
1238
  clear() {
1188
- super.clear();
1189
1239
  this.value = '';
1240
+ super.clear();
1190
1241
  this.__shouldAutocompleteNextUpdate = true;
1191
1242
  }
1192
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`
@@ -72,15 +72,18 @@ export async function mimicUserTypingAdvanced(el, values) {
72
72
  const selectionEnd = _inputNode.selectionEnd || 0;
73
73
  const hasSelection = selectionStart !== selectionEnd;
74
74
 
75
- if (key === 'Backspace') {
75
+ if (key === 'Backspace' || key === 'Delete') {
76
76
  if (hasSelection) {
77
77
  _inputNode.value =
78
78
  _inputNode.value.slice(0, selectionStart) + _inputNode.value.slice(selectionEnd);
79
79
  cursorPosition = selectionStart;
80
- } else if (cursorPosition > 0) {
80
+ } else if (cursorPosition > 0 && key === 'Backspace') {
81
81
  _inputNode.value =
82
82
  _inputNode.value.slice(0, cursorPosition - 1) + _inputNode.value.slice(cursorPosition);
83
83
  cursorPosition -= 1;
84
+ } else if (cursorPosition < _inputNode.value.length && key === 'Delete') {
85
+ _inputNode.value =
86
+ _inputNode.value.slice(0, cursorPosition) + _inputNode.value.slice(cursorPosition + 1);
84
87
  }
85
88
  } else if (hasSelection) {
86
89
  _inputNode.value =
@@ -115,6 +115,7 @@ export class LionDrawer extends LionCollapsible {
115
115
  const prop = this.position === 'top' ? 'height' : 'width';
116
116
 
117
117
  contentNode.style.setProperty(prop, /** @type {string} */ (min));
118
+ // eslint-disable-next-line no-promise-executor-return
118
119
  await new Promise(resolve => requestAnimationFrame(() => resolve(true)));
119
120
  contentNode.style.setProperty(prop, /** @type {string} */ (max));
120
121
  await this._waitForTransition({ contentNode });
@@ -27,6 +27,7 @@ const FocusMixinImplementation = superclass =>
27
27
  return {
28
28
  focused: { type: Boolean, reflect: true },
29
29
  focusedVisible: { type: Boolean, reflect: true, attribute: 'focused-visible' },
30
+ // eslint-disable-next-line lit/no-native-attributes
30
31
  autofocus: { type: Boolean, reflect: true }, // Required in Lit to observe autofocus
31
32
  };
32
33
  }
@@ -55,6 +56,7 @@ const FocusMixinImplementation = superclass =>
55
56
  * @param {import('lit').PropertyValues } changedProperties
56
57
  */
57
58
  firstUpdated(changedProperties) {
59
+ // eslint-disable-next-line lit/no-native-attributes
58
60
  super.firstUpdated(changedProperties);
59
61
  this.__registerEventsForFocusMixin();
60
62
  this.__syncAutofocusToFocusableElement();
@@ -381,7 +381,7 @@ const FormControlMixinImplementation = superclass =>
381
381
  * Also, contents of id references that will be put in the <lion-field>._ariaDescribedby property
382
382
  * from an external context, will be read by a screen reader.
383
383
  * @param {string} attrName
384
- * @param {HTMLElement[]} nodes
384
+ * @param {Element[]} nodes
385
385
  * @param {boolean|undefined} reorder
386
386
  */
387
387
  __reflectAriaAttr(attrName, nodes, reorder) {
@@ -169,6 +169,7 @@ const ChoiceGroupMixinImplementation = superclass =>
169
169
  /** @param {import('lit').PropertyValues} changedProperties */
170
170
  updated(changedProperties) {
171
171
  super.updated(changedProperties);
172
+
172
173
  if (changedProperties.has('name') && this.name !== changedProperties.get('name')) {
173
174
  this.formElements.forEach(child => {
174
175
  // eslint-disable-next-line no-param-reassign