@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.
- package/README.md +44 -9
- package/components/dialog/test/lion-dialog.integration.test.js +170 -0
- package/components/form-core/src/validate/LionValidationFeedback.js +2 -2
- package/components/form-core/src/validate/ValidateMixin.js +3 -3
- package/components/form-core/test/validate/lion-validation-feedback.test.js +2 -2
- package/components/form-core/test-suites/ValidateMixin.suite.js +23 -11
- package/components/form-core/translations/bg.js +4 -4
- package/components/form-core/translations/cs.js +4 -4
- package/components/form-core/translations/de.js +4 -4
- package/components/form-core/translations/en.js +4 -4
- package/components/form-core/translations/es.js +4 -4
- package/components/form-core/translations/fr.js +4 -4
- package/components/form-core/translations/hu.js +4 -4
- package/components/form-core/translations/it.js +4 -4
- package/components/form-core/translations/nl.js +4 -4
- package/components/form-core/translations/pl.js +4 -4
- package/components/form-core/translations/ro.js +4 -4
- package/components/form-core/translations/ru.js +4 -4
- package/components/form-core/translations/sk.js +4 -4
- package/components/form-core/translations/uk.js +4 -4
- package/components/form-core/translations/zh.js +4 -4
- package/components/input-amount-dropdown/src/localizeNamespaceLoader.js +53 -0
- package/components/input-amount-dropdown/translations/bg.js +5 -0
- package/components/input-amount-dropdown/translations/cs.js +5 -0
- package/components/input-amount-dropdown/translations/de.js +5 -0
- package/components/input-amount-dropdown/translations/es.js +5 -0
- package/components/input-amount-dropdown/translations/fr.js +5 -0
- package/components/input-amount-dropdown/translations/hu.js +5 -0
- package/components/input-amount-dropdown/translations/id.js +5 -0
- package/components/input-amount-dropdown/translations/it.js +5 -0
- package/components/input-amount-dropdown/translations/nl.js +5 -0
- package/components/input-amount-dropdown/translations/pl.js +5 -0
- package/components/input-amount-dropdown/translations/ro.js +5 -0
- package/components/input-amount-dropdown/translations/ru.js +5 -0
- package/components/input-amount-dropdown/translations/sk.js +5 -0
- package/components/input-amount-dropdown/translations/uk.js +5 -0
- package/components/input-amount-dropdown/translations/zh.js +5 -0
- package/components/input-stepper/src/LionInputStepper.js +48 -52
- package/components/input-stepper/test/lion-input-stepper.test.js +253 -41
- package/components/overlays/src/OverlayController.js +34 -7
- package/components/overlays/test/OverlayMixin.test.js +2 -2
- package/components/overlays/test-suites/OverlayMixin.suite.js +57 -0
- package/components/pagination/src/LionPagination.js +2 -1
- package/components/pagination/test/lion-pagination.test.js +41 -0
- package/components/validate-messages/translations/it.js +2 -2
- package/custom-elements.json +158 -170
- package/dist-types/components/dialog/test/lion-dialog.integration.test.d.ts +3 -0
- package/dist-types/components/dialog/test/lion-dialog.integration.test.d.ts.map +1 -0
- package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
- package/dist-types/components/input-amount-dropdown/src/localizeNamespaceLoader.d.ts +1 -1
- package/dist-types/components/input-amount-dropdown/src/localizeNamespaceLoader.d.ts.map +1 -1
- package/dist-types/components/input-amount-dropdown/translations/bg.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/bg.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/cs.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/cs.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/de.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/de.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/es.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/es.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/fr.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/fr.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/hu.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/hu.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/id.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/id.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/it.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/it.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/nl.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/nl.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/pl.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/pl.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/ro.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/ro.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/ru.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/ru.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/sk.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/sk.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/uk.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/uk.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/translations/zh.d.ts +7 -0
- package/dist-types/components/input-amount-dropdown/translations/zh.d.ts.map +1 -0
- package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -20
- package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
- package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts +13 -4
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
- package/dist-types/components/pagination/src/LionPagination.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/checkbox-group/use-cases.md +3 -9
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/dialog/use-cases.md +2 -2
- package/docs/components/fieldset/overview.md +5 -5
- package/docs/components/form/overview.md +3 -3
- package/docs/components/form/use-cases.md +2 -2
- package/docs/components/icon/use-cases.md +1 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/api-table.md +2 -3
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/radio-group/use-cases.md +3 -9
- package/docs/components/select/overview.md +1 -1
- package/docs/components/select/use-cases.md +6 -6
- package/docs/components/select-rich/use-cases.md +2 -2
- package/docs/components/tabs/examples.md +1 -1
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
- package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
- package/docs/fundamentals/rationales/accessibility.md +1 -1
- package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
- package/docs/fundamentals/systems/core/overview.md +1 -1
- package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
- package/docs/fundamentals/systems/form/model-value.md +1 -1
- package/docs/fundamentals/systems/form/overview.md +28 -28
- package/docs/fundamentals/systems/form/validate.md +2 -2
- package/docs/fundamentals/systems/localize/overview.md +4 -4
- package/docs/fundamentals/systems/overlays/configuration.md +2 -2
- package/docs/fundamentals/systems/overlays/overview.md +5 -5
- package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
- package/docs/fundamentals/tools/helpers/overview.md +1 -1
- package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
- package/docs/guides/how-to/create-a-custom-field.md +5 -5
- package/docs/guides/how-to/create-a-form.md +4 -4
- package/docs/guides/how-to/extend-a-native-input.md +2 -2
- package/docs/guides/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
290
|
+
let newValue;
|
|
272
291
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
321
|
+
const remainder = (this.currentValue - stepMin) % step;
|
|
322
|
+
const isAligned = Math.abs(remainder) < epsilon || Math.abs(remainder - step) < epsilon;
|
|
294
323
|
|
|
295
|
-
if (
|
|
296
|
-
// If the value is not aligned to step, align it to the
|
|
297
|
-
newValue = Math.floor(this.currentValue / step) * 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
|
-
<
|
|
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('
|
|
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?.
|
|
457
|
-
expect(
|
|
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('
|
|
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?.
|
|
470
|
-
expect(
|
|
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 (
|
|
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 === '
|
|
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
|
-
}
|
|
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 === '
|
|
1292
|
+
if (phase === 'init') {
|
|
1293
|
+
document.removeEventListener('keyup', this.#outsideEscKeyHandler);
|
|
1267
1294
|
document.addEventListener('keyup', this.#outsideEscKeyHandler);
|
|
1268
|
-
} else if (phase === '
|
|
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
|
-
<
|
|
10
|
+
<slot name="invoker"></slot>
|
|
11
11
|
<div id="overlay-content-node-wrapper">
|
|
12
|
-
<
|
|
12
|
+
<slot name="content"></slot>
|
|
13
13
|
</div>
|
|
14
14
|
`;
|
|
15
15
|
}
|