@lion/ui 0.15.1 → 0.15.3

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 (143) hide show
  1. package/README.md +44 -9
  2. package/components/dialog/test/lion-dialog.integration.test.js +170 -0
  3. package/components/form-core/src/validate/LionValidationFeedback.js +2 -2
  4. package/components/form-core/src/validate/ValidateMixin.js +3 -3
  5. package/components/form-core/test/validate/lion-validation-feedback.test.js +2 -2
  6. package/components/form-core/test-suites/ValidateMixin.suite.js +23 -11
  7. package/components/form-core/translations/bg.js +4 -4
  8. package/components/form-core/translations/cs.js +4 -4
  9. package/components/form-core/translations/de.js +4 -4
  10. package/components/form-core/translations/en.js +4 -4
  11. package/components/form-core/translations/es.js +4 -4
  12. package/components/form-core/translations/fr.js +4 -4
  13. package/components/form-core/translations/hu.js +4 -4
  14. package/components/form-core/translations/it.js +4 -4
  15. package/components/form-core/translations/nl.js +4 -4
  16. package/components/form-core/translations/pl.js +4 -4
  17. package/components/form-core/translations/ro.js +4 -4
  18. package/components/form-core/translations/ru.js +4 -4
  19. package/components/form-core/translations/sk.js +4 -4
  20. package/components/form-core/translations/uk.js +4 -4
  21. package/components/form-core/translations/zh.js +4 -4
  22. package/components/input-amount-dropdown/src/localizeNamespaceLoader.js +53 -0
  23. package/components/input-amount-dropdown/translations/bg.js +5 -0
  24. package/components/input-amount-dropdown/translations/cs.js +5 -0
  25. package/components/input-amount-dropdown/translations/de.js +5 -0
  26. package/components/input-amount-dropdown/translations/es.js +5 -0
  27. package/components/input-amount-dropdown/translations/fr.js +5 -0
  28. package/components/input-amount-dropdown/translations/hu.js +5 -0
  29. package/components/input-amount-dropdown/translations/id.js +5 -0
  30. package/components/input-amount-dropdown/translations/it.js +5 -0
  31. package/components/input-amount-dropdown/translations/nl.js +5 -0
  32. package/components/input-amount-dropdown/translations/pl.js +5 -0
  33. package/components/input-amount-dropdown/translations/ro.js +5 -0
  34. package/components/input-amount-dropdown/translations/ru.js +5 -0
  35. package/components/input-amount-dropdown/translations/sk.js +5 -0
  36. package/components/input-amount-dropdown/translations/uk.js +5 -0
  37. package/components/input-amount-dropdown/translations/zh.js +5 -0
  38. package/components/input-stepper/src/LionInputStepper.js +48 -52
  39. package/components/input-stepper/test/lion-input-stepper.test.js +253 -41
  40. package/components/overlays/src/OverlayController.js +34 -7
  41. package/components/overlays/test/OverlayMixin.test.js +2 -2
  42. package/components/overlays/test-suites/OverlayMixin.suite.js +57 -0
  43. package/components/pagination/src/LionPagination.js +2 -1
  44. package/components/pagination/test/lion-pagination.test.js +41 -0
  45. package/components/validate-messages/translations/it.js +2 -2
  46. package/custom-elements.json +158 -170
  47. package/dist-types/components/dialog/test/lion-dialog.integration.test.d.ts +3 -0
  48. package/dist-types/components/dialog/test/lion-dialog.integration.test.d.ts.map +1 -0
  49. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  50. package/dist-types/components/input-amount-dropdown/src/localizeNamespaceLoader.d.ts +1 -1
  51. package/dist-types/components/input-amount-dropdown/src/localizeNamespaceLoader.d.ts.map +1 -1
  52. package/dist-types/components/input-amount-dropdown/translations/bg.d.ts +7 -0
  53. package/dist-types/components/input-amount-dropdown/translations/bg.d.ts.map +1 -0
  54. package/dist-types/components/input-amount-dropdown/translations/cs.d.ts +7 -0
  55. package/dist-types/components/input-amount-dropdown/translations/cs.d.ts.map +1 -0
  56. package/dist-types/components/input-amount-dropdown/translations/de.d.ts +7 -0
  57. package/dist-types/components/input-amount-dropdown/translations/de.d.ts.map +1 -0
  58. package/dist-types/components/input-amount-dropdown/translations/es.d.ts +7 -0
  59. package/dist-types/components/input-amount-dropdown/translations/es.d.ts.map +1 -0
  60. package/dist-types/components/input-amount-dropdown/translations/fr.d.ts +7 -0
  61. package/dist-types/components/input-amount-dropdown/translations/fr.d.ts.map +1 -0
  62. package/dist-types/components/input-amount-dropdown/translations/hu.d.ts +7 -0
  63. package/dist-types/components/input-amount-dropdown/translations/hu.d.ts.map +1 -0
  64. package/dist-types/components/input-amount-dropdown/translations/id.d.ts +7 -0
  65. package/dist-types/components/input-amount-dropdown/translations/id.d.ts.map +1 -0
  66. package/dist-types/components/input-amount-dropdown/translations/it.d.ts +7 -0
  67. package/dist-types/components/input-amount-dropdown/translations/it.d.ts.map +1 -0
  68. package/dist-types/components/input-amount-dropdown/translations/nl.d.ts +7 -0
  69. package/dist-types/components/input-amount-dropdown/translations/nl.d.ts.map +1 -0
  70. package/dist-types/components/input-amount-dropdown/translations/pl.d.ts +7 -0
  71. package/dist-types/components/input-amount-dropdown/translations/pl.d.ts.map +1 -0
  72. package/dist-types/components/input-amount-dropdown/translations/ro.d.ts +7 -0
  73. package/dist-types/components/input-amount-dropdown/translations/ro.d.ts.map +1 -0
  74. package/dist-types/components/input-amount-dropdown/translations/ru.d.ts +7 -0
  75. package/dist-types/components/input-amount-dropdown/translations/ru.d.ts.map +1 -0
  76. package/dist-types/components/input-amount-dropdown/translations/sk.d.ts +7 -0
  77. package/dist-types/components/input-amount-dropdown/translations/sk.d.ts.map +1 -0
  78. package/dist-types/components/input-amount-dropdown/translations/uk.d.ts +7 -0
  79. package/dist-types/components/input-amount-dropdown/translations/uk.d.ts.map +1 -0
  80. package/dist-types/components/input-amount-dropdown/translations/zh.d.ts +7 -0
  81. package/dist-types/components/input-amount-dropdown/translations/zh.d.ts.map +1 -0
  82. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -20
  83. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  84. package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts.map +1 -1
  85. package/dist-types/components/overlays/src/OverlayController.d.ts +13 -4
  86. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  87. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
  88. package/dist-types/components/pagination/src/LionPagination.d.ts.map +1 -1
  89. package/dist-types/tsconfig.tsbuildinfo +1 -1
  90. package/docs/components/checkbox-group/overview.md +2 -2
  91. package/docs/components/checkbox-group/use-cases.md +3 -9
  92. package/docs/components/collapsible/overview.md +1 -1
  93. package/docs/components/combobox/overview.md +1 -1
  94. package/docs/components/combobox/use-cases.md +1 -1
  95. package/docs/components/dialog/overview.md +1 -1
  96. package/docs/components/dialog/use-cases.md +2 -2
  97. package/docs/components/fieldset/overview.md +5 -5
  98. package/docs/components/form/overview.md +3 -3
  99. package/docs/components/form/use-cases.md +2 -2
  100. package/docs/components/icon/use-cases.md +1 -1
  101. package/docs/components/input/overview.md +1 -1
  102. package/docs/components/input-amount/overview.md +3 -3
  103. package/docs/components/input-date/overview.md +3 -3
  104. package/docs/components/input-datepicker/overview.md +5 -5
  105. package/docs/components/input-email/overview.md +2 -2
  106. package/docs/components/input-file/overview.md +1 -1
  107. package/docs/components/input-iban/overview.md +2 -2
  108. package/docs/components/input-range/overview.md +2 -2
  109. package/docs/components/input-stepper/api-table.md +2 -3
  110. package/docs/components/input-stepper/overview.md +1 -1
  111. package/docs/components/input-tel/overview.md +1 -1
  112. package/docs/components/input-tel/use-cases.md +6 -6
  113. package/docs/components/input-tel-dropdown/overview.md +1 -1
  114. package/docs/components/listbox/use-cases.md +1 -1
  115. package/docs/components/radio-group/overview.md +3 -3
  116. package/docs/components/radio-group/use-cases.md +3 -9
  117. package/docs/components/select/overview.md +1 -1
  118. package/docs/components/select/use-cases.md +6 -6
  119. package/docs/components/select-rich/use-cases.md +2 -2
  120. package/docs/components/tabs/examples.md +1 -1
  121. package/docs/fundamentals/index.md +4 -4
  122. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  123. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  124. package/docs/fundamentals/rationales/accessibility.md +1 -1
  125. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  126. package/docs/fundamentals/systems/core/overview.md +1 -1
  127. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  128. package/docs/fundamentals/systems/form/model-value.md +1 -1
  129. package/docs/fundamentals/systems/form/overview.md +28 -28
  130. package/docs/fundamentals/systems/form/validate.md +2 -2
  131. package/docs/fundamentals/systems/localize/overview.md +4 -4
  132. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  133. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  134. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  135. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  136. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  137. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  138. package/docs/guides/how-to/create-a-form.md +4 -4
  139. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  140. package/docs/guides/index.md +8 -8
  141. package/docs/guides/principles/index.md +5 -5
  142. package/docs/guides/principles/styling.md +1 -1
  143. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Toutes les devises',
3
+ selectCurrency: 'Sélectionner une devise',
4
+ suggestedCurrencies: 'Devises préférées',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Minden pénznem',
3
+ selectCurrency: 'Pénznem kiválasztása',
4
+ suggestedCurrencies: 'Előnyben részesített pénznemek',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Semua mata uang',
3
+ selectCurrency: 'Pilih mata uang',
4
+ suggestedCurrencies: 'Mata uang pilihan',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Tutte le valute',
3
+ selectCurrency: 'Seleziona valuta',
4
+ suggestedCurrencies: 'Valute preferite',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: "Alle valuta's",
3
+ selectCurrency: 'Selecteer valuta',
4
+ suggestedCurrencies: "Voorkeurs valuta's",
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Wszystkie waluty',
3
+ selectCurrency: 'Wybierz walutę',
4
+ suggestedCurrencies: 'Preferowane waluty',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Toate valutele',
3
+ selectCurrency: 'Selectare valută',
4
+ suggestedCurrencies: 'Valute preferate',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Все валюты',
3
+ selectCurrency: 'Выбрать валюту',
4
+ suggestedCurrencies: 'Предпочтительные валюты',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Všetky meny',
3
+ selectCurrency: 'Zvoliť menu',
4
+ suggestedCurrencies: 'Uprednostňované meny',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: 'Усі валюти',
3
+ selectCurrency: 'Вибрати валюту',
4
+ suggestedCurrencies: 'Обрані валюти',
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ allCurrencies: '所有货币',
3
+ selectCurrency: '选择货币',
4
+ suggestedCurrencies: '首选货币',
5
+ };
@@ -1,4 +1,4 @@
1
- import { html, css, render } from 'lit';
1
+ import { html, css, render, nothing } from 'lit';
2
2
  import { formatNumber, LocalizeMixin, parseNumber } from '@lion/ui/localize-no-side-effects.js';
3
3
  import { LionInput } from '@lion/ui/input.js';
4
4
  import { IsNumber, MinNumber, MaxNumber } from '@lion/ui/form-core.js';
@@ -96,8 +96,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
96
96
 
97
97
  this._increment = this._increment.bind(this);
98
98
  this._decrement = this._decrement.bind(this);
99
- this._onEnterButton = this._onEnterButton.bind(this);
100
- this._onLeaveButton = this._onLeaveButton.bind(this);
101
99
  }
102
100
 
103
101
  connectedCallback() {
@@ -244,6 +242,26 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
244
242
  this._inputNode.removeAttribute('aria-valuenow');
245
243
  this._inputNode.removeAttribute('aria-valuetext');
246
244
  }
245
+ this._destroyOutputContent();
246
+ }
247
+
248
+ _destroyOutputContent() {
249
+ const outputElement = /** @type {HTMLElement} */ (
250
+ this.shadowRoot?.querySelector('.input-stepper__value')
251
+ );
252
+
253
+ const timeoutValue = outputElement?.dataset?.selfDestruct
254
+ ? Number(outputElement.dataset.selfDestruct)
255
+ : 2000;
256
+ clearTimeout(this.timer);
257
+ if (outputElement) {
258
+ this.timer = setTimeout(() => {
259
+ if (outputElement.parentNode) {
260
+ this.__valueText = nothing;
261
+ this.requestUpdate();
262
+ }
263
+ }, timeoutValue);
264
+ }
247
265
  }
248
266
 
249
267
  /**
@@ -267,12 +285,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
267
285
  _increment() {
268
286
  const { step, min, max } = this.values;
269
287
  const stepMin = min !== Infinity ? min : 0;
288
+ const epsilon = 1e-10; // Tolerance for floating-point comparison
270
289
 
271
- let newValue = this.currentValue + step;
290
+ let newValue;
272
291
 
273
- if ((this.currentValue + stepMin) % step !== 0) {
274
- // If the value is not aligned to step, align it to the nearest step
275
- newValue = Math.floor(this.currentValue / step) * step + step + (stepMin % step);
292
+ const remainder = (this.currentValue - stepMin) % step;
293
+ const isAligned = Math.abs(remainder) < epsilon || Math.abs(remainder - step) < epsilon;
294
+
295
+ if (!isAligned) {
296
+ // If the value is not aligned to step, align it to the next valid step
297
+ newValue = Math.ceil((this.currentValue - stepMin) / step) * step + stepMin;
298
+ } else {
299
+ // If the value is aligned, just add the step
300
+ newValue = this.currentValue + step;
276
301
  }
277
302
 
278
303
  if (newValue <= max || max === Infinity) {
@@ -289,12 +314,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
289
314
  _decrement() {
290
315
  const { step, max, min } = this.values;
291
316
  const stepMin = min !== Infinity ? min : 0;
317
+ const epsilon = 1e-10; // Tolerance for floating-point comparison
318
+
319
+ let newValue;
292
320
 
293
- let newValue = this.currentValue - step;
321
+ const remainder = (this.currentValue - stepMin) % step;
322
+ const isAligned = Math.abs(remainder) < epsilon || Math.abs(remainder - step) < epsilon;
294
323
 
295
- if ((this.currentValue + stepMin) % step !== 0) {
296
- // If the value is not aligned to step, align it to the nearest step
297
- newValue = Math.floor(this.currentValue / step) * step + (stepMin % step);
324
+ if (!isAligned) {
325
+ // If the value is not aligned to step, align it to the previous valid step
326
+ newValue = Math.floor((this.currentValue - stepMin) / step) * step + stepMin;
327
+ } else {
328
+ // If the value is aligned, just subtract the step
329
+ newValue = this.currentValue - step;
298
330
  }
299
331
 
300
332
  if (newValue >= min || min === Infinity) {
@@ -380,10 +412,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
380
412
  <button
381
413
  ?disabled=${this.disabled || this.readOnly}
382
414
  @click=${this._decrement}
383
- @focus=${this._onEnterButton}
384
- @blur=${this._onLeaveButton}
385
415
  type="button"
386
- aria-label="${this.msgLit('lion-input-stepper:decrease')}"
416
+ aria-label="${this.msgLit('lion-input-stepper:decrease')} ${this.fieldName}"
387
417
  >
388
418
  ${this._decrementorSignTemplate()}
389
419
  </button>
@@ -400,10 +430,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
400
430
  <button
401
431
  ?disabled=${this.disabled || this.readOnly}
402
432
  @click=${this._increment}
403
- @focus=${this._onEnterButton}
404
- @blur=${this._onLeaveButton}
405
433
  type="button"
406
- aria-label="${this.msgLit('lion-input-stepper:increase')}"
434
+ aria-label="${this.msgLit('lion-input-stepper:increase')} ${this.fieldName}"
407
435
  >
408
436
  ${this._incrementorSignTemplate()}
409
437
  </button>
@@ -413,7 +441,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
413
441
  /** @protected */
414
442
  _inputGroupTemplate() {
415
443
  return html`
416
- <div class="input-stepper__value">${this.__valueText}</div>
444
+ <output for="${this._inputId}" data-self-destruct="2000" class="input-stepper__value"
445
+ >${this.__valueText}</output
446
+ >
417
447
  <div class="input-group">
418
448
  ${this._inputGroupBeforeTemplate()}
419
449
  <div class="input-group__container">
@@ -424,38 +454,4 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
424
454
  </div>
425
455
  `;
426
456
  }
427
-
428
- /**
429
- * @protected
430
- * @param {Event} ev
431
- */
432
- // eslint-disable-next-line no-unused-vars
433
- _onEnterButton(ev) {
434
- const valueNode = /** @type {HTMLElement} */ (
435
- this.shadowRoot?.querySelector('.input-stepper__value')
436
- );
437
- valueNode.setAttribute('aria-live', 'assertive');
438
- }
439
-
440
- /**
441
- * Redispatch leave event on host when catching leave event
442
- * on the incrementor and decrementor button.
443
- *
444
- * This redispatched leave event will be caught by
445
- * InteractionStateMixin to set "touched" state to true.
446
- *
447
- * Interacting with the buttons is "user interactions"
448
- * the same way as focusing + blurring the field (native input)
449
- *
450
- * @protected
451
- * @param {Event} ev
452
- */
453
- // eslint-disable-next-line no-unused-vars
454
- _onLeaveButton(ev) {
455
- const valueNode = /** @type {HTMLElement} */ (
456
- this.shadowRoot?.querySelector('.input-stepper__value')
457
- );
458
- valueNode.removeAttribute('aria-live');
459
- this.dispatchEvent(new Event(this._leaveEvent));
460
- }
461
457
  }
@@ -1,4 +1,5 @@
1
1
  import { expect, fixture as _fixture, nextFrame } from '@open-wc/testing';
2
+ import { nothing } from 'lit';
2
3
  import { html } from 'lit/static-html.js';
3
4
  import sinon from 'sinon';
4
5
  import { formatNumber } from '@lion/ui/localize-no-side-effects.js';
@@ -17,6 +18,7 @@ const inputStepperWithAttrs = html`<lion-input-stepper
17
18
  step="10"
18
19
  min="100"
19
20
  max="200"
21
+ label="Label"
20
22
  ></lion-input-stepper>`;
21
23
 
22
24
  describe('<lion-input-stepper>', () => {
@@ -177,31 +179,6 @@ describe('<lion-input-stepper>', () => {
177
179
  expect(counter).to.equal(1);
178
180
  });
179
181
 
180
- it('fires a leave event ("blur") on button clicks', async () => {
181
- const blurSpy = sinon.spy();
182
- const el = await fixture(html`
183
- <lion-input-stepper @blur=${blurSpy} name="year" label="Years"></lion-input-stepper>
184
- `);
185
-
186
- expect(el.value).to.equal('');
187
- const decrementButton = el.querySelector('[slot=prefix]');
188
- decrementButton?.dispatchEvent(new Event('focus'));
189
- decrementButton?.dispatchEvent(new Event('click'));
190
- decrementButton?.dispatchEvent(new Event('blur'));
191
- expect(el.value).to.equal('−1');
192
- expect(blurSpy.calledOnce).to.be.true;
193
- expect(el.touched).to.be.true;
194
-
195
- el.touched = false;
196
- const incrementButton = el.querySelector('[slot=suffix]');
197
- incrementButton?.dispatchEvent(new Event('focus'));
198
- incrementButton?.dispatchEvent(new Event('click'));
199
- incrementButton?.dispatchEvent(new Event('blur'));
200
- expect(el.value).to.equal('0');
201
- expect(blurSpy.calledTwice).to.be.true;
202
- expect(el.touched).to.be.true;
203
- });
204
-
205
182
  it('should update min and max attributes when min and max property change', async () => {
206
183
  const el = await fixture(inputStepperWithAttrs);
207
184
  el.min = 100;
@@ -358,6 +335,251 @@ describe('<lion-input-stepper>', () => {
358
335
  await el.updateComplete;
359
336
  expect(el.modelValue).to.equal(-13, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7
360
337
  });
338
+
339
+ it('handles decimal step values correctly', async () => {
340
+ // Test with decimal step 0.1
341
+ let el = await fixture(
342
+ html`<lion-input-stepper step="0.1" min="0" max="9" value="5.55"></lion-input-stepper>`,
343
+ );
344
+
345
+ // Test increment with decimal step
346
+ let incrementButton = el.querySelector('[slot=suffix]');
347
+ incrementButton?.dispatchEvent(new Event('click'));
348
+ await el.updateComplete;
349
+ expect(el.modelValue).to.equal(5.6, 'Fail + : (0 > 9 by 0.1; val 5.55)');
350
+
351
+ // Test decrement with decimal step
352
+ let decrementButton = el.querySelector('[slot=prefix]');
353
+ decrementButton?.dispatchEvent(new Event('click'));
354
+ await el.updateComplete;
355
+ expect(el.modelValue).to.equal(5.5, 'Fail - : (0 > 9 by 0.1; val 5.6)');
356
+
357
+ // Test with value that needs alignment
358
+ el = await fixture(
359
+ html`<lion-input-stepper step="0.1" min="0" max="9" value="3.27"></lion-input-stepper>`,
360
+ );
361
+
362
+ // Should align to next step when incrementing
363
+ incrementButton = el.querySelector('[slot=suffix]');
364
+ incrementButton?.dispatchEvent(new Event('click'));
365
+ await el.updateComplete;
366
+ expect(el.modelValue).to.equal(3.3, 'Fail + alignment: (0 > 9 by 0.1; val 3.27)');
367
+
368
+ // Reset and test decrement alignment
369
+ el = await fixture(
370
+ html`<lion-input-stepper step="0.1" min="0" max="9" value="3.27"></lion-input-stepper>`,
371
+ );
372
+
373
+ // Should align to previous step when decrementing
374
+ decrementButton = el.querySelector('[slot=prefix]');
375
+ decrementButton?.dispatchEvent(new Event('click'));
376
+ await el.updateComplete;
377
+ expect(el.modelValue).to.equal(3.2, 'Fail - alignment: (0 > 9 by 0.1; val 3.27)');
378
+ });
379
+ });
380
+ });
381
+
382
+ describe('_destroyOutputContent method', () => {
383
+ beforeEach(() => {
384
+ // Ensure clean state for each test
385
+ sinon.restore();
386
+ });
387
+
388
+ it('should clear existing timer before setting a new one', async () => {
389
+ const el = await fixture(defaultInputStepper);
390
+ const clearTimeoutSpy = sinon.spy(window, 'clearTimeout');
391
+
392
+ // Set an initial timer
393
+ el.timer = setTimeout(() => {}, 1000);
394
+ const initialTimer = el.timer;
395
+
396
+ // Call _destroyOutputContent
397
+ el._destroyOutputContent();
398
+
399
+ expect(clearTimeoutSpy.calledWith(initialTimer)).to.be.true;
400
+ clearTimeoutSpy.restore();
401
+ });
402
+
403
+ it('should set __valueText to nothing and request update after timeout', async () => {
404
+ const el = await fixture(defaultInputStepper);
405
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
406
+ const clock = sinon.useFakeTimers();
407
+
408
+ // Set initial value text
409
+ el.__valueText = 'test value';
410
+
411
+ // Call _destroyOutputContent
412
+ el._destroyOutputContent();
413
+
414
+ // Fast forward time by 2000ms (default timeout)
415
+ clock.tick(2000);
416
+
417
+ await expect(el.__valueText).to.equal(nothing);
418
+ expect(requestUpdateSpy.calledOnce).to.be.true;
419
+
420
+ clock.restore();
421
+ requestUpdateSpy.restore();
422
+ });
423
+
424
+ it('should use custom timeout from data-self-destruct attribute', async () => {
425
+ const el = await fixture(html`
426
+ <lion-input-stepper name="test" label="Test"> </lion-input-stepper>
427
+ `);
428
+
429
+ await el.updateComplete;
430
+
431
+ // Get the output element and set custom self-destruct value
432
+ const outputElement = /** @type {HTMLElement} */ (
433
+ el.shadowRoot?.querySelector('.input-stepper__value')
434
+ );
435
+ if (outputElement) {
436
+ outputElement.dataset.selfDestruct = '5000';
437
+ }
438
+
439
+ const clock = sinon.useFakeTimers();
440
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
441
+
442
+ el.__valueText = 'test value';
443
+ el._destroyOutputContent();
444
+
445
+ // Should not trigger after default 2000ms
446
+ clock.tick(2000);
447
+ await expect(el.__valueText).to.equal('test value');
448
+ expect(requestUpdateSpy.called).to.be.false;
449
+
450
+ // Should trigger after custom 5000ms
451
+ clock.tick(3000); // Total 5000ms
452
+ await expect(el.__valueText).to.equal(nothing);
453
+ expect(requestUpdateSpy.calledOnce).to.be.true;
454
+
455
+ clock.restore();
456
+ requestUpdateSpy.restore();
457
+ });
458
+
459
+ it('should handle invalid data-self-destruct value by using default timeout', async () => {
460
+ const el = await fixture(html`
461
+ <lion-input-stepper name="test" label="Test"> </lion-input-stepper>
462
+ `);
463
+
464
+ await el.updateComplete;
465
+
466
+ // Get the output element and set invalid self-destruct value
467
+ const outputElement = /** @type {HTMLElement} */ (
468
+ el.shadowRoot?.querySelector('.input-stepper__value')
469
+ );
470
+ if (outputElement) {
471
+ outputElement.dataset.selfDestruct = 'invalid';
472
+ }
473
+
474
+ const clock = sinon.useFakeTimers();
475
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
476
+
477
+ el.__valueText = 'test value';
478
+ el._destroyOutputContent();
479
+
480
+ // Should use default 2000ms when invalid value is provided
481
+ clock.tick(2000);
482
+ await expect(el.__valueText).to.equal(nothing);
483
+ expect(requestUpdateSpy.calledOnce).to.be.true;
484
+
485
+ clock.restore();
486
+ requestUpdateSpy.restore();
487
+ });
488
+
489
+ it('should not set timeout if output element does not exist', async () => {
490
+ const el = await fixture(defaultInputStepper);
491
+ const setTimeoutSpy = sinon.spy(window, 'setTimeout');
492
+
493
+ // Mock shadowRoot to return null for querySelector
494
+ const originalQuerySelector = el.shadowRoot?.querySelector;
495
+ if (el.shadowRoot) {
496
+ el.shadowRoot.querySelector = () => null;
497
+ }
498
+
499
+ el._destroyOutputContent();
500
+
501
+ expect(setTimeoutSpy.called).to.be.false;
502
+
503
+ // Restore original querySelector
504
+ if (el.shadowRoot && originalQuerySelector) {
505
+ el.shadowRoot.querySelector = originalQuerySelector;
506
+ }
507
+ setTimeoutSpy.restore();
508
+ });
509
+
510
+ it('should only execute timeout callback if output element still has parent', async () => {
511
+ const el = await fixture(defaultInputStepper);
512
+ const clock = sinon.useFakeTimers();
513
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
514
+
515
+ el.__valueText = 'test value';
516
+ el._destroyOutputContent();
517
+
518
+ // Remove the output element from DOM before timeout
519
+ const outputElement = el.shadowRoot?.querySelector('.input-stepper__value');
520
+ if (outputElement && outputElement.parentNode) {
521
+ outputElement.parentNode.removeChild(outputElement);
522
+ }
523
+
524
+ // Fast forward time
525
+ clock.tick(2000);
526
+
527
+ // Should not have updated since element was removed
528
+ expect(requestUpdateSpy.called).to.be.false;
529
+ expect(el.__valueText).to.equal('test value');
530
+
531
+ clock.restore();
532
+ requestUpdateSpy.restore();
533
+ });
534
+
535
+ it('should be called when increment button is clicked', async () => {
536
+ const el = await fixture(defaultInputStepper);
537
+ const destroyOutputSpy = sinon.spy(el, '_destroyOutputContent');
538
+
539
+ const incrementButton = el.querySelector('[slot=suffix]');
540
+ incrementButton?.dispatchEvent(new Event('click'));
541
+
542
+ expect(destroyOutputSpy.calledOnce).to.be.true;
543
+ destroyOutputSpy.restore();
544
+ });
545
+
546
+ it('should be called when decrement button is clicked', async () => {
547
+ const el = await fixture(defaultInputStepper);
548
+ const destroyOutputSpy = sinon.spy(el, '_destroyOutputContent');
549
+
550
+ const decrementButton = el.querySelector('[slot=prefix]');
551
+ decrementButton?.dispatchEvent(new Event('click'));
552
+
553
+ expect(destroyOutputSpy.calledOnce).to.be.true;
554
+ destroyOutputSpy.restore();
555
+ });
556
+
557
+ it('should handle multiple rapid calls by clearing previous timers', async () => {
558
+ const el = await fixture(defaultInputStepper);
559
+ const clock = sinon.useFakeTimers();
560
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
561
+
562
+ el.__valueText = 'test value';
563
+
564
+ // Call _destroyOutputContent multiple times rapidly
565
+ el._destroyOutputContent();
566
+ clock.tick(1000); // 1 second
567
+
568
+ el._destroyOutputContent(); // This should clear the previous timer
569
+ clock.tick(1000); // Total 2 seconds from first call, 1 second from second call
570
+
571
+ // Should not have triggered yet since second call reset the timer
572
+ expect(el.__valueText).to.equal('test value');
573
+ expect(requestUpdateSpy.called).to.be.false;
574
+
575
+ clock.tick(1000); // Total 2 seconds from second call
576
+
577
+ // Should trigger now
578
+ await expect(el.__valueText).to.equal(nothing);
579
+ expect(requestUpdateSpy.calledOnce).to.be.true;
580
+
581
+ clock.restore();
582
+ requestUpdateSpy.restore();
361
583
  });
362
584
  });
363
585
 
@@ -448,30 +670,20 @@ describe('<lion-input-stepper>', () => {
448
670
  expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
449
671
  });
450
672
 
451
- it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
673
+ it('decrease button should have aria-label with the component label', async () => {
452
674
  const el = await fixture(inputStepperWithAttrs);
453
- const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
454
675
  const decrementButton = el.querySelector('[slot=prefix]');
455
676
 
456
- decrementButton?.dispatchEvent(new Event('focus'));
457
- expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
458
- expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
459
-
460
- decrementButton?.dispatchEvent(new Event('blur'));
461
- expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
677
+ expect(decrementButton?.hasAttribute('aria-label')).to.be.true;
678
+ expect(decrementButton?.getAttribute('aria-label')).to.equal('Decrease Label');
462
679
  });
463
680
 
464
- it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
681
+ it('increase button should have aria-label with the component label', async () => {
465
682
  const el = await fixture(inputStepperWithAttrs);
466
- const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
467
683
  const incrementButton = el.querySelector('[slot=suffix]');
468
684
 
469
- incrementButton?.dispatchEvent(new Event('focus'));
470
- expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
471
- expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
472
-
473
- incrementButton?.dispatchEvent(new Event('blur'));
474
- expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
685
+ expect(incrementButton?.hasAttribute('aria-label')).to.be.true;
686
+ expect(incrementButton?.getAttribute('aria-label')).to.equal('Increase Label');
475
687
  });
476
688
  });
477
689
  });
@@ -197,15 +197,24 @@ export class OverlayController extends EventTarget {
197
197
  this._contentId = `overlay-content--${Math.random().toString(36).slice(2, 10)}`;
198
198
  /** @private */
199
199
  this.__originalAttrs = new Map();
200
+ /** @private */
201
+ this.__escKeyHandler = this.__escKeyHandler.bind(this);
200
202
  this.updateConfig(config);
201
203
  /** @private */
202
204
  this.__hasActiveTrapsKeyboardFocus = false;
203
205
  /** @private */
204
206
  this.__hasActiveBackdrop = true;
205
207
  /** @private */
206
- this.__escKeyHandler = this.__escKeyHandler.bind(this);
207
- /** @private */
208
208
  this.__cancelHandler = this.__cancelHandler.bind(this);
209
+ /**
210
+ * The property is used to skip `__escKeyHandler` handler for overlays that have been closed
211
+ * by `__escKeyHandlerCalled` previously.
212
+ * `__escKeyHandlerCalled` is set to `false` right before overlay show up
213
+ * `__escKeyHandlerCalled` is set to `true` when `__escKeyHandler` is called at least one time
214
+ * after each 'show' phase
215
+ * @private
216
+ */
217
+ this.__escKeyHandlerCalled = false;
209
218
  }
210
219
 
211
220
  /**
@@ -1211,13 +1220,21 @@ export class OverlayController extends EventTarget {
1211
1220
  * @returns {void}
1212
1221
  */
1213
1222
  __escKeyHandler(event) {
1214
- if (event.key !== 'Escape' || childDialogsClosedInEventLoopWeakmap.has(event)) return;
1223
+ if (
1224
+ event.key !== 'Escape' ||
1225
+ childDialogsClosedInEventLoopWeakmap.has(event) ||
1226
+ (!this.isShown && this.__escKeyHandlerCalled)
1227
+ ) {
1228
+ return;
1229
+ }
1215
1230
 
1216
1231
  const hasPressedInside =
1217
1232
  event.composedPath().includes(this.contentNode) ||
1233
+ (this.invokerNode && event.composedPath().includes(this.invokerNode)) ||
1218
1234
  deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1219
1235
 
1220
1236
  if (hasPressedInside) {
1237
+ this.__escKeyHandlerCalled = true;
1221
1238
  this.hide();
1222
1239
  // We could do event.stopPropagation() here, but we don't want to hide info for
1223
1240
  // the outside world about user interactions. Instead, we store the event in a WeakMap
@@ -1245,12 +1262,21 @@ export class OverlayController extends EventTarget {
1245
1262
  * @protected
1246
1263
  */
1247
1264
  _handleHidesOnEsc({ phase }) {
1248
- if (phase === 'show') {
1265
+ if (phase === 'init') {
1266
+ // we remove previously added (if any) event listener to guarantee
1267
+ // there is only one Escape handler added here.
1268
+ // Note `init` phase triggered on every `updateConfig` call and that
1269
+ // could happen multiple times during the component life cycle
1270
+ this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
1249
1271
  this.contentNode.addEventListener('keyup', this.__escKeyHandler);
1250
1272
  if (this.invokerNode) {
1251
1273
  this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
1252
1274
  }
1253
- } else if (phase === 'hide' || phase === 'teardown') {
1275
+ }
1276
+ if (phase === 'show') {
1277
+ this.__escKeyHandlerCalled = false;
1278
+ }
1279
+ if (phase === 'teardown') {
1254
1280
  this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
1255
1281
  if (this.invokerNode) {
1256
1282
  this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
@@ -1263,9 +1289,10 @@ export class OverlayController extends EventTarget {
1263
1289
  * @protected
1264
1290
  */
1265
1291
  _handleHidesOnOutsideEsc({ phase }) {
1266
- if (phase === 'show') {
1292
+ if (phase === 'init') {
1293
+ document.removeEventListener('keyup', this.#outsideEscKeyHandler);
1267
1294
  document.addEventListener('keyup', this.#outsideEscKeyHandler);
1268
- } else if (phase === 'hide' || phase === 'teardown') {
1295
+ } else if (phase === 'teardown') {
1269
1296
  document.removeEventListener('keyup', this.#outsideEscKeyHandler);
1270
1297
  }
1271
1298
  }
@@ -7,9 +7,9 @@ const tagString = defineCE(
7
7
  class extends OverlayMixin(LitElement) {
8
8
  render() {
9
9
  return html`
10
- <button slot="invoker">invoker button</button>
10
+ <slot name="invoker"></slot>
11
11
  <div id="overlay-content-node-wrapper">
12
- <div slot="content">content of the overlay</div>
12
+ <slot name="content"></slot>
13
13
  </div>
14
14
  `;
15
15
  }