@lion/ui 0.15.2 → 0.15.4

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 (169) hide show
  1. package/README.md +46 -11
  2. package/components/combobox/src/LionCombobox.js +4 -1
  3. package/components/combobox/test/lion-combobox-integrations.test.js +23 -0
  4. package/components/combobox/test/lion-combobox.test.js +20 -0
  5. package/components/core/src/ScopedStylesController.js +7 -10
  6. package/components/core/test-helpers/getCachedFixture.js +55 -0
  7. package/components/form-core/src/validate/LionValidationFeedback.js +2 -2
  8. package/components/form-core/src/validate/ValidateMixin.js +3 -3
  9. package/components/form-core/test/validate/lion-validation-feedback.test.js +2 -2
  10. package/components/form-core/test-suites/ValidateMixin.suite.js +23 -11
  11. package/components/form-core/translations/bg.js +4 -4
  12. package/components/form-core/translations/cs.js +4 -4
  13. package/components/form-core/translations/de.js +4 -4
  14. package/components/form-core/translations/en.js +4 -4
  15. package/components/form-core/translations/es.js +4 -4
  16. package/components/form-core/translations/fr.js +4 -4
  17. package/components/form-core/translations/hu.js +4 -4
  18. package/components/form-core/translations/it.js +4 -4
  19. package/components/form-core/translations/nl.js +4 -4
  20. package/components/form-core/translations/pl.js +4 -4
  21. package/components/form-core/translations/ro.js +4 -4
  22. package/components/form-core/translations/ru.js +4 -4
  23. package/components/form-core/translations/sk.js +4 -4
  24. package/components/form-core/translations/uk.js +4 -4
  25. package/components/form-core/translations/zh.js +4 -4
  26. package/components/input-amount/src/parsers.js +3 -2
  27. package/components/input-amount/test/parsers.test.js +6 -0
  28. package/components/input-amount-dropdown/src/currencyUtil.js +1 -1
  29. package/components/input-file/src/localizeNamespaceLoader.js +4 -0
  30. package/components/input-file/translations/bg.js +5 -0
  31. package/components/input-file/translations/cs.js +5 -0
  32. package/components/input-file/translations/es.js +5 -0
  33. package/components/input-file/translations/fr.js +5 -0
  34. package/components/input-file/translations/hu.js +5 -0
  35. package/components/input-file/translations/id-ID.js +5 -0
  36. package/components/input-file/translations/id.js +19 -0
  37. package/components/input-file/translations/it.js +5 -0
  38. package/components/input-file/translations/pl.js +5 -0
  39. package/components/input-file/translations/ro.js +5 -0
  40. package/components/input-file/translations/sk.js +5 -0
  41. package/components/input-file/translations/uk.js +5 -0
  42. package/components/input-file/translations/zh.js +5 -0
  43. package/components/input-stepper/src/LionInputStepper.js +48 -52
  44. package/components/input-stepper/test/lion-input-stepper-integrations.test.js +2 -19
  45. package/components/input-stepper/test/lion-input-stepper.test.js +2 -476
  46. package/components/input-stepper/test-suites/lion-input-stepper.integration.suite.js +19 -0
  47. package/components/input-stepper/test-suites/lion-input-stepper.suite.js +673 -0
  48. package/components/listbox/src/ListboxMixin.js +0 -1
  49. package/components/listbox/test-suites/ListboxMixin.suite.js +71 -2
  50. package/components/overlays/src/OverlayController.js +3 -0
  51. package/components/overlays/src/OverlaysManager.js +9 -2
  52. package/components/overlays/test/OverlayController.test.js +22 -9
  53. package/components/overlays/test/OverlaysManager.test.js +16 -0
  54. package/components/pagination/src/LionPagination.js +2 -1
  55. package/components/pagination/test/lion-pagination.test.js +41 -0
  56. package/components/validate-messages/translations/it.js +2 -2
  57. package/custom-elements.json +281 -179
  58. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  59. package/dist-types/components/core/src/ScopedStylesController.d.ts +1 -2
  60. package/dist-types/components/core/src/ScopedStylesController.d.ts.map +1 -1
  61. package/dist-types/components/core/test-helpers/getCachedFixture.d.ts +14 -0
  62. package/dist-types/components/core/test-helpers/getCachedFixture.d.ts.map +1 -0
  63. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  64. package/dist-types/components/form-integrations/test/helpers/helpers.d.ts +3 -3
  65. package/dist-types/components/form-integrations/test/helpers/helpers.d.ts.map +1 -1
  66. package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
  67. package/dist-types/components/input-file/src/localizeNamespaceLoader.d.ts +1 -1
  68. package/dist-types/components/input-file/src/localizeNamespaceLoader.d.ts.map +1 -1
  69. package/dist-types/components/input-file/translations/bg-BG.d.ts +5 -0
  70. package/dist-types/components/input-file/translations/bg.d.ts +5 -0
  71. package/dist-types/components/input-file/translations/cs-CZ.d.ts +5 -0
  72. package/dist-types/components/input-file/translations/cs.d.ts +5 -0
  73. package/dist-types/components/input-file/translations/es-ES.d.ts +5 -0
  74. package/dist-types/components/input-file/translations/es.d.ts +5 -0
  75. package/dist-types/components/input-file/translations/fr-BE.d.ts +5 -0
  76. package/dist-types/components/input-file/translations/fr-FR.d.ts +5 -0
  77. package/dist-types/components/input-file/translations/fr.d.ts +5 -0
  78. package/dist-types/components/input-file/translations/hu-HU.d.ts +5 -0
  79. package/dist-types/components/input-file/translations/hu.d.ts +5 -0
  80. package/dist-types/components/input-file/translations/id-ID.d.ts +20 -0
  81. package/dist-types/components/input-file/translations/id-ID.d.ts.map +1 -0
  82. package/dist-types/components/input-file/translations/id.d.ts +20 -0
  83. package/dist-types/components/input-file/translations/id.d.ts.map +1 -0
  84. package/dist-types/components/input-file/translations/it-IT.d.ts +5 -0
  85. package/dist-types/components/input-file/translations/it.d.ts +5 -0
  86. package/dist-types/components/input-file/translations/pl-PL.d.ts +5 -0
  87. package/dist-types/components/input-file/translations/pl.d.ts +5 -0
  88. package/dist-types/components/input-file/translations/ro-RO.d.ts +5 -0
  89. package/dist-types/components/input-file/translations/ro.d.ts +5 -0
  90. package/dist-types/components/input-file/translations/sk-SK.d.ts +5 -0
  91. package/dist-types/components/input-file/translations/sk.d.ts +5 -0
  92. package/dist-types/components/input-file/translations/uk-UA.d.ts +5 -0
  93. package/dist-types/components/input-file/translations/uk.d.ts +5 -0
  94. package/dist-types/components/input-file/translations/zh-CN.d.ts +5 -0
  95. package/dist-types/components/input-file/translations/zh.d.ts +5 -0
  96. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -20
  97. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  98. package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts +1 -2
  99. package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts.map +1 -1
  100. package/dist-types/components/input-stepper/test-suites/lion-input-stepper.integration.suite.d.ts +3 -0
  101. package/dist-types/components/input-stepper/test-suites/lion-input-stepper.integration.suite.d.ts.map +1 -0
  102. package/dist-types/components/input-stepper/test-suites/lion-input-stepper.suite.d.ts +4 -0
  103. package/dist-types/components/input-stepper/test-suites/lion-input-stepper.suite.d.ts.map +1 -0
  104. package/dist-types/components/listbox/src/ListboxMixin.d.ts.map +1 -1
  105. package/dist-types/components/listbox/test-suites/ListboxMixin.suite.d.ts.map +1 -1
  106. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  107. package/dist-types/components/overlays/src/OverlaysManager.d.ts +4 -1
  108. package/dist-types/components/overlays/src/OverlaysManager.d.ts.map +1 -1
  109. package/dist-types/components/pagination/src/LionPagination.d.ts.map +1 -1
  110. package/dist-types/exports/input-stepper-suites.d.ts +6 -0
  111. package/dist-types/exports/input-stepper-suites.d.ts.map +1 -0
  112. package/dist-types/tsconfig.tsbuildinfo +1 -1
  113. package/docs/components/checkbox-group/overview.md +2 -2
  114. package/docs/components/checkbox-group/use-cases.md +3 -9
  115. package/docs/components/collapsible/overview.md +1 -1
  116. package/docs/components/combobox/overview.md +1 -1
  117. package/docs/components/combobox/use-cases.md +1 -1
  118. package/docs/components/dialog/overview.md +1 -1
  119. package/docs/components/fieldset/overview.md +5 -5
  120. package/docs/components/form/overview.md +3 -3
  121. package/docs/components/form/use-cases.md +2 -2
  122. package/docs/components/icon/use-cases.md +1 -1
  123. package/docs/components/input/overview.md +1 -1
  124. package/docs/components/input-amount/overview.md +3 -3
  125. package/docs/components/input-date/overview.md +3 -3
  126. package/docs/components/input-datepicker/overview.md +5 -5
  127. package/docs/components/input-email/overview.md +2 -2
  128. package/docs/components/input-file/overview.md +1 -1
  129. package/docs/components/input-iban/overview.md +2 -2
  130. package/docs/components/input-range/overview.md +2 -2
  131. package/docs/components/input-stepper/api-table.md +2 -3
  132. package/docs/components/input-stepper/overview.md +1 -1
  133. package/docs/components/input-tel/overview.md +1 -1
  134. package/docs/components/input-tel/use-cases.md +6 -6
  135. package/docs/components/input-tel-dropdown/overview.md +1 -1
  136. package/docs/components/listbox/use-cases.md +1 -1
  137. package/docs/components/radio-group/overview.md +3 -3
  138. package/docs/components/radio-group/use-cases.md +3 -9
  139. package/docs/components/select/overview.md +1 -1
  140. package/docs/components/select/use-cases.md +6 -6
  141. package/docs/components/select-rich/use-cases.md +2 -2
  142. package/docs/components/tabs/examples.md +1 -1
  143. package/docs/fundamentals/index.md +4 -4
  144. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  145. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  146. package/docs/fundamentals/rationales/accessibility.md +1 -1
  147. package/docs/fundamentals/systems/core/SlotMixin.md +2 -2
  148. package/docs/fundamentals/systems/core/api-table.md +3 -4
  149. package/docs/fundamentals/systems/core/overview.md +1 -1
  150. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  151. package/docs/fundamentals/systems/form/model-value.md +1 -1
  152. package/docs/fundamentals/systems/form/overview.md +28 -28
  153. package/docs/fundamentals/systems/form/validate.md +38 -30
  154. package/docs/fundamentals/systems/localize/overview.md +4 -4
  155. package/docs/fundamentals/systems/overlays/api-table.md +1 -1
  156. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  157. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  158. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  159. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  160. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  161. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  162. package/docs/guides/how-to/create-a-form.md +4 -4
  163. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  164. package/docs/guides/index.md +8 -8
  165. package/docs/guides/principles/index.md +5 -5
  166. package/docs/guides/principles/styling.md +1 -1
  167. package/exports/input-stepper-suites.js +11 -0
  168. package/input-stepper-suites.js +11 -0
  169. package/package.json +1 -1
@@ -0,0 +1,673 @@
1
+ import {
2
+ expect,
3
+ defineCE,
4
+ fixture as _fixture,
5
+ nextFrame,
6
+ unsafeStatic,
7
+ html,
8
+ } from '@open-wc/testing';
9
+ import { nothing } from 'lit';
10
+ import sinon from 'sinon';
11
+ import { formatNumber } from '@lion/ui/localize-no-side-effects.js';
12
+ import { LionInputStepper } from '../src/LionInputStepper.js';
13
+
14
+ /**
15
+ * @typedef {import('lit').TemplateResult} TemplateResult
16
+ */
17
+ const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionInputStepper>} */ (_fixture);
18
+
19
+ export function runInputStepperSuite(klass = LionInputStepper) {
20
+ const tagString = defineCE(class extends klass {});
21
+ const tag = unsafeStatic(tagString);
22
+
23
+ const defaultInputStepper = html`
24
+ <${tag} name="year" label="Years"></${tag}>
25
+ `;
26
+ const inputStepperWithAttrs = html`<${tag}
27
+ step="10"
28
+ min="100"
29
+ max="200"
30
+ label="Label"
31
+ ></${tag}>`;
32
+
33
+ describe('InputStepper', async () => {
34
+ describe('Stepper', () => {
35
+ it('has a type text', async () => {
36
+ const el = await fixture(defaultInputStepper);
37
+ expect(el._inputNode.type).to.equal('text');
38
+ });
39
+
40
+ it('has a default min and max of Infinity', async () => {
41
+ const el = await fixture(defaultInputStepper);
42
+ expect(el.getAttribute('min')).to.equal('Infinity');
43
+ expect(el.getAttribute('max')).to.equal('Infinity');
44
+ });
45
+
46
+ it('has a default step of 1', async () => {
47
+ const el = await fixture(defaultInputStepper);
48
+ expect(el.getAttribute('step')).to.equal('1');
49
+ });
50
+
51
+ it('can set a step with which the value increases', async () => {
52
+ const el = await fixture(defaultInputStepper);
53
+ el.step = 10;
54
+ await el.updateComplete;
55
+ expect(el.value).to.equal('');
56
+ expect(el.getAttribute('step')).to.equal('10');
57
+
58
+ const incrementButton = el.querySelector('[slot=suffix]');
59
+ incrementButton?.dispatchEvent(new Event('click'));
60
+ expect(el.value).to.equal('10');
61
+ });
62
+ });
63
+
64
+ describe('Formatter', () => {
65
+ it('uses formatNumber for formatting', async () => {
66
+ const el = await fixture(defaultInputStepper);
67
+ expect(el.formatter).to.equal(formatNumber);
68
+ });
69
+
70
+ it('formatNumber uses locale provided in formatOptions', async () => {
71
+ let el = await fixture(html`
72
+ <${tag} .formatOptions="${{ locale: 'en-GB' }}" .modelValue="${1234.56}"
73
+ ></${tag}>
74
+ `);
75
+ expect(el.formattedValue).to.equal('1,234.56');
76
+ el = await fixture(html`
77
+ <${tag} .formatOptions="${{ locale: 'nl-NL' }}" .modelValue="${1234.56}"
78
+ ></${tag}>
79
+ `);
80
+ expect(el.formattedValue).to.equal('1.234,56');
81
+ });
82
+
83
+ it('supports overriding decimalSeparator in formatOptions', async () => {
84
+ const el = await fixture(
85
+ html`<${tag}
86
+ .formatOptions="${{ locale: 'nl-NL', decimalSeparator: '.' }}"
87
+ .modelValue="${12.34}"
88
+
89
+ ></${tag}>`,
90
+ );
91
+ expect(el.formattedValue).to.equal('12.34');
92
+ });
93
+
94
+ it('supports overriding groupSeparator in formatOptions', async () => {
95
+ const el = await fixture(
96
+ html`<${tag}
97
+ .formatOptions="${{ locale: 'nl-NL', groupSeparator: ',', decimalSeparator: '.' }}"
98
+ .modelValue="${1234.56}"
99
+
100
+ ></${tag}>`,
101
+ );
102
+ expect(el.formattedValue).to.equal('1,234.56');
103
+ });
104
+ });
105
+
106
+ describe('User interaction', () => {
107
+ it('should increment the value to 1 on [ArrowUp]', async () => {
108
+ const el = await fixture(defaultInputStepper);
109
+ expect(el.value).to.equal('');
110
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
111
+ await el.updateComplete;
112
+ expect(el.value).to.equal('1');
113
+ });
114
+
115
+ it('should increment the value to minValue on [ArrowUp] if value is below min', async () => {
116
+ const el = await fixture(inputStepperWithAttrs);
117
+ expect(el.value).to.equal('');
118
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
119
+ await el.updateComplete;
120
+ expect(el.value).to.equal('100');
121
+ });
122
+
123
+ it('should decrement the value to -1 on [ArrowDown]', async () => {
124
+ const el = await fixture(defaultInputStepper);
125
+ expect(el.value).to.equal('');
126
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
127
+ await el.updateComplete;
128
+ expect(el.value).to.equal('−1');
129
+ });
130
+
131
+ it('should increment the value to minValue on [ArrowDown] if value is below min', async () => {
132
+ const el = await fixture(inputStepperWithAttrs);
133
+ el.modelValue = 600;
134
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
135
+ await el.updateComplete;
136
+ expect(el.value).to.equal('200');
137
+ });
138
+
139
+ it('should increment the value to 1 on + button click', async () => {
140
+ const el = await fixture(defaultInputStepper);
141
+ expect(el.value).to.equal('');
142
+ const incrementButton = el.querySelector('[slot=suffix]');
143
+ incrementButton?.dispatchEvent(new Event('click'));
144
+ expect(el.value).to.equal('1');
145
+ });
146
+
147
+ it('should decrement the value to -1 on - button click', async () => {
148
+ const el = await fixture(defaultInputStepper);
149
+ expect(el.value).to.equal('');
150
+ const decrementButton = el.querySelector('[slot=prefix]');
151
+ decrementButton?.dispatchEvent(new Event('click'));
152
+ expect(el.value).to.equal('−1');
153
+ });
154
+
155
+ it('fires one "user-input-changed" event on + button click', async () => {
156
+ let counter = 0;
157
+ const el = await fixture(html`
158
+ <${tag}
159
+ name="year"
160
+ label="Years"
161
+ @user-input-changed="${() => {
162
+ counter += 1;
163
+ }}"
164
+ >
165
+
166
+ ></${tag}>
167
+ `);
168
+ const incrementButton = el.querySelector('[slot=suffix]');
169
+ incrementButton?.dispatchEvent(new Event('click'));
170
+ expect(counter).to.equal(1);
171
+ });
172
+
173
+ it('fires one "user-input-changed" event on - button click', async () => {
174
+ let counter = 0;
175
+ const el = await fixture(html`
176
+ <${tag}
177
+ name="year"
178
+ label="Years"
179
+ @user-input-changed="${() => {
180
+ counter += 1;
181
+ }}"
182
+ >
183
+
184
+ ></${tag}>
185
+ `);
186
+ const decrementButton = el.querySelector('[slot=prefix]');
187
+ decrementButton?.dispatchEvent(new Event('click'));
188
+ expect(counter).to.equal(1);
189
+ });
190
+
191
+ it('should update min and max attributes when min and max property change', async () => {
192
+ const el = await fixture(inputStepperWithAttrs);
193
+ el.min = 100;
194
+ el.max = 200;
195
+ await nextFrame();
196
+ expect(el._inputNode.min).to.equal(el.min.toString());
197
+ expect(el._inputNode.max).to.equal(el.max.toString());
198
+ });
199
+
200
+ it('should remove the disabled attribute of the decrement button when the min property changes to below the modelvalue', async () => {
201
+ const el = await fixture(inputStepperWithAttrs);
202
+ const decrementButton = el.querySelector('[slot=prefix]');
203
+ el.modelValue = 100;
204
+ await nextFrame();
205
+ expect(decrementButton?.getAttribute('disabled')).to.equal('true');
206
+ el.min = 99;
207
+ await nextFrame();
208
+ expect(decrementButton?.getAttribute('disabled')).to.equal(null);
209
+ });
210
+
211
+ it('should add the disabled attribute of the decrement button when the min property changes to the modelvalue', async () => {
212
+ const el = await fixture(inputStepperWithAttrs);
213
+ const decrementButton = el.querySelector('[slot=prefix]');
214
+ el.modelValue = 101;
215
+ await nextFrame();
216
+ expect(decrementButton?.getAttribute('disabled')).to.equal(null);
217
+ el.min = 101;
218
+ await nextFrame();
219
+ expect(decrementButton?.getAttribute('disabled')).to.equal('true');
220
+ });
221
+
222
+ it('should remove the disabled attribute of the increment button when the max property changes to above the modelvalue', async () => {
223
+ const el = await fixture(inputStepperWithAttrs);
224
+ const incrementButton = el.querySelector('[slot=suffix]');
225
+ el.modelValue = 200;
226
+ await nextFrame();
227
+ expect(incrementButton?.getAttribute('disabled')).to.equal('true');
228
+ el.max = 201;
229
+ await nextFrame();
230
+ expect(incrementButton?.getAttribute('disabled')).to.equal(null);
231
+ });
232
+
233
+ it('should add the disabled attribute of the increment button when the max property changes to the modelvalue', async () => {
234
+ const el = await fixture(inputStepperWithAttrs);
235
+ const incrementButton = el.querySelector('[slot=suffix]');
236
+ el.modelValue = 199;
237
+ await nextFrame();
238
+ expect(incrementButton?.getAttribute('disabled')).to.equal(null);
239
+ el.max = 199;
240
+ await nextFrame();
241
+ expect(incrementButton?.getAttribute('disabled')).to.equal('true');
242
+ });
243
+
244
+ it('should react to changes in the modelValue by adjusting the disabled state of the button', async () => {
245
+ const el = await fixture(inputStepperWithAttrs);
246
+ const incrementButton = el.querySelector('[slot=suffix]');
247
+ el.modelValue = 199;
248
+ await nextFrame();
249
+ expect(incrementButton?.getAttribute('disabled')).to.equal(null);
250
+ el.modelValue = 200;
251
+ await nextFrame();
252
+ expect(incrementButton?.getAttribute('disabled')).to.equal('true');
253
+ });
254
+
255
+ describe('It align to steps', () => {
256
+ it('aligns the value to the nearest step when incrementing', async () => {
257
+ let el = await fixture(html`<${tag} step="10" min="0" max="100" value="55"></${tag}>`);
258
+ let incrementButton = el.querySelector('[slot=suffix]');
259
+ incrementButton?.dispatchEvent(new Event('click'));
260
+ await el.updateComplete;
261
+ expect(el.modelValue).to.equal(60, 'Fail + : (0 > 100 by 10; val 55)');
262
+
263
+ // min 1
264
+ el = await fixture(html`<${tag} step="10" min="1" max="100" value="55"></${tag}>`);
265
+ incrementButton = el.querySelector('[slot=suffix]');
266
+ incrementButton?.dispatchEvent(new Event('click'));
267
+ await el.updateComplete;
268
+ expect(el.modelValue).to.equal(61, 'Fail + : (1 > 100 by 10; val 55)');
269
+
270
+ // min 34
271
+ el = await fixture(html`<${tag} step="10" min="34" max="100" value="55"></${tag}>`);
272
+ incrementButton = el.querySelector('[slot=suffix]');
273
+ incrementButton?.dispatchEvent(new Event('click'));
274
+ await el.updateComplete;
275
+ expect(el.modelValue).to.equal(64, 'Fail + : (34 > 100 by 10; val 55)');
276
+
277
+ // min -23
278
+ el = await fixture(html`<${tag} step="10" min="-23" max="100" value="55"></${tag}>`);
279
+ incrementButton = el.querySelector('[slot=suffix]');
280
+ incrementButton?.dispatchEvent(new Event('click'));
281
+ await el.updateComplete;
282
+ expect(el.modelValue).to.equal(57, 'Fail + : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 > ... > 57
283
+
284
+ // min -23
285
+ el = await fixture(html`<${tag} step="10" min="-23" max="100" value="-9"></${tag}>`);
286
+ incrementButton = el.querySelector('[slot=suffix]');
287
+ incrementButton?.dispatchEvent(new Event('click'));
288
+ await el.updateComplete;
289
+ expect(el.modelValue).to.equal(-3, 'Fail + : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7
290
+ });
291
+
292
+ it('aligns the value to the nearest step when decrementing', async () => {
293
+ let el = await fixture(html`<${tag} step="10" min="0" max="100" value="55"></${tag}>`);
294
+ let decrementButton = el.querySelector('[slot=prefix]');
295
+ decrementButton?.dispatchEvent(new Event('click'));
296
+ await el.updateComplete;
297
+ expect(el.modelValue).to.equal(50, 'Fail - : (0 > 100 by 10; val 55)');
298
+
299
+ // min 1
300
+ el = await fixture(html`<${tag} step="10" min="1" max="100" value="55"></${tag}>`);
301
+ decrementButton = el.querySelector('[slot=prefix]');
302
+ decrementButton?.dispatchEvent(new Event('click'));
303
+ await el.updateComplete;
304
+ expect(el.modelValue).to.equal(51, 'Fail - : (1 > 100 by 10; val 55)');
305
+
306
+ // min 34
307
+ el = await fixture(html`<${tag} step="10" min="34" max="100" value="55"></${tag}>`);
308
+ decrementButton = el.querySelector('[slot=prefix]');
309
+ decrementButton?.dispatchEvent(new Event('click'));
310
+ await el.updateComplete;
311
+ expect(el.modelValue).to.equal(54, 'Fail - : (34 > 100 by 10; val 55)');
312
+
313
+ // min -23
314
+ el = await fixture(html`<${tag} step="10" min="-23" max="100" value="55"></${tag}>`);
315
+ decrementButton = el.querySelector('[slot=prefix]');
316
+ decrementButton?.dispatchEvent(new Event('click'));
317
+ await el.updateComplete;
318
+ expect(el.modelValue).to.equal(47, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 > ... > 47
319
+
320
+ // min -23
321
+ el = await fixture(html`<${tag} step="10" min="-23" max="100" value="-9"></${tag}>`);
322
+ decrementButton = el.querySelector('[slot=prefix]');
323
+ decrementButton?.dispatchEvent(new Event('click'));
324
+ await el.updateComplete;
325
+ expect(el.modelValue).to.equal(-13, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7
326
+ });
327
+
328
+ it('handles decimal step values correctly', async () => {
329
+ // Test with decimal step 0.1
330
+ let el = await fixture(html`<${tag} step="0.1" min="0" max="9" value="5.55"></${tag}>`);
331
+
332
+ // Test increment with decimal step
333
+ let incrementButton = el.querySelector('[slot=suffix]');
334
+ incrementButton?.dispatchEvent(new Event('click'));
335
+ await el.updateComplete;
336
+ expect(el.modelValue).to.equal(5.6, 'Fail + : (0 > 9 by 0.1; val 5.55)');
337
+
338
+ // Test decrement with decimal step
339
+ let decrementButton = el.querySelector('[slot=prefix]');
340
+ decrementButton?.dispatchEvent(new Event('click'));
341
+ await el.updateComplete;
342
+ expect(el.modelValue).to.equal(5.5, 'Fail - : (0 > 9 by 0.1; val 5.6)');
343
+
344
+ // Test with value that needs alignment
345
+ el = await fixture(html`<${tag} step="0.1" min="0" max="9" value="3.27"></${tag}>`);
346
+
347
+ // Should align to next step when incrementing
348
+ incrementButton = el.querySelector('[slot=suffix]');
349
+ incrementButton?.dispatchEvent(new Event('click'));
350
+ await el.updateComplete;
351
+ expect(el.modelValue).to.equal(3.3, 'Fail + alignment: (0 > 9 by 0.1; val 3.27)');
352
+
353
+ // Reset and test decrement alignment
354
+ el = await fixture(html`<${tag} step="0.1" min="0" max="9" value="3.27"></${tag}>`);
355
+
356
+ // Should align to previous step when decrementing
357
+ decrementButton = el.querySelector('[slot=prefix]');
358
+ decrementButton?.dispatchEvent(new Event('click'));
359
+ await el.updateComplete;
360
+ expect(el.modelValue).to.equal(3.2, 'Fail - alignment: (0 > 9 by 0.1; val 3.27)');
361
+ });
362
+ });
363
+ });
364
+
365
+ describe('_destroyOutputContent method', () => {
366
+ beforeEach(() => {
367
+ // Ensure clean state for each test
368
+ sinon.restore();
369
+ });
370
+
371
+ it('should clear existing timer before setting a new one', async () => {
372
+ const el = await fixture(defaultInputStepper);
373
+ const clearTimeoutSpy = sinon.spy(window, 'clearTimeout');
374
+
375
+ // Set an initial timer
376
+ el.timer = setTimeout(() => {}, 1000);
377
+ const initialTimer = el.timer;
378
+
379
+ // Call _destroyOutputContent
380
+ el._destroyOutputContent();
381
+
382
+ expect(clearTimeoutSpy.calledWith(initialTimer)).to.be.true;
383
+ clearTimeoutSpy.restore();
384
+ });
385
+
386
+ it('should set __valueText to nothing and request update after timeout', async () => {
387
+ const el = await fixture(defaultInputStepper);
388
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
389
+ const clock = sinon.useFakeTimers();
390
+
391
+ // Set initial value text
392
+ el.__valueText = 'test value';
393
+
394
+ // Call _destroyOutputContent
395
+ el._destroyOutputContent();
396
+
397
+ // Fast forward time by 2000ms (default timeout)
398
+ clock.tick(2000);
399
+
400
+ await expect(el.__valueText).to.equal(nothing);
401
+ expect(requestUpdateSpy.calledOnce).to.be.true;
402
+
403
+ clock.restore();
404
+ requestUpdateSpy.restore();
405
+ });
406
+
407
+ it('should use custom timeout from data-self-destruct attribute', async () => {
408
+ const el = await fixture(html`
409
+ <${tag} name="test" label="Test"> ></${tag}>
410
+ `);
411
+
412
+ await el.updateComplete;
413
+
414
+ // Get the output element and set custom self-destruct value
415
+ const outputElement = /** @type {HTMLElement} */ (
416
+ el.shadowRoot?.querySelector('.input-stepper__value')
417
+ );
418
+ if (outputElement) {
419
+ outputElement.dataset.selfDestruct = '5000';
420
+ }
421
+
422
+ const clock = sinon.useFakeTimers();
423
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
424
+
425
+ el.__valueText = 'test value';
426
+ el._destroyOutputContent();
427
+
428
+ // Should not trigger after default 2000ms
429
+ clock.tick(2000);
430
+ await expect(el.__valueText).to.equal('test value');
431
+ expect(requestUpdateSpy.called).to.be.false;
432
+
433
+ // Should trigger after custom 5000ms
434
+ clock.tick(3000); // Total 5000ms
435
+ await expect(el.__valueText).to.equal(nothing);
436
+ expect(requestUpdateSpy.calledOnce).to.be.true;
437
+
438
+ clock.restore();
439
+ requestUpdateSpy.restore();
440
+ });
441
+
442
+ it('should handle invalid data-self-destruct value by using default timeout', async () => {
443
+ const el = await fixture(html`
444
+ <${tag} name="test" label="Test"> ></${tag}>
445
+ `);
446
+
447
+ await el.updateComplete;
448
+
449
+ // Get the output element and set invalid self-destruct value
450
+ const outputElement = /** @type {HTMLElement} */ (
451
+ el.shadowRoot?.querySelector('.input-stepper__value')
452
+ );
453
+ if (outputElement) {
454
+ outputElement.dataset.selfDestruct = 'invalid';
455
+ }
456
+
457
+ const clock = sinon.useFakeTimers();
458
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
459
+
460
+ el.__valueText = 'test value';
461
+ el._destroyOutputContent();
462
+
463
+ // Should use default 2000ms when invalid value is provided
464
+ clock.tick(2000);
465
+ await expect(el.__valueText).to.equal(nothing);
466
+ expect(requestUpdateSpy.calledOnce).to.be.true;
467
+
468
+ clock.restore();
469
+ requestUpdateSpy.restore();
470
+ });
471
+
472
+ it('should not set timeout if output element does not exist', async () => {
473
+ const el = await fixture(defaultInputStepper);
474
+ const setTimeoutSpy = sinon.spy(window, 'setTimeout');
475
+
476
+ // Mock shadowRoot to return null for querySelector
477
+ const originalQuerySelector = el.shadowRoot?.querySelector;
478
+ if (el.shadowRoot) {
479
+ el.shadowRoot.querySelector = () => null;
480
+ }
481
+
482
+ el._destroyOutputContent();
483
+
484
+ expect(setTimeoutSpy.called).to.be.false;
485
+
486
+ // Restore original querySelector
487
+ if (el.shadowRoot && originalQuerySelector) {
488
+ el.shadowRoot.querySelector = originalQuerySelector;
489
+ }
490
+ setTimeoutSpy.restore();
491
+ });
492
+
493
+ it('should only execute timeout callback if output element still has parent', async () => {
494
+ const el = await fixture(defaultInputStepper);
495
+ const clock = sinon.useFakeTimers();
496
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
497
+
498
+ el.__valueText = 'test value';
499
+ el._destroyOutputContent();
500
+
501
+ // Remove the output element from DOM before timeout
502
+ const outputElement = el.shadowRoot?.querySelector('.input-stepper__value');
503
+ if (outputElement && outputElement.parentNode) {
504
+ outputElement.parentNode.removeChild(outputElement);
505
+ }
506
+
507
+ // Fast forward time
508
+ clock.tick(2000);
509
+
510
+ // Should not have updated since element was removed
511
+ expect(requestUpdateSpy.called).to.be.false;
512
+ expect(el.__valueText).to.equal('test value');
513
+
514
+ clock.restore();
515
+ requestUpdateSpy.restore();
516
+ });
517
+
518
+ it('should be called when increment button is clicked', async () => {
519
+ const el = await fixture(defaultInputStepper);
520
+ const destroyOutputSpy = sinon.spy(el, '_destroyOutputContent');
521
+
522
+ const incrementButton = el.querySelector('[slot=suffix]');
523
+ incrementButton?.dispatchEvent(new Event('click'));
524
+
525
+ expect(destroyOutputSpy.calledOnce).to.be.true;
526
+ destroyOutputSpy.restore();
527
+ });
528
+
529
+ it('should be called when decrement button is clicked', async () => {
530
+ const el = await fixture(defaultInputStepper);
531
+ const destroyOutputSpy = sinon.spy(el, '_destroyOutputContent');
532
+
533
+ const decrementButton = el.querySelector('[slot=prefix]');
534
+ decrementButton?.dispatchEvent(new Event('click'));
535
+
536
+ expect(destroyOutputSpy.calledOnce).to.be.true;
537
+ destroyOutputSpy.restore();
538
+ });
539
+
540
+ it('should handle multiple rapid calls by clearing previous timers', async () => {
541
+ const el = await fixture(defaultInputStepper);
542
+ const clock = sinon.useFakeTimers();
543
+ const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
544
+
545
+ el.__valueText = 'test value';
546
+
547
+ // Call _destroyOutputContent multiple times rapidly
548
+ el._destroyOutputContent();
549
+ clock.tick(1000); // 1 second
550
+
551
+ el._destroyOutputContent(); // This should clear the previous timer
552
+ clock.tick(1000); // Total 2 seconds from first call, 1 second from second call
553
+
554
+ // Should not have triggered yet since second call reset the timer
555
+ expect(el.__valueText).to.equal('test value');
556
+ expect(requestUpdateSpy.called).to.be.false;
557
+
558
+ clock.tick(1000); // Total 2 seconds from second call
559
+
560
+ // Should trigger now
561
+ await expect(el.__valueText).to.equal(nothing);
562
+ expect(requestUpdateSpy.calledOnce).to.be.true;
563
+
564
+ clock.restore();
565
+ requestUpdateSpy.restore();
566
+ });
567
+ });
568
+
569
+ describe('Accessibility', () => {
570
+ it('is a11y AXE accessible', async () => {
571
+ const el = await fixture(defaultInputStepper);
572
+ await expect(el).to.be.accessible();
573
+ });
574
+
575
+ it('is accessible when disabled', async () => {
576
+ const el = await fixture(html`<${tag} label="rsvp" disabled></${tag}>`);
577
+ await expect(el).to.be.accessible();
578
+ });
579
+
580
+ it('has role="spinbutton"', async () => {
581
+ const el = await fixture(defaultInputStepper);
582
+ expect(el._inputNode.hasAttribute('role')).to.be.true;
583
+ expect(el._inputNode.getAttribute('role')).to.equal('spinbutton');
584
+ });
585
+
586
+ it('updates aria-valuenow when stepper is changed', async () => {
587
+ const el = await fixture(defaultInputStepper);
588
+ el.modelValue = 1;
589
+
590
+ await el.updateComplete;
591
+ expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.true;
592
+ expect(el._inputNode.getAttribute('aria-valuenow')).to.equal('1');
593
+
594
+ el.modelValue = '';
595
+ await el.updateComplete;
596
+ expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.false;
597
+ });
598
+
599
+ it('updates aria-valuetext when stepper is changed', async () => {
600
+ // VoiceOver announces percentages once the valuemin or valuemax are used.
601
+ // This can be fixed by setting valuetext to the same value as valuenow
602
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
603
+ const el = await fixture(defaultInputStepper);
604
+ el.modelValue = 1;
605
+ await el.updateComplete;
606
+
607
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
608
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('1');
609
+
610
+ el.modelValue = '';
611
+ await el.updateComplete;
612
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.false;
613
+ });
614
+
615
+ it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
616
+ const values = {
617
+ 1: 'first',
618
+ 2: 'second',
619
+ 3: 'third',
620
+ };
621
+ const el = await fixture(html`
622
+ <${tag} min="1" max="3" .valueTextMapping="${values}">></${tag}>
623
+ `);
624
+ el.modelValue = 1;
625
+ await el.updateComplete;
626
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
627
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('first');
628
+ });
629
+
630
+ it('updates aria-valuemin when stepper is changed', async () => {
631
+ const el = await fixture(inputStepperWithAttrs);
632
+ const incrementButton = el.querySelector('[slot=suffix]');
633
+ incrementButton?.dispatchEvent(new Event('click'));
634
+ expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
635
+ expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('100');
636
+
637
+ el.min = 0;
638
+ await el.updateComplete;
639
+ expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
640
+ expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('0');
641
+ });
642
+
643
+ it('updates aria-valuemax when stepper is changed', async () => {
644
+ const el = await fixture(inputStepperWithAttrs);
645
+ const incrementButton = el.querySelector('[slot=suffix]');
646
+ incrementButton?.dispatchEvent(new Event('click'));
647
+ expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
648
+ expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('200');
649
+
650
+ el.max = 1000;
651
+ await el.updateComplete;
652
+ expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
653
+ expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
654
+ });
655
+
656
+ it('decrease button should have aria-label with the component label', async () => {
657
+ const el = await fixture(inputStepperWithAttrs);
658
+ const decrementButton = el.querySelector('[slot=prefix]');
659
+
660
+ expect(decrementButton?.hasAttribute('aria-label')).to.be.true;
661
+ expect(decrementButton?.getAttribute('aria-label')).to.equal('Decrease Label');
662
+ });
663
+
664
+ it('increase button should have aria-label with the component label', async () => {
665
+ const el = await fixture(inputStepperWithAttrs);
666
+ const incrementButton = el.querySelector('[slot=suffix]');
667
+
668
+ expect(incrementButton?.hasAttribute('aria-label')).to.be.true;
669
+ expect(incrementButton?.getAttribute('aria-label')).to.equal('Increase Label');
670
+ });
671
+ });
672
+ });
673
+ }
@@ -427,7 +427,6 @@ const ListboxMixinImplementation = superclass =>
427
427
  clear() {
428
428
  super.clear();
429
429
  this.setCheckedIndex(-1);
430
- this.resetInteractionState();
431
430
  }
432
431
 
433
432
  /**