@lion/ui 0.10.1 → 0.11.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 (108) hide show
  1. package/README.md +2 -2
  2. package/components/checkbox-group/src/LionCheckboxIndeterminate.js +39 -16
  3. package/components/checkbox-group/test-suites/CheckboxIndeterminate.suite.js +189 -1
  4. package/components/form-core/src/FocusMixin.js +4 -4
  5. package/components/form-core/src/LionField.js +1 -1
  6. package/components/input-amount/test/lion-input-amount.test.js +48 -1
  7. package/components/input-amount/test/parsers.test.js +1 -1
  8. package/components/localize/src/number/parseNumber.js +21 -12
  9. package/components/localize/test/number/parseNumber.test.js +1 -1
  10. package/components/localize/types/LocalizeMixinTypes.ts +0 -5
  11. package/components/select-rich/src/LionSelectRich.js +18 -2
  12. package/components/select-rich/test/lion-select-rich-interaction.test.js +139 -20
  13. package/components/select-rich/test/lion-select-rich.test.js +13 -1
  14. package/components/validate-messages/translations/pl.js +4 -4
  15. package/custom-elements.json +47661 -47626
  16. package/dist-types/components/checkbox-group/src/LionCheckboxIndeterminate.d.ts +10 -0
  17. package/dist-types/components/checkbox-group/src/LionCheckboxIndeterminate.d.ts.map +1 -1
  18. package/dist-types/components/checkbox-group/test-suites/CheckboxIndeterminate.suite.d.ts.map +1 -1
  19. package/dist-types/components/localize/src/number/parseNumber.d.ts.map +1 -1
  20. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +0 -4
  21. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
  22. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  23. package/dist-types/tsconfig.tsbuildinfo +1 -1
  24. package/docs/components/accordion/api-table.md +6 -6
  25. package/docs/components/button/api-table.md +27 -30
  26. package/docs/components/calendar/api-table.md +13 -13
  27. package/docs/components/checkbox-group/api-table.md +305 -329
  28. package/docs/components/checkbox-group/overview.md +2 -2
  29. package/docs/components/collapsible/api-table.md +5 -5
  30. package/docs/components/collapsible/overview.md +1 -1
  31. package/docs/components/combobox/api-table.md +150 -154
  32. package/docs/components/combobox/overview.md +1 -1
  33. package/docs/components/combobox/use-cases.md +1 -1
  34. package/docs/components/dialog/api-table.md +12 -12
  35. package/docs/components/dialog/overview.md +1 -1
  36. package/docs/components/drawer/api-table.md +9 -10
  37. package/docs/components/fieldset/api-table.md +104 -118
  38. package/docs/components/fieldset/overview.md +5 -5
  39. package/docs/components/form/overview.md +3 -3
  40. package/docs/components/form/use-cases.md +2 -2
  41. package/docs/components/icon/api-table.md +3 -3
  42. package/docs/components/icon/use-cases.md +1 -1
  43. package/docs/components/input/api-table.md +110 -120
  44. package/docs/components/input/overview.md +1 -1
  45. package/docs/components/input-amount/api-table.md +129 -142
  46. package/docs/components/input-amount/overview.md +3 -3
  47. package/docs/components/input-date/api-table.md +110 -120
  48. package/docs/components/input-date/overview.md +3 -3
  49. package/docs/components/input-datepicker/api-table.md +148 -160
  50. package/docs/components/input-datepicker/overview.md +5 -5
  51. package/docs/components/input-email/api-table.md +110 -120
  52. package/docs/components/input-email/overview.md +2 -2
  53. package/docs/components/input-file/api-table.md +132 -143
  54. package/docs/components/input-file/overview.md +1 -1
  55. package/docs/components/input-iban/api-table.md +110 -120
  56. package/docs/components/input-iban/overview.md +2 -2
  57. package/docs/components/input-range/api-table.md +116 -126
  58. package/docs/components/input-range/overview.md +2 -2
  59. package/docs/components/input-stepper/api-table.md +123 -133
  60. package/docs/components/input-stepper/overview.md +1 -1
  61. package/docs/components/input-tel/api-table.md +118 -128
  62. package/docs/components/input-tel/overview.md +1 -1
  63. package/docs/components/input-tel/use-cases.md +6 -6
  64. package/docs/components/input-tel-dropdown/api-table.md +125 -135
  65. package/docs/components/input-tel-dropdown/overview.md +1 -1
  66. package/docs/components/listbox/api-table.md +126 -131
  67. package/docs/components/listbox/use-cases.md +1 -1
  68. package/docs/components/pagination/api-table.md +5 -5
  69. package/docs/components/progress-indicator/api-table.md +9 -10
  70. package/docs/components/radio-group/api-table.md +234 -258
  71. package/docs/components/radio-group/overview.md +3 -3
  72. package/docs/components/select/api-table.md +106 -116
  73. package/docs/components/select/use-cases.md +1 -1
  74. package/docs/components/select-rich/api-table.md +169 -178
  75. package/docs/components/select-rich/use-cases.md +1 -1
  76. package/docs/components/steps/api-table.md +6 -6
  77. package/docs/components/switch/api-table.md +117 -128
  78. package/docs/components/tabs/api-table.md +1 -1
  79. package/docs/components/tabs/examples.md +1 -1
  80. package/docs/components/textarea/api-table.md +114 -124
  81. package/docs/components/tooltip/api-table.md +13 -13
  82. package/docs/fundamentals/index.md +4 -4
  83. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  84. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  85. package/docs/fundamentals/rationales/accessibility.md +1 -1
  86. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  87. package/docs/fundamentals/systems/core/api-table.md +8 -11
  88. package/docs/fundamentals/systems/core/overview.md +1 -1
  89. package/docs/fundamentals/systems/form/api-table.md +77 -82
  90. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  91. package/docs/fundamentals/systems/form/model-value.md +1 -1
  92. package/docs/fundamentals/systems/form/overview.md +28 -28
  93. package/docs/fundamentals/systems/form/validate.md +2 -2
  94. package/docs/fundamentals/systems/localize/api-table.md +19 -19
  95. package/docs/fundamentals/systems/localize/overview.md +4 -4
  96. package/docs/fundamentals/systems/overlays/api-table.md +45 -46
  97. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  98. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  99. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  100. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  101. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  102. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  103. package/docs/guides/how-to/create-a-form.md +4 -4
  104. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  105. package/docs/guides/index.md +8 -8
  106. package/docs/guides/principles/index.md +5 -5
  107. package/docs/guides/principles/styling.md +1 -1
  108. package/package.json +1 -1
package/README.md CHANGED
@@ -45,7 +45,7 @@ They provide an unopinionated, white-label layer that can be extended to your ow
45
45
  - **Accessibility:** Aimed at compliance with the WCAG 2.2 AA standard to create components that are accessible for everybody.
46
46
  - **Flexibility:** Provides solutions through Web Components and JavaScript classes which can be used, adopted and extended to fit all needs.
47
47
  - **Modern Code:** Lion is distributes as pure es modules.
48
- - **Exposes functions/classes and Web Components:** Ships a functionality in it's most appropriate form.
48
+ - **Exposes functions/classes and Web Components:** Ships a functionality in its most appropriate form.
49
49
 
50
50
  > Note: Our demo examples might look simple and not very stylish. This is on purpose. They are designed to be basic so you can easily add your own styles to them to match your intended design, without dealing with styles that are already there.
51
51
 
@@ -113,7 +113,7 @@ You can also use the lion elements directly, although this is likely not a commo
113
113
 
114
114
  ## Issues
115
115
 
116
- If you encounter an issue with any of the packages we are offering please open a [new bug issue](https://github.com/ing-bank/lion/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a description of the expected and the current behavior - additional adding a [reproduction](https://webcomponents.dev/edit/kpZmz1CJN580OaXsk56f?pm=1) always helps.
116
+ If you encounter an issue with any of the packages we are offering please open a [new bug issue](https://github.com/ing-bank/lion/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a description of the expected and the current behavior - additionally adding a [reproduction](https://webcomponents.dev/edit/kpZmz1CJN580OaXsk56f?pm=1) always helps.
117
117
 
118
118
  ## Feature requests
119
119
 
@@ -49,17 +49,7 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
49
49
  * @protected
50
50
  */
51
51
  get _subCheckboxes() {
52
- let checkboxes = [];
53
- if (
54
- this._checkboxGroupNode &&
55
- this._checkboxGroupNode.formElements &&
56
- this._checkboxGroupNode.formElements.length > 0
57
- ) {
58
- checkboxes = this._checkboxGroupNode.formElements.filter(
59
- checkbox => checkbox !== this && this.contains(checkbox),
60
- );
61
- }
62
- return /** @type LionCheckbox[] */ (checkboxes);
52
+ return /** @type LionCheckbox[] */ (this.__subCheckboxes);
63
53
  }
64
54
 
65
55
  _storeIndeterminateState() {
@@ -97,9 +87,14 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
97
87
  this.indeterminate = false;
98
88
  this.checked = false;
99
89
  break;
100
- default:
90
+ default: {
101
91
  this.indeterminate = true;
102
- this.checked = false;
92
+ const disabledUncheckedElements = subCheckboxes.filter(
93
+ checkbox => checkbox.disabled && checkbox.checked === false,
94
+ );
95
+ this.checked =
96
+ subCheckboxes.length - checkedElements.length - disabledUncheckedElements.length === 0;
97
+ }
103
98
  }
104
99
  this.updateComplete.then(() => {
105
100
  this.__settingOwnChecked = false;
@@ -155,13 +150,12 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
155
150
  subCheckboxes.length > 0 && subCheckboxes.length === checkedElements.length;
156
151
  const allDisabled =
157
152
  subCheckboxes.length > 0 && subCheckboxes.length === disabledElements.length;
158
- const hasDisabledElements = disabledElements.length > 0;
159
153
 
160
154
  if (allDisabled) {
161
155
  this.checked = allChecked;
162
156
  }
163
157
 
164
- if (this.indeterminate && (this.mixedState || hasDisabledElements)) {
158
+ if (this.indeterminate && this.mixedState) {
165
159
  this._subCheckboxes.forEach((checkbox, i) => {
166
160
  // eslint-disable-next-line no-param-reassign
167
161
  checkbox.checked = this._indeterminateSubStates[i];
@@ -211,6 +205,7 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
211
205
  if (!(/** @type {HTMLElement} */ (ev.target).hasAttribute('role'))) {
212
206
  /** @type {HTMLElement} */ (ev.target)?.setAttribute('role', 'listitem');
213
207
  }
208
+ this.__addToSubCheckboxes(/** @type {CustomEvent} */ (ev).detail.element);
214
209
  this._setOwnCheckedState();
215
210
  }
216
211
 
@@ -223,6 +218,26 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
223
218
  if (/** @type {HTMLElement} */ (ev.target).getAttribute('role') === 'listitem') {
224
219
  /** @type {HTMLElement} */ (ev.target)?.removeAttribute('role');
225
220
  }
221
+ this.__removeFromSubCheckboxes(/** @type {CustomEvent} */ (ev).detail.element);
222
+ }
223
+
224
+ /**
225
+ * @param {HTMLElement} element
226
+ */
227
+ __addToSubCheckboxes(element) {
228
+ if (element !== this && this.contains(element)) {
229
+ this.__subCheckboxes.push(element);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * @param {HTMLElement} element
235
+ */
236
+ __removeFromSubCheckboxes(element) {
237
+ const index = this.__subCheckboxes.indexOf(element);
238
+ if (index !== -1) {
239
+ this.__subCheckboxes.splice(index, 1);
240
+ }
226
241
  }
227
242
 
228
243
  constructor() {
@@ -230,6 +245,8 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
230
245
  this.indeterminate = false;
231
246
  this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
232
247
  this.__onModelValueChanged = this.__onModelValueChanged.bind(this);
248
+ /** @type {HTMLElement[]} */
249
+ this.__subCheckboxes = [];
233
250
  /** @type {boolean[]} */
234
251
  this._indeterminateSubStates = [];
235
252
  this.mixedState = false;
@@ -259,7 +276,13 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
259
276
  /** @param {import('lit-element').PropertyValues } changedProperties */
260
277
  updated(changedProperties) {
261
278
  super.updated(changedProperties);
262
- if (changedProperties.has('indeterminate')) {
279
+
280
+ // When 1. sub checkboxes have disabled elements and 2. some elements are checked while the others are unchecked
281
+ // both this._inputNode.indeterminate and this.indeterminate are already true.
282
+ // If user clicks the input node, this._inputNode.indeterminate is turned to false by the browser
283
+ // while this.indeterminate is still true and the 'indeterminate' is not in the changedProperties
284
+ // because it hasn't been updated (true -> true) but checked would have been updated (false -> true).
285
+ if (changedProperties.has('indeterminate') || changedProperties.has('checked')) {
263
286
  this._inputNode.indeterminate = this.indeterminate;
264
287
  }
265
288
  }
@@ -205,7 +205,84 @@ export function runCheckboxIndeterminateSuite(customConfig) {
205
205
 
206
206
  // Assert
207
207
  expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true;
208
- expect(elIndeterminate?.checked).to.be.false;
208
+ expect(elIndeterminate?.checked).to.be.true;
209
+ });
210
+
211
+ it('should be checked when all children are prechecked', async () => {
212
+ // Arrange
213
+ const el = /** @type {LionCheckboxGroup} */ await fixture(html`
214
+ <${groupTag} name="scientists[]">
215
+ <${tag} label="Favorite scientists">
216
+ <${childTag} checked label="Archimedes"></${childTag}>
217
+ <${childTag} checked label="Francis Bacon"></${childTag}>
218
+ <${childTag} checked label="Marie Curie"></${childTag}>
219
+ </${tag}>
220
+ </${groupTag}>
221
+ `);
222
+
223
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
224
+ el.querySelector(`${cfg.tagString}`)
225
+ );
226
+
227
+ // Assert
228
+ expect(elIndeterminate?.hasAttribute('checked')).to.be.true;
229
+ });
230
+
231
+ it('should be checked when it has one prechecked child', async () => {
232
+ // Arrange
233
+ const el = /** @type {LionCheckboxGroup} */ await fixture(html`
234
+ <${groupTag} name="scientists[]">
235
+ <${tag} label="Favorite scientists">
236
+ <${childTag} checked label="Archimedes"></${childTag}>
237
+ </${tag}>
238
+ </${groupTag}>
239
+ `);
240
+
241
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
242
+ el.querySelector(`${cfg.tagString}`)
243
+ );
244
+
245
+ // Assert
246
+ expect(elIndeterminate?.hasAttribute('checked')).to.be.true;
247
+ });
248
+
249
+ it('should be unchecked when it has an disabled checked child and an unchecked child', async () => {
250
+ // Arrange
251
+ const el = /** @type {LionCheckboxGroup} */ await fixture(html`
252
+ <${groupTag} name="scientists[]">
253
+ <${tag} label="Favorite scientists">
254
+ <${childTag} disabled checked label="Archimedes"></${childTag}>
255
+ <${childTag} label="Francis Bacon"></${childTag}>
256
+ </${tag}>
257
+ </${groupTag}>
258
+ `);
259
+
260
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
261
+ el.querySelector(`${cfg.tagString}`)
262
+ );
263
+
264
+ // Assert
265
+ expect(elIndeterminate?.hasAttribute('checked')).to.be.false;
266
+ });
267
+
268
+ it('should be indeterminated when some of the children are prechecked', async () => {
269
+ // Arrange
270
+ const el = /** @type {LionCheckboxGroup} */ await fixture(html`
271
+ <${groupTag} name="scientists[]">
272
+ <${tag} label="Favorite scientists">
273
+ <${childTag} checked label="Archimedes"></${childTag}>
274
+ <${childTag} checked label="Francis Bacon"></${childTag}>
275
+ <${childTag} label="Marie Curie"></${childTag}>
276
+ </${tag}>
277
+ </${groupTag}>
278
+ `);
279
+
280
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
281
+ el.querySelector(`${cfg.tagString}`)
282
+ );
283
+
284
+ // Assert
285
+ expect(elIndeterminate.hasAttribute('indeterminate')).to.be.true;
209
286
  });
210
287
 
211
288
  it('should sync all children when parent is checked (from indeterminate to checked)', async () => {
@@ -384,6 +461,65 @@ export function runCheckboxIndeterminateSuite(customConfig) {
384
461
  expect(_subCheckboxes[2].hasAttribute('checked')).to.be.false;
385
462
  });
386
463
 
464
+ it('should sync all prechecked children when parent is indeterminate and some of the children are disabled (from checked to unchecked)', async () => {
465
+ // Arrange
466
+ const el = /** @type {LionCheckboxGroup} */ (
467
+ await fixture(html`
468
+ <${groupTag} name="scientists[]">
469
+ <${tag} label="Favorite scientists">
470
+ <${childTag} checked label="Archimedes"></${childTag}>
471
+ <${childTag} disabled label="Francis Bacon"></${childTag}>
472
+ <${childTag} checked label="Marie Curie"></${childTag}>
473
+ </${tag}>
474
+ </${groupTag}>
475
+ `)
476
+ );
477
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
478
+ el.querySelector(`${cfg.tagString}`)
479
+ );
480
+ const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
481
+
482
+ // Act
483
+ _inputNode.click();
484
+ await elIndeterminate.updateComplete;
485
+
486
+ // Assert
487
+ expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
488
+ expect(_inputNode?.hasAttribute('indeterminate')).to.be.false;
489
+ expect(_subCheckboxes[0].hasAttribute('checked')).to.be.false;
490
+ expect(_subCheckboxes[1].hasAttribute('checked')).to.be.false;
491
+ expect(_subCheckboxes[2].hasAttribute('checked')).to.be.false;
492
+ });
493
+
494
+ it('should stay as indeterminated after it is clicked, when it is interminated already and some children are disabled', async () => {
495
+ // Arrange
496
+ const el = /** @type {LionCheckboxGroup} */ (
497
+ await fixture(html`
498
+ <${groupTag} name="scientists[]">
499
+ <${tag} label="Favorite scientists">
500
+ <${childTag} checked label="Archimedes"></${childTag}>
501
+ <${childTag} disabled label="Francis Bacon"></${childTag}>
502
+ <${childTag} label="Marie Curie"></${childTag}>
503
+ </${tag}>
504
+ </${groupTag}>
505
+ `)
506
+ );
507
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
508
+ el.querySelector(`${cfg.tagString}`)
509
+ );
510
+ const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
511
+
512
+ // Act
513
+ _inputNode.click();
514
+ await elIndeterminate.updateComplete;
515
+
516
+ // Assert
517
+ expect(elIndeterminate.hasAttribute('indeterminate')).to.be.true;
518
+ expect(_inputNode.indeterminate).to.be.true;
519
+ expect(_subCheckboxes[0].hasAttribute('checked')).to.be.true;
520
+ expect(_subCheckboxes[2].hasAttribute('checked')).to.be.true;
521
+ });
522
+
387
523
  it('should work as expected with siblings checkbox-indeterminate', async () => {
388
524
  // Arrange
389
525
  const el = /** @type {LionCheckboxGroup} */ (
@@ -450,6 +586,58 @@ export function runCheckboxIndeterminateSuite(customConfig) {
450
586
  expect(elSecondSubCheckboxes._subCheckboxes[1].hasAttribute('checked')).to.be.false;
451
587
  });
452
588
 
589
+ it('should work as expected when new checkbox was added', async () => {
590
+ // Arrange
591
+ const el = /** @type {LionCheckboxGroup} */ await fixture(html`
592
+ <${groupTag} name="scientists[]">
593
+ <${tag} label="Favorite scientists">
594
+ <${childTag} checked label="Archimedes"></${childTag}>
595
+ <${childTag} checked label="Francis Bacon" checked></${childTag}>
596
+ <${childTag} checked label="Marie Curie"></${childTag}>
597
+ </${tag}>
598
+ </${groupTag}>
599
+ `);
600
+
601
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
602
+ el.querySelector(`${cfg.tagString}`)
603
+ );
604
+ expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
605
+
606
+ // Act
607
+ const newChild = document.createElement(/** @type {string} */ (cfg.childTagString));
608
+ elIndeterminate.appendChild(newChild);
609
+ await elIndeterminate.updateComplete;
610
+
611
+ // Assert
612
+ expect(elIndeterminate.hasAttribute('indeterminate')).to.be.true;
613
+ });
614
+
615
+ it('should work as expected when an existing checkbox was removed', async () => {
616
+ // Arrange
617
+ const el = /** @type {LionCheckboxGroup} */ await fixture(html`
618
+ <${groupTag} name="scientists[]">
619
+ <${tag} label="Favorite scientists">
620
+ <${childTag} checked label="Archimedes"></${childTag}>
621
+ <${childTag} checked label="Francis Bacon" checked></${childTag}>
622
+ <${childTag} label="Marie Curie"></${childTag}>
623
+ </${tag}>
624
+ </${groupTag}>
625
+ `);
626
+
627
+ const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (
628
+ el.querySelector(`${cfg.tagString}`)
629
+ );
630
+ expect(elIndeterminate.hasAttribute('indeterminate')).to.be.true;
631
+ expect(elIndeterminate.checked).to.be.false;
632
+
633
+ // Act
634
+ elIndeterminate.removeChild(/** @type {ChildNode} */ (elIndeterminate.lastChild));
635
+ await elIndeterminate.updateComplete;
636
+
637
+ // Assert
638
+ expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true;
639
+ });
640
+
453
641
  it('should work as expected with nested indeterminate checkboxes', async () => {
454
642
  // Arrange
455
643
  const el = /** @type {LionCheckboxGroup} */ (
@@ -198,19 +198,19 @@ const FocusMixinImplementation = superclass =>
198
198
  * @private
199
199
  */
200
200
  __teardownEventsForFocusMixin() {
201
- this._focusableNode.removeEventListener(
201
+ this._focusableNode?.removeEventListener(
202
202
  'focus',
203
203
  /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus),
204
204
  );
205
- this._focusableNode.removeEventListener(
205
+ this._focusableNode?.removeEventListener(
206
206
  'blur',
207
207
  /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur),
208
208
  );
209
- this._focusableNode.removeEventListener(
209
+ this._focusableNode?.removeEventListener(
210
210
  'focusin',
211
211
  /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin),
212
212
  );
213
- this._focusableNode.removeEventListener(
213
+ this._focusableNode?.removeEventListener(
214
214
  'focusout',
215
215
  /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusout),
216
216
  );
@@ -44,7 +44,7 @@ export class LionField extends FormControlMixin(
44
44
 
45
45
  disconnectedCallback() {
46
46
  super.disconnectedCallback();
47
- this._inputNode.removeEventListener('change', this._onChange);
47
+ this._inputNode?.removeEventListener('change', this._onChange);
48
48
  }
49
49
 
50
50
  resetInteractionState() {
@@ -131,7 +131,7 @@ describe('<lion-input-amount>', () => {
131
131
  expect(_inputNode.value).to.equal('100.12');
132
132
  });
133
133
 
134
- it('adjusts formats with locale when formatOptions.mode is "user-edited"', async () => {
134
+ it('formats with locale when formatOptions.mode is "user-edited" and value has three decimal places', async () => {
135
135
  const el = /** @type {LionInputAmount} */ (
136
136
  await fixture(
137
137
  html`<lion-input-amount
@@ -154,6 +154,14 @@ describe('<lion-input-amount>', () => {
154
154
  expect(el.modelValue).to.equal(123456);
155
155
  expect(el.formattedValue).to.equal('123.456,00');
156
156
 
157
+ // When editing an already existing value, we interpet the separators as they are.
158
+ // However, only when the decimal places are 3 or more.
159
+ mimicUserInput(el, '123.456.00');
160
+ expect(parserSpy.lastCall.args[1]?.mode).to.equal('user-edited');
161
+ expect(formatterSpy.lastCall.args[1]?.mode).to.equal('user-edited');
162
+ expect(el.modelValue).to.equal(123456);
163
+ expect(el.formattedValue).to.equal('123.456,00');
164
+
157
165
  // Formatting should only affect values that should be formatted / parsed as a consequence of user input.
158
166
  // When a user finished editing, the default should be restored.
159
167
  // (think of a programmatically set modelValue, that should behave idempotent, regardless of when it is set)
@@ -162,6 +170,45 @@ describe('<lion-input-amount>', () => {
162
170
  expect(formatterSpy.lastCall.args[1]?.mode).to.equal('auto');
163
171
  });
164
172
 
173
+ it('formats with heuristic when formatOptions.mode is "user-edited" and value has two decimal places', async () => {
174
+ const el = /** @type {LionInputAmount} */ (
175
+ await fixture(
176
+ html`<lion-input-amount
177
+ .modelValue=${64}
178
+ currency="EUR"
179
+ .formatOptions="${{ locale: 'en-GB' }}"
180
+ ></lion-input-amount>`,
181
+ )
182
+ );
183
+ const parserSpy = sinon.spy(el, 'parser');
184
+ const formatterSpy = sinon.spy(el, 'formatter');
185
+
186
+ expect(el.formattedValue).to.equal('64.00');
187
+ // @ts-expect-error [allow-protected] in test
188
+ expect(el._inputNode.value).to.equal('64.00');
189
+
190
+ // When editing an already existing value, we interpret the separators based on decimal places when there's 1 or
191
+ // less separator in total (otherwise we would accidentally multiply or divide by 1000)
192
+ mimicUserInput(el, '64,00');
193
+ expect(parserSpy.lastCall.args[1]?.mode).to.equal('user-edited');
194
+ expect(formatterSpy.lastCall.args[1]?.mode).to.equal('user-edited');
195
+ expect(el.modelValue).to.equal(64);
196
+ expect(el.formattedValue).to.equal('64.00');
197
+
198
+ mimicUserInput(el, '64,0');
199
+ expect(parserSpy.lastCall.args[1]?.mode).to.equal('user-edited');
200
+ expect(formatterSpy.lastCall.args[1]?.mode).to.equal('user-edited');
201
+ expect(el.modelValue).to.equal(64);
202
+ expect(el.formattedValue).to.equal('64.00');
203
+
204
+ // Formatting should only affect values that should be formatted / parsed as a consequence of user input.
205
+ // When a user finished editing, the default should be restored.
206
+ // (think of a programmatically set modelValue, that should behave idempotent, regardless of when it is set)
207
+ el.modelValue = 1234;
208
+ expect(el.formattedValue).to.equal('1,234.00');
209
+ expect(formatterSpy.lastCall.args[1]?.mode).to.equal('auto');
210
+ });
211
+
165
212
  it('sets inputmode attribute to decimal', async () => {
166
213
  const el = /** @type {LionInputAmount} */ (
167
214
  await fixture(`<lion-input-amount></lion-input-amount>`)
@@ -69,6 +69,6 @@ describe('parseAmount()', async () => {
69
69
  expect(parseAmount('123.456,78', { mode: 'auto' })).to.equal(123456.78);
70
70
  expect(
71
71
  parseAmount('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
72
- ).to.equal(123.45678);
72
+ ).to.equal(123456.78);
73
73
  });
74
74
  });
@@ -20,25 +20,34 @@ import { getDecimalSeparator } from './getDecimalSeparator.js';
20
20
  * @return {string} unparseable|withLocale|heuristic
21
21
  */
22
22
  function getParseMode(value, { mode = 'auto', viewValueStates } = {}) {
23
- const separators = value.match(/[., ]/g);
23
+ const separatorsRe = /[., ]/g;
24
+ const separators = value.match(separatorsRe) || [];
25
+ // @ts-expect-error [wait-for-platform-types]
26
+ const decimalPlaces = (value.split(separatorsRe) || []).at(-1).length;
27
+ const isCurViewFormattedAndUserIsEditing =
28
+ viewValueStates?.includes('formatted') && mode === 'user-edited';
24
29
 
25
30
  // 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';
31
+ // For best UX, we stick with this locale. However, we only do this when the user has
32
+ // entered at least 3 decimal places.
33
+ const shouldUseCurLocale = isCurViewFormattedAndUserIsEditing && decimalPlaces > 2;
29
34
 
30
- if (!separators || shouldAlignWithExistingSeparators) {
35
+ const shouldUseLocale =
36
+ shouldUseCurLocale ||
37
+ !separators.length ||
38
+ (mode === 'auto' && separators.length === 1 && decimalPlaces >= 3);
39
+ if (shouldUseLocale) {
31
40
  return 'withLocale';
32
41
  }
33
- if (mode === 'auto' && separators.length === 1) {
34
- const decimalLength = value.split(`${separators}`)[1].length;
35
- if (decimalLength >= 3) {
36
- return 'withLocale';
37
- }
38
- }
39
- if (separators.length === 1 || separators[0] !== separators[separators.length - 1]) {
42
+
43
+ const shouldUseHeuristic =
44
+ (isCurViewFormattedAndUserIsEditing && decimalPlaces <= 2) ||
45
+ separators.length === 1 ||
46
+ separators[0] !== separators[separators.length - 1];
47
+ if (shouldUseHeuristic) {
40
48
  return 'heuristic';
41
49
  }
50
+
42
51
  return 'unparseable';
43
52
  }
44
53
 
@@ -82,7 +82,7 @@ describe('parseNumber()', () => {
82
82
  expect(parseNumber('123.456,78', { mode: 'auto' })).to.equal(123456.78);
83
83
  expect(
84
84
  parseNumber('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
85
- ).to.equal(123.45678);
85
+ ).to.equal(123456.78);
86
86
  });
87
87
 
88
88
  it('detects separators unparseable when there are 2 same ones e.g. 1.234.56', () => {
@@ -67,11 +67,6 @@ export declare class LocalizeMixinHost {
67
67
 
68
68
  public localizeNamespacesLoaded: Promise<Object> | undefined;
69
69
 
70
- /**
71
- * Hook into LitElement to only render once all translations are loaded
72
- */
73
- public performUpdate(): Promise<void>;
74
-
75
70
  public onLocaleReady(): void;
76
71
  public onLocaleChanged(newLocale: string, oldLocale: string): void;
77
72
  public onLocaleUpdated(): void;
@@ -489,9 +489,9 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
489
489
  * @param {KeyboardEvent} ev
490
490
  * @protected
491
491
  */
492
- // TODO: rename to _onKeyUp in v1
492
+ // TODO: rename to #invokerOnKeyUp() (and move event listener to the invoker) in v1
493
493
  __onKeyUp(ev) {
494
- if (this.disabled || this.readOnly) {
494
+ if (this.disabled || this.readOnly || this.singleOption) {
495
495
  return;
496
496
  }
497
497
 
@@ -529,6 +529,22 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
529
529
  this.opened = true;
530
530
  }
531
531
  break;
532
+ case 'ArrowLeft':
533
+ ev.preventDefault();
534
+ if (this.navigateWithinInvoker) {
535
+ this.setCheckedIndex(
536
+ this._getPreviousEnabledOption(/** @type {number} */ (this.checkedIndex)),
537
+ );
538
+ }
539
+ break;
540
+ case 'ArrowRight':
541
+ ev.preventDefault();
542
+ if (this.navigateWithinInvoker) {
543
+ this.setCheckedIndex(
544
+ this._getNextEnabledOption(/** @type {number} */ (this.checkedIndex)),
545
+ );
546
+ }
547
+ break;
532
548
  default:
533
549
  if (!this._noTypeAhead) {
534
550
  this._handleTypeAhead(ev, { setAsChecked: true });