From 67ecc95633492b9fe0035ebcf06cfe28ea3bb51e Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Mon, 15 Jul 2024 15:49:19 -0300 Subject: [PATCH] Add reference to number control (#49357) * Add ref to NumberControl * Add identifier # Conflicts: # packages/js/product-editor/src/hooks/use-error-handler.ts * Remove console.log * Add changelog * Stop using setTimeout and use mutation observer to check if the tab is visible before focusing field * Rename prop * Refactor number-control functions * Fix lint * resetIncrement funciton --------- Co-authored-by: Nathan Schneider --- .../dev-46768_add_ref_to_number_control | 4 + .../shipping-dimensions/edit.tsx | 20 +- .../number-control/number-control.tsx | 327 +++++++++--------- .../validation-context/use-validations.ts | 16 +- 4 files changed, 207 insertions(+), 160 deletions(-) create mode 100644 packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control 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 && ( - <> -