@lion/ui 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/components/accordion/src/LionAccordion.js +3 -2
  2. package/components/button/src/LionButton.js +6 -10
  3. package/components/button/src/LionButtonReset.js +2 -0
  4. package/components/button/src/LionButtonSubmit.js +2 -0
  5. package/components/button/test-suites/LionButton.suite.js +0 -14
  6. package/components/checkbox-group/src/LionCheckbox.js +3 -0
  7. package/components/checkbox-group/src/LionCheckboxGroup.js +2 -0
  8. package/components/checkbox-group/src/LionCheckboxIndeterminate.js +3 -0
  9. package/components/collapsible/src/LionCollapsible.js +4 -1
  10. package/components/combobox/src/LionCombobox.js +31 -8
  11. package/components/combobox/test/lion-combobox.test.js +206 -57
  12. package/components/dialog/src/LionDialog.js +3 -0
  13. package/components/drawer/src/LionDrawer.js +9 -0
  14. package/components/fieldset/src/LionFieldset.js +1 -1
  15. package/components/form/src/LionForm.js +0 -1
  16. package/components/form-core/src/FocusMixin.js +1 -0
  17. package/components/form-core/src/FormControlMixin.js +33 -18
  18. package/components/form-core/src/FormatMixin.js +1 -1
  19. package/components/form-core/src/validate/LionValidationFeedback.js +8 -10
  20. package/components/form-core/src/validate/ValidateMixin.js +40 -4
  21. package/components/form-core/test/FormControlMixin.test.js +68 -0
  22. package/components/form-core/test/validate/lion-validation-feedback.test.js +0 -22
  23. package/components/form-core/test-suites/ValidateMixin.suite.js +19 -3
  24. package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +119 -0
  25. package/components/form-core/types/validate/validate.ts +1 -0
  26. package/components/icon/src/LionIcon.js +2 -0
  27. package/components/input-file/src/LionInputFile.js +9 -0
  28. package/components/input-tel/src/LionInputTel.js +3 -0
  29. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +2 -0
  30. package/components/listbox/src/LionListbox.js +2 -0
  31. package/components/overlays/src/OverlayController.js +32 -5
  32. package/components/overlays/src/OverlayMixin.js +5 -0
  33. package/components/overlays/src/configurations/withDropdownConfig.js +0 -1
  34. package/components/overlays/src/configurations/withTooltipConfig.js +0 -1
  35. package/components/overlays/test/OverlayController.test.js +16 -4
  36. package/components/overlays/test-suites/OverlayMixin.suite.js +71 -0
  37. package/components/progress-indicator/src/LionProgressIndicator.js +4 -0
  38. package/components/radio-group/src/LionRadioGroup.js +3 -1
  39. package/components/select/src/LionSelect.js +4 -0
  40. package/components/select-rich/src/LionSelectInvoker.js +2 -0
  41. package/components/select-rich/src/LionSelectRich.js +2 -0
  42. package/components/steps/src/LionStep.js +0 -1
  43. package/components/steps/src/LionSteps.js +0 -1
  44. package/components/switch/src/LionSwitch.js +3 -0
  45. package/components/switch/src/LionSwitchButton.js +5 -0
  46. package/components/tabs/src/LionTabs.js +8 -0
  47. package/custom-elements.json +1271 -406
  48. package/dist-types/components/accordion/src/LionAccordion.d.ts +3 -2
  49. package/dist-types/components/accordion/src/LionAccordion.d.ts.map +1 -1
  50. package/dist-types/components/button/src/LionButton.d.ts +2 -1
  51. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  52. package/dist-types/components/button/src/LionButtonReset.d.ts +2 -0
  53. package/dist-types/components/button/src/LionButtonReset.d.ts.map +1 -1
  54. package/dist-types/components/button/src/LionButtonSubmit.d.ts +2 -0
  55. package/dist-types/components/button/src/LionButtonSubmit.d.ts.map +1 -1
  56. package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
  57. package/dist-types/components/checkbox-group/src/LionCheckbox.d.ts +3 -0
  58. package/dist-types/components/checkbox-group/src/LionCheckbox.d.ts.map +1 -1
  59. package/dist-types/components/checkbox-group/src/LionCheckboxGroup.d.ts +2 -0
  60. package/dist-types/components/checkbox-group/src/LionCheckboxGroup.d.ts.map +1 -1
  61. package/dist-types/components/checkbox-group/src/LionCheckboxIndeterminate.d.ts +3 -0
  62. package/dist-types/components/checkbox-group/src/LionCheckboxIndeterminate.d.ts.map +1 -1
  63. package/dist-types/components/collapsible/src/LionCollapsible.d.ts +3 -1
  64. package/dist-types/components/collapsible/src/LionCollapsible.d.ts.map +1 -1
  65. package/dist-types/components/combobox/src/LionCombobox.d.ts +5 -0
  66. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  67. package/dist-types/components/core/src/ScopedElementsMixin.d.ts.map +1 -1
  68. package/dist-types/components/dialog/src/LionDialog.d.ts +3 -0
  69. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  70. package/dist-types/components/drawer/src/LionDrawer.d.ts +9 -0
  71. package/dist-types/components/drawer/src/LionDrawer.d.ts.map +1 -1
  72. package/dist-types/components/fieldset/src/LionFieldset.d.ts +1 -1
  73. package/dist-types/components/form/src/LionForm.d.ts.map +1 -1
  74. package/dist-types/components/form-core/src/FocusMixin.d.ts.map +1 -1
  75. package/dist-types/components/form-core/src/FormControlMixin.d.ts.map +1 -1
  76. package/dist-types/components/form-core/src/validate/LionValidationFeedback.d.ts +4 -3
  77. package/dist-types/components/form-core/src/validate/LionValidationFeedback.d.ts.map +1 -1
  78. package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts.map +1 -1
  79. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  80. package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
  81. package/dist-types/components/form-core/types/validate/validate.d.ts +1 -0
  82. package/dist-types/components/form-core/types/validate/validate.d.ts.map +1 -1
  83. package/dist-types/components/icon/src/LionIcon.d.ts +2 -0
  84. package/dist-types/components/icon/src/LionIcon.d.ts.map +1 -1
  85. package/dist-types/components/input-file/src/LionInputFile.d.ts +9 -0
  86. package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
  87. package/dist-types/components/input-tel/src/LionInputTel.d.ts +3 -0
  88. package/dist-types/components/input-tel/src/LionInputTel.d.ts.map +1 -1
  89. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +2 -0
  90. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
  91. package/dist-types/components/listbox/src/LionListbox.d.ts +2 -0
  92. package/dist-types/components/listbox/src/LionListbox.d.ts.map +1 -1
  93. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  94. package/dist-types/components/overlays/src/OverlayMixin.d.ts +4 -0
  95. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  96. package/dist-types/components/overlays/src/configurations/withDropdownConfig.d.ts.map +1 -1
  97. package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts.map +1 -1
  98. package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
  99. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
  100. package/dist-types/components/progress-indicator/src/LionProgressIndicator.d.ts +3 -0
  101. package/dist-types/components/progress-indicator/src/LionProgressIndicator.d.ts.map +1 -1
  102. package/dist-types/components/radio-group/src/LionRadioGroup.d.ts +3 -1
  103. package/dist-types/components/radio-group/src/LionRadioGroup.d.ts.map +1 -1
  104. package/dist-types/components/select/src/LionSelect.d.ts +3 -0
  105. package/dist-types/components/select/src/LionSelect.d.ts.map +1 -1
  106. package/dist-types/components/select-rich/src/LionSelectInvoker.d.ts +2 -0
  107. package/dist-types/components/select-rich/src/LionSelectInvoker.d.ts.map +1 -1
  108. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +3 -1
  109. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  110. package/dist-types/components/steps/src/LionStep.d.ts +0 -1
  111. package/dist-types/components/steps/src/LionStep.d.ts.map +1 -1
  112. package/dist-types/components/steps/src/LionSteps.d.ts +0 -1
  113. package/dist-types/components/steps/src/LionSteps.d.ts.map +1 -1
  114. package/dist-types/components/switch/src/LionSwitch.d.ts +3 -0
  115. package/dist-types/components/switch/src/LionSwitch.d.ts.map +1 -1
  116. package/dist-types/components/switch/src/LionSwitchButton.d.ts +5 -0
  117. package/dist-types/components/switch/src/LionSwitchButton.d.ts.map +1 -1
  118. package/dist-types/components/tabs/src/LionTabs.d.ts +8 -0
  119. package/dist-types/components/tabs/src/LionTabs.d.ts.map +1 -1
  120. package/dist-types/tsconfig.tsbuildinfo +1 -1
  121. package/docs/components/accordion/api-table.md +8 -0
  122. package/docs/components/button/api-table.md +0 -3
  123. package/docs/components/checkbox-group/api-table.md +6 -6
  124. package/docs/components/checkbox-group/overview.md +2 -2
  125. package/docs/components/collapsible/api-table.md +7 -0
  126. package/docs/components/collapsible/overview.md +1 -1
  127. package/docs/components/combobox/api-table.md +9 -2
  128. package/docs/components/combobox/overview.md +1 -1
  129. package/docs/components/combobox/use-cases.md +90 -1
  130. package/docs/components/dialog/overview.md +1 -1
  131. package/docs/components/drawer/api-table.md +8 -0
  132. package/docs/components/fieldset/api-table.md +2 -2
  133. package/docs/components/fieldset/overview.md +5 -5
  134. package/docs/components/form/overview.md +3 -3
  135. package/docs/components/form/use-cases.md +2 -2
  136. package/docs/components/icon/use-cases.md +1 -1
  137. package/docs/components/input/api-table.md +2 -2
  138. package/docs/components/input/overview.md +1 -1
  139. package/docs/components/input-amount/api-table.md +2 -2
  140. package/docs/components/input-amount/overview.md +3 -3
  141. package/docs/components/input-date/api-table.md +2 -2
  142. package/docs/components/input-date/overview.md +3 -3
  143. package/docs/components/input-datepicker/api-table.md +2 -2
  144. package/docs/components/input-datepicker/overview.md +5 -5
  145. package/docs/components/input-email/api-table.md +2 -2
  146. package/docs/components/input-email/overview.md +2 -2
  147. package/docs/components/input-file/api-table.md +9 -2
  148. package/docs/components/input-file/overview.md +1 -1
  149. package/docs/components/input-iban/api-table.md +2 -2
  150. package/docs/components/input-iban/overview.md +2 -2
  151. package/docs/components/input-range/api-table.md +2 -2
  152. package/docs/components/input-range/overview.md +2 -2
  153. package/docs/components/input-stepper/api-table.md +2 -2
  154. package/docs/components/input-stepper/overview.md +1 -1
  155. package/docs/components/input-tel/api-table.md +2 -2
  156. package/docs/components/input-tel/overview.md +1 -1
  157. package/docs/components/input-tel/use-cases.md +6 -6
  158. package/docs/components/input-tel-dropdown/api-table.md +2 -2
  159. package/docs/components/input-tel-dropdown/overview.md +1 -1
  160. package/docs/components/listbox/api-table.md +2 -2
  161. package/docs/components/listbox/use-cases.md +1 -1
  162. package/docs/components/radio-group/api-table.md +4 -4
  163. package/docs/components/radio-group/overview.md +3 -3
  164. package/docs/components/select/api-table.md +2 -2
  165. package/docs/components/select/use-cases.md +1 -1
  166. package/docs/components/select-rich/api-table.md +2 -3
  167. package/docs/components/select-rich/use-cases.md +1 -1
  168. package/docs/components/switch/api-table.md +2 -2
  169. package/docs/components/tabs/api-table.md +7 -0
  170. package/docs/components/tabs/examples.md +1 -1
  171. package/docs/components/textarea/api-table.md +2 -2
  172. package/docs/fundamentals/index.md +4 -4
  173. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  174. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  175. package/docs/fundamentals/rationales/accessibility.md +1 -1
  176. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  177. package/docs/fundamentals/systems/core/overview.md +1 -1
  178. package/docs/fundamentals/systems/form/api-table.md +2 -2
  179. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  180. package/docs/fundamentals/systems/form/model-value.md +1 -1
  181. package/docs/fundamentals/systems/form/overview.md +28 -28
  182. package/docs/fundamentals/systems/form/styling.md +3 -20
  183. package/docs/fundamentals/systems/form/validate.md +19 -3
  184. package/docs/fundamentals/systems/localize/overview.md +4 -4
  185. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  186. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  187. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  188. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  189. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  190. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  191. package/docs/guides/how-to/create-a-form.md +4 -4
  192. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  193. package/docs/guides/index.md +8 -8
  194. package/docs/guides/principles/index.md +5 -5
  195. package/docs/guides/principles/styling.md +1 -1
  196. package/package.json +3 -3
@@ -24,6 +24,16 @@ import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
24
24
  * This Mixin is a shared fundament for all form components, it's applied on:
25
25
  * - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
26
26
  * - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
27
+ *
28
+ * @slot label - The label for the form control
29
+ * @slot help-text - The help text for the form control
30
+ * @slot input - The input element for the form control, e.g. <input>, <textarea>, <select>
31
+ * @slot feedback - The validation feedback for the form control
32
+ * @slot prefix - The prefix for the form control, e.g. currency symbol
33
+ * @slot suffix - The suffix for the form control, e.g. currency symbol
34
+ * @slot before - The before element for the form control, e.g. a label
35
+ * @slot after - The after element for the form control, e.g. a label
36
+ *
27
37
  * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
28
38
  * @type {FormControlMixin}
29
39
  */
@@ -52,7 +62,7 @@ const FormControlMixinImplementation = superclass =>
52
62
  * @type {string}
53
63
  */
54
64
  get label() {
55
- return this.__label || (this._labelNode && this._labelNode.textContent) || '';
65
+ return this.__label ?? (this._labelNode?.textContent || '');
56
66
  }
57
67
 
58
68
  /**
@@ -71,7 +81,7 @@ const FormControlMixinImplementation = superclass =>
71
81
  * @type {string}
72
82
  */
73
83
  get helpText() {
74
- return this.__helpText || (this._helpTextNode && this._helpTextNode.textContent) || '';
84
+ return this.__helpText ?? (this._helpTextNode?.textContent || '');
75
85
  }
76
86
 
77
87
  /**
@@ -158,6 +168,21 @@ const FormControlMixinImplementation = superclass =>
158
168
  */
159
169
  static enabledWarnings = super.enabledWarnings?.filter(w => w !== 'change-in-update') || [];
160
170
 
171
+ // N.B. add these label/helpText props for types + ce manifest output
172
+ // (explicity setting them to undefined does not have desired result)
173
+
174
+ /**
175
+ * The label text for the input node.
176
+ * When no value is defined, textContent of [slot=label] will be used
177
+ * @property {string} label
178
+ */
179
+
180
+ /**
181
+ * The helpt text for the input node.
182
+ * When no value is defined, textContent of [slot=help-text] will be used
183
+ * @property {string} helpText
184
+ */
185
+
161
186
  constructor() {
162
187
  super();
163
188
 
@@ -171,26 +196,12 @@ const FormControlMixinImplementation = superclass =>
171
196
  */
172
197
  this.readOnly = false;
173
198
 
174
- /**
175
- * The label text for the input node.
176
- * When no value is defined, textContent of [slot=label] will be used
177
- * @type {string}
178
- */
179
- this.label = '';
180
-
181
199
  /**
182
200
  * The label will only be visible for srceen readers when true
183
201
  * @type {boolean}
184
202
  */
185
203
  this.labelSrOnly = false;
186
204
 
187
- /**
188
- * The helpt text for the input node.
189
- * When no value is defined, textContent of [slot=help-text] will be used
190
- * @type {string}
191
- */
192
- this.helpText = '';
193
-
194
205
  /**
195
206
  * The model value is the result of the parser function(when available).
196
207
  * It should be considered as the internal value used for validation and reasoning/logic.
@@ -293,11 +304,15 @@ const FormControlMixinImplementation = superclass =>
293
304
  );
294
305
  }
295
306
 
296
- if (changedProperties.has('label') && this.__label && this._labelNode) {
307
+ if (changedProperties.has('label') && this.__label !== undefined && this._labelNode) {
297
308
  this._labelNode.textContent = this.label;
298
309
  }
299
310
 
300
- if (changedProperties.has('helpText') && this.__helpText && this._helpTextNode) {
311
+ if (
312
+ changedProperties.has('helpText') &&
313
+ this.__helpText !== undefined &&
314
+ this._helpTextNode
315
+ ) {
301
316
  this._helpTextNode.textContent = this.helpText;
302
317
  }
303
318
 
@@ -25,7 +25,7 @@ import { Unparseable } from './validate/Unparseable.js';
25
25
  // formattedValue condition needs to be evaluated right before syncing back to the view
26
26
 
27
27
  /**
28
- * @desc Designed to be applied on top of a LionField.
28
+ * Designed to be applied on top of a LionField.
29
29
  * To understand all concepts within the Mixin, please consult the flow diagram in the
30
30
  * documentation.
31
31
  *
@@ -48,6 +48,14 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
48
48
  ];
49
49
  }
50
50
 
51
+ constructor() {
52
+ super();
53
+ /**
54
+ * @type {FeedbackMessage[] | undefined}
55
+ */
56
+ this.feedbackData = undefined;
57
+ }
58
+
51
59
  /**
52
60
  * @overridable
53
61
  * @param {Object} opts
@@ -69,16 +77,6 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
69
77
  if (this.feedbackData && this.feedbackData[0]) {
70
78
  this.setAttribute('type', this.feedbackData[0].type);
71
79
  this.currentType = this.feedbackData[0].type;
72
- window.clearTimeout(this.removeMessage);
73
- // TODO: this logic should be in ValidateMixin, so that [show-feedback-for] is in sync,
74
- // plus duration should be configurable
75
- if (this.currentType === 'success') {
76
- this.removeMessage = window.setTimeout(() => {
77
- this.removeAttribute('type');
78
- /** @type {FeedbackMessage[]} */
79
- this.feedbackData = [];
80
- }, 3000);
81
- }
82
80
  } else if (this.currentType !== 'success') {
83
81
  this.removeAttribute('type');
84
82
  }
@@ -420,6 +420,22 @@ export const ValidateMixinImplementation = superclass =>
420
420
  await this.__executeValidators();
421
421
  }
422
422
 
423
+ /**
424
+ * @param {Validator} validator
425
+ * @returns {boolean}
426
+ */
427
+ #isInstanceOfRequiredValidator(validator) {
428
+ // walk prototype and check if validatorName is 'Required'
429
+ let validatorLvlInPrototype = validator;
430
+ while (validatorLvlInPrototype) {
431
+ const ctor = /** @type {typeof Validator} */ (validatorLvlInPrototype.constructor);
432
+ if (ctor.validatorName === 'Required') return true;
433
+
434
+ validatorLvlInPrototype = Object.getPrototypeOf(validatorLvlInPrototype);
435
+ }
436
+ return false;
437
+ }
438
+
423
439
  /**
424
440
  * @desc step a1-3 + b (as explained in `validate()`)
425
441
  */
@@ -482,7 +498,7 @@ export const ValidateMixinImplementation = superclass =>
482
498
  for (const v of this._allValidators) {
483
499
  if (/** @type {MetaValidator} */ (v)?.executeOnResults) {
484
500
  metaValidators.push(/** @type {MetaValidator} */ (v));
485
- } else if (/** @type {typeof Validator} */ (v.constructor)?.validatorName === 'Required') {
501
+ } else if (this.#isInstanceOfRequiredValidator(v)) {
486
502
  // Required validator was already handled
487
503
  } else if (/** @type {typeof Validator} */ (v.constructor).async) {
488
504
  asyncValidators.push(v);
@@ -574,7 +590,7 @@ export const ValidateMixinImplementation = superclass =>
574
590
  return [];
575
591
  }
576
592
 
577
- // If empty, do not show the ResulValidation message (e.g. Correct!)
593
+ // If empty, do not show the ResultValidation message (e.g. Correct!)
578
594
  if (this._isEmpty(this.modelValue)) {
579
595
  this.__prevShownValidationResult = [];
580
596
  return [];
@@ -603,7 +619,7 @@ export const ValidateMixinImplementation = superclass =>
603
619
  * - on sync validation
604
620
  * - on async validation (can depend on server response)
605
621
  *
606
- * This method inishes a pass by adding the properties to the instance:
622
+ * This method finishes a pass by adding the properties to the instance:
607
623
  * - validationStates
608
624
  * - hasFeedbackFor
609
625
  *
@@ -747,6 +763,7 @@ export const ValidateMixinImplementation = superclass =>
747
763
  * @property {string} type will be 'error' for messages from default Validators. Could be
748
764
  * 'warning', 'info' etc. for Validators with custom types. Needed as a directive for
749
765
  * feedbackNode how to render a message of a certain type
766
+ * @property {number} visibilityDuration duration in ms for how long the message should be shown
750
767
  * @property {Validator} [validator] when the message is directly coupled to a Validator
751
768
  * (in most cases), this property is filled. When a message is not coupled to a Validator
752
769
  * (in case of success feedback which is based on a diff or current and previous validation
@@ -772,7 +789,12 @@ export const ValidateMixinImplementation = superclass =>
772
789
  fieldName,
773
790
  outcome,
774
791
  });
775
- return { message, type: validator.type, validator };
792
+ return {
793
+ message,
794
+ type: validator.type,
795
+ validator,
796
+ visibilityDuration: validator.config?.visibilityDuration || 3000,
797
+ };
776
798
  }),
777
799
  );
778
800
  }
@@ -793,6 +815,8 @@ export const ValidateMixinImplementation = superclass =>
793
815
  * @protected
794
816
  */
795
817
  _updateFeedbackComponent() {
818
+ window.clearTimeout(this.removeMessage);
819
+
796
820
  const { _feedbackNode } = this;
797
821
  if (!_feedbackNode) {
798
822
  return;
@@ -824,6 +848,18 @@ export const ValidateMixinImplementation = superclass =>
824
848
 
825
849
  const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
826
850
  _feedbackNode.feedbackData = messageMap || [];
851
+
852
+ if (
853
+ messageMap?.[0] &&
854
+ messageMap[0].type === 'success' &&
855
+ messageMap[0].visibilityDuration !== Infinity
856
+ ) {
857
+ this.removeMessage = window.setTimeout(() => {
858
+ _feedbackNode.removeAttribute('type');
859
+ /** @type {FeedbackMessage[]} */
860
+ _feedbackNode.feedbackData = [];
861
+ }, messageMap[0].visibilityDuration);
862
+ }
827
863
  });
828
864
  } else {
829
865
  this.__feedbackQueue.add(async () => {
@@ -86,6 +86,40 @@ describe('FormControlMixin', () => {
86
86
  expect(el.label).to.equal('');
87
87
  });
88
88
 
89
+ it('supports empty string as label', async () => {
90
+ const el = /** @type {FormControlMixinClass} */ (
91
+ await fixture(html`
92
+ <${tag} label="With Value">${inputSlot}</${tag}>
93
+ `)
94
+ );
95
+
96
+ expect(el.label).to.equal('With Value');
97
+ // @ts-expect-error
98
+ expect(el._labelNode.innerHTML).to.equal('With Value');
99
+
100
+ el.label = '';
101
+ await el.updateComplete;
102
+
103
+ expect(el.label).to.equal('');
104
+ // @ts-expect-error
105
+ expect(el._labelNode.innerHTML).to.equal('');
106
+
107
+ // Reset
108
+ el.label = 'With Value';
109
+ await el.updateComplete;
110
+
111
+ expect(el.label).to.equal('With Value');
112
+ // @ts-expect-error
113
+ expect(el._labelNode.innerHTML).to.equal('With Value');
114
+
115
+ el.setAttribute('label', '');
116
+ await el.updateComplete;
117
+
118
+ expect(el.label).to.equal('');
119
+ // @ts-expect-error
120
+ expect(el._labelNode.innerHTML).to.equal('');
121
+ });
122
+
89
123
  /**
90
124
  * N.B. For platform controls, the same would be achieved with <input aria-label="My label">
91
125
  * However, since FormControl is usually not the activeElement (_inputNode is), this
@@ -176,6 +210,40 @@ describe('FormControlMixin', () => {
176
210
  );
177
211
  expect(el.helpText).to.equal('');
178
212
  });
213
+
214
+ it('supports empty string as help-text', async () => {
215
+ const el = /** @type {FormControlMixinClass} */ (
216
+ await fixture(html`
217
+ <${tag} help-text="With Value">${inputSlot}</${tag}>
218
+ `)
219
+ );
220
+
221
+ expect(el.helpText).to.equal('With Value');
222
+ // @ts-expect-error
223
+ expect(el._helpTextNode.innerHTML).to.equal('With Value');
224
+
225
+ el.helpText = '';
226
+ await el.updateComplete;
227
+
228
+ expect(el.helpText).to.equal('');
229
+ // @ts-expect-error
230
+ expect(el._helpTextNode.innerHTML).to.equal('');
231
+
232
+ // Reset
233
+ el.helpText = 'With Value';
234
+ await el.updateComplete;
235
+
236
+ expect(el.helpText).to.equal('With Value');
237
+ // @ts-expect-error
238
+ expect(el._helpTextNode.innerHTML).to.equal('With Value');
239
+
240
+ el.setAttribute('help-text', '');
241
+ await el.updateComplete;
242
+
243
+ expect(el.helpText).to.equal('');
244
+ // @ts-expect-error
245
+ expect(el._helpTextNode.innerHTML).to.equal('');
246
+ });
179
247
  });
180
248
 
181
249
  describe('Accessibility', () => {
@@ -33,28 +33,6 @@ describe('lion-validation-feedback', () => {
33
33
  expect(el.getAttribute('type')).to.equal('warning');
34
34
  });
35
35
 
36
- it('success message clears after 3s', async () => {
37
- const el = /** @type {LionValidationFeedback} */ (
38
- await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
39
- );
40
-
41
- const clock = sinon.useFakeTimers();
42
-
43
- el.feedbackData = [{ message: 'hello', type: 'success', validator: new AlwaysValid() }];
44
- await el.updateComplete;
45
- expect(el.getAttribute('type')).to.equal('success');
46
-
47
- clock.tick(2900);
48
-
49
- expect(el.getAttribute('type')).to.equal('success');
50
-
51
- clock.tick(200);
52
-
53
- expect(el).to.not.have.attribute('type');
54
-
55
- clock.restore();
56
- });
57
-
58
36
  it('does not clear error messages', async () => {
59
37
  const el = /** @type {LionValidationFeedback} */ (
60
38
  await fixture(html`<lion-validation-feedback></lion-validation-feedback>`)
@@ -130,6 +130,7 @@ export function runValidateMixinSuite(customConfig) {
130
130
  expect(el.hasFeedbackFor).to.deep.equal(['error']);
131
131
  });
132
132
 
133
+ // TODO: keep this use case for backwards compatibility or just required extending Required?
133
134
  it('determines whether the "Required" validator was already handled by judging the validatorName', async () => {
134
135
  class BundledValidator extends EventTarget {
135
136
  static ['_$isValidator$'] = true;
@@ -163,9 +164,7 @@ export function runValidateMixinSuite(customConfig) {
163
164
  }
164
165
 
165
166
  class BundledRequired extends BundledValidator {
166
- static get validatorName() {
167
- return 'Required';
168
- }
167
+ static validatorName = 'Required';
169
168
  }
170
169
 
171
170
  const el = /** @type {ValidateElement} */ (
@@ -180,6 +179,23 @@ export function runValidateMixinSuite(customConfig) {
180
179
  expect(el.hasFeedbackFor).to.deep.equal([]);
181
180
  });
182
181
 
182
+ it('determines whether the "Required" validator was already handled by judging the validatorName', async () => {
183
+ class MyRequired extends Required {
184
+ static validatorName = 'SomethingOtherThanRequired';
185
+ }
186
+
187
+ const el = /** @type {ValidateElement} */ (
188
+ await fixture(html`
189
+ <${tag}
190
+ .validators=${[new MyRequired()]}
191
+ .modelValue=${'myValue'}
192
+ >${lightDom}</${tag}>
193
+ `)
194
+ );
195
+
196
+ expect(el.hasFeedbackFor).to.deep.equal([]);
197
+ });
198
+
183
199
  it('determines whether the passed Validators are ResultValidators judging by the presence of "executeOnResults"', async () => {
184
200
  class ValidateElementWithSuccessType extends ValidateElement {
185
201
  static get validationTypes() {
@@ -349,6 +349,125 @@ export function runValidateMixinFeedbackPart() {
349
349
  expect(_feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
350
350
  });
351
351
 
352
+ it('shows success message and clears after 3s', async () => {
353
+ class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
354
+ static get validationTypes() {
355
+ return ['error', 'success'];
356
+ }
357
+ }
358
+ const clock = sinon.useFakeTimers();
359
+ const elTagString = defineCE(ValidateElementCustomTypes);
360
+ const elTag = unsafeStatic(elTagString);
361
+ const el = /** @type {ValidateElementCustomTypes} */ (
362
+ await fixture(html`
363
+ <${elTag}
364
+ .submitted=${true}
365
+ .validators=${[
366
+ new MinLength(3),
367
+ new DefaultSuccess(null, { getMessage: () => 'This is a success message' }),
368
+ ]}
369
+ >${lightDom}</${elTag}>
370
+ `)
371
+ );
372
+ const { _feedbackNode } = getFormControlMembers(el);
373
+
374
+ el.modelValue = 'a';
375
+ await el.updateComplete;
376
+ await el.feedbackComplete;
377
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
378
+
379
+ el.modelValue = 'abcd';
380
+ await el.updateComplete;
381
+ await el.feedbackComplete;
382
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
383
+ clock.tick(2900);
384
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
385
+ clock.tick(200);
386
+ expect(_feedbackNode.feedbackData).to.be.empty;
387
+ });
388
+
389
+ it('shows success message and clears after configured time', async () => {
390
+ class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
391
+ static get validationTypes() {
392
+ return ['error', 'success'];
393
+ }
394
+ }
395
+ const clock = sinon.useFakeTimers();
396
+ const elTagString = defineCE(ValidateElementCustomTypes);
397
+ const elTag = unsafeStatic(elTagString);
398
+ const el = /** @type {ValidateElementCustomTypes} */ (
399
+ await fixture(html`
400
+ <${elTag}
401
+ .submitted=${true}
402
+ .validators=${[
403
+ new MinLength(3),
404
+ new DefaultSuccess(null, {
405
+ getMessage: () => 'This is a success message',
406
+ visibilityDuration: 6000,
407
+ }),
408
+ ]}
409
+ >${lightDom}</${elTag}>
410
+ `)
411
+ );
412
+ const { _feedbackNode } = getFormControlMembers(el);
413
+
414
+ el.modelValue = 'a';
415
+ await el.updateComplete;
416
+ await el.feedbackComplete;
417
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
418
+
419
+ el.modelValue = 'abcd';
420
+ await el.updateComplete;
421
+ await el.feedbackComplete;
422
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
423
+ clock.tick(5900);
424
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
425
+ clock.tick(200);
426
+ expect(_feedbackNode.feedbackData).to.be.empty;
427
+ });
428
+
429
+ it('shows success message and stays persistent', async () => {
430
+ class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
431
+ static get validationTypes() {
432
+ return ['error', 'success'];
433
+ }
434
+ }
435
+ const clock = sinon.useFakeTimers();
436
+ const elTagString = defineCE(ValidateElementCustomTypes);
437
+ const elTag = unsafeStatic(elTagString);
438
+ const el = /** @type {ValidateElementCustomTypes} */ (
439
+ await fixture(html`
440
+ <${elTag}
441
+ .submitted=${true}
442
+ .validators=${[
443
+ new MinLength(3),
444
+ new DefaultSuccess(null, {
445
+ getMessage: () => 'This is a success message',
446
+ visibilityDuration: Infinity,
447
+ }),
448
+ ]}
449
+ >${lightDom}</${elTag}>
450
+ `)
451
+ );
452
+ const { _feedbackNode } = getFormControlMembers(el);
453
+
454
+ el.modelValue = 'a';
455
+ await el.updateComplete;
456
+ await el.feedbackComplete;
457
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
458
+
459
+ el.modelValue = 'abcd';
460
+ await el.updateComplete;
461
+ await el.feedbackComplete;
462
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
463
+ clock.tick(2900);
464
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
465
+ clock.tick(200);
466
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
467
+ clock.tick(20000);
468
+ expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
469
+ });
470
+
352
471
  it('shows success message after fixing an error', async () => {
353
472
  class ValidateElementCustomTypes extends ValidateMixin(LitElement) {
354
473
  static get validationTypes() {
@@ -31,6 +31,7 @@ export type ValidatorConfig = {
31
31
  type?: ValidationType;
32
32
  node?: FormControlHost;
33
33
  fieldName?: string | Promise<string>;
34
+ visibilityDuration?: number;
34
35
  };
35
36
 
36
37
  /**
@@ -29,6 +29,8 @@ function validateSvg(svg) {
29
29
 
30
30
  /**
31
31
  * Custom element for rendering SVG icons
32
+ *
33
+ * @customElement lion-icon
32
34
  */
33
35
  export class LionIcon extends LitElement {
34
36
  static get properties() {
@@ -31,6 +31,15 @@ function formatBytes(bytes, decimals = 2) {
31
31
  return `${parseFloat((bytes / k ** i).toFixed(dm))}${sizes[i]}`;
32
32
  }
33
33
 
34
+ /**
35
+ * LionInputFile: A file input component
36
+ * This component allows users to select files, displays selected files, and handles file uploads.
37
+ *
38
+ * @slot file-select-button - [private] The button to open the file dialog
39
+ * @slot selected-file-list - [private] The list of selected files
40
+ *
41
+ * @customElement lion-input-file
42
+ */
34
43
  export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField)) {
35
44
  static get scopedElements() {
36
45
  return {
@@ -18,6 +18,9 @@ import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
18
18
  * @typedef {FormatOptions & {regionCode: RegionCode; formatStrategy: PhoneNumberFormat; formatCountryCodeStyle: string;}} FormatOptionsTel
19
19
  */
20
20
 
21
+ /**
22
+ * @customElement lion-input-tel
23
+ */
21
24
  export class LionInputTel extends LocalizeMixin(LionInput) {
22
25
  /**
23
26
  * @configure LitElement
@@ -40,6 +40,8 @@ import { regionCodeToLocale } from './regionCodeToLocale.js';
40
40
  * possible. Also, it doesn't need to be a `FormControl`, because it's purely a helper element
41
41
  * to provide better UX: the modelValue (the text field) contains all needed info, since it's in
42
42
  * `e164` format that contains all info (both region code and national phone number).
43
+ *
44
+ * @customElement lion-input-tel-dropdown
43
45
  */
44
46
  export class LionInputTelDropdown extends LionInputTel {
45
47
  refs = {
@@ -7,6 +7,8 @@ import { ListboxMixin } from './ListboxMixin.js';
7
7
  /**
8
8
  * LionListbox: implements the wai-aria listbox design pattern and integrates it as a Lion
9
9
  * FormControl
10
+ *
11
+ * @customElement lion-listbox
10
12
  */
11
13
  export class LionListbox extends ListboxMixin(
12
14
  FocusMixin(InteractionStateMixin(ValidateMixin(LitElement))),
@@ -528,8 +528,6 @@ export class OverlayController extends EventTarget {
528
528
  this.__contentHasBeenInitialized = true;
529
529
  }
530
530
 
531
- this.__wrappingDialogNode?.addEventListener('cancel', this.__cancelHandler);
532
-
533
531
  // Reset all positioning styles (local, c.q. Popper) and classes (global)
534
532
  this.contentWrapperNode.removeAttribute('style');
535
533
  this.contentWrapperNode.removeAttribute('class');
@@ -618,8 +616,9 @@ export class OverlayController extends EventTarget {
618
616
  contentWrapperNodeL2: this.contentWrapperNode,
619
617
  contentNodeL3: this.contentNode,
620
618
  });
621
- // @ts-ignore
619
+ // @ts-expect-error
622
620
  wrappingDialogElement.open = true;
621
+
623
622
  if (this.isTooltip) {
624
623
  // needed to prevent tooltip getting focus in Safari and Firefox
625
624
  wrappingDialogElement.setAttribute('tabindex', '-1');
@@ -633,6 +632,35 @@ export class OverlayController extends EventTarget {
633
632
  // computed height of 0...
634
633
  this.contentNode.style.position = 'static';
635
634
  }
635
+
636
+ // Here we prevent any interference of the native <dialog> element with the keyboard behavior
637
+ // as defined by the OverlayController. This is needed until we can configure `closedby="none"`
638
+ // on the native dialog for all browsers: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
639
+ const hasClosedBySupport = HTMLDialogElement && 'closedBy' in HTMLDialogElement.prototype;
640
+ if (hasClosedBySupport) {
641
+ // @ts-expect-error
642
+ wrappingDialogElement.closedBy = 'none';
643
+ } else {
644
+ wrappingDialogElement.addEventListener(
645
+ 'keydown',
646
+ (/** @type {* & KeyboardEvent} */ event) => {
647
+ if (event.key === 'Escape') {
648
+ event.preventDefault();
649
+ }
650
+ },
651
+ );
652
+ wrappingDialogElement.addEventListener('keyup', (/** @type {* & KeyboardEvent} */ event) => {
653
+ if (event.key === 'Escape') {
654
+ event.preventDefault();
655
+ }
656
+ });
657
+ wrappingDialogElement.addEventListener('cancel', event => {
658
+ event.stopPropagation();
659
+ });
660
+ wrappingDialogElement.addEventListener('close', event => {
661
+ event.stopPropagation();
662
+ });
663
+ }
636
664
  }
637
665
 
638
666
  /**
@@ -1188,6 +1216,7 @@ export class OverlayController extends EventTarget {
1188
1216
  const hasPressedInside =
1189
1217
  event.composedPath().includes(this.contentNode) ||
1190
1218
  deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1219
+
1191
1220
  if (hasPressedInside) {
1192
1221
  this.hide();
1193
1222
  // We could do event.stopPropagation() here, but we don't want to hide info for
@@ -1384,8 +1413,6 @@ export class OverlayController extends EventTarget {
1384
1413
  teardown() {
1385
1414
  this.__handleOverlayStyles({ phase: 'teardown' });
1386
1415
  this._handleFeatures({ phase: 'teardown' });
1387
- this.__wrappingDialogNode?.removeEventListener('cancel', this.__cancelHandler);
1388
-
1389
1416
  if (this.#isRegisteredOnManager()) {
1390
1417
  this.manager.remove(this);
1391
1418
  }
@@ -9,6 +9,10 @@ import { isEqualConfig } from './utils/is-equal-config.js';
9
9
  */
10
10
 
11
11
  /**
12
+ * @slot backdrop - The backdrop element for the overlay
13
+ * @slot content - The content element for the overlay
14
+ * @slot invoker - The invoker element for the overlay
15
+ *
12
16
  * @type {OverlayMixin}
13
17
  * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
14
18
  */
@@ -179,6 +183,7 @@ export const OverlayMixinImplementation = superclass =>
179
183
  super.connectedCallback();
180
184
 
181
185
  this.updateComplete.then(() => {
186
+ if (!this.isConnected) return;
182
187
  if (this.#hasSetup) return;
183
188
  this._setupOverlayCtrl();
184
189
  this.#hasSetup = true;