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 {
|
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,
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue