Add step buttons for Number block and component (#43045)

* Add showStepButtons attribute for Number block and component

* Use native step buttons

* Revert "Use native step buttons"

This reverts commit 8425d17399.

* Use custom step buttons and hide them when field is not focused

* Revert type="number" as it's not working well with number formatting

* Disable autocomplete

* Remove leftover showStepButtons from documentation

* Prevent default event when pressing up/down

* Change to onKeyDown event

* Implement timeout to hold + and - buttons

* Remove unnecessary onBlur and prevent default event when onKeyUp

* Fix TS issue

* Update changelog
This commit is contained in:
Nathan Silveira 2024-01-09 18:08:38 -03:00 committed by GitHub
parent 4114f989e5
commit 548987d758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 9 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add step buttons for Number block and component

View File

@ -1,11 +1,20 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import {
createElement,
Fragment,
useEffect,
useRef,
useState,
} from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
import classNames from 'classnames';
import { plus, reset } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import {
BaseControl,
Button,
// @ts-expect-error `__experimentalInputControl` does exist.
__experimentalInputControl as InputControl,
} from '@wordpress/components';
@ -43,14 +52,45 @@ export const NumberControl: React.FC< NumberProps > = ( {
tooltip,
placeholder,
disabled,
step,
step = 1,
}: 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 ),
} );
const id = useInstanceId( BaseControl, 'product_number_field' ) as string;
const [ increment, setIncrement ] = useState( 0 );
const timeoutRef = useRef< number | null >( null );
const incrementValue = () =>
onChange( String( parseFloat( value || '0' ) + increment ) );
useEffect( () => {
if ( increment !== 0 ) {
timeoutRef.current = setTimeout( incrementValue, 100 );
} else if ( timeoutRef.current ) {
clearTimeout( timeoutRef.current );
}
return () => {
if ( timeoutRef.current ) {
clearTimeout( timeoutRef.current );
}
};
}, [ increment, value ] );
return (
<BaseControl
@ -71,10 +111,64 @@ export const NumberControl: React.FC< NumberProps > = ( {
{ ...inputProps }
step={ step }
disabled={ disabled }
autoComplete="off"
id={ id }
suffix={ suffix }
className="woocommerce-number-control"
suffix={
<>
{ suffix }
{ isFocused && (
<>
<Button
className="woocommerce-number-control__increment"
icon={ plus }
onMouseDown={ () => {
onChange(
String(
parseFloat( value || '0' ) +
step
)
);
setIncrement( step );
} }
onMouseUp={ () => setIncrement( 0 ) }
onBlur={ unfocusIfOutside }
isSmall
aria-hidden="true"
aria-label={ __(
'Increment',
'woocommerce'
) }
tabIndex={ -1 }
/>
<Button
icon={ reset }
className="woocommerce-number-control__decrement"
onBlur={ unfocusIfOutside }
onMouseDown={ () => {
onChange(
String(
parseFloat( value || '0' ) -
step
)
);
setIncrement( -step );
} }
onMouseUp={ () => setIncrement( 0 ) }
isSmall
aria-hidden="true"
aria-label={ __(
'Decrement',
'woocommerce'
) }
tabIndex={ -1 }
/>
</>
) }
</>
}
placeholder={ placeholder }
onBlur={ onBlur }
onBlur={ unfocusIfOutside }
/>
</BaseControl>
);

View File

@ -8,6 +8,7 @@ export type NumberInputProps = {
value: string;
onChange: ( value: string ) => void;
onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void;
onKeyDown: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
@ -15,14 +16,14 @@ type Props = {
value: string;
onChange: ( value: string ) => void;
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
export const useNumberInputProps = ( {
value,
onChange,
onFocus,
onKeyUp,
onKeyDown,
}: Props ) => {
const { formatNumber, parseNumber } = useProductHelper();
@ -35,16 +36,23 @@ export const useNumberInputProps = ( {
}
},
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
if ( event.code === 'ArrowUp' || event.code === 'ArrowDown' ) {
event.preventDefault();
}
},
onKeyDown( event: React.KeyboardEvent< HTMLInputElement > ) {
const amount = Number.parseFloat( value || '0' );
const step = Number( event.currentTarget.step || '1' );
if ( event.code === 'ArrowUp' ) {
event.preventDefault();
onChange( String( amount + step ) );
}
if ( event.code === 'ArrowDown' ) {
event.preventDefault();
onChange( String( amount - step ) );
}
if ( onKeyUp ) {
onKeyUp( event );
if ( onKeyDown ) {
onKeyDown( event );
}
},
onChange( newValue: string ) {