From cf4a8fd1b045c23a3b2aa74ea1f6c758b7ef3c69 Mon Sep 17 00:00:00 2001 From: manuel Date: Wed, 14 May 2025 16:46:00 +0200 Subject: [PATCH] feat(input-stepper): align value with step size (#2511) * feat(input-stepper): add alignToStep property to align value with step size * Create silver-eyes-count.md * refactor: make alignTosTep property behavior as default behavior and remove property attribute * test(input-stepper): handle min value as Infinity and update tests for decrement/increment behavior --------- Co-authored-by: Manuel Servais --- .changeset/silver-eyes-count.md | 5 + docs/components/input-stepper/use-cases.md | 16 ++++ .../input-stepper/src/LionInputStepper.js | 27 +++++- .../test/lion-input-stepper.test.js | 94 +++++++++++++++++++ 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 .changeset/silver-eyes-count.md diff --git a/.changeset/silver-eyes-count.md b/.changeset/silver-eyes-count.md new file mode 100644 index 000000000..08ba9fa51 --- /dev/null +++ b/.changeset/silver-eyes-count.md @@ -0,0 +1,5 @@ +--- +"@lion/ui": patch +--- + +feat(input-stepper): add alignToStep property to align value with step value diff --git a/docs/components/input-stepper/use-cases.md b/docs/components/input-stepper/use-cases.md index 4cb286f48..1cda48828 100644 --- a/docs/components/input-stepper/use-cases.md +++ b/docs/components/input-stepper/use-cases.md @@ -111,3 +111,19 @@ export const formatting = () => { `; }; ``` + +### Aligning to step + +The value is always aligned with the defined step size when using the increase or decrease buttons. The input value will be adjusted to the nearest valid step multiple. For example, with a step of `10` starting from `1`, if the user enters `55` and clicks the increase button, the value will align to `61` instead of `65` (1 > 11 > 21 > ... > 51 > 61). + +```html preview-story + +``` diff --git a/packages/ui/components/input-stepper/src/LionInputStepper.js b/packages/ui/components/input-stepper/src/LionInputStepper.js index 1f45b9723..fb866455b 100644 --- a/packages/ui/components/input-stepper/src/LionInputStepper.js +++ b/packages/ui/components/input-stepper/src/LionInputStepper.js @@ -265,8 +265,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { * @private */ __increment() { - const { step, min, max } = this.values; - const newValue = this.currentValue + step; + const { step, max } = this.values; + let { min } = this.values; + if (min === Infinity) { + min = 0; + } + + let newValue = this.currentValue + step; + + if ((this.currentValue + min) % step !== 0) { + // If the value is not aligned to step, align it to the nearest step + newValue = Math.floor(this.currentValue / step) * step + step + (min % step); + } + if (newValue <= max || max === Infinity) { this.modelValue = newValue < min && min !== Infinity ? `${min}` : `${newValue}`; this.__toggleSpinnerButtonsState(); @@ -279,8 +290,16 @@ export class LionInputStepper extends LocalizeMixin(LionInput) { * @private */ __decrement() { - const { step, min, max } = this.values; - const newValue = this.currentValue - step; + const { step, max, min } = this.values; + const stepMin = min !== Infinity ? min : 0; + + let newValue = this.currentValue - step; + + if ((this.currentValue + stepMin) % step !== 0) { + // If the value is not aligned to step, align it to the nearest step + newValue = Math.floor(this.currentValue / step) * step + (stepMin % step); + } + if (newValue >= min || min === Infinity) { this.modelValue = newValue > max && max !== Infinity ? `${max}` : `${newValue}`; this.__toggleSpinnerButtonsState(); diff --git a/packages/ui/components/input-stepper/test/lion-input-stepper.test.js b/packages/ui/components/input-stepper/test/lion-input-stepper.test.js index 72133bde2..88d72f3ba 100644 --- a/packages/ui/components/input-stepper/test/lion-input-stepper.test.js +++ b/packages/ui/components/input-stepper/test/lion-input-stepper.test.js @@ -265,6 +265,100 @@ describe('', () => { await nextFrame(); expect(incrementButton?.getAttribute('disabled')).to.equal('true'); }); + + describe('It align to steps', () => { + it('aligns the value to the nearest step when incrementing', async () => { + let el = await fixture( + html``, + ); + let incrementButton = el.querySelector('[slot=suffix]'); + incrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(60, 'Fail + : (0 > 100 by 10; val 55)'); + + // min 1 + el = await fixture( + html``, + ); + incrementButton = el.querySelector('[slot=suffix]'); + incrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(61, 'Fail + : (1 > 100 by 10; val 55)'); + + // min 34 + el = await fixture( + html``, + ); + incrementButton = el.querySelector('[slot=suffix]'); + incrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(64, 'Fail + : (34 > 100 by 10; val 55)'); + + // min -23 + el = await fixture( + html``, + ); + incrementButton = el.querySelector('[slot=suffix]'); + incrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(57, 'Fail + : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 > ... > 57 + + // min -23 + el = await fixture( + html``, + ); + incrementButton = el.querySelector('[slot=suffix]'); + incrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(-3, 'Fail + : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 + }); + + it('aligns the value to the nearest step when decrementing', async () => { + let el = await fixture( + html``, + ); + let decrementButton = el.querySelector('[slot=prefix]'); + decrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(50, 'Fail - : (0 > 100 by 10; val 55)'); + + // min 1 + el = await fixture( + html``, + ); + decrementButton = el.querySelector('[slot=prefix]'); + decrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(51, 'Fail - : (1 > 100 by 10; val 55)'); + + // min 34 + el = await fixture( + html``, + ); + decrementButton = el.querySelector('[slot=prefix]'); + decrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(54, 'Fail - : (34 > 100 by 10; val 55)'); + + // min -23 + el = await fixture( + html``, + ); + decrementButton = el.querySelector('[slot=prefix]'); + decrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(47, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 > ... > 47 + + // min -23 + el = await fixture( + html``, + ); + decrementButton = el.querySelector('[slot=prefix]'); + decrementButton?.dispatchEvent(new Event('click')); + await el.updateComplete; + expect(el.modelValue).to.equal(-13, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 + }); + }); }); describe('Accessibility', () => {