Skip to content

Commit

Permalink
Donations block: Make currency and amounts editable (#16593)
Browse files Browse the repository at this point in the history
Adds the ability to edit the currency and amounts in a donations block.
  • Loading branch information
mmtr authored Aug 4, 2020
1 parent f3ab334 commit aefee69
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 121 deletions.
150 changes: 150 additions & 0 deletions extensions/blocks/donations/amount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* External dependencies
*/
import formatCurrency, { CURRENCIES } from '@automattic/format-currency';
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { RichText } from '@wordpress/block-editor';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { minimumTransactionAmountForCurrency } from '../../shared/currencies';

const Amount = ( {
className = '',
currency = null,
defaultValue = null,
editable = false,
label = '',
onChange = null,
value = '',
} ) => {
const [ editedValue, setEditedValue ] = useState(
formatCurrency( value, currency, { symbol: '' } )
);
const [ isFocused, setIsFocused ] = useState( false );
const [ isInvalid, setIsInvalid ] = useState( false );
const richTextRef = useRef( null );

const parseAmount = useCallback(
amount => {
if ( ! amount ) {
return null;
}

if ( typeof amount === 'number' ) {
return amount;
}

amount = parseFloat(
amount
// Remove any thousand grouping separator.
.replace( new RegExp( '\\' + CURRENCIES[ currency ].grouping, 'g' ), '' )
// Replace the localized decimal separator with a dot (the standard decimal separator in float numbers).
.replace( new RegExp( '\\' + CURRENCIES[ currency ].decimal, 'g' ), '.' )
);

if ( isNaN( amount ) ) {
return null;
}

return amount;
},
[ currency ]
);

const setAmount = useCallback(
amount => {
setEditedValue( amount );

if ( ! onChange ) {
return;
}

const parsedAmount = parseAmount( amount, currency );
if ( parsedAmount && parsedAmount >= minimumTransactionAmountForCurrency( currency ) ) {
onChange( parsedAmount );
setIsInvalid( false );
} else if ( amount ) {
setIsInvalid( true );
}
},
[ currency, parseAmount, onChange ]
);

const setFocus = () => {
if ( ! richTextRef.current ) {
return;
}

richTextRef.current.focus();
setIsFocused( true );
};

// Tracks when user clicks out the input. Cannot be done with an `onBlur` prop because `RichText` does not support it.
useEffect( () => {
if ( ! richTextRef.current ) {
return;
}

richTextRef.current.addEventListener( 'blur', () => setIsFocused( false ) );
}, [ richTextRef ] );

// Sets a default value if empty when user clicks out the input.
useEffect( () => {
if ( isFocused || editedValue ) {
return;
}

setAmount( formatCurrency( defaultValue, currency, { symbol: '' } ) );
}, [ currency, defaultValue, editedValue, isFocused, setAmount ] );

// Syncs the edited value with the actual value whenever the latter changes (e.g. new default amount after a currency change).
useEffect( () => {
if ( isFocused || isInvalid ) {
return;
}
setEditedValue( formatCurrency( value, currency, { symbol: '' } ) );
}, [ currency, isFocused, isInvalid, setAmount, value ] );

return (
<div className={ classnames( 'wp-block-button', 'donations__amount', className ) }>
<div
className={ classnames( 'wp-block-button__link', {
'has-focus': isFocused,
'has-error': isInvalid,
} ) }
onClick={ setFocus }
onKeyDown={ setFocus }
role="button"
tabIndex={ 0 }
>
{ CURRENCIES[ currency ].symbol }
{ editable ? (
<RichText
allowedFormats={ [] }
aria-label={ label }
keepPlaceholderOnFocus={ true }
multiline={ false }
onChange={ amount => setAmount( amount ) }
placeholder={ formatCurrency( defaultValue, currency, { symbol: '' } ) }
ref={ richTextRef }
value={ editedValue }
withoutInteractiveFormatting
/>
) : (
<span className="donations__amount-value">
{ formatCurrency( value ? value : defaultValue, currency, { symbol: '' } ) }
</span>
) }
</div>
</div>
);
};

export default Amount;
9 changes: 8 additions & 1 deletion extensions/blocks/donations/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export default {
type: 'string',
default: 'USD',
},
amounts: {
type: 'array',
items: {
type: 'number',
},
default: [ 5, 15, 100 ],
},
oneTimePlanId: {
type: 'number',
default: null,
Expand Down Expand Up @@ -38,7 +45,7 @@ export default {
},
chooseAmountText: {
type: 'string',
default: __( 'Choose an amount (USD)', 'jetpack' ),
default: __( 'Choose an amount', 'jetpack' ),
},
customAmountText: {
type: 'string',
Expand Down
125 changes: 96 additions & 29 deletions extensions/blocks/donations/controls.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,107 @@
/**
* WordPress dependencies
*/
import { ExternalLink, PanelBody, ToggleControl } from '@wordpress/components';
import { BlockControls, InspectorControls } from '@wordpress/block-editor';
import {
Button,
Dashicon,
Dropdown,
ExternalLink,
MenuGroup,
MenuItem,
PanelBody,
ToggleControl,
ToolbarGroup,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import { DOWN } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { SUPPORTED_CURRENCIES } from '../../shared/currencies';
import { CURRENCIES } from '@automattic/format-currency';

const Controls = props => {
const { attributes, setAttributes, products, siteSlug } = props;
const { monthlyPlanId, annuallyPlanId, showCustomAmount } = attributes;
const { currency, monthlyPlanId, annuallyPlanId, showCustomAmount } = attributes;

return (
<InspectorControls>
<PanelBody title={ __( 'Settings', 'jetpack' ) }>
<ToggleControl
checked={ !! monthlyPlanId }
onChange={ value =>
setAttributes( { monthlyPlanId: value ? products[ '1 month' ] : null } )
}
label={ __( 'Show monthly donations', 'jetpack' ) }
/>
<ToggleControl
checked={ !! annuallyPlanId }
onChange={ value =>
setAttributes( { annuallyPlanId: value ? products[ '1 year' ] : null } )
}
label={ __( 'Show annual donations', 'jetpack' ) }
/>
<ToggleControl
checked={ showCustomAmount }
onChange={ value => setAttributes( { showCustomAmount: value } ) }
label={ __( 'Show custom amount option', 'jetpack' ) }
/>
<ExternalLink href={ `https://wordpress.com/earn/payments/${ siteSlug }` }>
{ __( 'View donation earnings', 'jetpack' ) }
</ExternalLink>
</PanelBody>
</InspectorControls>
<>
<BlockControls>
<ToolbarGroup>
<Dropdown
contentClassName="jetpack-donations__currency-popover"
renderToggle={ ( { onToggle, isOpen } ) => {
const openOnArrowDown = event => {
if ( ! isOpen && event.keyCode === DOWN ) {
event.preventDefault();
event.stopPropagation();
onToggle();
}
};

return (
<Button
className="jetpack-donations__currency-toggle"
icon={
<>
{ CURRENCIES[ currency ].symbol + ' - ' + currency }
<Dashicon icon="arrow-down" />
</>
}
label={ __( 'Change currency', 'jetpack' ) }
onClick={ onToggle }
onKeyDown={ openOnArrowDown }
/>
);
} }
renderContent={ ( { onClose } ) => (
<MenuGroup>
{ Object.keys( SUPPORTED_CURRENCIES ).map( ccy => (
<MenuItem
isSelected={ ccy === currency }
onClick={ () => {
setAttributes( { currency: ccy } );
onClose();
} }
key={ `jetpack-donations-currency-${ ccy }` }
>
{ CURRENCIES[ ccy ].symbol + ' - ' + ccy }
</MenuItem>
) ) }
</MenuGroup>
) }
/>
</ToolbarGroup>
</BlockControls>
<InspectorControls>
<PanelBody title={ __( 'Settings', 'jetpack' ) }>
<ToggleControl
checked={ !! monthlyPlanId }
onChange={ value =>
setAttributes( { monthlyPlanId: value ? products[ '1 month' ] : null } )
}
label={ __( 'Show monthly donations', 'jetpack' ) }
/>
<ToggleControl
checked={ !! annuallyPlanId }
onChange={ value =>
setAttributes( { annuallyPlanId: value ? products[ '1 year' ] : null } )
}
label={ __( 'Show annual donations', 'jetpack' ) }
/>
<ToggleControl
checked={ showCustomAmount }
onChange={ value => setAttributes( { showCustomAmount: value } ) }
label={ __( 'Show custom amount option', 'jetpack' ) }
/>
<ExternalLink href={ `https://wordpress.com/earn/payments/${ siteSlug }` }>
{ __( 'View donation earnings', 'jetpack' ) }
</ExternalLink>
</PanelBody>
</InspectorControls>
</>
);
};

Expand Down
1 change: 0 additions & 1 deletion extensions/blocks/donations/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ const Edit = props => {
return (
<Tabs
{ ...props }
className={ className }
products={ products }
shouldUpgrade={ shouldUpgrade }
siteSlug={ siteSlug }
Expand Down
54 changes: 46 additions & 8 deletions extensions/blocks/donations/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,48 @@
background-color: $white;
color: $dark-gray-800;
border: 1px solid $light-gray-700;
}
cursor: text;

.donations__custom-amount {
margin-bottom: 30px;
&.has-focus {
box-shadow: 0 0 0 1px $white, 0 0 0 3px $blue-wordpress-700;
outline: 2px solid transparent;
outline-offset: -2px;
}

&.has-error {
box-shadow: 0 0 0 1px $white, 0 0 0 3px $alert-red;
outline: 2px solid transparent;
outline-offset: -2px;
}
}

.donations__custom-amount .wp-block-button__link {
cursor: default;
.donations__amount .block-editor-rich-text__editable {
display: inline-block;
text-align: left;

&:focus {
box-shadow: none;
outline: none;
outline-offset: 0;
}
}

.donations__custom-amount-placeholder {
margin-left: 8px;
.donations__amount.wp-block-button:not(.has-text-color):not(.is-style-outline) [data-rich-text-placeholder]:after {
color: $light-gray-700;
padding-right: 40px;
}

.donations__custom-amount {
margin-bottom: 30px;

.wp-block-button__link {
cursor: default;
}

.donations__amount-value {
margin-left: 4px;
min-width: 60px;
color: $light-gray-700;
}
}

.donations__separator {
Expand All @@ -90,3 +118,13 @@
max-width: none;
}
}

.jetpack-donations__currency-toggle {
font-weight: bold;
line-height: 100%;
width: max-content;
}

.jetpack-donations__currency-popover .components-popover__content {
min-width: 120px;
}
Loading

0 comments on commit aefee69

Please sign in to comment.