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,6 +125,7 @@ const AdvancedOptionsForm = () => {
return ( return (
<> <>
<BusyStateWrapper>
<SettingsToggleBlock <SettingsToggleBlock
label={ __( label={ __(
'Enable Sandbox Mode', 'Enable Sandbox Mode',
@ -135,7 +137,6 @@ const AdvancedOptionsForm = () => {
) } ) }
isToggled={ !! isSandboxMode } isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode } setToggled={ setSandboxMode }
isLoading={ isBusy }
> >
<ConnectionButton <ConnectionButton
title={ __( title={ __(
@ -149,16 +150,22 @@ const AdvancedOptionsForm = () => {
} }
/> />
</SettingsToggleBlock> </SettingsToggleBlock>
</BusyStateWrapper>
<Separator withLine={ false } /> <Separator withLine={ false } />
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock <SettingsToggleBlock
label={ label={ __(
__( 'Manually Connect', 'woocommerce-paypal-payments' ) + 'Manually Connect',
( isBusy ? ' ...' : '' ) 'woocommerce-paypal-payments'
} ) }
description={ advancedUsersDescription } description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode } isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode } setToggled={ setManualConnectionMode }
isLoading={ isBusy }
> >
<DataStoreControl <DataStoreControl
control={ TextControl } control={ TextControl }
@ -184,9 +191,13 @@ const AdvancedOptionsForm = () => {
type="password" type="password"
/> />
<Button variant="secondary" onClick={ handleManualConnect }> <Button variant="secondary" onClick={ handleManualConnect }>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) } { __(
'Connect Account',
'woocommerce-paypal-payments'
) }
</Button> </Button>
</SettingsToggleBlock> </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 (
<BusyStateWrapper>
<Button <Button
className={ className } className={ className }
variant={ variant } variant={ variant }
icon={ showIcon ? openSignup : null } icon={ showIcon ? openSignup : null }
onClick={ handleConnectClick } onClick={ handleConnectClick }
disabled={ isBusy }
> >
<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,6 +35,7 @@ const StepWelcome = ( { setStep, currentStep } ) => {
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
</p> </p>
<BusyStateWrapper>
<Button <Button
className="ppcp-r-button-activate-paypal" className="ppcp-r-button-activate-paypal"
variant="primary" variant="primary"
@ -44,6 +46,7 @@ const StepWelcome = ( { setStep, currentStep } ) => {
'woocommerce-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