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:
parent
6bc551c71b
commit
67ecc95633
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add reference to number control #49357
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue