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', () => {