/* * Copyright (c) 2015 Nordic Semiconductor ASA * * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause */ import React from 'react'; import { isFactor } from '../Slider/factor'; import { getStep, isValues, type Range, type RangeOrValues, useValidatedRange, type Values, } from '../Slider/range'; import InlineInput from './InlineInput'; import './number-inline-input.scss'; interface NumberInlineInput { disabled?: boolean; value: number; range: RangeOrValues; className?: string; title?: string; onChange: (value: number) => void; onChangeComplete?: (value: number) => void; textAlignLeft?: boolean; onValidityChanged?: (validity: boolean) => void; preventDefaultInvalidStyle?: boolean; minSize?: number; preAllocateSize?: boolean; } const isInValues = (value: number, values: Values) => values.includes(value); const isInRange = (value: number, { min, max, decimals, step }: Range) => value >= min && value <= max && value === Number(value.toFixed(decimals)) && (step == null ? true : isFactor(value - min, step)); const isValid = (value: number, rangeOrValues: RangeOrValues) => isValues(rangeOrValues) ? isInValues(value, rangeOrValues) : isInRange(value, rangeOrValues); const nextInValues = ( current: number, values: Values, steps: number, ): number | undefined => { const currentIndex = values.indexOf(current); const newIndex = currentIndex + steps; return values[newIndex]; }; const nextInRange = (current: number, range: Range, steps: number) => { const newValue = Number( (current + steps * getStep(range)).toFixed(range.decimals), ); if (newValue >= range.min && newValue <= range.max) { return newValue; } }; const changeValueStepwise = ( current: number, rangeOrValues: RangeOrValues, steps: number, ) => { const nextValue = isValues(rangeOrValues) ? nextInValues(current, rangeOrValues, steps) : nextInRange(current, rangeOrValues, steps); if (nextValue != null) { return nextValue; } return nextValue != null ? nextValue : current; }; const handleInfinityToString = (val: number) => Math.abs(val) === Infinity ? `${val < 0 ? '-' : ''}∞` : String(val); const handleInfinityToNumber = (val: string) => { if (val === '∞') { return Infinity; } if (val === '-∞') { return -Infinity; } return Number(val); }; const calculateMaxCharactersFromRange = (range: RangeOrValues): number => { if (isValues(range)) { return Math.max( ...range.map(v => Number.isFinite(v) ? handleInfinityToString(v).length : v.toString().length, ), ); } const { min, max, decimals = 0 } = range; const minStr = Number.isFinite(min) ? min.toFixed(decimals) : handleInfinityToString(min); const maxStr = Number.isFinite(max) ? max.toFixed(decimals) : handleInfinityToString(max); return Math.max(minStr.length, maxStr.length); }; export default ({ disabled, value, range, className, title, onChange, onChangeComplete = () => {}, textAlignLeft, onValidityChanged, preventDefaultInvalidStyle, minSize, preAllocateSize = true, }: NumberInlineInput) => { useValidatedRange(range); return ( onChange(handleInfinityToNumber(newValue))} onChangeComplete={newValue => onChangeComplete(handleInfinityToNumber(newValue)) } onKeyboardIncrementAction={() => handleInfinityToString( changeValueStepwise(value, range, 1), ).toString() } onKeyboardDecrementAction={() => handleInfinityToString( changeValueStepwise(value, range, -1), ).toString() } textAlignLeft={textAlignLeft} isValid={newValue => { const validity = isValid( handleInfinityToNumber(newValue), range, ); if (onValidityChanged != null) { // Then propagate the validity back to parent onValidityChanged(validity); } return validity; }} onValidityChanged={onValidityChanged} preventDefaultInvalidStyle={preventDefaultInvalidStyle} minSize={ preAllocateSize ? (minSize ?? Math.max( ...(isValues(range) ? range.map(v => Number.isFinite(v) ? handleInfinityToString(v) : v.toString(), ) : [range.min, range.max].map(v => Number.isFinite(v) ? v.toFixed(range.decimals) : handleInfinityToString(v), ) ).map(v => v.length), )) : undefined } maxLength={calculateMaxCharactersFromRange(range)} /> ); };