initial commit
This commit is contained in:
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||
import Button from '@woocommerce/base-components/button';
|
||||
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
import {
|
||||
ValidationInputError,
|
||||
useValidationContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import { Panel } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const TotalsCoupon = ( {
|
||||
instanceId,
|
||||
isLoading = false,
|
||||
initialOpen = false,
|
||||
onSubmit = () => {},
|
||||
} ) => {
|
||||
const [ couponValue, setCouponValue ] = useState( '' );
|
||||
const currentIsLoading = useRef( false );
|
||||
const { getValidationError, getValidationErrorId } = useValidationContext();
|
||||
const validationError = getValidationError( 'coupon' );
|
||||
|
||||
useEffect( () => {
|
||||
if ( currentIsLoading.current !== isLoading ) {
|
||||
if ( ! isLoading && couponValue && ! validationError ) {
|
||||
setCouponValue( '' );
|
||||
}
|
||||
currentIsLoading.current = isLoading;
|
||||
}
|
||||
}, [ isLoading, couponValue, validationError ] );
|
||||
|
||||
const textInputId = `wc-block-components-totals-coupon__input-${ instanceId }`;
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className="wc-block-components-totals-coupon"
|
||||
hasBorder={ false }
|
||||
initialOpen={ initialOpen }
|
||||
title={
|
||||
<Label
|
||||
label={ __(
|
||||
'Coupon code',
|
||||
'woocommerce'
|
||||
) }
|
||||
screenReaderLabel={ __(
|
||||
'Apply a coupon code',
|
||||
'woocommerce'
|
||||
) }
|
||||
htmlFor={ textInputId }
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LoadingMask
|
||||
screenReaderLabel={ __(
|
||||
'Applying coupon…',
|
||||
'woocommerce'
|
||||
) }
|
||||
isLoading={ isLoading }
|
||||
showSpinner={ false }
|
||||
>
|
||||
<div className="wc-block-components-totals-coupon__content">
|
||||
<form className="wc-block-components-totals-coupon__form">
|
||||
<ValidatedTextInput
|
||||
id={ textInputId }
|
||||
errorId="coupon"
|
||||
className="wc-block-components-totals-coupon__input"
|
||||
label={ __(
|
||||
'Enter code',
|
||||
'woocommerce'
|
||||
) }
|
||||
value={ couponValue }
|
||||
ariaDescribedBy={ getValidationErrorId(
|
||||
textInputId
|
||||
) }
|
||||
onChange={ ( newCouponValue ) => {
|
||||
setCouponValue( newCouponValue );
|
||||
} }
|
||||
validateOnMount={ false }
|
||||
focusOnMount={ true }
|
||||
showError={ false }
|
||||
/>
|
||||
<Button
|
||||
className="wc-block-components-totals-coupon__button"
|
||||
disabled={ isLoading || ! couponValue }
|
||||
showSpinner={ isLoading }
|
||||
onClick={ ( e ) => {
|
||||
e.preventDefault();
|
||||
onSubmit( couponValue );
|
||||
} }
|
||||
type="submit"
|
||||
>
|
||||
{ __( 'Apply', 'woocommerce' ) }
|
||||
</Button>
|
||||
</form>
|
||||
<ValidationInputError
|
||||
propertyName="coupon"
|
||||
elementId={ textInputId }
|
||||
/>
|
||||
</div>
|
||||
</LoadingMask>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
TotalsCoupon.propTypes = {
|
||||
onSubmit: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withInstanceId( TotalsCoupon );
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { text, boolean } from '@storybook/addon-knobs';
|
||||
import {
|
||||
useValidationContext,
|
||||
ValidationContextProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TotalsCoupon from '../';
|
||||
|
||||
export default {
|
||||
title:
|
||||
'WooCommerce Blocks/@base-components/cart-checkout/totals/TotalsCoupon',
|
||||
component: TotalsCoupon,
|
||||
};
|
||||
|
||||
const StoryComponent = ( { validCoupon, isLoading, invalidCouponText } ) => {
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const onSubmit = ( coupon ) => {
|
||||
if ( coupon !== validCoupon ) {
|
||||
setValidationErrors( { coupon: invalidCouponText } );
|
||||
}
|
||||
};
|
||||
return <TotalsCoupon isLoading={ isLoading } onSubmit={ onSubmit } />;
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const validCoupon = text( 'A valid coupon code', 'validcoupon' );
|
||||
const invalidCouponText = text(
|
||||
'Error message for invalid code',
|
||||
'Invalid coupon code.'
|
||||
);
|
||||
const isLoading = boolean( 'Toggle isLoading state', false );
|
||||
return (
|
||||
<ValidationContextProvider>
|
||||
<StoryComponent
|
||||
validCoupon={ validCoupon }
|
||||
isLoading={ isLoading }
|
||||
invalidCouponText={ invalidCouponText }
|
||||
/>
|
||||
</ValidationContextProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
.wc-block-components-totals-coupon {
|
||||
|
||||
.wc-block-components-panel__button {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.wc-block-components-panel__content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-totals-coupon__form {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
|
||||
.wc-block-components-totals-coupon__input {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.wc-block-components-totals-coupon__button {
|
||||
height: em(48px);
|
||||
flex-shrink: 0;
|
||||
margin-left: $gap-smaller;
|
||||
padding-left: $gap-large;
|
||||
padding-right: $gap-large;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wc-block-components-totals-coupon__button.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-totals-coupon__content {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { RemovableChip } from '@woocommerce/base-components/chip';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
__experimentalApplyCheckoutFilter,
|
||||
TotalsItem,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const filteredCartCouponsFilterArg = {
|
||||
context: 'summary',
|
||||
};
|
||||
|
||||
const TotalsDiscount = ( {
|
||||
cartCoupons = [],
|
||||
currency,
|
||||
isRemovingCoupon,
|
||||
removeCoupon,
|
||||
values,
|
||||
} ) => {
|
||||
const {
|
||||
total_discount: totalDiscount,
|
||||
total_discount_tax: totalDiscountTax,
|
||||
} = values;
|
||||
const discountValue = parseInt( totalDiscount, 10 );
|
||||
|
||||
if ( ! discountValue && cartCoupons.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const discountTaxValue = parseInt( totalDiscountTax, 10 );
|
||||
const discountTotalValue = getSetting(
|
||||
'displayCartPricesIncludingTax',
|
||||
false
|
||||
)
|
||||
? discountValue + discountTaxValue
|
||||
: discountValue;
|
||||
|
||||
const filteredCartCoupons = __experimentalApplyCheckoutFilter( {
|
||||
arg: filteredCartCouponsFilterArg,
|
||||
filterName: 'coupons',
|
||||
defaultValue: cartCoupons,
|
||||
} );
|
||||
|
||||
return (
|
||||
<TotalsItem
|
||||
className="wc-block-components-totals-discount"
|
||||
currency={ currency }
|
||||
description={
|
||||
filteredCartCoupons.length !== 0 && (
|
||||
<LoadingMask
|
||||
screenReaderLabel={ __(
|
||||
'Removing coupon…',
|
||||
'woocommerce'
|
||||
) }
|
||||
isLoading={ isRemovingCoupon }
|
||||
showSpinner={ false }
|
||||
>
|
||||
<ul className="wc-block-components-totals-discount__coupon-list">
|
||||
{ filteredCartCoupons.map( ( cartCoupon ) => {
|
||||
return (
|
||||
<RemovableChip
|
||||
key={ 'coupon-' + cartCoupon.code }
|
||||
className="wc-block-components-totals-discount__coupon-list-item"
|
||||
text={ cartCoupon.label }
|
||||
screenReaderText={ sprintf(
|
||||
/* translators: %s Coupon code. */
|
||||
__(
|
||||
'Coupon: %s',
|
||||
'woocommerce'
|
||||
),
|
||||
cartCoupon.label
|
||||
) }
|
||||
disabled={ isRemovingCoupon }
|
||||
onRemove={ () => {
|
||||
removeCoupon( cartCoupon.code );
|
||||
} }
|
||||
radius="large"
|
||||
ariaLabel={ sprintf(
|
||||
/* translators: %s is a coupon code. */
|
||||
__(
|
||||
'Remove coupon "%s"',
|
||||
'woocommerce'
|
||||
),
|
||||
cartCoupon.label
|
||||
) }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
</LoadingMask>
|
||||
)
|
||||
}
|
||||
label={
|
||||
!! discountTotalValue
|
||||
? __( 'Discount', 'woocommerce' )
|
||||
: __( 'Coupons', 'woocommerce' )
|
||||
}
|
||||
value={ discountTotalValue ? discountTotalValue * -1 : '-' }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TotalsDiscount.propTypes = {
|
||||
cartCoupons: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
code: PropTypes.string.isRequired,
|
||||
} )
|
||||
),
|
||||
currency: PropTypes.object.isRequired,
|
||||
isRemovingCoupon: PropTypes.bool.isRequired,
|
||||
removeCoupon: PropTypes.func.isRequired,
|
||||
values: PropTypes.shape( {
|
||||
total_discount: PropTypes.string,
|
||||
total_discount_tax: PropTypes.string,
|
||||
} ).isRequired,
|
||||
};
|
||||
|
||||
export default TotalsDiscount;
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { text, boolean } from '@storybook/addon-knobs';
|
||||
import { currencyKnob } from '@woocommerce/knobs';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TotalsDiscount from '../';
|
||||
|
||||
export default {
|
||||
title:
|
||||
'WooCommerce Blocks/@base-components/cart-checkout/totals/TotalsDiscount',
|
||||
component: TotalsDiscount,
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const cartCoupons = [ { code: 'COUPON' } ];
|
||||
const currency = currencyKnob();
|
||||
const isRemovingCoupon = boolean( 'Toggle isRemovingCoupon state', false );
|
||||
const totalDiscount = text( 'Total discount', '1000' );
|
||||
const totalDiscountTax = text( 'Total discount tax', '200' );
|
||||
|
||||
return (
|
||||
<TotalsDiscount
|
||||
cartCoupons={ cartCoupons }
|
||||
currency={ currency }
|
||||
isRemovingCoupon={ isRemovingCoupon }
|
||||
removeCoupon={ () => void null }
|
||||
values={ {
|
||||
total_discount: totalDiscount,
|
||||
total_discount_tax: totalDiscountTax,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
.wc-block-components-totals-discount__coupon-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wc-block-components-totals-discount .wc-block-components-totals-item__value {
|
||||
color: $discount-color;
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
__experimentalApplyCheckoutFilter,
|
||||
TotalsItem,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const TotalsFooterItem = ( { currency, values } ) => {
|
||||
const SHOW_TAXES =
|
||||
getSetting( 'taxesEnabled', true ) &&
|
||||
getSetting( 'displayCartPricesIncludingTax', false );
|
||||
|
||||
const { total_price: totalPrice, total_tax: totalTax } = values;
|
||||
|
||||
// Prepare props to pass to the __experimentalApplyCheckoutFilter filter.
|
||||
// We need to pluck out receiveCart.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { receiveCart, ...cart } = useStoreCart();
|
||||
const label = __experimentalApplyCheckoutFilter( {
|
||||
filterName: 'totalLabel',
|
||||
defaultValue: __( 'Total', 'woocommerce' ),
|
||||
extensions: cart.extensions,
|
||||
arg: { cart },
|
||||
} );
|
||||
|
||||
const parsedTaxValue = parseInt( totalTax, 10 );
|
||||
|
||||
return (
|
||||
<TotalsItem
|
||||
className="wc-block-components-totals-footer-item"
|
||||
currency={ currency }
|
||||
label={ label }
|
||||
value={ parseInt( totalPrice, 10 ) }
|
||||
description={
|
||||
SHOW_TAXES &&
|
||||
parsedTaxValue !== 0 && (
|
||||
<p className="wc-block-components-totals-footer-item-tax">
|
||||
{ createInterpolateElement(
|
||||
__(
|
||||
'Including <TaxAmount/> in taxes',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
TaxAmount: (
|
||||
<FormattedMonetaryAmount
|
||||
className="wc-block-components-totals-footer-item-tax-value"
|
||||
currency={ currency }
|
||||
value={ parsedTaxValue }
|
||||
/>
|
||||
),
|
||||
}
|
||||
) }
|
||||
</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TotalsFooterItem.propTypes = {
|
||||
currency: PropTypes.object.isRequired,
|
||||
values: PropTypes.shape( {
|
||||
total_price: PropTypes.string,
|
||||
total_tax: PropTypes.string,
|
||||
} ).isRequired,
|
||||
};
|
||||
|
||||
export default TotalsFooterItem;
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { currencyKnob } from '@woocommerce/knobs';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TotalsFooterItem from '../';
|
||||
|
||||
export default {
|
||||
title:
|
||||
'WooCommerce Blocks/@base-components/cart-checkout/totals/TotalsFooterItem',
|
||||
component: TotalsFooterItem,
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const currency = currencyKnob();
|
||||
const totalPrice = text( 'Total price', '1200' );
|
||||
const totalTax = text( 'Total tax', '200' );
|
||||
|
||||
return (
|
||||
<TotalsFooterItem
|
||||
currency={ currency }
|
||||
values={ {
|
||||
total_price: totalPrice,
|
||||
total_tax: totalTax,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
.wc-block-components-totals-footer-item {
|
||||
.wc-block-components-totals-item__value,
|
||||
.wc-block-components-totals-item__label {
|
||||
@include font-size(large);
|
||||
}
|
||||
|
||||
.wc-block-components-totals-item__label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wc-block-components-totals-footer-item-tax {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TotalsFooterItem Does not show the "including %s of tax" line if tax is 0 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="wc-block-components-totals-item wc-block-components-totals-footer-item"
|
||||
>
|
||||
<span
|
||||
class="wc-block-components-totals-item__label"
|
||||
>
|
||||
Total
|
||||
</span>
|
||||
<span
|
||||
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-item__value"
|
||||
>
|
||||
£85.00
|
||||
</span>
|
||||
<div
|
||||
class="wc-block-components-totals-item__description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TotalsFooterItem Does not show the "including %s of tax" line if tax is disabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="wc-block-components-totals-item wc-block-components-totals-footer-item"
|
||||
>
|
||||
<span
|
||||
class="wc-block-components-totals-item__label"
|
||||
>
|
||||
Total
|
||||
</span>
|
||||
<span
|
||||
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-item__value"
|
||||
>
|
||||
£85.00
|
||||
</span>
|
||||
<div
|
||||
class="wc-block-components-totals-item__description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TotalsFooterItem Shows the "including %s of tax" line if tax is greater than 0 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="wc-block-components-totals-item wc-block-components-totals-footer-item"
|
||||
>
|
||||
<span
|
||||
class="wc-block-components-totals-item__label"
|
||||
>
|
||||
Total
|
||||
</span>
|
||||
<span
|
||||
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-item__value"
|
||||
>
|
||||
£85.00
|
||||
</span>
|
||||
<div
|
||||
class="wc-block-components-totals-item__description"
|
||||
>
|
||||
<p
|
||||
class="wc-block-components-totals-footer-item-tax"
|
||||
>
|
||||
Including
|
||||
<span
|
||||
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-footer-item-tax-value"
|
||||
>
|
||||
£1.00
|
||||
</span>
|
||||
in taxes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TotalsFooterItem from '../index';
|
||||
import { allSettings } from '../../../../../../settings/shared/settings-init';
|
||||
|
||||
describe( 'TotalsFooterItem', () => {
|
||||
beforeEach( () => {
|
||||
allSettings.taxesEnabled = true;
|
||||
allSettings.displayCartPricesIncludingTax = true;
|
||||
} );
|
||||
const currency = {
|
||||
code: 'GBP',
|
||||
decimalSeparator: '.',
|
||||
minorUnit: 2,
|
||||
prefix: '£',
|
||||
suffix: '',
|
||||
symbol: '£',
|
||||
thousandSeparator: ',',
|
||||
};
|
||||
|
||||
const values = {
|
||||
currency_code: 'GBP',
|
||||
currency_decimal_separator: '.',
|
||||
currency_minor_unit: 2,
|
||||
currency_prefix: '£',
|
||||
currency_suffix: '',
|
||||
currency_symbol: '£',
|
||||
currency_thousand_separator: ',',
|
||||
tax_lines: [],
|
||||
length: 2,
|
||||
total_discount: '0',
|
||||
total_discount_tax: '0',
|
||||
total_fees: '0',
|
||||
total_fees_tax: '0',
|
||||
total_items: '7100',
|
||||
total_items_tax: '0',
|
||||
total_price: '8500',
|
||||
total_shipping: '0',
|
||||
total_shipping_tax: '0',
|
||||
total_tax: '0',
|
||||
};
|
||||
|
||||
it( 'Does not show the "including %s of tax" line if tax is 0', () => {
|
||||
const { container } = render(
|
||||
<TotalsFooterItem currency={ currency } values={ values } />
|
||||
);
|
||||
expect( container ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'Does not show the "including %s of tax" line if tax is disabled', () => {
|
||||
allSettings.taxesEnabled = false;
|
||||
/* This shouldn't ever happen if taxes are disabled, but this is to test whether the taxesEnabled setting works */
|
||||
const valuesWithTax = {
|
||||
...values,
|
||||
total_tax: '100',
|
||||
total_items_tax: '100',
|
||||
};
|
||||
const { container } = render(
|
||||
<TotalsFooterItem currency={ currency } values={ valuesWithTax } />
|
||||
);
|
||||
expect( container ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'Shows the "including %s of tax" line if tax is greater than 0', () => {
|
||||
const valuesWithTax = {
|
||||
...values,
|
||||
total_tax: '100',
|
||||
total_items_tax: '100',
|
||||
};
|
||||
const { container } = render(
|
||||
<TotalsFooterItem currency={ currency } values={ valuesWithTax } />
|
||||
);
|
||||
expect( container ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
@ -0,0 +1,4 @@
|
||||
export { default as TotalsCoupon } from './coupon';
|
||||
export { default as TotalsDiscount } from './discount';
|
||||
export { default as TotalsFooterItem } from './footer-item';
|
||||
export { default as TotalsShipping } from './shipping';
|
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Searches an array of packages/rates to see if there are actually any rates
|
||||
* available.
|
||||
*
|
||||
* @param {Array} shippingRatePackages An array of packages and rates.
|
||||
* @return {boolean} True if a rate exists.
|
||||
*/
|
||||
const hasShippingRate = ( shippingRatePackages ) => {
|
||||
return shippingRatePackages.some(
|
||||
( shippingRatePackage ) => shippingRatePackage.shipping_rates.length
|
||||
);
|
||||
};
|
||||
|
||||
export default hasShippingRate;
|
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { TotalsItem } from '@woocommerce/blocks-checkout';
|
||||
import type { Currency } from '@woocommerce/price-format';
|
||||
import type { ReactElement } from 'react';
|
||||
import { getSetting, EnteredAddress } from '@woocommerce/settings';
|
||||
import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ShippingRateSelector from './shipping-rate-selector';
|
||||
import hasShippingRate from './has-shipping-rate';
|
||||
import ShippingCalculator from '../../shipping-calculator';
|
||||
import ShippingLocation from '../../shipping-location';
|
||||
import './style.scss';
|
||||
|
||||
interface CalculatorButtonProps {
|
||||
label?: string;
|
||||
isShippingCalculatorOpen: boolean;
|
||||
setIsShippingCalculatorOpen: ( isShippingCalculatorOpen: boolean ) => void;
|
||||
}
|
||||
|
||||
const CalculatorButton = ( {
|
||||
label = __( 'Calculate', 'woo-gutenberg-products-block' ),
|
||||
isShippingCalculatorOpen,
|
||||
setIsShippingCalculatorOpen,
|
||||
}: CalculatorButtonProps ): ReactElement => {
|
||||
return (
|
||||
<button
|
||||
className="wc-block-components-totals-shipping__change-address-button"
|
||||
onClick={ () => {
|
||||
setIsShippingCalculatorOpen( ! isShippingCalculatorOpen );
|
||||
} }
|
||||
aria-expanded={ isShippingCalculatorOpen }
|
||||
>
|
||||
{ label }
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface ShippingAddressProps {
|
||||
showCalculator: boolean;
|
||||
isShippingCalculatorOpen: boolean;
|
||||
setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ];
|
||||
shippingAddress: EnteredAddress;
|
||||
}
|
||||
|
||||
const ShippingAddress = ( {
|
||||
showCalculator,
|
||||
isShippingCalculatorOpen,
|
||||
setIsShippingCalculatorOpen,
|
||||
shippingAddress,
|
||||
}: ShippingAddressProps ): ReactElement | null => {
|
||||
return (
|
||||
<>
|
||||
<ShippingLocation address={ shippingAddress } />
|
||||
{ showCalculator && (
|
||||
<CalculatorButton
|
||||
label={ __(
|
||||
'(change address)',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
isShippingCalculatorOpen={ isShippingCalculatorOpen }
|
||||
setIsShippingCalculatorOpen={ setIsShippingCalculatorOpen }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface NoShippingPlaceholderProps {
|
||||
showCalculator: boolean;
|
||||
isShippingCalculatorOpen: boolean;
|
||||
setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ];
|
||||
}
|
||||
|
||||
const NoShippingPlaceholder = ( {
|
||||
showCalculator,
|
||||
isShippingCalculatorOpen,
|
||||
setIsShippingCalculatorOpen,
|
||||
}: NoShippingPlaceholderProps ): ReactElement => {
|
||||
if ( ! showCalculator ) {
|
||||
return (
|
||||
<em>
|
||||
{ __(
|
||||
'Calculated during checkout',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</em>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CalculatorButton
|
||||
isShippingCalculatorOpen={ isShippingCalculatorOpen }
|
||||
setIsShippingCalculatorOpen={ setIsShippingCalculatorOpen }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface TotalShippingProps {
|
||||
currency: Currency;
|
||||
values: {
|
||||
total_shipping: string;
|
||||
total_shipping_tax: string;
|
||||
}; // Values in use
|
||||
showCalculator?: boolean; //Whether to display the rate selector below the shipping total.
|
||||
showRateSelector?: boolean; // Whether to show shipping calculator or not.
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TotalsShipping = ( {
|
||||
currency,
|
||||
values,
|
||||
showCalculator = true,
|
||||
showRateSelector = true,
|
||||
className,
|
||||
}: TotalShippingProps ): ReactElement => {
|
||||
const [ isShippingCalculatorOpen, setIsShippingCalculatorOpen ] = useState(
|
||||
false
|
||||
);
|
||||
const {
|
||||
shippingAddress,
|
||||
cartHasCalculatedShipping,
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
} = useStoreCart();
|
||||
|
||||
const totalShippingValue = getSetting(
|
||||
'displayCartPricesIncludingTax',
|
||||
false
|
||||
)
|
||||
? parseInt( values.total_shipping, 10 ) +
|
||||
parseInt( values.total_shipping_tax, 10 )
|
||||
: parseInt( values.total_shipping, 10 );
|
||||
const hasRates = hasShippingRate( shippingRates ) || totalShippingValue;
|
||||
const calculatorButtonProps = {
|
||||
isShippingCalculatorOpen,
|
||||
setIsShippingCalculatorOpen,
|
||||
};
|
||||
|
||||
const selectedShippingRates = shippingRates.flatMap(
|
||||
( shippingPackage ) => {
|
||||
return shippingPackage.shipping_rates
|
||||
.filter( ( rate ) => rate.selected )
|
||||
.flatMap( ( rate ) => rate.name );
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-components-totals-shipping',
|
||||
className
|
||||
) }
|
||||
>
|
||||
<TotalsItem
|
||||
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
|
||||
value={
|
||||
cartHasCalculatedShipping ? (
|
||||
totalShippingValue
|
||||
) : (
|
||||
<NoShippingPlaceholder
|
||||
showCalculator={ showCalculator }
|
||||
{ ...calculatorButtonProps }
|
||||
/>
|
||||
)
|
||||
}
|
||||
description={
|
||||
<>
|
||||
{ cartHasCalculatedShipping && (
|
||||
<>
|
||||
<ShippingVia
|
||||
selectedShippingRates={
|
||||
selectedShippingRates
|
||||
}
|
||||
/>
|
||||
<ShippingAddress
|
||||
shippingAddress={ shippingAddress }
|
||||
showCalculator={ showCalculator }
|
||||
{ ...calculatorButtonProps }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
}
|
||||
currency={ currency }
|
||||
/>
|
||||
{ showCalculator && isShippingCalculatorOpen && (
|
||||
<ShippingCalculator
|
||||
onUpdate={ () => {
|
||||
setIsShippingCalculatorOpen( false );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
{ showRateSelector && cartHasCalculatedShipping && (
|
||||
<ShippingRateSelector
|
||||
hasRates={ hasRates }
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={ shippingRatesLoading }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalsShipping;
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Notice } from 'wordpress-components';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ShippingRatesControl from '../../shipping-rates-control';
|
||||
|
||||
const ShippingRateSelector = ( {
|
||||
hasRates,
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
} ) => {
|
||||
const legend = hasRates
|
||||
? __( 'Shipping options', 'woocommerce' )
|
||||
: __( 'Choose a shipping option', 'woocommerce' );
|
||||
return (
|
||||
<fieldset className="wc-block-components-totals-shipping__fieldset">
|
||||
<legend className="screen-reader-text">{ legend }</legend>
|
||||
<ShippingRatesControl
|
||||
className="wc-block-components-totals-shipping__options"
|
||||
collapsible={ true }
|
||||
noResultsMessage={
|
||||
<Notice
|
||||
isDismissible={ false }
|
||||
className={ classnames(
|
||||
'wc-block-components-shipping-rates-control__no-results-notice',
|
||||
'woocommerce-error'
|
||||
) }
|
||||
>
|
||||
{ __(
|
||||
'No shipping options were found.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Notice>
|
||||
}
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={ shippingRatesLoading }
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShippingRateSelector;
|
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const ShippingVia = ( {
|
||||
selectedShippingRates,
|
||||
}: {
|
||||
selectedShippingRates: string[];
|
||||
} ): JSX.Element => {
|
||||
return (
|
||||
<div className="wc-block-components-totals-item__description wc-block-components-totals-shipping__via">
|
||||
{ __( 'via', 'woo-gutenberg-products-block' ) }{ ' ' }
|
||||
{ selectedShippingRates.join( ', ' ) }
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { boolean, text } from '@storybook/addon-knobs';
|
||||
import { currencyKnob } from '@woocommerce/knobs';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TotalsShipping from '../';
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Blocks/@blocks-checkout/TotalsShipping',
|
||||
component: TotalsShipping,
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const currency = currencyKnob();
|
||||
const showCalculator = boolean( 'Show calculator', true );
|
||||
const showRateSelector = boolean( 'Show rate selector', true );
|
||||
const totalShipping = text( 'Total shipping', '1000' );
|
||||
const totalShippingTax = text( 'Total shipping tax', '200' );
|
||||
|
||||
return (
|
||||
<TotalsShipping
|
||||
currency={ currency }
|
||||
showCalculator={ showCalculator }
|
||||
showRateSelector={ showRateSelector }
|
||||
values={ {
|
||||
total_shipping: totalShipping,
|
||||
total_shipping_tax: totalShippingTax,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
.wc-block-components-totals-shipping {
|
||||
// Added extra label for specificity.
|
||||
fieldset.wc-block-components-totals-shipping__fieldset {
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wc-block-components-totals-shipping__via {
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
|
||||
.wc-block-components-totals-shipping__options {
|
||||
.wc-block-components-radio-control__label,
|
||||
.wc-block-components-radio-control__description,
|
||||
.wc-block-components-radio-control__secondary-label,
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
flex-basis: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-shipping-rates-control__no-results-notice {
|
||||
margin: 0 0 em($gap-small);
|
||||
}
|
||||
|
||||
.wc-block-components-totals-shipping__change-address-button {
|
||||
@include link-button();
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extra classes for specificity.
|
||||
.theme-twentytwentyone.theme-twentytwentyone.theme-twentytwentyone .wc-block-components-totals-shipping__change-address-button {
|
||||
@include link-button();
|
||||
}
|
Reference in New Issue
Block a user