diff --git a/packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control b/packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control
new file mode 100644
index 00000000000..4f9650d9d7d
--- /dev/null
+++ b/packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add reference to number control #49357
diff --git a/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx
index 76a606a70dc..7e67365b0ee 100644
--- a/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx
@@ -88,12 +88,14 @@ export function Edit( {
};
}
+ const widthFieldId = `dimensions_width-${ clientId }`;
+
const {
ref: dimensionsWidthRef,
error: dimensionsWidthValidationError,
validate: validateDimensionsWidth,
} = useValidation< Product >(
- `dimensions_width-${ clientId }`,
+ widthFieldId,
async function dimensionsWidthValidator() {
if ( dimensions?.width && +dimensions.width <= 0 ) {
return {
@@ -108,12 +110,14 @@ export function Edit( {
[ dimensions?.width ]
);
+ const lengthFieldId = `dimensions_length-${ clientId }`;
+
const {
ref: dimensionsLengthRef,
error: dimensionsLengthValidationError,
validate: validateDimensionsLength,
} = useValidation< Product >(
- `dimensions_length-${ clientId }`,
+ lengthFieldId,
async function dimensionsLengthValidator() {
if ( dimensions?.length && +dimensions.length <= 0 ) {
return {
@@ -128,12 +132,14 @@ export function Edit( {
[ dimensions?.length ]
);
+ const heightFieldId = `dimensions_height-${ clientId }`;
+
const {
ref: dimensionsHeightRef,
error: dimensionsHeightValidationError,
validate: validateDimensionsHeight,
} = useValidation< Product >(
- `dimensions_height-${ clientId }`,
+ heightFieldId,
async function dimensionsHeightValidator() {
if ( dimensions?.height && +dimensions.height <= 0 ) {
return {
@@ -148,12 +154,14 @@ export function Edit( {
[ dimensions?.height ]
);
+ const weightFieldId = `weight-${ clientId }`;
+
const {
ref: weightRef,
error: weightValidationError,
validate: validateWeight,
} = useValidation< Product >(
- `weight-${ clientId }`,
+ weightFieldId,
async function weightValidator() {
if ( weight && +weight <= 0 ) {
return {
@@ -172,18 +180,22 @@ export function Edit( {
...getDimensionsControlProps( 'width', 'A' ),
ref: dimensionsWidthRef,
onBlur: validateDimensionsWidth,
+ id: widthFieldId,
};
const dimensionsLengthProps = {
...getDimensionsControlProps( 'length', 'B' ),
ref: dimensionsLengthRef,
onBlur: validateDimensionsLength,
+ id: lengthFieldId,
};
const dimensionsHeightProps = {
...getDimensionsControlProps( 'height', 'C' ),
ref: dimensionsHeightRef,
onBlur: validateDimensionsHeight,
+ id: heightFieldId,
};
const weightProps = {
+ id: weightFieldId,
name: 'weight',
value: weight ?? '',
onChange: setWeight,
diff --git a/packages/js/product-editor/src/components/number-control/number-control.tsx b/packages/js/product-editor/src/components/number-control/number-control.tsx
index 13160a9ab8f..bb4a6b8daa8 100644
--- a/packages/js/product-editor/src/components/number-control/number-control.tsx
+++ b/packages/js/product-editor/src/components/number-control/number-control.tsx
@@ -3,6 +3,7 @@
*/
import {
createElement,
+ forwardRef,
Fragment,
isValidElement,
useEffect,
@@ -13,6 +14,7 @@ import { useInstanceId } from '@wordpress/compose';
import classNames from 'classnames';
import { plus, reset } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
+import type { ForwardedRef } from 'react';
import {
BaseControl,
Button,
@@ -27,6 +29,7 @@ import { useNumberInputProps } from '../../hooks/use-number-input-props';
import { Label } from '../label/label';
export type NumberProps = {
+ id?: string;
value: string;
onChange: ( selected: string ) => void;
label: string | JSX.Element;
@@ -47,164 +50,180 @@ export type NumberProps = {
const MEDIUM_DELAY = 500;
const SHORT_DELAY = 100;
-export const NumberControl: React.FC< NumberProps > = ( {
- value,
- onChange,
- label,
- suffix,
- help,
- error,
- onBlur,
- onFocus,
- required,
- tooltip,
- placeholder,
- disabled,
- step = 1,
- min = -Infinity,
- max = Infinity,
-}: NumberProps ) => {
- const id = useInstanceId( BaseControl, 'product_number_field' ) as string;
- const [ isFocused, setIsFocused ] = useState( false );
- const unfocusIfOutside = ( event: React.FocusEvent ) => {
- if (
- ! document
- .getElementById( id )
- ?.parentElement?.contains( event.relatedTarget )
- ) {
- setIsFocused( false );
- onBlur?.();
- }
- };
- const inputProps = useNumberInputProps( {
- value: value || '',
- onChange,
- onFocus: () => {
- setIsFocused( true );
- onFocus?.();
- },
- min,
- max,
- } );
-
- const [ increment, setIncrement ] = useState( 0 );
-
- const timeoutRef = useRef< number | null >( null );
-
- const isInitialClick = useRef< boolean >( false );
-
- const incrementValue = () => {
- const newValue = parseFloat( value || '0' ) + increment;
- if ( newValue >= min && newValue <= max )
- onChange( String( newValue ) );
- };
-
- useEffect( () => {
- if ( increment !== 0 ) {
- timeoutRef.current = setTimeout(
- incrementValue,
- isInitialClick.current ? MEDIUM_DELAY : SHORT_DELAY
- );
- isInitialClick.current = false;
- } else if ( timeoutRef.current ) {
- clearTimeout( timeoutRef.current );
- }
- return () => {
- if ( timeoutRef.current ) {
- clearTimeout( timeoutRef.current );
+export const NumberControl: React.FC< NumberProps > = forwardRef(
+ (
+ {
+ id,
+ value,
+ onChange,
+ label,
+ suffix,
+ help,
+ error,
+ onBlur,
+ onFocus,
+ required,
+ tooltip,
+ placeholder,
+ disabled,
+ step = 1,
+ min = -Infinity,
+ max = Infinity,
+ }: NumberProps,
+ ref: ForwardedRef< HTMLInputElement >
+ ) => {
+ const instanceId = useInstanceId(
+ BaseControl,
+ 'product_number_field'
+ ) as string;
+ const identifier = id ?? instanceId;
+ const [ isFocused, setIsFocused ] = useState( false );
+ const unfocusIfOutside = ( event: React.FocusEvent ) => {
+ if (
+ ! document
+ .getElementById( identifier )
+ ?.parentElement?.contains( event.relatedTarget )
+ ) {
+ setIsFocused( false );
+ onBlur?.();
}
};
- }, [ increment, value ] );
- 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;
+ function handleOnFocus() {
+ setIsFocused( true );
+ onFocus?.();
}
- };
- return (
-
- )
+ const inputProps = useNumberInputProps( {
+ value: value || '',
+ onChange,
+ onFocus: handleOnFocus,
+ min,
+ max,
+ } );
+
+ const [ increment, setIncrement ] = useState( 0 );
+
+ const timeoutRef = useRef< number | null >( null );
+
+ const isInitialClick = useRef< boolean >( false );
+
+ function incrementValue() {
+ const newValue = parseFloat( value || '0' ) + increment;
+ if ( newValue >= min && newValue <= max )
+ onChange( String( newValue ) );
+ }
+
+ useEffect( () => {
+ if ( increment !== 0 ) {
+ timeoutRef.current = setTimeout(
+ incrementValue,
+ isInitialClick.current ? MEDIUM_DELAY : SHORT_DELAY
+ );
+ isInitialClick.current = false;
+ } else if ( timeoutRef.current ) {
+ clearTimeout( timeoutRef.current );
}
- help={ error || help }
- >
-
- { suffix }
- { isFocused && (
- <>
-