@lion/ui 0.9.1 → 0.10.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 (172) hide show
  1. package/components/button/src/LionButton.js +5 -1
  2. package/components/button/test-suites/LionButton.suite.js +1 -1
  3. package/components/calendar/src/LionCalendar.js +5 -14
  4. package/components/calendar/test/lion-calendar.test.js +88 -0
  5. package/components/calendar/translations/bg.js +5 -0
  6. package/components/calendar/translations/cs.js +5 -0
  7. package/components/calendar/translations/de.js +5 -0
  8. package/components/calendar/translations/en.js +5 -0
  9. package/components/calendar/translations/es.js +5 -0
  10. package/components/calendar/translations/fr.js +5 -0
  11. package/components/calendar/translations/hu.js +5 -0
  12. package/components/calendar/translations/it.js +5 -0
  13. package/components/calendar/translations/nl.js +5 -0
  14. package/components/calendar/translations/pl.js +5 -0
  15. package/components/calendar/translations/ro.js +5 -0
  16. package/components/calendar/translations/ru.js +5 -0
  17. package/components/calendar/translations/sk.js +5 -0
  18. package/components/calendar/translations/uk.js +3 -0
  19. package/components/calendar/translations/zh.js +3 -0
  20. package/components/combobox/test/lion-combobox.test.js +2 -0
  21. package/components/core/src/SlotMixin.js +15 -0
  22. package/components/core/test/ScopedElementsMixin.test.js +3 -3
  23. package/components/core/test/SlotMixin.test.js +50 -0
  24. package/components/dialog/src/LionDialog.js +13 -0
  25. package/components/dialog/test/lion-dialog.test.js +110 -0
  26. package/components/dialog/test-helpers/test-router.js +43 -0
  27. package/components/form-core/src/FormatMixin.js +66 -18
  28. package/components/form-core/src/choice-group/ChoiceInputMixin.js +3 -1
  29. package/components/form-core/test-suites/FormatMixin.suite.js +107 -8
  30. package/components/form-core/types/FormatMixinTypes.ts +4 -1
  31. package/components/form-integrations/test/dialog-integrations.test.js +6 -2
  32. package/components/input-amount/src/parsers.js +3 -3
  33. package/components/input-amount/test/lion-input-amount.test.js +36 -2
  34. package/components/input-amount/test/parsers.test.js +11 -0
  35. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +1 -1
  36. package/components/localize/src/number/getDecimalSeparator.js +1 -1
  37. package/components/localize/src/number/parseNumber.js +9 -6
  38. package/components/localize/test/number/parseNumber.test.js +11 -0
  39. package/components/localize/types/LocalizeMixinTypes.ts +3 -3
  40. package/components/overlays/src/OverlayController.js +21 -6
  41. package/components/overlays/src/OverlayMixin.js +20 -14
  42. package/components/overlays/test/OverlayController.test.js +40 -8
  43. package/components/overlays/test-suites/OverlayMixin.suite.js +184 -9
  44. package/components/overlays/types/ArrowMixinTypes.ts +11 -2
  45. package/components/overlays/types/OverlayConfig.ts +19 -15
  46. package/components/select-rich/src/LionSelectRich.js +3 -1
  47. package/components/select-rich/test/lion-select-rich.test.js +44 -0
  48. package/custom-elements.json +1340 -89
  49. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  50. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  51. package/dist-types/components/calendar/translations/bg.d.ts +3 -0
  52. package/dist-types/components/calendar/translations/cs.d.ts +3 -0
  53. package/dist-types/components/calendar/translations/de.d.ts +3 -0
  54. package/dist-types/components/calendar/translations/en.d.ts +3 -0
  55. package/dist-types/components/calendar/translations/es.d.ts +3 -0
  56. package/dist-types/components/calendar/translations/fr.d.ts +3 -0
  57. package/dist-types/components/calendar/translations/hu.d.ts +3 -0
  58. package/dist-types/components/calendar/translations/it.d.ts +3 -0
  59. package/dist-types/components/calendar/translations/nl.d.ts +3 -0
  60. package/dist-types/components/calendar/translations/pl.d.ts +3 -0
  61. package/dist-types/components/calendar/translations/ro.d.ts +3 -0
  62. package/dist-types/components/calendar/translations/ru.d.ts +3 -0
  63. package/dist-types/components/calendar/translations/sk.d.ts +3 -0
  64. package/dist-types/components/calendar/translations/uk.d.ts +3 -0
  65. package/dist-types/components/calendar/translations/zh.d.ts +3 -0
  66. package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
  67. package/dist-types/components/dialog/src/LionDialog.d.ts +32 -0
  68. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  69. package/dist-types/components/dialog/test/lion-dialog.test.d.ts.map +1 -1
  70. package/dist-types/components/dialog/test-helpers/test-router.d.ts +2 -0
  71. package/dist-types/components/dialog/test-helpers/test-router.d.ts.map +1 -0
  72. package/dist-types/components/form-core/src/FormatMixin.d.ts +1 -1
  73. package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
  74. package/dist-types/components/form-core/src/choice-group/ChoiceInputMixin.d.ts.map +1 -1
  75. package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
  76. package/dist-types/components/form-core/types/FormatMixinTypes.d.ts +3 -2
  77. package/dist-types/components/form-core/types/FormatMixinTypes.d.ts.map +1 -1
  78. package/dist-types/components/form-integrations/test/dialog-integrations.test.d.ts.map +1 -1
  79. package/dist-types/components/input-amount/src/parsers.d.ts +2 -2
  80. package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
  81. package/dist-types/components/input-amount/test/lion-input-amount.test.d.ts.map +1 -1
  82. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -1
  83. package/dist-types/components/localize/src/number/parseNumber.d.ts.map +1 -1
  84. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +3 -2
  85. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
  86. package/dist-types/components/overlays/src/OverlayController.d.ts +5 -0
  87. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  88. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  89. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts +4 -4
  90. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
  91. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts +2 -2
  92. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
  93. package/dist-types/components/overlays/types/OverlayConfig.d.ts +8 -6
  94. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  95. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +2 -1
  96. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  97. package/dist-types/components/select-rich/test/lion-select-rich.test.d.ts.map +1 -1
  98. package/dist-types/tsconfig.tsbuildinfo +1 -1
  99. package/docs/components/checkbox-group/api-table.md +2 -2
  100. package/docs/components/checkbox-group/overview.md +2 -2
  101. package/docs/components/collapsible/overview.md +1 -1
  102. package/docs/components/combobox/api-table.md +1 -0
  103. package/docs/components/combobox/overview.md +1 -1
  104. package/docs/components/combobox/use-cases.md +1 -1
  105. package/docs/components/dialog/api-table.md +6 -3
  106. package/docs/components/dialog/overview.md +1 -1
  107. package/docs/components/dialog/use-cases.md +32 -1
  108. package/docs/components/fieldset/overview.md +5 -5
  109. package/docs/components/form/overview.md +3 -3
  110. package/docs/components/form/use-cases.md +2 -2
  111. package/docs/components/icon/use-cases.md +1 -1
  112. package/docs/components/input/api-table.md +1 -1
  113. package/docs/components/input/overview.md +1 -1
  114. package/docs/components/input-amount/api-table.md +1 -1
  115. package/docs/components/input-amount/overview.md +3 -3
  116. package/docs/components/input-date/api-table.md +1 -1
  117. package/docs/components/input-date/overview.md +3 -3
  118. package/docs/components/input-datepicker/api-table.md +2 -1
  119. package/docs/components/input-datepicker/overview.md +5 -5
  120. package/docs/components/input-email/api-table.md +1 -1
  121. package/docs/components/input-email/overview.md +2 -2
  122. package/docs/components/input-file/api-table.md +1 -1
  123. package/docs/components/input-file/overview.md +1 -1
  124. package/docs/components/input-iban/api-table.md +1 -1
  125. package/docs/components/input-iban/overview.md +2 -2
  126. package/docs/components/input-range/api-table.md +1 -1
  127. package/docs/components/input-range/overview.md +2 -2
  128. package/docs/components/input-stepper/api-table.md +1 -1
  129. package/docs/components/input-stepper/overview.md +1 -1
  130. package/docs/components/input-tel/api-table.md +1 -1
  131. package/docs/components/input-tel/overview.md +1 -1
  132. package/docs/components/input-tel/use-cases.md +6 -6
  133. package/docs/components/input-tel-dropdown/api-table.md +1 -1
  134. package/docs/components/input-tel-dropdown/overview.md +1 -1
  135. package/docs/components/listbox/api-table.md +1 -1
  136. package/docs/components/listbox/use-cases.md +1 -1
  137. package/docs/components/radio-group/api-table.md +1 -1
  138. package/docs/components/radio-group/overview.md +3 -3
  139. package/docs/components/select/api-table.md +1 -1
  140. package/docs/components/select/use-cases.md +1 -1
  141. package/docs/components/select-rich/api-table.md +1 -0
  142. package/docs/components/select-rich/use-cases.md +1 -1
  143. package/docs/components/switch/api-table.md +1 -1
  144. package/docs/components/tabs/examples.md +1 -1
  145. package/docs/components/textarea/api-table.md +1 -1
  146. package/docs/components/tooltip/api-table.md +1 -0
  147. package/docs/fundamentals/index.md +4 -4
  148. package/docs/fundamentals/node-tools/providence-analytics/overview.md +5 -5
  149. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  150. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  151. package/docs/fundamentals/rationales/accessibility.md +1 -1
  152. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  153. package/docs/fundamentals/systems/core/overview.md +1 -1
  154. package/docs/fundamentals/systems/form/api-table.md +1 -1
  155. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  156. package/docs/fundamentals/systems/form/model-value.md +1 -1
  157. package/docs/fundamentals/systems/form/overview.md +28 -28
  158. package/docs/fundamentals/systems/form/validate.md +2 -2
  159. package/docs/fundamentals/systems/localize/overview.md +4 -4
  160. package/docs/fundamentals/systems/overlays/api-table.md +35 -34
  161. package/docs/fundamentals/systems/overlays/configuration.md +32 -2
  162. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  163. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  164. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  165. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  166. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  167. package/docs/guides/how-to/create-a-form.md +10 -9
  168. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  169. package/docs/guides/index.md +8 -8
  170. package/docs/guides/principles/index.md +5 -5
  171. package/docs/guides/principles/styling.md +1 -1
  172. package/package.json +1 -1
@@ -1,14 +1,15 @@
1
1
  /* eslint-disable class-methods-use-this */
2
2
 
3
3
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
4
+
5
+ import { ValidateMixin } from './validate/ValidateMixin.js';
4
6
  import { FormControlMixin } from './FormControlMixin.js';
5
7
  import { Unparseable } from './validate/Unparseable.js';
6
- import { ValidateMixin } from './validate/ValidateMixin.js';
7
8
 
8
9
  /**
9
- * @typedef {import('../types/FormatMixinTypes.js').FormatMixin} FormatMixin
10
- * @typedef {import('../types/FormatMixinTypes.js').FormatOptions} FormatOptions
11
10
  * @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
11
+ * @typedef {import('../types/FormatMixinTypes.js').FormatOptions} FormatOptions
12
+ * @typedef {import('../types/FormatMixinTypes.js').FormatMixin} FormatMixin
12
13
  */
13
14
 
14
15
  // For a future breaking release:
@@ -69,6 +70,16 @@ const FormatMixinImplementation = superclass =>
69
70
  };
70
71
  }
71
72
 
73
+ /**
74
+ * During format/parse, we sometimes need to know how the current viewValue the user
75
+ * interacts with "came to be". Was it already formatted before for instance?
76
+ * If so, in case of an amount input, we might want to stick to the curent interpretation of separators.
77
+ */
78
+ #viewValueState = {
79
+ didFormatterOutputSyncToView: false,
80
+ didFormatterRun: false,
81
+ };
82
+
72
83
  /**
73
84
  * @param {string} [name]
74
85
  * @param {unknown} [oldValue]
@@ -93,7 +104,7 @@ const FormatMixinImplementation = superclass =>
93
104
  * The view value. Will be delegated to `._inputNode.value`
94
105
  */
95
106
  get value() {
96
- return (this._inputNode && this._inputNode.value) || this.__value || '';
107
+ return this._inputNode?.value || this.__value || '';
97
108
  }
98
109
 
99
110
  /** @param {string} value */
@@ -249,7 +260,11 @@ const FormatMixinImplementation = superclass =>
249
260
  // Apparently, the parser was not able to produce a satisfactory output for the desired
250
261
  // modelValue type, based on the current viewValue. Unparseable allows to restore all
251
262
  // states (for instance from a lost user session), since it saves the current viewValue.
252
- const result = this.parser(value, this.formatOptions);
263
+ const result = this.parser(value, {
264
+ ...this.formatOptions,
265
+ mode: this.#getFormatOptionsMode(),
266
+ viewValueStates: this.#getFormatOptionsViewValueStates(),
267
+ });
253
268
  return result !== undefined ? result : new Unparseable(value);
254
269
  }
255
270
 
@@ -258,6 +273,8 @@ const FormatMixinImplementation = superclass =>
258
273
  * @private
259
274
  */
260
275
  _callFormatter() {
276
+ this.#viewValueState.didFormatterRun = false;
277
+
261
278
  // - Why check for this.hasError?
262
279
  // We only want to format values that are considered valid. For best UX,
263
280
  // we only 'reward' valid inputs.
@@ -269,13 +286,8 @@ const FormatMixinImplementation = superclass =>
269
286
  // imperatively, we DO want to format a value (it is the only way to get meaningful
270
287
  // input into `._inputNode` with modelValue as input)
271
288
 
272
- if (
273
- this._isHandlingUserInput &&
274
- this.hasFeedbackFor?.length &&
275
- this.hasFeedbackFor.includes('error') &&
276
- this._inputNode
277
- ) {
278
- return this._inputNode ? this.value : undefined;
289
+ if (this._isHandlingUserInput && this.hasFeedbackFor?.includes('error') && this._inputNode) {
290
+ return this.value;
279
291
  }
280
292
 
281
293
  if (this.modelValue instanceof Unparseable) {
@@ -285,7 +297,13 @@ const FormatMixinImplementation = superclass =>
285
297
  return this.modelValue.viewValue;
286
298
  }
287
299
 
288
- return this.formatter(this.modelValue, this.formatOptions);
300
+ this.#viewValueState.didFormatterRun = true;
301
+
302
+ return this.formatter(this.modelValue, {
303
+ ...this.formatOptions,
304
+ mode: this.#getFormatOptionsMode(),
305
+ viewValueStates: this.#getFormatOptionsViewValueStates(),
306
+ });
289
307
  }
290
308
 
291
309
  /**
@@ -348,7 +366,6 @@ const FormatMixinImplementation = superclass =>
348
366
  * @private
349
367
  */
350
368
  __handlePreprocessor() {
351
- const unprocessedValue = this.value;
352
369
  let currentCaretIndex = this.value.length;
353
370
  // Be gentle with Safari
354
371
  if (
@@ -364,7 +381,6 @@ const FormatMixinImplementation = superclass =>
364
381
  prevViewValue: this.__prevViewValue,
365
382
  });
366
383
 
367
- this.__prevViewValue = unprocessedValue;
368
384
  if (preprocessedValue === undefined) {
369
385
  // Make sure we do no set back original value, so we preserve
370
386
  // caret index (== selectionStart/selectionEnd)
@@ -393,6 +409,8 @@ const FormatMixinImplementation = superclass =>
393
409
  if (this._reflectBackOn()) {
394
410
  // Text 'undefined' should not end up in <input>
395
411
  this.value = typeof this.formattedValue !== 'undefined' ? this.formattedValue : '';
412
+ this.#viewValueState.didFormatterOutputSyncToView =
413
+ Boolean(this.formattedValue) && this.#viewValueState.didFormatterRun;
396
414
  }
397
415
  }
398
416
 
@@ -459,7 +477,7 @@ const FormatMixinImplementation = superclass =>
459
477
  /**
460
478
  * Configuration object that will be available inside the formatter function
461
479
  */
462
- this.formatOptions = /** @type {FormatOptions} */ ({});
480
+ this.formatOptions = /** @type {FormatOptions} */ ({ mode: 'auto' });
463
481
 
464
482
  /**
465
483
  * The view value is the result of the formatter function (when available).
@@ -543,10 +561,8 @@ const FormatMixinImplementation = superclass =>
543
561
  */
544
562
  __onPaste() {
545
563
  this._isPasting = true;
546
- this.formatOptions.mode = 'pasted';
547
564
  setTimeout(() => {
548
565
  this._isPasting = false;
549
- this.formatOptions.mode = 'auto';
550
566
  });
551
567
  }
552
568
 
@@ -588,6 +604,38 @@ const FormatMixinImplementation = superclass =>
588
604
  this._inputNode.removeEventListener('compositionend', this.__onCompositionEvent);
589
605
  }
590
606
  }
607
+
608
+ /**
609
+ * This method will be called right before a parser or formatter gets called and computes
610
+ * the formatOptions.mode. In short, it gives meta info about how the user interacted with
611
+ * the form control, as these user interactions can influence the formatting/parsing behavior.
612
+ *
613
+ * It's important that the behavior of these can be different during a paste or user edit,
614
+ * but not during programmatic setting of values (like setting modelValue).
615
+ * @returns {'auto'|'pasted'|'user-edited'}
616
+ */
617
+ #getFormatOptionsMode() {
618
+ if (this._isPasting) {
619
+ return 'pasted';
620
+ }
621
+ const isUserEditing = this._isHandlingUserInput && this.__prevViewValue;
622
+ if (isUserEditing) {
623
+ return 'user-edited';
624
+ }
625
+ return 'auto';
626
+ }
627
+
628
+ /**
629
+ * @returns {FormatOptions['viewValueStates']}
630
+ */
631
+ #getFormatOptionsViewValueStates() {
632
+ /** @type {FormatOptions['viewValueStates']}} */
633
+ const states = [];
634
+ if (this.#viewValueState.didFormatterOutputSyncToView) {
635
+ states.push('formatted');
636
+ }
637
+ return states;
638
+ }
591
639
  };
592
640
 
593
641
  export const FormatMixin = dedupeMixin(FormatMixinImplementation);
@@ -171,7 +171,9 @@ const ChoiceInputMixinImplementation = superclass =>
171
171
  render() {
172
172
  return html`
173
173
  <slot name="input"></slot>
174
- <div class="choice-field__graphic-container">${this._choiceGraphicTemplate()}</div>
174
+ <div class="choice-field__graphic-container" aria-hidden="true">
175
+ ${this._choiceGraphicTemplate()}
176
+ </div>
175
177
  <div class="choice-field__label">
176
178
  <slot name="label"></slot>
177
179
  </div>
@@ -5,6 +5,8 @@ import sinon from 'sinon';
5
5
  import { Unparseable, Validator, FormatMixin } from '@lion/ui/form-core.js';
6
6
  import { getFormControlMembers, mimicUserInput } from '@lion/ui/form-core-test-helpers.js';
7
7
 
8
+ const isLionInputStepper = (/** @type {FormatClass} */ el) => 'valueTextMapping' in el;
9
+
8
10
  /**
9
11
  * @typedef {import('../types/FormControlMixinTypes.js').FormControlHost} FormControlHost
10
12
  * @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType
@@ -480,7 +482,7 @@ export function runFormatMixinSuite(customConfig) {
480
482
  /**
481
483
  * @param {FormatClass} el
482
484
  */
483
- function paste(el, val = 'lorem') {
485
+ function mimicPaste(el, val = 'lorem') {
484
486
  const { _inputNode } = getFormControlMembers(el);
485
487
  _inputNode.value = val;
486
488
  _inputNode.dispatchEvent(new ClipboardEvent('paste', { bubbles: true }));
@@ -494,12 +496,17 @@ export function runFormatMixinSuite(customConfig) {
494
496
  `)
495
497
  );
496
498
  const formatterSpy = sinon.spy(el, 'formatter');
497
- paste(el);
499
+ mimicPaste(el);
498
500
  expect(formatterSpy).to.be.called;
499
- expect(/** @type {{mode: string}} */ (formatterSpy.args[0][1]).mode).to.equal('pasted');
501
+ expect(/** @type {{mode: string}} */ (formatterSpy.lastCall.args[1]).mode).to.equal(
502
+ 'pasted',
503
+ );
504
+ // give microtask of _isPasting chance to reset
500
505
  await aTimeout(0);
501
- mimicUserInput(el, '');
502
- expect(/** @type {{mode: string}} */ (formatterSpy.args[0][1]).mode).to.equal('auto');
506
+ el.modelValue = 'foo';
507
+ expect(/** @type {{mode: string}} */ (formatterSpy.lastCall.args[1]).mode).to.equal(
508
+ 'auto',
509
+ );
503
510
  });
504
511
 
505
512
  it('sets protected value "_isPasting" for Subclassers', async () => {
@@ -509,7 +516,7 @@ export function runFormatMixinSuite(customConfig) {
509
516
  `)
510
517
  );
511
518
  const formatterSpy = sinon.spy(el, 'formatter');
512
- paste(el);
519
+ mimicPaste(el);
513
520
  expect(formatterSpy).to.have.been.called;
514
521
  // @ts-ignore [allow-protected] in test
515
522
  expect(el._isPasting).to.be.true;
@@ -526,7 +533,7 @@ export function runFormatMixinSuite(customConfig) {
526
533
  );
527
534
  // @ts-ignore [allow-protected] in test
528
535
  const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
529
- paste(el);
536
+ mimicPaste(el);
530
537
  expect(reflectBackSpy).to.have.been.called;
531
538
  });
532
539
 
@@ -538,10 +545,65 @@ export function runFormatMixinSuite(customConfig) {
538
545
  );
539
546
  // @ts-ignore [allow-protected] in test
540
547
  const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
541
- paste(el);
548
+ mimicPaste(el);
542
549
  expect(reflectBackSpy).to.have.been.called;
543
550
  });
544
551
  });
552
+
553
+ describe('On user input', () => {
554
+ it('adjusts formatOptions.mode to "user-edited" for parser when user changes value', async () => {
555
+ const el = /** @type {FormatClass} */ (
556
+ await fixture(html`<${tag}><input slot="input"></${tag}>`)
557
+ );
558
+
559
+ const parserSpy = sinon.spy(el, 'parser');
560
+
561
+ // Here we get auto as we start from '' (so there was no value to edit)
562
+ mimicUserInput(el, 'some val');
563
+ expect(/** @type {{mode: string}} */ (parserSpy.lastCall.args[1]).mode).to.equal(
564
+ 'auto',
565
+ );
566
+ await el.updateComplete;
567
+
568
+ mimicUserInput(el, 'some other val');
569
+ expect(/** @type {{mode: string}} */ (parserSpy.lastCall.args[1]).mode).to.equal(
570
+ 'user-edited',
571
+ );
572
+ });
573
+
574
+ it('adjusts formatOptions.viewValueStates when user already formatted value', async () => {
575
+ const el = /** @type {FormatClass} */ (
576
+ await fixture(html`<${tag}><input slot="input"></${tag}>`)
577
+ );
578
+
579
+ // Neutralize config, so that we know for sure that the user has formatted the value
580
+ // when this suite is run on extended classes
581
+
582
+ // always reflect back formattedVlaue to view (instead of on blur or else)
583
+ // @ts-expect-error [allow-protected] in test
584
+ el._reflectBackOn = () => true;
585
+ // avoid non-formatted values due to validators in extended classes
586
+ el.defaultValidators = [];
587
+ // avoid Unparseable due to parsers in extended classes
588
+ el.parser = v => v;
589
+ // @ts-expect-error
590
+ el.formatter = v => v;
591
+
592
+ const parserSpy = sinon.spy(el, 'parser');
593
+ const formatterSpy = sinon.spy(el, 'formatter');
594
+
595
+ mimicUserInput(el, 'some val');
596
+ expect(parserSpy.lastCall.args[1].viewValueStates).to.not.include('formatted');
597
+ // @ts-expect-error
598
+ expect(formatterSpy.lastCall.args[1].viewValueStates).to.not.include('formatted');
599
+ await el.updateComplete;
600
+
601
+ mimicUserInput(el, 'some other val');
602
+ expect(parserSpy.lastCall.args[1].viewValueStates).to.include('formatted');
603
+ // @ts-expect-error
604
+ expect(formatterSpy.lastCall.args[1].viewValueStates).to.include('formatted');
605
+ });
606
+ });
545
607
  });
546
608
 
547
609
  describe('Parser', () => {
@@ -597,6 +659,43 @@ export function runFormatMixinSuite(customConfig) {
597
659
  expect(_inputNode.value).to.equal(val);
598
660
  });
599
661
 
662
+ it('does only calculate derived values as consequence of user input when preprocessed value is different from previous view value', async () => {
663
+ const val = generateValueBasedOnType({ viewValue: true }) || 'init-value';
664
+ if (typeof val !== 'string') return;
665
+
666
+ const preprocessorSpy = sinon.spy(v => v.replace(/\$$/g, ''));
667
+ const el = /** @type {FormatClass} */ (
668
+ await fixture(html`
669
+ <${tag} .preprocessor=${preprocessorSpy}>
670
+ <input slot="input" .value="${val}">
671
+ </${tag}>
672
+ `)
673
+ );
674
+
675
+ // TODO: find out why we need to skip this for lion-input-stepper
676
+ if (isLionInputStepper(el)) return;
677
+
678
+ /**
679
+ * The _calculateValues method is called inside _onUserInputChanged w/o providing args
680
+ * @param {sinon.SinonSpyCall} call
681
+ * @returns {boolean}
682
+ */
683
+ const isCalculateCallAfterUserInput = call => call.args[0]?.length === 0;
684
+
685
+ const didRecalculateAfterUserInput = (/** @type {sinon.SinonSpy<any[], any>} */ spy) =>
686
+ spy.callCount > 1 && !spy.getCalls().find(isCalculateCallAfterUserInput);
687
+
688
+ // @ts-expect-error [allow-protected] in test
689
+ const calcValuesSpy = sinon.spy(el, '_calculateValues');
690
+ // this value gets preprocessed to 'val'
691
+ mimicUserInput(el, `${val}$`);
692
+ expect(didRecalculateAfterUserInput(calcValuesSpy)).to.be.false;
693
+
694
+ // this value gets preprocessed to 'value' (and thus differs from previous)
695
+ mimicUserInput(el, `${val}ue$`);
696
+ expect(didRecalculateAfterUserInput(calcValuesSpy)).to.be.true;
697
+ });
698
+
600
699
  it('does not preprocess during composition', async () => {
601
700
  const el = /** @type {FormatClass} */ (
602
701
  await fixture(html`
@@ -3,7 +3,10 @@ import { LitElement } from 'lit';
3
3
  import { ValidateHost } from './validate/ValidateMixinTypes.js';
4
4
  import { FormControlHost } from './FormControlMixinTypes.js';
5
5
 
6
- export type FormatOptions = { mode: 'pasted' | 'auto' } & object;
6
+ export type FormatOptions = {
7
+ mode: 'pasted' | 'auto' | 'user-edited';
8
+ viewValueStates?: 'formatted'[];
9
+ };
7
10
  export declare class FormatHost {
8
11
  /**
9
12
  * Converts viewValue to modelValue
@@ -9,6 +9,7 @@ import { html } from 'lit';
9
9
  import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
10
10
  import { getAllTagNames } from './helpers/helpers.js';
11
11
  import './helpers/umbrella-form.js';
12
+
12
13
  /**
13
14
  * @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
14
15
  * @typedef {import('../../dialog/src/LionDialog.js').LionDialog} LionDialog
@@ -25,10 +26,12 @@ describe('Form inside dialog Integrations', () => {
25
26
  </lion-dialog>`,
26
27
  );
27
28
 
28
- // @ts-ignore
29
- const formEl = /** @type {LionForm} */ (el._overlayCtrl.contentNode._lionFormNode);
29
+ // @ts-expect-error [allow-protected-in-tests]
30
+ const umbrellaEl = el._overlayCtrl.contentNode;
31
+ const formEl = /** @type {LionForm} */ (umbrellaEl._lionFormNode);
30
32
  await formEl.registrationComplete;
31
33
  const registeredEls = getAllTagNames(formEl);
34
+ await umbrellaEl.waitForAllChildrenUpdates();
32
35
 
33
36
  expect(registeredEls).to.eql([
34
37
  'lion-fieldset',
@@ -86,6 +89,7 @@ describe('Form inside dialog Integrations', () => {
86
89
  </div>
87
90
  </lion-dialog>
88
91
  `);
92
+
89
93
  // @ts-expect-error [allow-protected-in-tests]
90
94
  el._overlayInvokerNode.click();
91
95
  const lionInput = el.querySelector('[name="input"]');
@@ -1,7 +1,7 @@
1
1
  import { parseNumber, getFractionDigits } from '@lion/ui/localize-no-side-effects.js';
2
2
 
3
3
  /**
4
- * @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} FormatOptions
4
+ * @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} FormatNumberOptions
5
5
  */
6
6
 
7
7
  /**
@@ -28,7 +28,7 @@ function round(value, decimals) {
28
28
  * parseAmount('1,234.56', {currency: 'JOD'}); => 1234.560
29
29
  *
30
30
  * @param {string} value Number to be parsed
31
- * @param {FormatOptions} [givenOptions] Locale Options
31
+ * @param {FormatNumberOptions} [givenOptions] Locale Options
32
32
  */
33
33
  export function parseAmount(value, givenOptions) {
34
34
  const unmatchedInput = value.match(/[^0-9,.\- ]/g);
@@ -44,7 +44,7 @@ export function parseAmount(value, givenOptions) {
44
44
  return undefined;
45
45
  }
46
46
 
47
- /** @type {FormatOptions} */
47
+ /** @type {FormatNumberOptions} */
48
48
  const options = {
49
49
  ...givenOptions,
50
50
  };
@@ -1,10 +1,11 @@
1
1
  import { aTimeout, expect, fixture } from '@open-wc/testing';
2
2
  import { html } from 'lit';
3
- import { localize } from '@lion/ui/localize.js';
3
+ import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
4
4
  import { localizeTearDown } from '@lion/ui/localize-test-helpers.js';
5
5
  import { getInputMembers } from '@lion/ui/input-test-helpers.js';
6
6
  import { LionInputAmount, formatAmount, parseAmount } from '@lion/ui/input-amount.js';
7
-
7
+ import { mimicUserInput } from '@lion/ui/form-core-test-helpers.js';
8
+ import sinon from 'sinon';
8
9
  import '@lion/ui/define/lion-input-amount.js';
9
10
 
10
11
  /**
@@ -12,6 +13,8 @@ import '@lion/ui/define/lion-input-amount.js';
12
13
  */
13
14
 
14
15
  describe('<lion-input-amount>', () => {
16
+ const localize = getLocalizeManager();
17
+
15
18
  beforeEach(() => {
16
19
  localizeTearDown();
17
20
  });
@@ -128,6 +131,37 @@ describe('<lion-input-amount>', () => {
128
131
  expect(_inputNode.value).to.equal('100.12');
129
132
  });
130
133
 
134
+ it('adjusts formats with locale when formatOptions.mode is "user-edited"', async () => {
135
+ const el = /** @type {LionInputAmount} */ (
136
+ await fixture(
137
+ html`<lion-input-amount
138
+ .modelValue=${123456.78}
139
+ currency="EUR"
140
+ .formatOptions="${{ locale: 'nl-NL' }}"
141
+ ></lion-input-amount>`,
142
+ )
143
+ );
144
+ const parserSpy = sinon.spy(el, 'parser');
145
+ const formatterSpy = sinon.spy(el, 'formatter');
146
+
147
+ // @ts-expect-error [allow-protected] in test
148
+ expect(el._inputNode.value).to.equal('123.456,78');
149
+
150
+ // When editing an already existing value, we interpet the separators as they are
151
+ mimicUserInput(el, '123.456');
152
+ expect(parserSpy.lastCall.args[1]?.mode).to.equal('user-edited');
153
+ expect(formatterSpy.lastCall.args[1]?.mode).to.equal('user-edited');
154
+ expect(el.modelValue).to.equal(123456);
155
+ expect(el.formattedValue).to.equal('123.456,00');
156
+
157
+ // Formatting should only affect values that should be formatted / parsed as a consequence of user input.
158
+ // When a user finished editing, the default should be restored.
159
+ // (think of a programmatically set modelValue, that should behave idempotent, regardless of when it is set)
160
+ el.modelValue = 1234;
161
+ expect(el.formattedValue).to.equal('1.234,00');
162
+ expect(formatterSpy.lastCall.args[1]?.mode).to.equal('auto');
163
+ });
164
+
131
165
  it('sets inputmode attribute to decimal', async () => {
132
166
  const el = /** @type {LionInputAmount} */ (
133
167
  await fixture(`<lion-input-amount></lion-input-amount>`)
@@ -60,4 +60,15 @@ describe('parseAmount()', async () => {
60
60
  expect(parseAmount('foo1', { mode: 'pasted' })).to.equal(1);
61
61
  expect(parseAmount('EUR 1,50', { mode: 'pasted' })).to.equal(1.5);
62
62
  });
63
+
64
+ it('parses based on locale when "user-edited" mode used combined with viewValueStates "formatted"', async () => {
65
+ expect(parseAmount('123,456.78', { mode: 'auto' })).to.equal(123456.78);
66
+ expect(
67
+ parseAmount('123,456.78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
68
+ ).to.equal(123456.78);
69
+ expect(parseAmount('123.456,78', { mode: 'auto' })).to.equal(123456.78);
70
+ expect(
71
+ parseAmount('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
72
+ ).to.equal(123.45678);
73
+ });
63
74
  });
@@ -181,7 +181,7 @@ export class LionInputTelDropdown extends LionInputTel {
181
181
 
182
182
  return {
183
183
  template: templates.dropdown(this._templateDataDropdown),
184
- renderAsDirectHostChild: Boolean,
184
+ renderAsDirectHostChild: true,
185
185
  };
186
186
  },
187
187
  };
@@ -8,7 +8,7 @@ import { getLocale } from '../utils/getLocale.js';
8
8
  * @returns {string} The separator
9
9
  */
10
10
  export function getDecimalSeparator(locale, options) {
11
- if (options && options.decimalSeparator) {
11
+ if (options?.decimalSeparator) {
12
12
  return options.decimalSeparator;
13
13
  }
14
14
  const computedLocale = getLocale(locale);
@@ -16,14 +16,18 @@ import { getDecimalSeparator } from './getDecimalSeparator.js';
16
16
  * parseNumber('1,234.56'); // method: heuristic => 1234.56
17
17
  *
18
18
  * @param {string} value Clean number (only [0-9 ,.]) to be parsed
19
- * @param {object} options
20
- * @param {string?} [options.mode] auto|pasted
19
+ * @param {{mode?:'auto'|'pasted'|'user-edited'; viewValueStates?:string[]}} options
21
20
  * @return {string} unparseable|withLocale|heuristic
22
21
  */
23
- function getParseMode(value, { mode = 'auto' } = {}) {
22
+ function getParseMode(value, { mode = 'auto', viewValueStates } = {}) {
24
23
  const separators = value.match(/[., ]/g);
25
24
 
26
- if (!separators) {
25
+ // When a user edits an existing value, we already formatted it with a certain locale.
26
+ // For best UX, we stick with this locale
27
+ const shouldAlignWithExistingSeparators =
28
+ viewValueStates?.includes('formatted') && mode === 'user-edited';
29
+
30
+ if (!separators || shouldAlignWithExistingSeparators) {
27
31
  return 'withLocale';
28
32
  }
29
33
  if (mode === 'auto' && separators.length === 1) {
@@ -52,8 +56,7 @@ function getParseMode(value, { mode = 'auto' } = {}) {
52
56
  * @param {import('../../types/LocalizeMixinTypes.js').FormatNumberOptions} options Locale Options
53
57
  */
54
58
  function parseWithLocale(value, options) {
55
- const locale = options && options.locale ? options.locale : undefined;
56
- const separator = getDecimalSeparator(locale, options);
59
+ const separator = getDecimalSeparator(options?.locale, options);
57
60
  const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g');
58
61
  let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator)?.join('');
59
62
  if (separator === ',') {
@@ -74,6 +74,17 @@ describe('parseNumber()', () => {
74
74
  expect(parseNumber('123456.78', { mode: 'pasted' })).to.equal(123456.78);
75
75
  });
76
76
 
77
+ it('detects separators withLocale when "user-edited" mode combined with viewValueStates "formatted" used e.g. 123.456,78', async () => {
78
+ expect(parseNumber('123,456.78', { mode: 'auto' })).to.equal(123456.78);
79
+ expect(
80
+ parseNumber('123,456.78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
81
+ ).to.equal(123456.78);
82
+ expect(parseNumber('123.456,78', { mode: 'auto' })).to.equal(123456.78);
83
+ expect(
84
+ parseNumber('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
85
+ ).to.equal(123.45678);
86
+ });
87
+
77
88
  it('detects separators unparseable when there are 2 same ones e.g. 1.234.56', () => {
78
89
  expect(parseNumber('1.234.56')).to.equal(123456);
79
90
  expect(parseNumber('1,234,56')).to.equal(123456);
@@ -21,7 +21,7 @@ export declare interface FormatDateOptions extends Intl.DateTimeFormatOptions {
21
21
 
22
22
  roundMode?: string;
23
23
  returnIfNaN?: string;
24
- mode?: 'pasted' | 'auto';
24
+ mode?: 'pasted' | 'auto' | 'user-edited';
25
25
 
26
26
  postProcessors?: Map<string, DatePostProcessor>;
27
27
  }
@@ -41,8 +41,8 @@ export declare interface FormatNumberOptions extends Intl.NumberFormatOptions {
41
41
  // https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping
42
42
  // note the half space in there as well
43
43
  groupSeparator?: ',' | '.' | ' ' | '_' | ' ' | "'";
44
- mode?: 'pasted' | 'auto';
45
-
44
+ mode?: 'pasted' | 'auto' | 'user-edited';
45
+ viewValueStates?: 'formatted'[];
46
46
  postProcessors?: Map<string, NumberPostProcessor>;
47
47
  }
48
48