Add improvements to NumberControl (#47121)
* Use min and max values if provided when sanitizing values * Pass min and max attributes to NumberControl * Use 'min' and 'max' values when incrementing through + and - buttons * Add changelog * Handle increment when pressing buttons * Show + and - buttons disabled when max/min has been reached * Prevent suffix from being selected
This commit is contained in:
parent
d0a8477b51
commit
7c7d9837a4
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update number control to not allow invalid values through arrow keys or buttons
|
|
@ -95,6 +95,8 @@ export function Edit( {
|
||||||
tooltip={ tooltip }
|
tooltip={ tooltip }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
step={ step }
|
step={ step }
|
||||||
|
min={ min }
|
||||||
|
max={ max }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -38,6 +38,8 @@ export type NumberProps = {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
step?: number;
|
step?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MEDIUM_DELAY = 500;
|
const MEDIUM_DELAY = 500;
|
||||||
|
@ -57,6 +59,8 @@ export const NumberControl: React.FC< NumberProps > = ( {
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
step = 1,
|
step = 1,
|
||||||
|
min = -Infinity,
|
||||||
|
max = +Infinity,
|
||||||
}: NumberProps ) => {
|
}: NumberProps ) => {
|
||||||
const id = useInstanceId( BaseControl, 'product_number_field' ) as string;
|
const id = useInstanceId( BaseControl, 'product_number_field' ) as string;
|
||||||
const [ isFocused, setIsFocused ] = useState( false );
|
const [ isFocused, setIsFocused ] = useState( false );
|
||||||
|
@ -74,6 +78,8 @@ export const NumberControl: React.FC< NumberProps > = ( {
|
||||||
value: value || '',
|
value: value || '',
|
||||||
onChange,
|
onChange,
|
||||||
onFocus: () => setIsFocused( true ),
|
onFocus: () => setIsFocused( true ),
|
||||||
|
min,
|
||||||
|
max,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const [ increment, setIncrement ] = useState( 0 );
|
const [ increment, setIncrement ] = useState( 0 );
|
||||||
|
@ -82,8 +88,11 @@ export const NumberControl: React.FC< NumberProps > = ( {
|
||||||
|
|
||||||
const isInitialClick = useRef< boolean >( false );
|
const isInitialClick = useRef< boolean >( false );
|
||||||
|
|
||||||
const incrementValue = () =>
|
const incrementValue = () => {
|
||||||
onChange( String( parseFloat( value || '0' ) + increment ) );
|
const newValue = parseFloat( value || '0' ) + increment;
|
||||||
|
if ( newValue >= min && newValue <= max )
|
||||||
|
onChange( String( newValue ) );
|
||||||
|
};
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( increment !== 0 ) {
|
if ( increment !== 0 ) {
|
||||||
|
@ -104,6 +113,15 @@ export const NumberControl: React.FC< NumberProps > = ( {
|
||||||
|
|
||||||
const resetIncrement = () => setIncrement( 0 );
|
const resetIncrement = () => setIncrement( 0 );
|
||||||
|
|
||||||
|
const handleIncrement = ( thisStep: number ) => {
|
||||||
|
const newValue = parseFloat( value || '0' ) + thisStep;
|
||||||
|
if ( newValue >= min && newValue <= max ) {
|
||||||
|
onChange( String( parseFloat( value || '0' ) + thisStep ) );
|
||||||
|
setIncrement( thisStep );
|
||||||
|
isInitialClick.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseControl
|
<BaseControl
|
||||||
className={ classNames( {
|
className={ classNames( {
|
||||||
|
@ -134,16 +152,12 @@ export const NumberControl: React.FC< NumberProps > = ( {
|
||||||
<Button
|
<Button
|
||||||
className="woocommerce-number-control__increment"
|
className="woocommerce-number-control__increment"
|
||||||
icon={ plus }
|
icon={ plus }
|
||||||
onMouseDown={ () => {
|
disabled={
|
||||||
onChange(
|
parseFloat( value || '0' ) >= max
|
||||||
String(
|
}
|
||||||
parseFloat( value || '0' ) +
|
onMouseDown={ () =>
|
||||||
step
|
handleIncrement( step )
|
||||||
)
|
}
|
||||||
);
|
|
||||||
setIncrement( step );
|
|
||||||
isInitialClick.current = true;
|
|
||||||
} }
|
|
||||||
onMouseLeave={ resetIncrement }
|
onMouseLeave={ resetIncrement }
|
||||||
onMouseUp={ resetIncrement }
|
onMouseUp={ resetIncrement }
|
||||||
onBlur={ unfocusIfOutside }
|
onBlur={ unfocusIfOutside }
|
||||||
|
@ -157,18 +171,14 @@ export const NumberControl: React.FC< NumberProps > = ( {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={ reset }
|
icon={ reset }
|
||||||
|
disabled={
|
||||||
|
parseFloat( value || '0' ) <= min
|
||||||
|
}
|
||||||
className="woocommerce-number-control__decrement"
|
className="woocommerce-number-control__decrement"
|
||||||
onBlur={ unfocusIfOutside }
|
onBlur={ unfocusIfOutside }
|
||||||
onMouseDown={ () => {
|
onMouseDown={ () =>
|
||||||
onChange(
|
handleIncrement( -step )
|
||||||
String(
|
}
|
||||||
parseFloat( value || '0' ) -
|
|
||||||
step
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setIncrement( -step );
|
|
||||||
isInitialClick.current = true;
|
|
||||||
} }
|
|
||||||
onMouseLeave={ resetIncrement }
|
onMouseLeave={ resetIncrement }
|
||||||
onMouseUp={ resetIncrement }
|
onMouseUp={ resetIncrement }
|
||||||
isSmall
|
isSmall
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.woocommerce-number-control {
|
||||||
|
.components-input-control__suffix {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ type Props = {
|
||||||
onChange: ( value: string ) => void;
|
onChange: ( value: string ) => void;
|
||||||
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
||||||
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NOT_NUMBERS_OR_SEPARATORS_REGEX = /[^0-9,.]/g;
|
const NOT_NUMBERS_OR_SEPARATORS_REGEX = /[^0-9,.]/g;
|
||||||
|
@ -26,6 +28,8 @@ export const useNumberInputProps = ( {
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
min = -Infinity,
|
||||||
|
max = +Infinity,
|
||||||
}: Props ) => {
|
}: Props ) => {
|
||||||
const { formatNumber, parseNumber } = useProductHelper();
|
const { formatNumber, parseNumber } = useProductHelper();
|
||||||
|
|
||||||
|
@ -47,20 +51,27 @@ export const useNumberInputProps = ( {
|
||||||
const step = Number( event.currentTarget.step || '1' );
|
const step = Number( event.currentTarget.step || '1' );
|
||||||
if ( event.code === 'ArrowUp' ) {
|
if ( event.code === 'ArrowUp' ) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onChange( String( amount + step ) );
|
if ( amount + step <= max ) onChange( String( amount + step ) );
|
||||||
}
|
}
|
||||||
if ( event.code === 'ArrowDown' ) {
|
if ( event.code === 'ArrowDown' ) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onChange( String( amount - step ) );
|
if ( amount - step >= min ) onChange( String( amount - step ) );
|
||||||
}
|
}
|
||||||
if ( onKeyDown ) {
|
if ( onKeyDown ) {
|
||||||
onKeyDown( event );
|
onKeyDown( event );
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChange( newValue: string ) {
|
onChange( newValue: string ) {
|
||||||
const sanitizeValue = parseNumber(
|
let sanitizeValue = parseNumber(
|
||||||
newValue.replace( NOT_NUMBERS_OR_SEPARATORS_REGEX, '' )
|
newValue.replace( NOT_NUMBERS_OR_SEPARATORS_REGEX, '' )
|
||||||
);
|
);
|
||||||
|
const numberValue = Number( sanitizeValue );
|
||||||
|
if ( sanitizeValue && numberValue >= max ) {
|
||||||
|
sanitizeValue = String( max );
|
||||||
|
}
|
||||||
|
if ( sanitizeValue && numberValue <= min ) {
|
||||||
|
sanitizeValue = String( min );
|
||||||
|
}
|
||||||
onChange( sanitizeValue );
|
onChange( sanitizeValue );
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
@import "components/custom-fields/style.scss";
|
@import "components/custom-fields/style.scss";
|
||||||
@import "components/text-control/style.scss";
|
@import "components/text-control/style.scss";
|
||||||
@import "components/attribute-combobox-field/styles.scss";
|
@import "components/attribute-combobox-field/styles.scss";
|
||||||
|
@import "components/number-control/style.scss";
|
||||||
|
|
||||||
/* Field Blocks */
|
/* Field Blocks */
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue