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 <nsschneider1@gmail.com>
This commit is contained in:
Fernando Marichal 2024-07-15 15:49:19 -03:00 committed by GitHub
parent 6bc551c71b
commit 67ecc95633
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 207 additions and 160 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add reference to number control #49357

View File

@ -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,

View File

@ -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 (
<BaseControl
className={ classNames( {
'has-error': error,
} ) }
id={ id }
label={
isValidElement( label ) ? (
label
) : (
<Label
label={ label as string }
required={ required }
tooltip={ tooltip }
/>
)
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 }
>
<InputControl
{ ...inputProps }
step={ step }
disabled={ disabled }
autoComplete="off"
id={ id }
className="woocommerce-number-control"
suffix={
<>
{ suffix }
{ isFocused && (
<>
<Button
className="woocommerce-number-control__increment"
icon={ plus }
disabled={
parseFloat( value || '0' ) >= max
}
onMouseDown={ () =>
handleIncrement( step )
}
onMouseLeave={ resetIncrement }
onMouseUp={ resetIncrement }
onBlur={ unfocusIfOutside }
isSmall
aria-hidden="true"
aria-label={ __(
'Increment',
'woocommerce'
) }
tabIndex={ -1 }
/>
<Button
icon={ reset }
disabled={
parseFloat( value || '0' ) <= min
}
className="woocommerce-number-control__decrement"
onBlur={ unfocusIfOutside }
onMouseDown={ () =>
handleIncrement( -step )
}
onMouseLeave={ resetIncrement }
onMouseUp={ resetIncrement }
isSmall
aria-hidden="true"
aria-label={ __(
'Decrement',
'woocommerce'
) }
tabIndex={ -1 }
/>
</>
) }
</>
return () => {
if ( timeoutRef.current ) {
clearTimeout( timeoutRef.current );
}
placeholder={ placeholder }
onBlur={ unfocusIfOutside }
/>
</BaseControl>
);
};
};
}, [ increment, value ] );
function resetIncrement() {
setIncrement( 0 );
}
function 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 (
<BaseControl
className={ classNames( {
'has-error': error,
} ) }
id={ identifier }
label={
isValidElement( label ) ? (
label
) : (
<Label
label={ label as string }
required={ required }
tooltip={ tooltip }
/>
)
}
help={ error || help }
>
<InputControl
{ ...inputProps }
ref={ ref }
step={ step }
disabled={ disabled }
autoComplete="off"
id={ identifier }
className="woocommerce-number-control"
suffix={
<>
{ suffix }
{ isFocused && (
<>
<Button
className="woocommerce-number-control__increment"
icon={ plus }
disabled={
parseFloat( value || '0' ) >= max
}
onMouseDown={ () =>
handleIncrement( step )
}
onMouseLeave={ resetIncrement }
onMouseUp={ resetIncrement }
onBlur={ unfocusIfOutside }
isSmall
aria-hidden="true"
aria-label={ __(
'Increment',
'woocommerce'
) }
tabIndex={ -1 }
/>
<Button
icon={ reset }
disabled={
parseFloat( value || '0' ) <= min
}
className="woocommerce-number-control__decrement"
onBlur={ unfocusIfOutside }
onMouseDown={ () =>
handleIncrement( -step )
}
onMouseLeave={ resetIncrement }
onMouseUp={ resetIncrement }
isSmall
aria-hidden="true"
aria-label={ __(
'Decrement',
'woocommerce'
) }
tabIndex={ -1 }
/>
</>
) }
</>
}
placeholder={ placeholder }
onBlur={ unfocusIfOutside }
/>
</BaseControl>
);
}
);

View File

@ -19,8 +19,20 @@ export function useValidations< T = unknown >() {
async function focusByValidatorId( validatorId: string ) {
const field = await context.getFieldByValidatorId( validatorId );
if ( field ) {
field.focus();
const tab = field.closest(
'.wp-block-woocommerce-product-tab__content'
);
const observer = new MutationObserver( () => {
if ( tab && getComputedStyle( tab ).display !== 'none' ) {
field.focus();
observer.disconnect();
}
} );
if ( tab ) {
observer.observe( tab, {
attributes: true,
} );
}
}