Introduce new BusyStateWrapper component

This commit is contained in:
Philipp Stracker 2024-12-09 18:16:13 +01:00
parent 5c50a3d970
commit 1826b95c08
No known key found for this signature in database
9 changed files with 176 additions and 103 deletions

View file

@ -0,0 +1,10 @@
.ppcp-r-busy-wrapper {
position: relative;
&.ppcp--is-loading {
pointer-events: none;
user-select: none;
--spinner-overlay-color: #fff4;
}
}

View file

@ -31,10 +31,4 @@
&__toggled-content { &__toggled-content {
margin-top: 24px; margin-top: 24px;
} }
&.ppcp--is-loading {
pointer-events: none;
--spinner-overlay-color: #fff4;
}
} }

View file

@ -3,10 +3,11 @@
#ppcp-settings-container { #ppcp-settings-container {
@import './global'; @import './global';
@import './components/reusable-components/onboarding-header'; @import './components/reusable-components/busy-state';
@import './components/reusable-components/button'; @import './components/reusable-components/button';
@import './components/reusable-components/settings-toggle-block';
@import './components/reusable-components/separator'; @import './components/reusable-components/separator';
@import './components/reusable-components/onboarding-header';
@import './components/reusable-components/settings-toggle-block';
@import './components/reusable-components/payment-method-icons'; @import './components/reusable-components/payment-method-icons';
@import "./components/reusable-components/payment-method-item"; @import "./components/reusable-components/payment-method-item";
@import './components/reusable-components/settings-wrapper'; @import './components/reusable-components/settings-wrapper';

View file

@ -0,0 +1,57 @@
import {
Children,
isValidElement,
cloneElement,
useMemo,
} from '@wordpress/element';
import classNames from 'classnames';
import { CommonHooks } from '../../data';
import SpinnerOverlay from './SpinnerOverlay';
/**
* Wraps interactive child elements and modifies their behavior based on the global `isBusy` state.
* Allows custom processing of child props via the `onBusy` callback.
*
* @param {Object} props - Component properties.
* @param {Children} props.children - Child components to wrap.
* @param {boolean} props.enabled - Enables or disables the busy-state logic.
* @param {string} props.className - Additional class names for the wrapper.
* @param {Function} props.onBusy - Callback to process child props when busy.
*/
const BusyStateWrapper = ( {
children,
enabled = true,
className = '',
onBusy = () => ( { disabled: true } ),
} ) => {
const { isBusy } = CommonHooks.useBusyState();
const markAsBusy = isBusy && enabled;
const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, {
'ppcp--is-loading': markAsBusy,
} );
const memoizedChildren = useMemo(
() =>
Children.map( children, ( child ) =>
isValidElement( child )
? cloneElement(
child,
markAsBusy ? onBusy( child.props ) : {}
)
: child
),
[ children, markAsBusy, onBusy ]
);
return (
<div className={ wrapperClassName }>
{ markAsBusy && <SpinnerOverlay /> }
{ memoizedChildren }
</div>
);
};
export default BusyStateWrapper;

View file

@ -1,23 +1,17 @@
import { ToggleControl } from '@wordpress/components'; import { ToggleControl } from '@wordpress/components';
import { useRef } from '@wordpress/element'; import { useRef } from '@wordpress/element';
import SpinnerOverlay from './SpinnerOverlay';
const SettingsToggleBlock = ( { const SettingsToggleBlock = ( {
isToggled, isToggled,
setToggled, setToggled,
isLoading = false, disabled = false,
...props ...props
} ) => { } ) => {
const toggleRef = useRef( null ); const toggleRef = useRef( null );
const blockClasses = [ 'ppcp-r-toggle-block' ]; const blockClasses = [ 'ppcp-r-toggle-block' ];
if ( isLoading ) {
blockClasses.push( 'ppcp--is-loading' );
}
const handleLabelClick = () => { const handleLabelClick = () => {
if ( ! toggleRef.current || isLoading ) { if ( ! toggleRef.current || disabled ) {
return; return;
} }
@ -52,13 +46,12 @@ const SettingsToggleBlock = ( {
ref={ toggleRef } ref={ toggleRef }
checked={ isToggled } checked={ isToggled }
onChange={ ( newState ) => setToggled( newState ) } onChange={ ( newState ) => setToggled( newState ) }
disabled={ isLoading } disabled={ disabled }
/> />
</div> </div>
</div> </div>
{ props.children && isToggled && ( { props.children && isToggled && (
<div className="ppcp-r-toggle-block__toggled-content"> <div className="ppcp-r-toggle-block__toggled-content">
{ isLoading && <SpinnerOverlay /> }
{ props.children } { props.children }
</div> </div>
) } ) }

View file

@ -20,6 +20,7 @@ import {
} from '../../../../hooks/useHandleConnections'; } from '../../../../hooks/useHandleConnections';
import ConnectionButton from './ConnectionButton'; import ConnectionButton from './ConnectionButton';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const FORM_ERRORS = { const FORM_ERRORS = {
noClientId: __( noClientId: __(
@ -89,7 +90,7 @@ const AdvancedOptionsForm = () => {
handleConnectViaIdAndSecret( { handleConnectViaIdAndSecret( {
validation: validateManualConnectionForm, validation: validateManualConnectionForm,
} ), } ),
[ validateManualConnectionForm ] [ handleConnectViaIdAndSecret, validateManualConnectionForm ]
); );
useEffect( () => { useEffect( () => {
@ -124,69 +125,79 @@ const AdvancedOptionsForm = () => {
return ( return (
<> <>
<SettingsToggleBlock <BusyStateWrapper>
label={ __( <SettingsToggleBlock
'Enable Sandbox Mode', label={ __(
'woocommerce-paypal-payments' 'Enable Sandbox Mode',
) }
description={ __(
'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
'woocommerce-paypal-payments'
) }
isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode }
isLoading={ isBusy }
>
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
showIcon={ false } description={ __(
variant="secondary" 'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
isSandbox={ 'woocommerce-paypal-payments'
true /* This button always connects to sandbox */ ) }
} isToggled={ !! isSandboxMode }
/> setToggled={ setSandboxMode }
</SettingsToggleBlock> >
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
showIcon={ false }
variant="secondary"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
<Separator withLine={ false } /> <Separator withLine={ false } />
<SettingsToggleBlock <BusyStateWrapper
label={ onBusy={ ( props ) => ( {
__( 'Manually Connect', 'woocommerce-paypal-payments' ) + disabled: true,
( isBusy ? ' ...' : '' ) label: props.label + ' ...',
} } ) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
isLoading={ isBusy }
> >
<DataStoreControl <SettingsToggleBlock
control={ TextControl } label={ __(
ref={ refClientId } 'Manually Connect',
label={ clientIdLabel } 'woocommerce-paypal-payments'
value={ clientId } ) }
onChange={ setClientId } description={ advancedUsersDescription }
className={ classNames( { isToggled={ !! isManualConnectionMode }
'has-error': ! clientValid, setToggled={ setManualConnectionMode }
} ) } >
/> <DataStoreControl
{ clientValid || ( control={ TextControl }
<p className="client-id-error"> ref={ refClientId }
{ FORM_ERRORS.invalidClientId } label={ clientIdLabel }
</p> value={ clientId }
) } onChange={ setClientId }
<DataStoreControl className={ classNames( {
control={ TextControl } 'has-error': ! clientValid,
ref={ refClientSecret } } ) }
label={ secretKeyLabel } />
value={ clientSecret } { clientValid || (
onChange={ setClientSecret } <p className="client-id-error">
type="password" { FORM_ERRORS.invalidClientId }
/> </p>
<Button variant="secondary" onClick={ handleManualConnect }> ) }
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) } <DataStoreControl
</Button> control={ TextControl }
</SettingsToggleBlock> ref={ refClientSecret }
label={ secretKeyLabel }
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button variant="secondary" onClick={ handleManualConnect }>
{ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
</Button>
</SettingsToggleBlock>
</BusyStateWrapper>
</> </>
); );
}; };

View file

@ -8,6 +8,7 @@ import {
useProductionConnection, useProductionConnection,
useSandboxConnection, useSandboxConnection,
} from '../../../../hooks/useHandleConnections'; } from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const ConnectionButton = ( { const ConnectionButton = ( {
title, title,
@ -15,13 +16,11 @@ const ConnectionButton = ( {
variant = 'primary', variant = 'primary',
showIcon = true, showIcon = true,
} ) => { } ) => {
const { isBusy } = CommonHooks.useBusyState();
const { handleSandboxConnect } = useSandboxConnection(); const { handleSandboxConnect } = useSandboxConnection();
const { handleProductionConnect } = useProductionConnection(); const { handleProductionConnect } = useProductionConnection();
const className = classNames( 'ppcp-r-connection-button', { const className = classNames( 'ppcp-r-connection-button', {
'sandbox-mode': isSandbox, 'sandbox-mode': isSandbox,
'live-mode': ! isSandbox, 'live-mode': ! isSandbox,
'ppcp--is-loading': isBusy,
} ); } );
const handleConnectClick = async () => { const handleConnectClick = async () => {
@ -33,15 +32,16 @@ const ConnectionButton = ( {
}; };
return ( return (
<Button <BusyStateWrapper>
className={ className } <Button
variant={ variant } className={ className }
icon={ showIcon ? openSignup : null } variant={ variant }
onClick={ handleConnectClick } icon={ showIcon ? openSignup : null }
disabled={ isBusy } onClick={ handleConnectClick }
> >
<span className="button-title">{ title }</span> <span className="button-title">{ title }</span>
</Button> </Button>
</BusyStateWrapper>
); );
}; };

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { OnboardingHooks } from '../../../../data'; import { OnboardingHooks } from '../../../../data';
import useIsScrolled from '../../../../hooks/useIsScrolled'; import useIsScrolled from '../../../../hooks/useIsScrolled';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const { title, isFirst, percentage, showNext, canProceed } = stepDetails; const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
@ -20,7 +21,10 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
return ( return (
<div className={ className }> <div className={ className }>
<div className="ppcp-r-navigation"> <div className="ppcp-r-navigation">
<div className="ppcp-r-navigation--left"> <BusyStateWrapper
className="ppcp-r-navigation--left"
enabled={ ! isFirst }
>
<Button <Button
variant="link" variant="link"
onClick={ isFirst ? onExit : onPrev } onClick={ isFirst ? onExit : onPrev }
@ -31,7 +35,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
{ title } { title }
</span> </span>
</Button> </Button>
</div> </BusyStateWrapper>
{ ! isFirst && { ! isFirst &&
NextButton( { showNext, isDisabled, onNext, onExit } ) } NextButton( { showNext, isDisabled, onNext, onExit } ) }
<ProgressBar percent={ percentage } /> <ProgressBar percent={ percentage } />
@ -42,7 +46,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
return ( return (
<div className="ppcp-r-navigation--right"> <BusyStateWrapper className="ppcp-r-navigation--right">
<Button variant="link" onClick={ onExit }> <Button variant="link" onClick={ onExit }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) } { __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button> </Button>
@ -55,7 +59,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
{ __( 'Continue', 'woocommerce-paypal-payments' ) } { __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button> </Button>
) } ) }
</div> </BusyStateWrapper>
); );
}; };

View file

@ -9,6 +9,7 @@ import AccordionSection from '../../ReusableComponents/AccordionSection';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
import { CommonHooks } from '../../../data'; import { CommonHooks } from '../../../data';
import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper';
const StepWelcome = ( { setStep, currentStep } ) => { const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
@ -34,16 +35,18 @@ const StepWelcome = ( { setStep, currentStep } ) => {
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
</p> </p>
<Button <BusyStateWrapper>
className="ppcp-r-button-activate-paypal" <Button
variant="primary" className="ppcp-r-button-activate-paypal"
onClick={ () => setStep( currentStep + 1 ) } variant="primary"
> onClick={ () => setStep( currentStep + 1 ) }
{ __( >
'Activate PayPal Payments', { __(
'woocommerce-paypal-payments' 'Activate PayPal Payments',
) } 'woocommerce-paypal-payments'
</Button> ) }
</Button>
</BusyStateWrapper>
</div> </div>
<Separator className="ppcp-r-page-welcome-mode-separator" /> <Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs <WelcomeDocs