@lion/ui 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/components/combobox/src/LionCombobox.js +6 -2
  2. package/components/combobox/test/lion-combobox.test.js +58 -3
  3. package/components/core/test/ScopedElementsMixin.test.js +3 -3
  4. package/components/dialog/test/lion-dialog.test.js +54 -0
  5. package/components/dialog/test-helpers/test-router.js +43 -0
  6. package/components/form-core/src/FormatMixin.js +66 -18
  7. package/components/form-core/test-suites/FormatMixin.suite.js +107 -8
  8. package/components/form-core/types/FormatMixinTypes.ts +4 -1
  9. package/components/form-integrations/test/dialog-integrations.test.js +6 -2
  10. package/components/form-integrations/test/form-integrations.test.js +2 -2
  11. package/components/input-amount/src/parsers.js +3 -3
  12. package/components/input-amount/test/lion-input-amount.test.js +36 -2
  13. package/components/input-amount/test/parsers.test.js +11 -0
  14. package/components/input-datepicker/translations/es.js +1 -1
  15. package/components/input-stepper/src/LionInputStepper.js +70 -42
  16. package/components/input-stepper/test/lion-input-stepper.test.js +48 -12
  17. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +7 -6
  18. package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +50 -0
  19. package/components/localize/src/number/getDecimalSeparator.js +1 -1
  20. package/components/localize/src/number/parseNumber.js +9 -6
  21. package/components/localize/test/number/parseNumber.test.js +11 -0
  22. package/components/localize/types/LocalizeMixinTypes.ts +3 -3
  23. package/components/overlays/src/OverlayController.js +4 -5
  24. package/components/overlays/src/OverlayMixin.js +20 -14
  25. package/components/overlays/test/OverlayController.test.js +11 -8
  26. package/components/overlays/test-suites/OverlayMixin.suite.js +184 -9
  27. package/components/overlays/types/ArrowMixinTypes.ts +11 -2
  28. package/components/overlays/types/OverlayConfig.ts +15 -13
  29. package/components/select-rich/src/LionSelectRich.js +3 -1
  30. package/components/select-rich/test/lion-select-rich.test.js +44 -0
  31. package/custom-elements.json +1337 -115
  32. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  33. package/dist-types/components/dialog/test/lion-dialog.test.d.ts.map +1 -1
  34. package/dist-types/components/dialog/test-helpers/test-router.d.ts +2 -0
  35. package/dist-types/components/dialog/test-helpers/test-router.d.ts.map +1 -0
  36. package/dist-types/components/form-core/src/FormatMixin.d.ts +1 -1
  37. package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
  38. package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
  39. package/dist-types/components/form-core/types/FormatMixinTypes.d.ts +3 -2
  40. package/dist-types/components/form-core/types/FormatMixinTypes.d.ts.map +1 -1
  41. package/dist-types/components/form-integrations/test/dialog-integrations.test.d.ts.map +1 -1
  42. package/dist-types/components/input-amount/src/parsers.d.ts +2 -2
  43. package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
  44. package/dist-types/components/input-amount/test/lion-input-amount.test.d.ts.map +1 -1
  45. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +22 -24
  46. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  47. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
  48. package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts.map +1 -1
  49. package/dist-types/components/localize/src/number/parseNumber.d.ts.map +1 -1
  50. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +3 -2
  51. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
  52. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  53. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  54. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts +4 -4
  55. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
  56. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts +2 -2
  57. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
  58. package/dist-types/components/overlays/types/OverlayConfig.d.ts +4 -4
  59. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  60. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -1
  61. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  62. package/dist-types/components/select-rich/test/lion-select-rich.test.d.ts.map +1 -1
  63. package/dist-types/tsconfig.tsbuildinfo +1 -1
  64. package/docs/components/checkbox-group/api-table.md +2 -2
  65. package/docs/components/checkbox-group/overview.md +2 -2
  66. package/docs/components/collapsible/overview.md +1 -1
  67. package/docs/components/combobox/api-table.md +1 -0
  68. package/docs/components/combobox/overview.md +1 -1
  69. package/docs/components/combobox/use-cases.md +1 -1
  70. package/docs/components/dialog/api-table.md +1 -0
  71. package/docs/components/dialog/overview.md +1 -1
  72. package/docs/components/fieldset/overview.md +5 -5
  73. package/docs/components/form/overview.md +3 -3
  74. package/docs/components/form/use-cases.md +2 -2
  75. package/docs/components/icon/use-cases.md +1 -1
  76. package/docs/components/input/api-table.md +1 -1
  77. package/docs/components/input/overview.md +1 -1
  78. package/docs/components/input-amount/api-table.md +1 -1
  79. package/docs/components/input-amount/overview.md +3 -3
  80. package/docs/components/input-date/api-table.md +1 -1
  81. package/docs/components/input-date/overview.md +3 -3
  82. package/docs/components/input-datepicker/api-table.md +2 -1
  83. package/docs/components/input-datepicker/overview.md +5 -5
  84. package/docs/components/input-email/api-table.md +1 -1
  85. package/docs/components/input-email/overview.md +2 -2
  86. package/docs/components/input-file/api-table.md +1 -1
  87. package/docs/components/input-file/overview.md +1 -1
  88. package/docs/components/input-iban/api-table.md +1 -1
  89. package/docs/components/input-iban/overview.md +2 -2
  90. package/docs/components/input-range/api-table.md +1 -1
  91. package/docs/components/input-range/overview.md +2 -2
  92. package/docs/components/input-stepper/api-table.md +4 -4
  93. package/docs/components/input-stepper/overview.md +1 -1
  94. package/docs/components/input-tel/api-table.md +1 -1
  95. package/docs/components/input-tel/overview.md +1 -1
  96. package/docs/components/input-tel/use-cases.md +6 -6
  97. package/docs/components/input-tel-dropdown/api-table.md +1 -1
  98. package/docs/components/input-tel-dropdown/overview.md +1 -1
  99. package/docs/components/listbox/api-table.md +1 -1
  100. package/docs/components/listbox/use-cases.md +1 -1
  101. package/docs/components/radio-group/api-table.md +1 -1
  102. package/docs/components/radio-group/overview.md +3 -3
  103. package/docs/components/select/api-table.md +1 -1
  104. package/docs/components/select/use-cases.md +1 -1
  105. package/docs/components/select-rich/api-table.md +1 -0
  106. package/docs/components/select-rich/use-cases.md +1 -1
  107. package/docs/components/switch/api-table.md +1 -1
  108. package/docs/components/tabs/examples.md +1 -1
  109. package/docs/components/textarea/api-table.md +1 -1
  110. package/docs/components/tooltip/api-table.md +1 -0
  111. package/docs/fundamentals/index.md +4 -4
  112. package/docs/fundamentals/node-tools/providence-analytics/overview.md +5 -5
  113. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  114. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  115. package/docs/fundamentals/rationales/accessibility.md +1 -1
  116. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  117. package/docs/fundamentals/systems/core/overview.md +1 -1
  118. package/docs/fundamentals/systems/form/api-table.md +1 -1
  119. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  120. package/docs/fundamentals/systems/form/model-value.md +1 -1
  121. package/docs/fundamentals/systems/form/overview.md +28 -28
  122. package/docs/fundamentals/systems/form/validate.md +2 -2
  123. package/docs/fundamentals/systems/localize/overview.md +4 -4
  124. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  125. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  126. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  127. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  128. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  129. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  130. package/docs/guides/how-to/create-a-form.md +4 -4
  131. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  132. package/docs/guides/index.md +8 -8
  133. package/docs/guides/principles/index.md +5 -5
  134. package/docs/guides/principles/styling.md +1 -1
  135. package/package.json +1 -1
@@ -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
  });
@@ -1,3 +1,3 @@
1
1
  export default {
2
- openDatepickerLabel: 'Selector de fecha abierta',
2
+ openDatepickerLabel: 'Abrir selector de fecha',
3
3
  };
@@ -21,6 +21,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
21
21
  .input-group__container > .input-group__input ::slotted(.form-control) {
22
22
  text-align: center;
23
23
  }
24
+
25
+ .input-stepper__value {
26
+ position: absolute;
27
+ width: 1px;
28
+ height: 1px;
29
+ overflow: hidden;
30
+ clip-path: inset(100%);
31
+ clip: rect(1px, 1px, 1px, 1px);
32
+ white-space: nowrap;
33
+ border: 0;
34
+ margin: 0;
35
+ padding: 0;
36
+ }
24
37
  `,
25
38
  ];
26
39
  }
@@ -83,7 +96,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
83
96
 
84
97
  this.__increment = this.__increment.bind(this);
85
98
  this.__decrement = this.__decrement.bind(this);
86
- this.__boundOnLeaveButton = this._onLeaveButton.bind(this);
99
+ this._onEnterButton = this._onEnterButton.bind(this);
100
+ this._onLeaveButton = this._onLeaveButton.bind(this);
87
101
  }
88
102
 
89
103
  connectedCallback() {
@@ -93,11 +107,12 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
93
107
  min: this.min,
94
108
  step: this.step,
95
109
  };
96
-
97
- this.role = 'spinbutton';
110
+ if (this._inputNode) {
111
+ this._inputNode.role = 'spinbutton';
112
+ this._inputNode.setAttribute('inputmode', 'decimal');
113
+ this._inputNode.setAttribute('autocomplete', 'off');
114
+ }
98
115
  this.addEventListener('keydown', this.__keyDownHandler);
99
- this._inputNode.setAttribute('inputmode', 'decimal');
100
- this._inputNode.setAttribute('autocomplete', 'off');
101
116
  this.__setDefaultValidators();
102
117
  this.__toggleSpinnerButtonsState();
103
118
  }
@@ -119,9 +134,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
119
134
  this._inputNode.min = `${this.min}`;
120
135
  this.values.min = this.min;
121
136
  if (this.min !== Infinity) {
122
- this.setAttribute('aria-valuemin', `${this.min}`);
137
+ this._inputNode.setAttribute('aria-valuemin', `${this.min}`);
123
138
  } else {
124
- this.removeAttribute('aria-valuemin');
139
+ this._inputNode.removeAttribute('aria-valuemin');
125
140
  }
126
141
  this.__toggleSpinnerButtonsState();
127
142
  }
@@ -130,9 +145,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
130
145
  this._inputNode.max = `${this.max}`;
131
146
  this.values.max = this.max;
132
147
  if (this.max !== Infinity) {
133
- this.setAttribute('aria-valuemax', `${this.max}`);
148
+ this._inputNode.setAttribute('aria-valuemax', `${this.max}`);
134
149
  } else {
135
- this.removeAttribute('aria-valuemax');
150
+ this._inputNode.removeAttribute('aria-valuemax');
136
151
  }
137
152
  this.__toggleSpinnerButtonsState();
138
153
  }
@@ -145,14 +160,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
145
160
  this._inputNode.step = `${this.step}`;
146
161
  this.values.step = this.step;
147
162
  }
148
-
149
- if (changedProperties.has('_ariaLabelledNodes')) {
150
- this.__reflectAriaAttrToSpinButton('aria-labelledby', this._ariaLabelledNodes);
151
- }
152
-
153
- if (changedProperties.has('_ariaDescribedNodes')) {
154
- this.__reflectAriaAttrToSpinButton('aria-describedby', this._ariaDescribedNodes);
155
- }
156
163
  }
157
164
 
158
165
  get slots() {
@@ -163,22 +170,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
163
170
  };
164
171
  }
165
172
 
166
- /**
167
- * Based on FormControlMixin __reflectAriaAttr()
168
- *
169
- * Will handle help text, validation feedback and character counter,
170
- * prefix/suffix/before/after (if they contain data-description flag attr).
171
- * Also, contents of id references that will be put in the <lion-field>._ariaDescribedby property
172
- * from an external context, will be read by a screen reader.
173
- * @param {string} attrName
174
- * @param {Element[]} nodes
175
- * @private
176
- */
177
- __reflectAriaAttrToSpinButton(attrName, nodes) {
178
- const string = nodes.map(n => n.id).join(' ');
179
- this.setAttribute(attrName, string);
180
- }
181
-
182
173
  /**
183
174
  * Set aria labels and apply validators
184
175
  * @private
@@ -227,7 +218,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
227
218
  }
228
219
  decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
229
220
  incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
230
-
231
221
  this._updateAriaAttributes();
232
222
  }
233
223
 
@@ -237,21 +227,22 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
237
227
  _updateAriaAttributes() {
238
228
  const displayValue = this._inputNode.value;
239
229
  if (displayValue) {
240
- this.setAttribute('aria-valuenow', `${displayValue}`);
230
+ this._inputNode.setAttribute('aria-valuenow', `${displayValue}`);
241
231
  if (
242
232
  Object.keys(this.valueTextMapping).length !== 0 &&
243
233
  Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
244
234
  ) {
245
- this.setAttribute('aria-valuetext', `${this.valueTextMapping[this.currentValue]}`);
235
+ this.__valueText = this.valueTextMapping[this.currentValue];
246
236
  } else {
247
237
  // VoiceOver announces percentages once the valuemin or valuemax are used.
248
238
  // This can be fixed by setting valuetext to the same value as valuenow
249
239
  // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
250
- this.setAttribute('aria-valuetext', `${displayValue}`);
240
+ this.__valueText = displayValue;
251
241
  }
242
+ this._inputNode.setAttribute('aria-valuetext', `${this.__valueText}`);
252
243
  } else {
253
- this.removeAttribute('aria-valuenow');
254
- this.removeAttribute('aria-valuetext');
244
+ this._inputNode.removeAttribute('aria-valuenow');
245
+ this._inputNode.removeAttribute('aria-valuetext');
255
246
  }
256
247
  }
257
248
 
@@ -373,7 +364,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
373
364
  <button
374
365
  ?disabled=${this.disabled || this.readOnly}
375
366
  @click=${this.__decrement}
376
- @blur=${this.__boundOnLeaveButton}
367
+ @focus=${this._onEnterButton}
368
+ @blur=${this._onLeaveButton}
377
369
  type="button"
378
370
  aria-label="${this.msgLit('lion-input-stepper:decrease')}"
379
371
  >
@@ -392,7 +384,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
392
384
  <button
393
385
  ?disabled=${this.disabled || this.readOnly}
394
386
  @click=${this.__increment}
395
- @blur=${this.__boundOnLeaveButton}
387
+ @focus=${this._onEnterButton}
388
+ @blur=${this._onLeaveButton}
396
389
  type="button"
397
390
  aria-label="${this.msgLit('lion-input-stepper:increase')}"
398
391
  >
@@ -401,6 +394,33 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
401
394
  `;
402
395
  }
403
396
 
397
+ /** @protected */
398
+ _inputGroupTemplate() {
399
+ return html`
400
+ <div class="input-stepper__value">${this.__valueText}</div>
401
+ <div class="input-group">
402
+ ${this._inputGroupBeforeTemplate()}
403
+ <div class="input-group__container">
404
+ ${this._inputGroupPrefixTemplate()} ${this._inputGroupInputTemplate()}
405
+ ${this._inputGroupSuffixTemplate()}
406
+ </div>
407
+ ${this._inputGroupAfterTemplate()}
408
+ </div>
409
+ `;
410
+ }
411
+
412
+ /**
413
+ * @protected
414
+ * @param {Event} ev
415
+ */
416
+ // eslint-disable-next-line no-unused-vars
417
+ _onEnterButton(ev) {
418
+ const valueNode = /** @type {HTMLElement} */ (
419
+ this.shadowRoot?.querySelector('.input-stepper__value')
420
+ );
421
+ valueNode.setAttribute('aria-live', 'assertive');
422
+ }
423
+
404
424
  /**
405
425
  * Redispatch leave event on host when catching leave event
406
426
  * on the incrementor and decrementor button.
@@ -410,8 +430,16 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
410
430
  *
411
431
  * Interacting with the buttons is "user interactions"
412
432
  * the same way as focusing + blurring the field (native input)
433
+ *
434
+ * @protected
435
+ * @param {Event} ev
413
436
  */
414
- _onLeaveButton() {
437
+ // eslint-disable-next-line no-unused-vars
438
+ _onLeaveButton(ev) {
439
+ const valueNode = /** @type {HTMLElement} */ (
440
+ this.shadowRoot?.querySelector('.input-stepper__value')
441
+ );
442
+ valueNode.removeAttribute('aria-live');
415
443
  this.dispatchEvent(new Event(this._leaveEvent));
416
444
  }
417
445
  }
@@ -278,17 +278,23 @@ describe('<lion-input-stepper>', () => {
278
278
  await expect(el).to.be.accessible();
279
279
  });
280
280
 
281
+ it('has role="spinbutton"', async () => {
282
+ const el = await fixture(defaultInputStepper);
283
+ expect(el._inputNode.hasAttribute('role')).to.be.true;
284
+ expect(el._inputNode.getAttribute('role')).to.equal('spinbutton');
285
+ });
286
+
281
287
  it('updates aria-valuenow when stepper is changed', async () => {
282
288
  const el = await fixture(defaultInputStepper);
283
289
  el.modelValue = 1;
284
290
 
285
291
  await el.updateComplete;
286
- expect(el.hasAttribute('aria-valuenow')).to.be.true;
287
- expect(el.getAttribute('aria-valuenow')).to.equal('1');
292
+ expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.true;
293
+ expect(el._inputNode.getAttribute('aria-valuenow')).to.equal('1');
288
294
 
289
295
  el.modelValue = '';
290
296
  await el.updateComplete;
291
- expect(el.hasAttribute('aria-valuenow')).to.be.false;
297
+ expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.false;
292
298
  });
293
299
 
294
300
  it('updates aria-valuetext when stepper is changed', async () => {
@@ -299,12 +305,12 @@ describe('<lion-input-stepper>', () => {
299
305
  el.modelValue = 1;
300
306
  await el.updateComplete;
301
307
 
302
- expect(el.hasAttribute('aria-valuetext')).to.be.true;
303
- expect(el.getAttribute('aria-valuetext')).to.equal('1');
308
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
309
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('1');
304
310
 
305
311
  el.modelValue = '';
306
312
  await el.updateComplete;
307
- expect(el.hasAttribute('aria-valuetext')).to.be.false;
313
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.false;
308
314
  });
309
315
 
310
316
  it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
@@ -318,30 +324,60 @@ describe('<lion-input-stepper>', () => {
318
324
  `);
319
325
  el.modelValue = 1;
320
326
  await el.updateComplete;
321
- expect(el.hasAttribute('aria-valuetext')).to.be.true;
322
- expect(el.getAttribute('aria-valuetext')).to.equal('first');
327
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
328
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('first');
323
329
  });
324
330
 
325
331
  it('updates aria-valuemin when stepper is changed', async () => {
326
332
  const el = await fixture(inputStepperWithAttrs);
327
333
  const incrementButton = el.querySelector('[slot=suffix]');
328
334
  incrementButton?.dispatchEvent(new Event('click'));
329
- expect(el).to.have.attribute('aria-valuemin', '100');
335
+ expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
336
+ expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('100');
330
337
 
331
338
  el.min = 0;
332
339
  await el.updateComplete;
333
- expect(el).to.have.attribute('aria-valuemin', '0');
340
+ expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
341
+ expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('0');
334
342
  });
335
343
 
336
344
  it('updates aria-valuemax when stepper is changed', async () => {
337
345
  const el = await fixture(inputStepperWithAttrs);
338
346
  const incrementButton = el.querySelector('[slot=suffix]');
339
347
  incrementButton?.dispatchEvent(new Event('click'));
340
- expect(el).to.have.attribute('aria-valuemax', '200');
348
+ expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
349
+ expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('200');
341
350
 
342
351
  el.max = 1000;
343
352
  await el.updateComplete;
344
- expect(el).to.have.attribute('aria-valuemax', '1000');
353
+ expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
354
+ expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
355
+ });
356
+
357
+ it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
358
+ const el = await fixture(inputStepperWithAttrs);
359
+ const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
360
+ const decrementButton = el.querySelector('[slot=prefix]');
361
+
362
+ decrementButton?.dispatchEvent(new Event('focus'));
363
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
364
+ expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
365
+
366
+ decrementButton?.dispatchEvent(new Event('blur'));
367
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
368
+ });
369
+
370
+ it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
371
+ const el = await fixture(inputStepperWithAttrs);
372
+ const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
373
+ const incrementButton = el.querySelector('[slot=suffix]');
374
+
375
+ incrementButton?.dispatchEvent(new Event('focus'));
376
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
377
+ expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
378
+
379
+ incrementButton?.dispatchEvent(new Event('blur'));
380
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
345
381
  });
346
382
  });
347
383
  });
@@ -158,11 +158,11 @@ export class LionInputTelDropdown extends LionInputTel {
158
158
  super.styles,
159
159
  css`
160
160
  /**
161
- * We need to align the height of the dropdown with the height of the text field.
162
- * We target the HTMLDivElement (render wrapper from SlotMixin) here. Its child,
163
- * [data-ref=dropdown], recieves a 100% height as well via inline styles (since we
164
- * can't target from shadow styles).
165
- */
161
+ * We need to align the height of the dropdown with the height of the text field.
162
+ * We target the HTMLDivElement (render wrapper from SlotMixin) here. Its child,
163
+ * [data-ref=dropdown], receives a 100% height as well via inline styles (since we
164
+ * can't target from shadow styles).
165
+ */
166
166
  ::slotted([slot='prefix']) {
167
167
  height: 100%;
168
168
  }
@@ -301,7 +301,8 @@ export class LionInputTelDropdown extends LionInputTel {
301
301
  } else {
302
302
  this.__initializedRegionCode = `+${countryCode}`;
303
303
  }
304
- this.modelValue = this.__initializedRegionCode;
304
+ // on init we set the value to the _inputNode and not the modelValue to prevent validation
305
+ this._inputNode.value = this.__initializedRegionCode;
305
306
  this._initialModelValue = this.__initializedRegionCode;
306
307
  this.initInteractionState();
307
308
  }
@@ -9,8 +9,10 @@ import { LionOption } from '@lion/ui/listbox.js';
9
9
  import { ref } from 'lit/directives/ref.js';
10
10
  import { html } from 'lit';
11
11
 
12
+ import { mockPhoneUtilManager, restorePhoneUtilManager } from '@lion/ui/input-tel-test-helpers.js';
12
13
  import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
13
14
  import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
15
+ import '@lion/ui/define/lion-input-tel-dropdown.js';
14
16
 
15
17
  /**
16
18
  * @typedef {import('../types/index.js').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
@@ -67,6 +69,18 @@ describe('WithFormControlInputTelDropdown', () => {
67
69
  // Runs it for LionSelectRich, which uses .modelValue/@model-value-changed instead of .value/@change
68
70
  runInputTelDropdownSuite({ klass: WithFormControlInputTelDropdown });
69
71
 
72
+ it("it doesn't set the country as modelValue, only as viewValue", async () => {
73
+ const el = /** @type {LionInputTelDropdown} */ (
74
+ await fixture(html`
75
+ <lion-input-tel-dropdown .allowedRegions="${['NL']}"></lion-input-tel-dropdown>
76
+ `)
77
+ );
78
+
79
+ // @ts-expect-error [allow-protected-in-tests]
80
+ expect(el._inputNode.value).to.equal('+31');
81
+ expect(el.modelValue).to.equal('');
82
+ });
83
+
70
84
  it('focuses the textbox right after selection if selected via opened dropdown if interaction-mode is mac', async () => {
71
85
  class InputTelDropdownMac extends LionInputTelDropdown {
72
86
  static templates = {
@@ -172,4 +186,40 @@ describe('WithFormControlInputTelDropdown', () => {
172
186
  expect(isActiveElement(el._inputNode)).to.be.false;
173
187
  }
174
188
  });
189
+
190
+ describe('defaultValidators', () => {
191
+ /** @type {(value:any) => void} */
192
+ let resolveLoaded;
193
+ beforeEach(() => {
194
+ ({ resolveLoaded } = mockPhoneUtilManager());
195
+ });
196
+
197
+ afterEach(() => {
198
+ restorePhoneUtilManager();
199
+ });
200
+
201
+ it('without interaction are not called', async () => {
202
+ const el = /** @type {LionInputTelDropdown} */ (
203
+ await fixture(html`
204
+ <lion-input-tel-dropdown .allowedRegions="${['NL']}"></lion-input-tel-dropdown>
205
+ `)
206
+ );
207
+ resolveLoaded(undefined);
208
+ await aTimeout(0);
209
+ expect(el.hasFeedbackFor).to.deep.equal([]);
210
+ });
211
+
212
+ it('with interaction are called', async () => {
213
+ const el = /** @type {LionInputTelDropdown} */ (
214
+ await fixture(html`
215
+ <lion-input-tel-dropdown .allowedRegions="${['NL']}"></lion-input-tel-dropdown>
216
+ `)
217
+ );
218
+ el.modelValue = '+31 6';
219
+
220
+ resolveLoaded(undefined);
221
+ await aTimeout(0);
222
+ expect(el.hasFeedbackFor).to.deep.equal(['error']);
223
+ });
224
+ });
175
225
  });
@@ -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
 
@@ -1191,9 +1191,8 @@ export class OverlayController extends EventTarget {
1191
1191
  const hasPressedInside =
1192
1192
  event.composedPath().includes(this.contentNode) ||
1193
1193
  deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1194
- if (!hasPressedInside) {
1195
- this.hide();
1196
- }
1194
+ if (hasPressedInside) return;
1195
+ this.hide();
1197
1196
  };
1198
1197
 
1199
1198
  /**
@@ -1206,7 +1205,7 @@ export class OverlayController extends EventTarget {
1206
1205
  if (this.invokerNode) {
1207
1206
  this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
1208
1207
  }
1209
- } else if (phase === 'hide') {
1208
+ } else if (phase === 'hide' || phase === 'teardown') {
1210
1209
  this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
1211
1210
  if (this.invokerNode) {
1212
1211
  this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
@@ -1221,7 +1220,7 @@ export class OverlayController extends EventTarget {
1221
1220
  _handleHidesOnOutsideEsc({ phase }) {
1222
1221
  if (phase === 'show') {
1223
1222
  document.addEventListener('keyup', this.#outsideEscKeyHandler);
1224
- } else if (phase === 'hide') {
1223
+ } else if (phase === 'hide' || phase === 'teardown') {
1225
1224
  document.removeEventListener('keyup', this.#outsideEscKeyHandler);
1226
1225
  }
1227
1226
  }