diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3a962de..0d4a7f035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Combo - Rendering issue after Edge browser autofill behavior [#1497](https://github.com/IgniteUI/igniteui-webcomponents/issues/1497) +- Input - Incorrect validation with numeric input and step attribute [#1521](https://github.com/IgniteUI/igniteui-webcomponents/issues/1521) ## [5.1.2] - 2024-11-04 ### Added diff --git a/src/components/common/util.ts b/src/components/common/util.ts index e31ff8ea1..f676cc12c 100644 --- a/src/components/common/util.ts +++ b/src/components/common/util.ts @@ -15,6 +15,16 @@ export const asPercent = (part: number, whole: number) => (part / whole) * 100; export const clamp = (number: number, min: number, max: number) => Math.max(min, Math.min(number, max)); +export function numberOfDecimals(number: number): number { + const decimals = last(number.toString().split('.')); + return decimals ? decimals.length : 0; +} + +export function roundPrecise(number: number, magnitude = 1): number { + const factor = 10 ** magnitude; + return Math.round(number * factor) / factor; +} + export function numberInRangeInclusive( value: number, min: number, diff --git a/src/components/common/validators.ts b/src/components/common/validators.ts index 64e539b45..fa4b4345f 100644 --- a/src/components/common/validators.ts +++ b/src/components/common/validators.ts @@ -1,6 +1,12 @@ import { DateTimeUtil } from '../date-time-input/date-util.js'; import validatorMessages from './localization/validation-en.js'; -import { asNumber, formatString, isDefined } from './util.js'; +import { + asNumber, + formatString, + isDefined, + numberOfDecimals, + roundPrecise, +} from './util.js'; type ValidatorHandler = (host: T) => boolean; type ValidatorMessageFormat = (host: T) => string; @@ -93,10 +99,20 @@ export const stepValidator: Validator<{ }> = { key: 'stepMismatch', message: 'Value does not conform to step constraint', - isValid: ({ min, step, value }) => - isDefined(value) && value !== '' && isDefined(step) - ? (asNumber(value) - asNumber(min)) % asNumber(step, 1) === 0 - : true, + isValid: ({ min, step, value }) => { + if (isDefined(value) && value !== '' && isDefined(step)) { + const _value = asNumber(value) - asNumber(min); + const _step = asNumber(step); + const magnitude = numberOfDecimals(_step) + 1; + const rem = roundPrecise( + Math.abs(_value - _step * Math.round(_value / _step)), + magnitude + ); + + return !rem; + } + return true; + }, }; export const emailValidator: Validator<{ value: string }> = { diff --git a/src/components/input/input.spec.ts b/src/components/input/input.spec.ts index c33916573..b09482547 100644 --- a/src/components/input/input.spec.ts +++ b/src/components/input/input.spec.ts @@ -356,6 +356,30 @@ describe('Input component', () => { }); }); + describe('issue-1521', () => { + let input: IgcInputComponent; + + beforeEach(async () => { + input = await fixture(html` + + `); + }); + + it('', () => { + input.value = '1'; + expect(input.checkValidity()).to.be.true; + + input.value = '1.1'; + expect(input.checkValidity()).to.be.true; + + input.value = '1.11'; + expect(input.checkValidity()).to.be.false; + + input.step = 0.01; + expect(input.checkValidity()).to.be.true; + }); + }); + describe('Form integration', () => { const spec = createFormAssociatedTestBed( html`` diff --git a/src/components/rating/rating.ts b/src/components/rating/rating.ts index 63a700385..823318647 100644 --- a/src/components/rating/rating.ts +++ b/src/components/rating/rating.ts @@ -35,6 +35,8 @@ import { formatString, isEmpty, isLTR, + numberOfDecimals, + roundPrecise, } from '../common/util.js'; import IgcIconComponent from '../icon/icon.js'; import IgcRatingSymbolComponent from './rating-symbol.js'; @@ -328,17 +330,8 @@ export default class IgcRatingComponent extends FormAssociatedMixin( return clamp(value, this.step, this.max); } - protected getPrecision(num: number) { - const [_, decimal] = num.toString().split('.'); - return decimal ? decimal.length : 0; - } - protected round(value: number) { - return Number( - (Math.round(value / this.step) * this.step).toFixed( - this.getPrecision(this.step) - ) - ); + return roundPrecise(value, numberOfDecimals(this.step)); } protected clipSymbol(index: number, isLTR = true) {