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 { const {
ref: dimensionsWidthRef, ref: dimensionsWidthRef,
error: dimensionsWidthValidationError, error: dimensionsWidthValidationError,
validate: validateDimensionsWidth, validate: validateDimensionsWidth,
} = useValidation< Product >( } = useValidation< Product >(
`dimensions_width-${ clientId }`, widthFieldId,
async function dimensionsWidthValidator() { async function dimensionsWidthValidator() {
if ( dimensions?.width && +dimensions.width <= 0 ) { if ( dimensions?.width && +dimensions.width <= 0 ) {
return { return {
@ -108,12 +110,14 @@ export function Edit( {
[ dimensions?.width ] [ dimensions?.width ]
); );
const lengthFieldId = `dimensions_length-${ clientId }`;
const { const {
ref: dimensionsLengthRef, ref: dimensionsLengthRef,
error: dimensionsLengthValidationError, error: dimensionsLengthValidationError,
validate: validateDimensionsLength, validate: validateDimensionsLength,
} = useValidation< Product >( } = useValidation< Product >(
`dimensions_length-${ clientId }`, lengthFieldId,
async function dimensionsLengthValidator() { async function dimensionsLengthValidator() {
if ( dimensions?.length && +dimensions.length <= 0 ) { if ( dimensions?.length && +dimensions.length <= 0 ) {
return { return {
@ -128,12 +132,14 @@ export function Edit( {
[ dimensions?.length ] [ dimensions?.length ]
); );
const heightFieldId = `dimensions_height-${ clientId }`;
const { const {
ref: dimensionsHeightRef, ref: dimensionsHeightRef,
error: dimensionsHeightValidationError, error: dimensionsHeightValidationError,
validate: validateDimensionsHeight, validate: validateDimensionsHeight,
} = useValidation< Product >( } = useValidation< Product >(
`dimensions_height-${ clientId }`, heightFieldId,
async function dimensionsHeightValidator() { async function dimensionsHeightValidator() {
if ( dimensions?.height && +dimensions.height <= 0 ) { if ( dimensions?.height && +dimensions.height <= 0 ) {
return { return {
@ -148,12 +154,14 @@ export function Edit( {
[ dimensions?.height ] [ dimensions?.height ]
); );
const weightFieldId = `weight-${ clientId }`;
const { const {
ref: weightRef, ref: weightRef,
error: weightValidationError, error: weightValidationError,
validate: validateWeight, validate: validateWeight,
} = useValidation< Product >( } = useValidation< Product >(
`weight-${ clientId }`, weightFieldId,
async function weightValidator() { async function weightValidator() {
if ( weight && +weight <= 0 ) { if ( weight && +weight <= 0 ) {
return { return {
@ -172,18 +180,22 @@ export function Edit( {
...getDimensionsControlProps( 'width', 'A' ), ...getDimensionsControlProps( 'width', 'A' ),
ref: dimensionsWidthRef, ref: dimensionsWidthRef,
onBlur: validateDimensionsWidth, onBlur: validateDimensionsWidth,
id: widthFieldId,
}; };
const dimensionsLengthProps = { const dimensionsLengthProps = {
...getDimensionsControlProps( 'length', 'B' ), ...getDimensionsControlProps( 'length', 'B' ),
ref: dimensionsLengthRef, ref: dimensionsLengthRef,
onBlur: validateDimensionsLength, onBlur: validateDimensionsLength,
id: lengthFieldId,
}; };
const dimensionsHeightProps = { const dimensionsHeightProps = {
...getDimensionsControlProps( 'height', 'C' ), ...getDimensionsControlProps( 'height', 'C' ),
ref: dimensionsHeightRef, ref: dimensionsHeightRef,
onBlur: validateDimensionsHeight, onBlur: validateDimensionsHeight,
id: heightFieldId,
}; };
const weightProps = { const weightProps = {
id: weightFieldId,
name: 'weight', name: 'weight',
value: weight ?? '', value: weight ?? '',
onChange: setWeight, onChange: setWeight,

View File

@ -3,6 +3,7 @@
*/ */
import { import {
createElement, createElement,
forwardRef,
Fragment, Fragment,
isValidElement, isValidElement,
useEffect, useEffect,
@ -13,6 +14,7 @@ import { useInstanceId } from '@wordpress/compose';
import classNames from 'classnames'; import classNames from 'classnames';
import { plus, reset } from '@wordpress/icons'; import { plus, reset } from '@wordpress/icons';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import type { ForwardedRef } from 'react';
import { import {
BaseControl, BaseControl,
Button, Button,
@ -27,6 +29,7 @@ import { useNumberInputProps } from '../../hooks/use-number-input-props';
import { Label } from '../label/label'; import { Label } from '../label/label';
export type NumberProps = { export type NumberProps = {
id?: string;
value: string; value: string;
onChange: ( selected: string ) => void; onChange: ( selected: string ) => void;
label: string | JSX.Element; label: string | JSX.Element;
@ -47,164 +50,180 @@ export type NumberProps = {
const MEDIUM_DELAY = 500; const MEDIUM_DELAY = 500;
const SHORT_DELAY = 100; const SHORT_DELAY = 100;
export const NumberControl: React.FC< NumberProps > = ( { export const NumberControl: React.FC< NumberProps > = forwardRef(
value, (
onChange, {
label, id,
suffix, value,
help, onChange,
error, label,
onBlur, suffix,
onFocus, help,
required, error,
tooltip, onBlur,
placeholder, onFocus,
disabled, required,
step = 1, tooltip,
min = -Infinity, placeholder,
max = Infinity, disabled,
}: NumberProps ) => { step = 1,
const id = useInstanceId( BaseControl, 'product_number_field' ) as string; min = -Infinity,
const [ isFocused, setIsFocused ] = useState( false ); max = Infinity,
const unfocusIfOutside = ( event: React.FocusEvent ) => { }: NumberProps,
if ( ref: ForwardedRef< HTMLInputElement >
! document ) => {
.getElementById( id ) const instanceId = useInstanceId(
?.parentElement?.contains( event.relatedTarget ) BaseControl,
) { 'product_number_field'
setIsFocused( false ); ) as string;
onBlur?.(); const identifier = id ?? instanceId;
} const [ isFocused, setIsFocused ] = useState( false );
}; const unfocusIfOutside = ( event: React.FocusEvent ) => {
const inputProps = useNumberInputProps( { if (
value: value || '', ! document
onChange, .getElementById( identifier )
onFocus: () => { ?.parentElement?.contains( event.relatedTarget )
setIsFocused( true ); ) {
onFocus?.(); setIsFocused( false );
}, onBlur?.();
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 );
} }
}; };
}, [ increment, value ] );
const resetIncrement = () => setIncrement( 0 ); function handleOnFocus() {
setIsFocused( true );
const handleIncrement = ( thisStep: number ) => { onFocus?.();
const newValue = parseFloat( value || '0' ) + thisStep;
if ( newValue >= min && newValue <= max ) {
onChange( String( parseFloat( value || '0' ) + thisStep ) );
setIncrement( thisStep );
isInitialClick.current = true;
} }
};
return ( const inputProps = useNumberInputProps( {
<BaseControl value: value || '',
className={ classNames( { onChange,
'has-error': error, onFocus: handleOnFocus,
} ) } min,
id={ id } max,
label={ } );
isValidElement( label ) ? (
label const [ increment, setIncrement ] = useState( 0 );
) : (
<Label const timeoutRef = useRef< number | null >( null );
label={ label as string }
required={ required } const isInitialClick = useRef< boolean >( false );
tooltip={ tooltip }
/> 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 } return () => {
> if ( timeoutRef.current ) {
<InputControl clearTimeout( timeoutRef.current );
{ ...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 }
/>
</>
) }
</>
} }
placeholder={ placeholder } };
onBlur={ unfocusIfOutside } }, [ increment, value ] );
/>
</BaseControl> 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 ) { async function focusByValidatorId( validatorId: string ) {
const field = await context.getFieldByValidatorId( validatorId ); const field = await context.getFieldByValidatorId( validatorId );
if ( field ) { const tab = field.closest(
field.focus(); '.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,
} );
} }
} }