diff --git a/packages/components/src/range-control/index.js b/packages/components/src/range-control/index.js index b1e3f19ab924b5..fa00d50d3f1f3b 100644 --- a/packages/components/src/range-control/index.js +++ b/packages/components/src/range-control/index.js @@ -19,7 +19,7 @@ import BaseControl from '../base-control'; import Button from '../button'; import Icon from '../icon'; import { COLORS, useControlledValue } from '../utils'; -import { floatClamp } from './utils'; +import { useUnimpededRangedNumberEntry } from './utils'; import InputRange from './input-range'; import RangeRail from './rail'; import SimpleTooltip from './tooltip'; @@ -73,29 +73,17 @@ function RangeControl( const isResetPendent = useRef( false ); const [ value, setValue ] = useControlledValue( { defaultValue: initialPosition ?? null, - value: isResetPendent.current ? undefined : valueProp, + value: valueProp, onChange: ( nextValue ) => { - /* - * Calls onChange only when nextValue is numeric - * otherwise may queue a reset for the blur event. - */ - if ( ! isNaN( nextValue ) ) { - if ( nextValue < min || nextValue > max ) { - nextValue = floatClamp( nextValue, min, max ); - } - onChange( nextValue ); - isResetPendent.current = false; - } else if ( nextValue === null ) { + if ( nextValue === null ) { /* - * handleOnReset has led the execution here due to lack of a - * defined resetFallbackValue. In such conditions the onChange - * callback receives undefined as that was the behavior when the - * component was stablized. + * The value is reset without a resetFallbackValue. In such + * conditions the onChange callback receives undefined as that + * was the behavior when the component was stablized. */ - onChange( undefined ); - } else if ( allowReset ) { - isResetPendent.current = true; + nextValue = undefined; } + onChange( nextValue ); }, } ); @@ -149,10 +137,19 @@ function RangeControl( setValue( nextValue ); }; - const handleOnChange = ( nextValue ) => { - nextValue = parseFloat( nextValue ); - setValue( nextValue ); - }; + const someNumberInputProps = useUnimpededRangedNumberEntry( { + max, + min, + value: inputSliderValue, + onChange: ( nextValue ) => { + if ( ! isNaN( nextValue ) ) { + setValue( nextValue ); + isResetPendent.current = false; + } else if ( allowReset ) { + isResetPendent.current = true; + } + }, + } ); const handleOnInputNumberBlur = () => { if ( isResetPendent.current ) { @@ -275,13 +272,10 @@ function RangeControl( disabled={ disabled } inputMode="decimal" isShiftStepEnabled={ isShiftStepEnabled } - max={ max } - min={ min } onBlur={ handleOnInputNumberBlur } - onChange={ handleOnChange } shiftStep={ shiftStep } step={ step } - value={ inputSliderValue } + { ...someNumberInputProps } /> ) } { allowReset && ( diff --git a/packages/components/src/range-control/utils.js b/packages/components/src/range-control/utils.js index 255c2aaaa7d34a..506b32f514797f 100644 --- a/packages/components/src/range-control/utils.js +++ b/packages/components/src/range-control/utils.js @@ -26,6 +26,43 @@ export function floatClamp( value, min, max ) { return parseFloat( clamp( value, min, max ) ); } +/** + * Enables entry of out-of-range and invalid values that diverge from state. + * + * @param {Object} props Props + * @param {number|null} props.value Incoming value. + * @param {number} props.max Maximum valid value. + * @param {number} props.min Minimum valid value. + * @param {(next: number) => void} props.onChange Callback for changes. + * + * @return {Object} Assorted props for the input. + */ +export function useUnimpededRangedNumberEntry( { max, min, onChange, value } ) { + const ref = useRef(); + const isDiverging = useRef( false ); + /** @type {import('../input-control/types').InputChangeCallback}*/ + const changeHandler = ( next ) => { + next = parseFloat( next ); + const isOverflow = next < min || next > max; + isDiverging.current = isNaN( next ) || isOverflow; + if ( isOverflow ) { + next = Math.max( min, Math.min( max, next ) ); + } + onChange( next ); + }; + useEffect( () => { + if ( ref.current && isDiverging.current ) { + const input = ref.current; + const entry = input.value; + const { defaultView } = input.ownerDocument; + defaultView.requestAnimationFrame( () => ( input.value = entry ) ); + isDiverging.current = false; + } + }, [ value ] ); + + return { max, min, ref, value, onChange: changeHandler }; +} + /** * Hook to encapsulate the debouncing "hover" to better handle the showing * and hiding of the Tooltip.