rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -4,7 +4,7 @@
import { useEffect } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { Button, Disabled, Tooltip } from '@wordpress/components';
import { Disabled, Tooltip } from '@wordpress/components';
import { Skeleton } from '@woocommerce/base-components/skeleton';
import { BlockEditProps } from '@wordpress/blocks';
@@ -37,31 +37,28 @@ const Edit = ( props: BlockEditProps< Attributes > ) => {
return (
<div { ...blockProps }>
<Tooltip
text="Customer will see product add-to-cart options in this space, dependend on the product type. "
text="Customer will see product add-to-cart options in this space, dependent on the product type. "
position="bottom right"
>
<div className="wc-block-editor-container">
<div className="wc-block-editor-add-to-cart-form-container">
<Skeleton numberOfLines={ 3 } />
<Disabled>
<input
type={ 'number' }
value={ '1' }
className={
'wc-block-editor-add-to-cart-form__quantity'
}
readOnly
/>
<Button
variant={ 'primary' }
className={
'wc-block-editor-add-to-cart-form__button'
}
<div className="quantity">
<input
type={ 'number' }
value={ '1' }
className={ 'input-text qty text' }
readOnly
/>
</div>
<button
className={ `single_add_to_cart_button button alt wp-element-button` }
>
{ __(
'Add to cart',
'woo-gutenberg-products-block'
) }
</Button>
</button>
</Disabled>
</div>
</Tooltip>

View File

@@ -1,30 +1,4 @@
.wc-block-editor-add-to-cart-form {
display: flex;
flex-direction: column;
row-gap: $default-block-margin;
}
input.wc-block-editor-add-to-cart-form__quantity[type="number"] {
max-width: 50px;
min-height: 23px;
float: left;
padding: 6px 6px 6px 12px;
margin-right: 10px;
font-size: 13px;
height: inherit;
}
input[type="number"]::-webkit-inner-spin-button {
opacity: 1;
}
button.components-button.wc-block-add-to-cart-form__button {
float: left;
padding: 20px 30px;
border-radius: 0;
}
.wc-block-editor-container {
.wc-block-editor-add-to-cart-form-container {
cursor: help;
gap: 10px;
display: flex;

View File

@@ -1,4 +1,4 @@
.wp-block-add-to-cart-form {
.wc-block-add-to-cart-form {
width: unset;
/**
* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
@@ -9,4 +9,17 @@
font-size: var(--wp--preset--font-size--small);
padding: 0.9rem 1.1rem;
}
.quantity {
display: inline-block;
float: none;
margin-right: 4px;
vertical-align: middle;
.qty {
margin-right: 0.5rem;
width: 3.631em;
text-align: center;
}
}
}

View File

@@ -7,7 +7,7 @@ import { SelectControl } from 'wordpress-components';
import type { SelectControl as SelectControlType } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import classnames from 'classnames';
import { ValidationInputError } from '@woocommerce/blocks-checkout';
import { ValidationInputError } from '@woocommerce/blocks-components';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';

View File

@@ -13,8 +13,7 @@
display: inline-flex;
justify-content: center;
text-align: center;
// Set button font size and padding so it inherits from parent.
padding: 0.5em 1em;
// Set button font size so it inherits from parent.
font-size: 1em;
&.loading {
@@ -25,6 +24,7 @@
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e031";
animation: spin 2s linear infinite;
margin-right: 0;
margin-left: 0.5em;
display: inline-block;
width: auto;
@@ -70,8 +70,9 @@
justify-content: center;
white-space: normal;
word-break: break-word;
width: 150px;
overflow: hidden;
align-items: center;
line-height: inherit;
span {

View File

@@ -0,0 +1,55 @@
/**
* External dependencies
*/
import {
BlockConfiguration,
registerBlockType,
unregisterBlockType,
registerBlockVariation,
unregisterBlockVariation,
BlockVariation,
BlockAttributes,
} from '@wordpress/blocks';
export interface BlockRegistrationStrategy {
register(
blockNameOrMetadata: string | Partial< BlockConfiguration >,
blockSettings: Partial< BlockConfiguration >
): boolean;
unregister( blockName: string, variationName?: string ): boolean;
}
export class BlockTypeStrategy implements BlockRegistrationStrategy {
register(
blockNameOrMetadata: string | Partial< BlockConfiguration >,
blockSettings: Partial< BlockConfiguration >
): boolean {
return Boolean(
// @ts-expect-error: `registerBlockType` is typed in WordPress core
registerBlockType( blockNameOrMetadata, blockSettings )
);
}
unregister( blockName: string ): boolean {
return Boolean( unregisterBlockType( blockName ) );
}
}
// Strategy for BlockVariation
export class BlockVariationStrategy implements BlockRegistrationStrategy {
register(
blockName: string,
blockSettings: Partial< BlockConfiguration >
): boolean {
return Boolean(
registerBlockVariation(
blockName,
blockSettings as BlockVariation< BlockAttributes >
)
);
}
unregister( blockName: string, variationName: string ): boolean {
return Boolean( unregisterBlockVariation( blockName, variationName ) );
}
}

View File

@@ -0,0 +1,182 @@
/**
* External dependencies
*/
import { getBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import {
TemplateChangeDetector,
TemplateChangeDetectorObserver,
} from './template-change-detector';
import {
BlockRegistrationStrategy,
BlockTypeStrategy,
BlockVariationStrategy,
} from './block-registration-strategy';
import { BLOCKS_WITH_RESTRICTION } from './blocks-with-restriction';
/**
* Manages the registration and unregistration of blocks based on template or page restrictions.
*
* This class implements the TemplateChangeDetectorObserver interface and is responsible for managing the registration and unregistration of blocks based on the restrictions defined in the BLOCKS_WITH_RESTRICTION constant.
*
* The class maintains a list of unregistered blocks and uses a block registration strategy to register and unregister blocks as needed. The strategy used depends on whether the block is a variation block or a regular block.
*
* The `run` method is the main entry point for the class. It is called with a TemplateChangeDetector object and registers and unregisters blocks based on the current template and whether the editor is in post or page mode.
*/
export class BlockRegistrationManager
implements TemplateChangeDetectorObserver
{
private unregisteredBlocks: string[] = [];
private blockRegistrationStrategy: BlockRegistrationStrategy;
constructor() {
this.blockRegistrationStrategy = new BlockTypeStrategy();
}
/**
* Determines whether a block should be registered based on the current template or page.
*
* This method checks whether a block with restrictions should be registered based on the current template ID and
* whether the editor is in post or page mode. It checks whether the current template ID starts with any of the
* allowed templates or template parts for the block, and whether the block is available in the post or page editor.
*
* @param {Object} params - The parameters for the method.
* @param {string} params.blockWithRestrictionName - The name of the block with restrictions.
* @param {string} params.currentTemplateId - The ID of the current template.
* @param {boolean} params.isPostOrPage - Whether the editor is in a post or page.
* @return {boolean} True if the block should be registered, false otherwise.
*/
private shouldBlockBeRegistered( {
blockWithRestrictionName,
currentTemplateId,
isPostOrPage,
}: {
blockWithRestrictionName: string;
currentTemplateId: string;
isPostOrPage: boolean;
} ) {
const {
allowedTemplates,
allowedTemplateParts,
availableInPostOrPageEditor,
} = BLOCKS_WITH_RESTRICTION[ blockWithRestrictionName ];
const shouldBeAvailableOnTemplate = Object.keys(
allowedTemplates
).some( ( allowedTemplate ) =>
currentTemplateId.startsWith( allowedTemplate )
);
const shouldBeAvailableOnTemplatePart = Object.keys(
allowedTemplateParts
).some( ( allowedTemplate ) =>
currentTemplateId.startsWith( allowedTemplate )
);
const shouldBeAvailableOnPostOrPageEditor =
isPostOrPage && availableInPostOrPageEditor;
return (
shouldBeAvailableOnTemplate ||
shouldBeAvailableOnTemplatePart ||
shouldBeAvailableOnPostOrPageEditor
);
}
/**
* Unregisters blocks before entering a restricted area based on the current template or page/post.
*
* This method iterates over all blocks with restrictions and unregisters them if they should not be registered
* based on the current template ID and whether the editor is in a post or page. It uses a block registration
* strategy to unregister the blocks, which depends on whether the block is a variation block or a regular block.
*
* @param {Object} params - The parameters for the method.
* @param {string} params.currentTemplateId - The ID of the current template.
* @param {boolean} params.isPostOrPage - Whether the editor is in post or page mode.
*/
unregisterBlocksBeforeEnteringRestrictedArea( {
currentTemplateId,
isPostOrPage,
}: {
currentTemplateId: string;
isPostOrPage: boolean;
} ) {
for ( const blockWithRestrictionName of Object.keys(
BLOCKS_WITH_RESTRICTION
) ) {
if (
this.shouldBlockBeRegistered( {
blockWithRestrictionName,
currentTemplateId,
isPostOrPage,
} )
) {
continue;
}
if ( ! getBlockType( blockWithRestrictionName ) ) {
continue;
}
this.blockRegistrationStrategy = BLOCKS_WITH_RESTRICTION[
blockWithRestrictionName
].isVariationBlock
? new BlockVariationStrategy()
: new BlockTypeStrategy();
this.blockRegistrationStrategy.unregister(
blockWithRestrictionName
);
this.unregisteredBlocks.push( blockWithRestrictionName );
}
}
/**
* Registers blocks after leaving a restricted area.
*
* This method iterates over all unregistered blocks and registers them if they are not restricted in the current context.
* It uses a block registration strategy to register the blocks, which depends on whether the block is a variation block or a regular block.
* If the block is successfully registered, it is removed from the list of unregistered blocks.
*/
registerBlocksAfterLeavingRestrictedArea() {
for ( const unregisteredBlockName of this.unregisteredBlocks ) {
if ( ! getBlockType( unregisteredBlockName ) ) {
continue;
}
const restrictedBlockData =
BLOCKS_WITH_RESTRICTION[ unregisteredBlockName ];
this.blockRegistrationStrategy = BLOCKS_WITH_RESTRICTION[
unregisteredBlockName
].isVariationBlock
? new BlockVariationStrategy()
: new BlockTypeStrategy();
const isBlockRegistered = this.blockRegistrationStrategy.register(
restrictedBlockData.blockMetadata,
restrictedBlockData.blockSettings
);
this.unregisteredBlocks = isBlockRegistered
? this.unregisteredBlocks.filter(
( blockName ) => blockName !== unregisteredBlockName
)
: this.unregisteredBlocks;
}
}
/**
* Runs the block registration manager.
*
* This method is the main entry point for the block registration manager. It is called with a TemplateChangeDetector object,
* and registers and unregisters blocks based on the current template and whether the editor is in a post or page.
*
* @param {TemplateChangeDetector} templateChangeDetector - The template change detector object.
*/
run( templateChangeDetector: TemplateChangeDetector ) {
this.registerBlocksAfterLeavingRestrictedArea();
this.unregisterBlocksBeforeEnteringRestrictedArea( {
currentTemplateId:
templateChangeDetector.getCurrentTemplateId() || '',
isPostOrPage: templateChangeDetector.getIsPostOrPage(),
} );
}
}

View File

@@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
import { ProductGalleryBlockSettings } from '@woocommerce/blocks/product-gallery/settings';
/**
* Internal dependencies
*/
import productGalleryBlockMetadata from '../../../blocks/product-gallery/block.json';
export interface BlocksWithRestriction {
[ key: string ]: {
blockMetadata: Partial< BlockConfiguration >;
blockSettings: Partial< BlockConfiguration >;
allowedTemplates: {
[ key: string ]: boolean;
};
allowedTemplateParts: {
[ key: string ]: boolean;
};
availableInPostOrPageEditor: boolean;
isVariationBlock: boolean;
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error: `metadata` currently does not have a type definition in WordPress core
export const BLOCKS_WITH_RESTRICTION: BlocksWithRestriction = {
[ productGalleryBlockMetadata.name ]: {
blockMetadata: productGalleryBlockMetadata,
blockSettings: ProductGalleryBlockSettings,
allowedTemplates: {
'single-product': true,
},
allowedTemplateParts: {
'product-gallery': true,
},
availableInPostOrPageEditor: false,
isVariationBlock: false,
},
};

View File

@@ -0,0 +1,16 @@
/**
* External dependencies
*/
import domReady from '@wordpress/dom-ready';
/**
* Internal dependencies
*/
import { BlockRegistrationManager } from './blocks-registration-manager';
import { TemplateChangeDetector } from './template-change-detector';
domReady( () => {
const templateChangeDetector = new TemplateChangeDetector();
const blockRegistrationManager = new BlockRegistrationManager();
templateChangeDetector.add( blockRegistrationManager );
} );

View File

@@ -0,0 +1,115 @@
/**
* External dependencies
*/
import { subscribe, select } from '@wordpress/data';
import { isNumber } from '@woocommerce/types';
interface TemplateChangeDetectorSubject {
add( observer: TemplateChangeDetectorObserver ): void;
getPreviousTemplateId(): string | undefined;
getCurrentTemplateId(): string | undefined;
notify(): void;
}
export interface TemplateChangeDetectorObserver {
run( subject: TemplateChangeDetectorSubject ): void;
}
/**
* This class implements the TemplateChangeDetectorSubject interface and is responsible for detecting changes in the
* current template or page and notifying any observers of these changes. It maintains a list of observers and provides methods
* to add observers and notify them of changes.
*
* The class also provides methods to get the previous and current template IDs and whether the editor is in a post or page.
*
* The `checkIfTemplateHasChangedAndNotifySubscribers` method is the main method of the class. It checks if the current
* template has changed and, if so, notifies all observers.
*/
export class TemplateChangeDetector implements TemplateChangeDetectorSubject {
private previousTemplateId: string | undefined;
private currentTemplateId: string | undefined;
private isPostOrPage: boolean;
private observers: TemplateChangeDetectorObserver[] = [];
constructor() {
this.isPostOrPage = false;
subscribe( () => {
this.checkIfTemplateHasChangedAndNotifySubscribers();
}, 'core/edit-site' );
}
public add( observer: TemplateChangeDetectorObserver ): void {
this.observers.push( observer );
}
/**
* Trigger an update in each subscriber.
*/
public notify(): void {
for ( const observer of this.observers ) {
observer.run( this );
}
}
public getPreviousTemplateId() {
return this.previousTemplateId;
}
public getCurrentTemplateId() {
return this.currentTemplateId;
}
public getIsPostOrPage() {
return this.isPostOrPage;
}
/**
* Parses the template ID.
*
* This method takes a template or a post ID and returns it parsed in the expected format.
*
* @param {string | number | undefined} templateId - The template ID to parse.
* @return {string | undefined} The parsed template ID.
*/
private parseTemplateId(
templateId: string | number | undefined
): string | undefined {
if ( isNumber( templateId ) ) {
return String( templateId );
}
return templateId?.split( '//' )[ 1 ];
}
/**
* Checks if the current template or page has changed and notifies subscribers.
*
* If the current template ID has changed and is not undefined (which means that it is not a page, post or template), it notifies all subscribers.
*/
public checkIfTemplateHasChangedAndNotifySubscribers(): void {
this.previousTemplateId = this.currentTemplateId;
const postOrPageId = select( 'core/editor' )?.getCurrentPostId<
string | number | undefined
>();
this.isPostOrPage = Boolean( postOrPageId );
const editedPostId =
postOrPageId ||
select( 'core/edit-site' )?.getEditedPostId<
string | number | undefined
>();
this.currentTemplateId = this.parseTemplateId( editedPostId );
const hasChangedTemplate =
this.previousTemplateId !== this.currentTemplateId;
const hasTemplateId = Boolean( this.currentTemplateId );
if ( ! hasChangedTemplate || ! hasTemplateId ) {
return;
}
this.notify();
}
}

View File

@@ -46,7 +46,7 @@ export const registerBlockSingleProductTemplate = ( {
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
currentTemplateId = parseTemplateId(
store?.getEditedPostId() as string | number | undefined
store?.getEditedPostId< string | number | undefined >()
);
const hasChangedTemplate = previousTemplateId !== currentTemplateId;
const hasTemplateId = Boolean( currentTemplateId );

View File

@@ -10,7 +10,7 @@ import Button, { ButtonProps } from '..';
const availableTypes = [ 'button', 'input', 'submit' ];
export default {
title: 'WooCommerce Blocks/@base-components/Button',
title: 'Base Components/Button',
argTypes: {
children: {
control: 'text',

View File

@@ -1,11 +1,11 @@
/**
* External dependencies
*/
import { isPostcode } from '@woocommerce/blocks-checkout';
import {
ValidatedTextInput,
isPostcode,
type ValidatedTextInputHandle,
} from '@woocommerce/blocks-checkout';
} from '@woocommerce/blocks-components';
import {
BillingCountryInput,
ShippingCountryInput,
@@ -180,6 +180,7 @@ const AddressForm = ( {
( fieldsRef.current[ field.key ] = el )
}
{ ...fieldProps }
type={ field.type }
value={ values[ field.key ] }
onChange={ ( newValue: string ) =>
onChange( {

View File

@@ -34,7 +34,8 @@ table.wc-block-cart-items {
}
.wc-block-cart-item__quantity {
.wc-block-cart-item__remove-link {
@include link-button;
@include link-button();
@include hover-effect();
@include font-size( smaller );
text-transform: none;
@@ -75,7 +76,7 @@ table.wc-block-cart-items {
.wc-block-cart-item__remove-link {
display: none;
}
&:not(.wc-block-mini-cart-items) {
&:not(.wc-block-mini-cart-items):not(:last-child) {
.wc-block-cart-items__row {
@include with-translucent-border( 0 0 1px );
}

View File

@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { useContainerWidthContext } from '@woocommerce/base-context';
import { Panel } from '@woocommerce/blocks-checkout';
import { Panel } from '@woocommerce/blocks-components';
import type { CartItem } from '@woocommerce/types';
/**

View File

@@ -5,7 +5,7 @@ import type { ShippingAddress } from '@woocommerce/settings';
import { useCustomerData } from '@woocommerce/base-context/hooks';
import { dispatch } from '@wordpress/data';
import { CART_STORE_KEY, processErrorResponse } from '@woocommerce/block-data';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { removeNoticesWithContext } from '@woocommerce/base-utils';
/**

View File

@@ -4,8 +4,7 @@
import classNames from 'classnames';
import { _n, sprintf } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { Panel } from '@woocommerce/blocks-checkout';
import { Label } from '@woocommerce/blocks-components';
import { Label, Panel } from '@woocommerce/blocks-components';
import { useCallback } from '@wordpress/element';
import { useShippingData } from '@woocommerce/base-context/hooks';
import { sanitizeHTML } from '@woocommerce/utils';

View File

@@ -9,11 +9,11 @@ import { withInstanceId } from '@wordpress/compose';
import {
ValidatedTextInput,
ValidationInputError,
} from '@woocommerce/blocks-checkout';
} from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import classnames from 'classnames';
import type { MouseEvent } from 'react';
import type { MouseEvent, MouseEventHandler } from 'react';
/**
* Internal dependencies
@@ -62,18 +62,18 @@ export const TotalsCoupon = ( {
validationErrorId: store.getValidationErrorId( textInputId ),
};
} );
const handleCouponAnchorClick = (
e: MouseEvent< HTMLAnchorElement, MouseEvent >
const handleCouponAnchorClick: MouseEventHandler< HTMLAnchorElement > = (
e: MouseEvent< HTMLAnchorElement >
) => {
e.preventDefault();
setIsCouponFormHidden( false );
};
const handleCouponSubmit = (
e: MouseEvent< HTMLButtonElement, MouseEvent >
const handleCouponSubmit: MouseEventHandler< HTMLButtonElement > = (
e: MouseEvent< HTMLButtonElement >
) => {
e.preventDefault();
if ( onSubmit !== undefined ) {
onSubmit( couponValue ).then( ( result ) => {
if ( typeof onSubmit !== 'undefined' ) {
onSubmit( couponValue )?.then( ( result ) => {
if ( result ) {
setCouponValue( '' );
setIsCouponFormHidden( true );

View File

@@ -13,7 +13,7 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { TotalsCoupon, TotalsCouponProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/totals/Coupon',
title: 'Base Components/Totals/Coupon',
component: TotalsCoupon,
args: {
initialOpen: true,

View File

@@ -3,8 +3,8 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { RemovableChip } from '@woocommerce/blocks-components';
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
import { RemovableChip, TotalsItem } from '@woocommerce/blocks-components';
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
import { getSetting } from '@woocommerce/settings';
import {
CartResponseCouponItemWithLabel,

View File

@@ -67,7 +67,7 @@ function extractValuesFromCoupons(
}
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/totals/Discount',
title: 'Base Components/Totals/Discount',
component: Discount,
argTypes: {
currency: currencyControl,

View File

@@ -4,8 +4,11 @@
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import { createInterpolateElement } from '@wordpress/element';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
import {
FormattedMonetaryAmount,
TotalsItem,
} from '@woocommerce/blocks-components';
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
import {

View File

@@ -21,7 +21,7 @@ const NZD: Currency = {
};
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/totals/FooterItem',
title: 'Base Components/Totals/FooterItem',
component: FooterItem,
args: {
currency: NZD,

View File

@@ -5,8 +5,8 @@ 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 { TotalsItem } from '@woocommerce/blocks-components';
import type { Currency } from '@woocommerce/types';
import { ShippingVia } from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-via';
import {
isAddressComplete,

View File

@@ -2,10 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
formatShippingAddress,
isAddressComplete,
} from '@woocommerce/base-utils';
import { formatShippingAddress } from '@woocommerce/base-utils';
import { useEditorContext } from '@woocommerce/base-context';
import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings';
import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location';
@@ -31,15 +28,19 @@ export const ShippingAddress = ( {
setIsShippingCalculatorOpen,
shippingAddress,
}: ShippingAddressProps ): JSX.Element | null => {
const addressComplete = isAddressComplete( shippingAddress );
const { isEditor } = useEditorContext();
const prefersCollection = useSelect( ( select ) =>
select( CHECKOUT_STORE_KEY ).prefersCollection()
);
// If the address is incomplete, and we're not in the editor, don't show anything.
if ( ! addressComplete && ! isEditor ) {
const hasFormattedAddress = !! formatShippingAddress( shippingAddress );
// If there is no default customer location set in the store, the customer hasn't provided their address,
// but a default shipping method is available for all locations,
// then the shipping calculator will be hidden to avoid confusion.
if ( ! hasFormattedAddress && ! isEditor ) {
return null;
}
const formattedLocation = formatShippingAddress( shippingAddress );
return (
<>

View File

@@ -229,8 +229,11 @@ describe( 'TotalsShipping', () => {
currency={ {
code: 'USD',
symbol: '$',
position: 'left',
precision: 2,
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
} }
values={ {
total_shipping: '0',
@@ -274,8 +277,11 @@ describe( 'TotalsShipping', () => {
currency={ {
code: 'USD',
symbol: '$',
position: 'left',
precision: 2,
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
} }
values={ {
total_shipping: '0',
@@ -295,4 +301,51 @@ describe( 'TotalsShipping', () => {
screen.queryByText( 'Add an address for shipping options' )
).not.toBeInTheDocument();
} );
it( 'does show the calculator button when default rates are available and has formatted address', () => {
baseContextHooks.useStoreCart.mockReturnValue( {
cartItems: mockPreviewCart.items,
cartTotals: [ mockPreviewCart.totals ],
cartCoupons: mockPreviewCart.coupons,
cartFees: mockPreviewCart.fees,
cartNeedsShipping: mockPreviewCart.needs_shipping,
shippingRates: mockPreviewCart.shipping_rates,
shippingAddress: {
...shippingAddress,
city: '',
state: 'California',
country: 'US',
postcode: '',
},
billingAddress: mockPreviewCart.billing_address,
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
isLoadingRates: false,
} );
render(
<SlotFillProvider>
<TotalsShipping
currency={ {
code: 'USD',
symbol: '$',
minorUnit: 2,
decimalSeparator: '.',
prefix: '',
suffix: '',
thousandSeparator: ', ',
} }
values={ {
total_shipping: '0',
total_shipping_tax: '0',
} }
showCalculator={ true }
showRateSelector={ true }
isCheckout={ false }
className={ '' }
/>
</SlotFillProvider>
);
expect( screen.queryByText( 'Change address' ) ).toBeInTheDocument();
expect(
screen.queryByText( 'Add an address for shipping options' )
).not.toBeInTheDocument();
} );
} );

View File

@@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n';
import { useEffect, useRef } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import { ComboboxControl } from 'wordpress-components';
import { ValidationInputError } from '@woocommerce/blocks-checkout';
import { ValidationInputError } from '@woocommerce/blocks-components';
import { isObject } from '@woocommerce/types';
import { useDispatch, useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
@@ -121,14 +121,26 @@ const Combobox = ( {
// Try to match.
const normalizedFilterValue =
filterValue.toLocaleUpperCase();
const foundOption = options.find(
// Try to find an exact match first using values.
const foundValue = options.find(
( option ) =>
option.label
.toLocaleUpperCase()
.startsWith( normalizedFilterValue ) ||
option.value.toLocaleUpperCase() ===
normalizedFilterValue
normalizedFilterValue
);
if ( foundValue ) {
onChange( foundValue.value );
return;
}
// Fallback to a label match.
const foundOption = options.find( ( option ) =>
option.label
.toLocaleUpperCase()
.startsWith( normalizedFilterValue )
);
if ( foundOption ) {
onChange( foundOption.value );
}

View File

@@ -15,7 +15,7 @@ import { countries } from './countries-filler';
type CountryCode = keyof typeof countries;
export default {
title: 'WooCommerce Blocks/@base-components/CountryInput',
title: 'Base Components/CountryInput',
component: CountryInput,
args: {
countries,

View File

@@ -10,7 +10,7 @@ import { useState } from '@wordpress/element';
import FormTokenField, { Props } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/FormTokenField',
title: 'Base Components/FormTokenField',
argTypes: {},
component: FormTokenField,
} as Meta< Props >;

View File

@@ -10,7 +10,6 @@ export * from './filter-reset-button';
export * from './filter-submit-button';
export * from './form';
export * from './form-token-field';
export * from './label';
export * from './load-more-button';
export * from './loading-mask';
export * from './noninteractive';
@@ -26,9 +25,6 @@ export * from './read-more';
export * from './reviews';
export * from './sidebar-layout';
export * from './snackbar-list';
export * from './sort-select';
export * from './state-input';
export * from './summary';
export * from './tabs';
export * from './textarea';
export * from './title';

View File

@@ -0,0 +1,112 @@
import { Canvas, Meta, ArgTypes, Primary, Source } from '@storybook/blocks';
import * as NoticeBannerStories from '../stories/index.stories.tsx';
<Meta name="Docs" of={ NoticeBannerStories } />
# NoticeBanner
An informational UI displayed near the top of the store pages.
<Primary />
## Design Guidelines
`NoticeBanner` is an informational UI element displayed near the top of store pages used to indicate the result of an action, or to draw the user's attention to necessary information.
Notices are color-coded to indicate the type of message being communicated, and also show an icon to reinforce the meaning of the message. The color and icon used for a notice are determined by the `status` prop.
### Default Notices
By default, noices are grey and used for less important messaging.
<Canvas of={ NoticeBannerStories.Default } />
### Informational Notices
Blue notices with an info icon are used for general information for buyers, but do not require them to take an action.
<Canvas of={ NoticeBannerStories.Info } />
### Error Notices
Red notices with an alert icon are used to show that an error has occurred and that the user needs to take action.
<Canvas of={ NoticeBannerStories.Error } />
### Success Notices
Green notices with a success icon are used to show an action was successful.
<Canvas of={ NoticeBannerStories.Success } />
### Warning Notices
Yellow notices with an alert icon are used to show that the user may need to take action, or needs to be aware of something important.
<Canvas of={ NoticeBannerStories.Warning } />
### Error Summary
If you provide a `summary` it will be displayed above the notice content. This can be useful for displaying a summary of errors in a list format.
<Canvas of={ NoticeBannerStories.ErrorSummary } />
## Development Guidelines
### Props
<ArgTypes />
### Usage examples
#### Example: string based notices
To display a basic notice, pass the notice message as a string:
```jsx
import { NoticeBanner } from '@woocommerce/base-components';
<NoticeBanner status="info">Your message here</NoticeBanner>;
```
#### Example: components within notices
For more complex markup, you can wrap any JSX element:
```jsx
import { NoticeBanner } from '@woocommerce/base-components';
<NoticeBanner status="error">
<p>
An error occurred: <code>{ errorDetails }</code>.
</p>
</NoticeBanner>;
```
#### Example: list of notices
In this example, the summary prop is used to indicate to the user that there are errors in the form submission.
```typescript
import { NoticeBanner } from '@woocommerce/base-components';
const errorMessages = [
'First error message',
'Second error message',
'Third error message',
];
<NoticeBanner
status="error"
summary="There are errors in your form submission:"
>
<ul>
{ errorMessages.map( ( message ) => (
<li key={ message }>{ message }</li>
) ) }
</ul>
</NoticeBanner>;
```
The list of error messages is rendered within the NoticeBanner component using an unordered list (`<ul>`) and list items (`<li>`). The `status` prop is set to `error` to indicate that the notice represents an error message.

View File

@@ -14,29 +14,20 @@ import Button from '../button';
import { useSpokenMessage } from '../../hooks';
export interface NoticeBannerProps {
// The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.
children: React.ReactNode;
// Additional class name to give to the notice.
className?: string | undefined;
// Determines whether the notice can be dismissed by the user.
isDismissible?: boolean | undefined;
// Function called when dismissing the notice.
onRemove?: ( () => void ) | undefined;
// Determines the level of politeness for the notice for assistive technology.
politeness?: 'polite' | 'assertive' | undefined;
// Optionally provided to change the spoken message for assistive technology.
spokenMessage?: string | React.ReactNode | undefined;
// Status determines the color of the notice and the icon.
className?: string;
isDismissible?: boolean;
onRemove?: () => void;
politeness?: 'polite' | 'assertive';
spokenMessage?: string | React.ReactNode;
status: 'success' | 'error' | 'info' | 'warning' | 'default';
// Optional summary text shown above notice content, used when several notices are listed together.
summary?: string | undefined;
summary?: string;
}
/**
* NoticeBanner: An informational UI displayed near the top of the store pages.
* NoticeBanner component.
*
* Notices are informational UI displayed near the top of store pages. WooCommerce blocks, themes, and plugins all use
* notices to indicate the result of an action, or to draw the users attention to necessary information.
* An informational UI displayed near the top of the store pages.
*/
const NoticeBanner = ( {
className,

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { Story, Meta } from '@storybook/react';
import type { StoryFn, Meta } from '@storybook/react';
/**
* Internal dependencies
@@ -10,7 +10,7 @@ import NoticeBanner, { NoticeBannerProps } from '../';
const availableStatus = [ 'default', 'success', 'error', 'warning', 'info' ];
export default {
title: 'WooCommerce Blocks/@base-components/NoticeBanner',
title: 'Base Components/NoticeBanner',
argTypes: {
status: {
control: 'radio',
@@ -20,7 +20,8 @@ export default {
},
isDismissible: {
control: 'boolean',
description: 'Determines whether the notice can be dismissed.',
description:
'Determines whether the notice can be dismissed by the user. When set to true, a close icon will be displayed on the banner.',
},
summary: {
description:
@@ -33,7 +34,7 @@ export default {
},
spokenMessage: {
description:
'Optionally provided to change the spoken message for assistive technology.',
'Optionally provided to change the spoken message for assistive technology. If not provided, the `children` prop will be used as the spoken message.',
control: 'text',
},
politeness: {
@@ -44,18 +45,19 @@ export default {
},
children: {
description:
'The content of the notice; either text or a React node such as a list of errors.',
'The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.',
disable: true,
},
onRemove: {
description: 'Function called when dismissing the notice.',
description:
'Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called.',
disable: true,
},
},
component: NoticeBanner,
} as Meta< NoticeBannerProps >;
const Template: Story< NoticeBannerProps > = ( args ) => {
const Template: StoryFn< NoticeBannerProps > = ( args ) => {
return <NoticeBanner { ...args } />;
};
@@ -84,7 +86,7 @@ Warning.args = {
export const Info = Template.bind( {} );
Info.args = {
children: 'This is an info notice',
children: 'This is an informational notice',
status: 'info',
};

View File

@@ -15,6 +15,10 @@
padding: 0.3em 0.6em;
min-width: 2.2em;
&:not([disabled]) {
cursor: pointer;
}
@include breakpoint("<782px") {
padding: 0.1em 0.2em;
min-width: 1.6em;

View File

@@ -11,7 +11,7 @@ import { currencies, currencyControl } from '@woocommerce/storybook-controls';
import PriceSlider, { PriceSliderProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/PriceSlider',
title: 'Base Components/PriceSlider',
component: PriceSlider,
args: {
currency: currencies.USD,

View File

@@ -2,7 +2,8 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import SortSelect from '@woocommerce/base-components/sort-select';
import { SortSelect } from '@woocommerce/blocks-components';
/**
* Internal dependencies
*/

View File

@@ -9,7 +9,7 @@ import type { Story, Meta } from '@storybook/react';
import ProductName, { ProductNameProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/ProductName',
title: 'Base Components/ProductName',
component: ProductName,
args: {
name: 'Test product',

View File

@@ -280,7 +280,7 @@ const ProductPrice = ( {
console.error( 'Price formats need to include the `<price/>` tag.' );
}
const isDiscounted = regularPrice && price !== regularPrice;
const isDiscounted = regularPrice && price && price < regularPrice;
let priceComponent = (
<span
className={ classNames(

View File

@@ -12,7 +12,7 @@ import ProductPrice, { ProductPriceProps } from '..';
const ALLOWED_ALIGN_VALUES = [ 'left', 'center', 'right' ];
export default {
title: 'WooCommerce Blocks/@base-components/ProductPrice',
title: 'Base Components/ProductPrice',
component: ProductPrice,
argTypes: {
align: {
@@ -25,6 +25,15 @@ export default {
align: 'left',
format: '<price/>',
price: 3000,
currency: {
code: 'USD',
symbol: '$',
thousandSeparator: ' ',
decimalSeparator: '.',
minorUnit: 2,
prefix: '$',
suffix: '',
},
},
} as Meta< ProductPriceProps >;

View File

@@ -10,7 +10,7 @@ import type { Story, Meta } from '@storybook/react';
import QuantitySelector, { QuantitySelectorProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/QuantitySelector',
title: 'Base Components/QuantitySelector',
component: QuantitySelector,
args: {
itemName: 'widgets',

View File

@@ -9,7 +9,7 @@ import type { Story, Meta } from '@storybook/react';
import ReadMore, { defaultProps, ReadMoreProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/ReadMore',
title: 'Base Components/ReadMore',
component: ReadMore,
args: defaultProps,
argTypes: {

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import SortSelect from '@woocommerce/base-components/sort-select';
import { SortSelect } from '@woocommerce/blocks-components';
import type { ChangeEventHandler } from 'react';
/**
@@ -12,7 +12,7 @@ import './style.scss';
interface ReviewSortSelectProps {
onChange: ChangeEventHandler;
readOnly: boolean;
readOnly?: boolean;
value: 'most-recent' | 'highest-rating' | 'lowest-rating';
}

View File

@@ -0,0 +1,90 @@
import { Canvas, Meta, ArgTypes } from '@storybook/blocks';
import * as SnackbarListStories from '../stories/index.stories.tsx';
import * as SnackbarStories from '../stories/snackbar.stories.tsx';
<Meta name="Docs" of={ SnackbarListStories } />
# SnackbarList
A temporary informational UI element displayed at the bottom of store pages.
<Canvas
of={ SnackbarListStories.Default }
layout="padded"
className={ 'force-canvas-height' }
/>
## Design Guidelines
The Snackbar is a temporary informational UI element displayed at the bottom of store pages. WooCommerce blocks, themes, and plugins all use snackbar notices to indicate the result of a successful action. For example, adding something to the cart.
Snackbar notices work in the same way as the NoticeBanner component, and support the same statuses and styles.
### Default Snackbars
By default, notices are grey and used for less important messaging.
<Canvas of={ SnackbarStories.Default } />
### Informational Snackbars
Blue notices with an info icon are used for general information for buyers, but do not require action.
<Canvas of={ SnackbarStories.Info } />
### Error Snackbars
Red notices with an alert icon are used to show that an error has occurred and that the user needs to take action.
<Canvas of={ SnackbarStories.Error } />
### Success Snackbars
Green notices with a success icon are used to show an action was successful.
<Canvas of={ SnackbarStories.Success } />
### Warning Snackbars
Yellow notices with an alert icon are used to show that the user may need to take action, or needs to be aware of something important.
<Canvas of={ SnackbarStories.Warning } />
## Development Guidelines
The component consuming `SnackbarList` is responsible for managing the notices state. The `SnackbarList` component will automatically remove notices from the list when they are dismissed by the user using the provided `onRemove` callback, and also when the notice times out after 10000ms.
### Props
<ArgTypes />
### NoticeType
Each notice can be provided with the following args.
- The `id` (required) prop is used to identify the notice and should be unique.
- The `content` (required) prop is the content to display in the notice.
- The `status` prop is used to determine the color of the notice and the icon. Acceptable values are 'success', 'error', 'info', 'warning', and 'default'.
- The `isDismissible` prop determines whether the notice can be dismissed by the user.
- The `spokenMessage` prop is used to change the spoken message for assistive technology. If not provided, the `content` prop will be used as the spoken message.
### Usage example
To display snackbar notices, pass an array of `notices` to the `SnackbarList` component:
```jsx
import { SnackbarList } from '@woocommerce/base-components';
const notices = [
{
id: '1',
content: 'This is a snackbar notice.',
status: 'default',
isDismissible: true,
spokenMessage: "Hello snackbar!"
}
];
<SnackbarList notices={ notices }>;
```

View File

@@ -2,6 +2,7 @@
* External dependencies
*/
import { useEffect } from '@wordpress/element';
import classNames from 'classnames';
/**
* Internal dependencies
@@ -9,15 +10,18 @@ import { useEffect } from '@wordpress/element';
import NoticeBanner, { NoticeBannerProps } from '../notice-banner';
import { SNACKBAR_TIMEOUT } from './constants';
const Snackbar = ( {
export interface SnackbarProps extends NoticeBannerProps {
// A ref to the list that contains the snackbar.
listRef?: React.MutableRefObject< HTMLDivElement | null >;
}
export const Snackbar = ( {
onRemove = () => void 0,
children,
listRef,
className,
...notice
}: {
// A ref to the list that contains the snackbar.
listRef?: React.MutableRefObject< HTMLDivElement | null >;
} & NoticeBannerProps ) => {
}: SnackbarProps ) => {
// Only set up the timeout dismiss if we're not explicitly dismissing.
useEffect( () => {
const timeoutHandle = setTimeout( () => {
@@ -29,6 +33,10 @@ const Snackbar = ( {
return (
<NoticeBanner
className={ classNames(
className,
'wc-block-components-notice-snackbar'
) }
{ ...notice }
onRemove={ () => {
// Prevent focus loss by moving it to the list element.

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { Story, Meta } from '@storybook/react';
import type { StoryFn, Meta } from '@storybook/react';
/**
* Internal dependencies
@@ -9,7 +9,7 @@ import type { Story, Meta } from '@storybook/react';
import SnackbarList, { SnackbarListProps } from '../';
export default {
title: 'WooCommerce Blocks/@base-components/SnackbarList',
title: 'Base Components/SnackbarList',
args: {
notices: [
{
@@ -28,18 +28,20 @@ export default {
control: 'text',
},
notices: {
description: 'List of notice objects to show as snackbar notices.',
description:
'A list of notices to display as snackbars. Each notice must have an `id` and `content` prop.',
disable: true,
},
onRemove: {
description: 'Function called when dismissing the notice(s).',
description:
'Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called. This is also called when the notice times out after 10000ms.',
disable: true,
},
},
component: SnackbarList,
} as Meta< SnackbarListProps >;
const Template: Story< SnackbarListProps > = ( args ) => {
const Template: StoryFn< SnackbarListProps > = ( args ) => {
return <SnackbarList { ...args } />;
};

View File

@@ -0,0 +1,97 @@
/**
* External dependencies
*/
import type { StoryFn, Meta } from '@storybook/react';
/**
* Internal dependencies
*/
import Snackbar, { SnackbarProps } from '../snackbar';
const availableStatus = [ 'default', 'success', 'error', 'warning', 'info' ];
export default {
title: 'Base Components/SnackbarList/Snackbar',
argTypes: {
status: {
control: 'radio',
options: availableStatus,
description:
'Status determines the color of the notice and the icon.',
},
isDismissible: {
control: 'boolean',
description:
'Determines whether the notice can be dismissed by the user. When set to true, a close icon will be displayed on the banner.',
},
summary: {
description:
'Optional summary text shown above notice content, used when several notices are listed together.',
control: 'text',
},
className: {
description: 'Additional class name to give to the notice.',
control: 'text',
},
spokenMessage: {
description:
'Optionally provided to change the spoken message for assistive technology. If not provided, the `children` prop will be used as the spoken message.',
control: 'text',
},
politeness: {
control: 'radio',
options: [ 'polite', 'assertive' ],
description:
'Determines the level of politeness for the notice for assistive technology.',
},
children: {
description:
'The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.',
disable: true,
},
onRemove: {
description:
'Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called.',
disable: true,
},
},
component: Snackbar,
} as Meta< SnackbarProps >;
const Template: StoryFn< SnackbarProps > = ( args ) => {
return <Snackbar { ...args } />;
};
export const Default = Template.bind( {} );
Default.args = {
children: 'This is a default snackbar notice',
status: 'default',
isDismissible: true,
summary: undefined,
className: undefined,
spokenMessage: undefined,
politeness: undefined,
};
export const Error = Template.bind( {} );
Error.args = {
children: 'This is an error snackbar notice',
status: 'error',
};
export const Warning = Template.bind( {} );
Warning.args = {
children: 'This is a warning snackbar notice',
status: 'warning',
};
export const Info = Template.bind( {} );
Info.args = {
children: 'This is an informational snackbar notice',
status: 'info',
};
export const Success = Template.bind( {} );
Success.args = {
children: 'This is a success snackbar notice',
status: 'success',
};

View File

@@ -9,25 +9,32 @@
bottom: $gap-large;
left: $gap-large;
right: $gap-large;
}
.wc-block-components-notice-banner {
display: inline-flex;
width: auto;
max-width: 600px;
pointer-events: all;
border: 1px solid transparent;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
position: relative;
margin: $gap-large 0 0;
.wc-block-components-notice-snackbar-list .wc-block-components-notice-banner,
.wc-block-components-notice-banner.wc-block-components-notice-snackbar {
display: inline-flex;
width: auto;
max-width: 600px;
pointer-events: all;
border: 1px solid transparent;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
position: relative;
margin: $gap-large $gap 0 0;
&.is-default {
border-color: $gray-800;
}
&.is-error,
&.is-info,
&.is-success {
border-color: transparent;
}
@include breakpoint("<782px") {
width: 100%;
max-width: none;
}
&.is-default {
border-color: $gray-800;
}
@include breakpoint("<782px") {
width: 100%;
max-width: none;
}
}

View File

@@ -1,103 +0,0 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import { withInstanceId } from '@wordpress/compose';
import type { ChangeEventHandler } from 'react';
/**
* Internal dependencies
*/
import './style.scss';
import Label from '../../../../../packages/components/label'; // Imported like this because importing from the components package loads the data stores unnecessarily - not a problem in the front end but would require a lot of unit test rewrites to prevent breaking tests due to incorrect mocks.
interface SortSelectProps {
/**
* Unique id for component instance.
*/
instanceId: number;
/**
* CSS class used.
*/
className?: string;
/**
* Label for the select.
*/
label?: string;
/**
* Function to call on the change event.
*/
onChange: ChangeEventHandler;
/**
* Option values for the select.
*/
options: {
key: string;
label: string;
}[];
/**
* Screen reader label.
*/
screenReaderLabel: string;
/**
* The selected value.
*/
value?: string;
/**
* Whether the select is read only.
*/
readOnly?: boolean;
}
/**
* Component used for 'Order by' selectors, which renders a label
* and a <select> with the options provided in the props.
*/
const SortSelect = ( {
className,
instanceId,
label = '',
onChange,
options,
screenReaderLabel,
value = '',
}: SortSelectProps ): JSX.Element => {
const selectId = `wc-block-components-sort-select__select-${ instanceId }`;
return (
<div
className={ classNames(
'wc-block-sort-select',
'wc-block-components-sort-select',
className
) }
>
<Label
label={ label }
screenReaderLabel={ screenReaderLabel }
wrapperElement="label"
wrapperProps={ {
className:
'wc-block-sort-select__label wc-block-components-sort-select__label',
htmlFor: selectId,
} }
/>
<select // eslint-disable-line jsx-a11y/no-onchange
id={ selectId }
className="wc-block-sort-select__select wc-block-components-sort-select__select"
onChange={ onChange }
value={ value }
>
{ options &&
options.map( ( option ) => (
<option key={ option.key } value={ option.key }>
{ option.label }
</option>
) ) }
</select>
</div>
);
};
export default withInstanceId( SortSelect );

View File

@@ -1,14 +0,0 @@
.wc-block-components-sort-select {
margin-bottom: $gap-small;
}
.wc-block-components-sort-select__label {
margin-right: $gap-small;
display: inline-block;
font-weight: normal;
}
.wc-block-components-sort-select__select {
font-size: inherit;
width: max-content;
}

View File

@@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
import classnames from 'classnames';
import { ValidatedTextInput } from '@woocommerce/blocks-checkout';
import { ValidatedTextInput } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -10,7 +10,7 @@ import { useState } from '@wordpress/element';
import { __TabsWithoutInstanceId as Tabs, TabsProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/Tabs',
title: 'Base Components/Tabs',
component: Tabs,
args: {
tabs: [

View File

@@ -1,36 +0,0 @@
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* Internal dependencies
*/
import './style.scss';
interface TextareaProps {
className?: string;
disabled: boolean;
onTextChange: ( newText: string ) => void;
placeholder: string;
value: string;
}
export const Textarea = ( {
className = '',
disabled = false,
onTextChange,
placeholder,
value = '',
}: TextareaProps ): JSX.Element => (
<textarea
className={ classnames( 'wc-block-components-textarea', className ) }
disabled={ disabled }
onChange={ ( event ) => {
onTextChange( event.target.value );
} }
placeholder={ placeholder }
rows={ 2 }
value={ value }
/>
);

View File

@@ -1,29 +0,0 @@
.wc-block-components-textarea {
@include font-size(regular);
background-color: #fff;
border: 1px solid $input-border-gray;
border-radius: $universal-border-radius;
color: $input-text-active;
font-family: inherit;
line-height: 1.375; // =22px when font-size is 16px.
margin: 0;
padding: em($gap-small) $gap;
width: 100%;
.has-dark-controls & {
background-color: $input-background-dark;
border-color: $input-border-dark;
color: $input-text-dark;
&::placeholder {
color: $input-placeholder-dark;
}
}
}
.theme-twentytwentyone {
.has-dark-controls .wc-block-components-textarea {
background-color: $input-background-dark;
color: $input-text-dark;
}
}

View File

@@ -1,48 +0,0 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import type { ReactNode } from 'react';
/**
* Internal dependencies
*/
import './style.scss';
/** @typedef {import('react')} React */
/**
* Component that renders a block title.
*
* @param {Object} props Incoming props for the component.
* @param {React.ReactNode} [props.children] Children elements this component wraps.
* @param {string} [props.className] CSS class used.
* @param {string} props.headingLevel Heading level for title.
* @param {Object} [props.props] Rest of props passed through to component.
*/
const Title = ( {
children,
className,
headingLevel,
...props
}: TitleProps ): JSX.Element => {
const buttonClassName = classNames(
'wc-block-components-title',
className
);
const TagName = `h${ headingLevel }` as keyof JSX.IntrinsicElements;
return (
<TagName className={ buttonClassName } { ...props }>
{ children }
</TagName>
);
};
interface TitleProps {
headingLevel: '1' | '2' | '3' | '4' | '5' | '6';
className: string;
children: ReactNode;
}
export default Title;

View File

@@ -1,22 +0,0 @@
// Extra class for specificity to overwrite editor styles.
.wc-block-components-title.wc-block-components-title {
@include reset-box();
@include font-size(large);
word-break: break-word;
textarea {
letter-spacing: inherit;
text-transform: inherit;
font-weight: inherit;
font-style: inherit;
}
}
// For Twenty Twenty we need to increase specificity a bit more.
.theme-twentytwenty {
.wc-block-components-title.wc-block-components-title {
@include reset-box();
@include font-size(large);
word-break: break-word;
}
}

View File

@@ -16,7 +16,7 @@ import {
PAYMENT_STORE_KEY,
CART_STORE_KEY,
} from '@woocommerce/block-data';
import { ValidationInputError } from '@woocommerce/blocks-checkout';
import { ValidationInputError } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -24,8 +24,6 @@ interface CheckoutAddress {
setShippingAddress: ( data: Partial< ShippingAddress > ) => void;
setBillingAddress: ( data: Partial< BillingAddress > ) => void;
setEmail: ( value: string ) => void;
setBillingPhone: ( value: string ) => void;
setShippingPhone: ( value: string ) => void;
useShippingAsBilling: boolean;
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
defaultAddressFields: AddressFields;
@@ -59,28 +57,13 @@ export const useCheckoutAddress = (): CheckoutAddress => {
} = useCustomerData();
const setEmail = useCallback(
( value ) =>
( value: string ) =>
void setBillingAddress( {
email: value,
} ),
[ setBillingAddress ]
);
const setBillingPhone = useCallback(
( value ) =>
void setBillingAddress( {
phone: value,
} ),
[ setBillingAddress ]
);
const setShippingPhone = useCallback(
( value ) =>
void setShippingAddress( {
phone: value,
} ),
[ setShippingAddress ]
);
const forcedBillingAddress: boolean = getSetting(
'forcedBillingAddress',
false
@@ -91,8 +74,6 @@ export const useCheckoutAddress = (): CheckoutAddress => {
setShippingAddress,
setBillingAddress,
setEmail,
setBillingPhone,
setShippingPhone,
defaultAddressFields,
useShippingAsBilling,
setUseShippingAsBilling: __internalSetUseShippingAsBilling,
@@ -101,8 +82,8 @@ export const useCheckoutAddress = (): CheckoutAddress => {
! forcedBillingAddress && needsShipping && ! prefersCollection,
showShippingMethods: needsShipping && ! prefersCollection,
showBillingFields:
! needsShipping || ! useShippingAsBilling || prefersCollection,
! needsShipping || ! useShippingAsBilling || !! prefersCollection,
forcedBillingAddress,
useBillingAsShipping: forcedBillingAddress || prefersCollection,
useBillingAsShipping: forcedBillingAddress || !! prefersCollection,
};
};

View File

@@ -35,12 +35,10 @@ describe( 'formatError', () => {
const mockResponse = new Response( mockMalformedJson, { status: 400 } );
const error = await formatError( mockResponse );
const expectedError = {
message:
'invalid json response body at reason: Unexpected end of JSON input',
type: 'general',
};
expect( error ).toEqual( expectedError );
expect( error.message ).toContain(
'invalid json response body at reason:'
);
expect( error.type ).toEqual( 'general' );
} );
} );

View File

@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
import { noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -3,8 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import Title from '@woocommerce/base-components/title';
import { Title, StoreNoticesContainer } from '@woocommerce/blocks-components';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data';

View File

@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { noticeContexts } from '@woocommerce/base-context';
import { NoticeType } from '@woocommerce/types';
interface PaymentMethodErrorBoundaryProps {

View File

@@ -14,10 +14,8 @@ import {
CartProvider,
noticeContexts,
} from '@woocommerce/base-context';
import {
SlotFillProvider,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
/**
* Internal dependencies

View File

@@ -4,7 +4,7 @@
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( { className }: { className: string } ): JSX.Element | null => {
const couponsEnabled = getSetting( 'couponsEnabled', true );

View File

@@ -2,15 +2,13 @@
* External dependencies
*/
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
useStoreCartCoupons,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import {
ExperimentalDiscountsMeta,
TotalsWrapper,
} from '@woocommerce/blocks-checkout';
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
const DiscountSlotFill = (): JSX.Element => {
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -4,7 +4,7 @@
import { TotalsShipping } from '@woocommerce/base-components/cart-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
import { getSetting } from '@woocommerce/settings';
const Block = ( { className }: { className: string } ): JSX.Element | null => {

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';

View File

@@ -96,6 +96,9 @@
display: none;
}
}
table.wc-block-cart-items {
margin: 0;
}
}
.is-medium,

View File

@@ -7,6 +7,7 @@ import type {
CartShippingAddress,
CartBillingAddress,
} from '@woocommerce/types';
import { AddressFields, AddressField } from '@woocommerce/settings';
/**
* Internal dependencies
@@ -17,12 +18,12 @@ const AddressCard = ( {
address,
onEdit,
target,
showPhoneField,
fieldConfig,
}: {
address: CartShippingAddress | CartBillingAddress;
onEdit: () => void;
target: string;
showPhoneField: boolean;
fieldConfig: Record< keyof AddressFields, Partial< AddressField > >;
} ): JSX.Element | null => {
return (
<div className="wc-block-components-address-card">
@@ -33,7 +34,7 @@ const AddressCard = ( {
<div className="wc-block-components-address-card__address-section">
{ [
address.address_1,
address.address_2,
! fieldConfig.address_2.hidden && address.address_2,
address.city,
address.state,
address.postcode,
@@ -46,7 +47,7 @@ const AddressCard = ( {
<span key={ `address-` + index }>{ field }</span>
) ) }
</div>
{ address.phone && showPhoneField ? (
{ address.phone && ! fieldConfig.phone.hidden ? (
<div
key={ `address-phone` }
className="wc-block-components-address-card__address-section"

View File

@@ -12,10 +12,8 @@ import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
import {
SlotFillProvider,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
import { useDispatch, useSelect } from '@wordpress/data';
import {

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import Title from '@woocommerce/base-components/title';
import { Title } from '@woocommerce/blocks-components';
/**
* Step Heading Component

View File

@@ -9,10 +9,8 @@ import {
} from '@woocommerce/base-components/cart-checkout';
import { useCheckoutSubmit } from '@woocommerce/base-context/hooks';
import { noticeContexts } from '@woocommerce/base-context';
import {
StoreNoticesContainer,
applyCheckoutFilter,
} from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
/**
* Internal dependencies

View File

@@ -14,9 +14,10 @@ import type {
AddressField,
AddressFields,
} from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY } from '@woocommerce/block-data';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
@@ -29,17 +30,19 @@ const Block = ( {
showPhoneField = false,
requireCompanyField = false,
requirePhoneField = false,
forceEditing = false,
}: {
showCompanyField: boolean;
showApartmentField: boolean;
showPhoneField: boolean;
requireCompanyField: boolean;
requirePhoneField: boolean;
forceEditing?: boolean;
} ): JSX.Element => {
const { billingAddress, setShippingAddress, useBillingAsShipping } =
useCheckoutAddress();
const {
shippingAddress,
billingAddress,
setShippingAddress,
useBillingAsShipping,
} = useCheckoutAddress();
const { isEditor } = useEditorContext();
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
@@ -71,11 +74,17 @@ const Block = ( {
address_2: {
hidden: ! showApartmentField,
},
phone: {
hidden: ! showPhoneField,
required: requirePhoneField,
},
};
}, [
showCompanyField,
requireCompanyField,
showApartmentField,
showPhoneField,
requirePhoneField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const WrapperComponent = isEditor ? Noninteractive : Fragment;
@@ -89,6 +98,20 @@ const Block = ( {
cartDataLoaded: store.hasFinishedResolution( 'getCartData' ),
};
} );
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
const hasAddress = !! (
billingAddress.address_1 &&
( billingAddress.first_name || billingAddress.last_name )
);
const { email, ...billingAddressWithoutEmail } = billingAddress;
const billingMatchesShipping = isShallowEqual(
billingAddressWithoutEmail,
shippingAddress
);
const defaultEditingAddress =
isEditor || ! hasAddress || billingMatchesShipping;
return (
<>
<StoreNoticesContainer context={ noticeContext } />
@@ -96,9 +119,7 @@ const Block = ( {
{ cartDataLoaded ? (
<CustomerAddress
addressFieldsConfig={ addressFieldsConfig }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
forceEditing={ forceEditing }
defaultEditing={ defaultEditingAddress }
/>
) : null }
</WrapperComponent>

View File

@@ -16,35 +16,24 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
* Internal dependencies
*/
import AddressWrapper from '../../address-wrapper';
import PhoneNumber from '../../phone-number';
import AddressCard from '../../address-card';
const CustomerAddress = ( {
addressFieldsConfig,
showPhoneField,
requirePhoneField,
forceEditing = false,
defaultEditing = false,
}: {
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
showPhoneField: boolean;
requirePhoneField: boolean;
forceEditing?: boolean;
defaultEditing?: boolean;
} ) => {
const {
defaultAddressFields,
billingAddress,
setShippingAddress,
setBillingAddress,
setBillingPhone,
setShippingPhone,
useBillingAsShipping,
} = useCheckoutAddress();
const { dispatchCheckoutEvent } = useStoreEvents();
const hasAddress = !! (
billingAddress.address_1 &&
( billingAddress.first_name || billingAddress.last_name )
);
const [ editing, setEditing ] = useState( ! hasAddress || forceEditing );
const [ editing, setEditing ] = useState( defaultEditing );
// Forces editing state if store has errors.
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
@@ -54,8 +43,9 @@ const CustomerAddress = ( {
invalidProps: Object.keys( billingAddress )
.filter( ( key ) => {
return (
key !== 'email' &&
store.getValidationError( 'billing_' + key ) !==
undefined
undefined
);
} )
.filter( Boolean ),
@@ -97,10 +87,10 @@ const CustomerAddress = ( {
onEdit={ () => {
setEditing( true );
} }
showPhoneField={ showPhoneField }
fieldConfig={ addressFieldsConfig }
/>
),
[ billingAddress, showPhoneField ]
[ billingAddress, addressFieldsConfig ]
);
const renderAddressFormComponent = useCallback(
@@ -114,39 +104,13 @@ const CustomerAddress = ( {
fields={ addressFieldKeys }
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id="billing-phone"
errorId={ 'billing_phone' }
isRequired={ requirePhoneField }
value={ billingAddress.phone }
onChange={ ( value ) => {
setBillingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
if ( useBillingAsShipping ) {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
}
} }
/>
) }
</>
),
[
addressFieldKeys,
addressFieldsConfig,
billingAddress,
dispatchCheckoutEvent,
onChangeAddress,
requirePhoneField,
setBillingPhone,
setShippingPhone,
showPhoneField,
useBillingAsShipping,
]
);

View File

@@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
import { useRef, useEffect } from '@wordpress/element';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/blocks-components';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
@@ -43,21 +42,8 @@ const FrontendBlock = ( {
showCompanyField,
showPhoneField,
} = useCheckoutBlockContext();
const {
showBillingFields,
forcedBillingAddress,
useBillingAsShipping,
useShippingAsBilling,
} = useCheckoutAddress();
// If initial state was true, force editing to true so address fields are visible if the useShippingAsBilling option is unchecked.
const toggledUseShippingAsBilling = useRef( useShippingAsBilling );
useEffect( () => {
if ( useShippingAsBilling ) {
toggledUseShippingAsBilling.current = true;
}
}, [ useShippingAsBilling ] );
const { showBillingFields, forcedBillingAddress, useBillingAsShipping } =
useCheckoutAddress();
if ( ! showBillingFields && ! useBillingAsShipping ) {
return null;
@@ -86,7 +72,6 @@ const FrontendBlock = ( {
showCompanyField={ showCompanyField }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
forceEditing={ toggledUseShippingAsBilling.current }
/>
{ children }
</FormStep>

View File

@@ -8,19 +8,15 @@ import {
noticeContexts,
} from '@woocommerce/base-context';
import { getSetting } from '@woocommerce/settings';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import {
CheckboxControl,
ValidatedTextInput,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
ValidatedTextInput,
} from '@woocommerce/blocks-components';
import { useDispatch, useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { isEmail } from '@wordpress/url';
/**
* Internal dependencies
*/
const Block = (): JSX.Element => {
const { customerId, shouldCreateAccount } = useSelect( ( select ) => {
const store = select( CHECKOUT_STORE_KEY );

View File

@@ -3,6 +3,8 @@
*/
import classnames from 'classnames';
import { Main } from '@woocommerce/base-components/sidebar-layout';
import { useStoreEvents } from '@woocommerce/base-context/hooks';
import { useEffect } from '@wordpress/element';
const FrontendBlock = ( {
children,
@@ -11,6 +13,14 @@ const FrontendBlock = ( {
children: JSX.Element;
className?: string;
} ): JSX.Element => {
const { dispatchCheckoutEvent } = useStoreEvents();
// Ignore changes to dispatchCheckoutEvent callback so this is ran on first mount only.
useEffect( () => {
dispatchCheckoutEvent( 'render-checkout-form' );
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );
return (
<Main className={ classnames( 'wc-block-checkout__main', className ) }>
<form className="wc-block-components-form wc-block-checkout__form">

View File

@@ -18,11 +18,11 @@
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0 calc(#{$gap-smaller} * 2); // Required for spacing especially when using flex-grow
.wc-block-components-text-input,
.wc-block-components-country-input,
.wc-block-components-state-input {
flex: 0 0 calc(50% - #{$gap-smaller});
flex: 1 0 calc(50% - #{$gap-smaller}); // "flex-grow = 1" allows the input to grow to fill the space
box-sizing: border-box;
&:nth-of-type(2),
@@ -33,7 +33,8 @@
.wc-block-components-address-form__company,
.wc-block-components-address-form__address_1,
.wc-block-components-address-form__address_2 {
.wc-block-components-address-form__address_2,
.wc-block-components-country-input {
flex: 0 0 100%;
}
}

View File

@@ -3,7 +3,7 @@
*/
import { OrderSummary } from '@woocommerce/base-components/cart-checkout';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( { className }: { className: string } ): JSX.Element => {
const { cartItems } = useStoreCart();

View File

@@ -4,7 +4,7 @@
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
const Block = ( {
className = '',

View File

@@ -2,15 +2,13 @@
* External dependencies
*/
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
import { TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
useStoreCartCoupons,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import {
ExperimentalDiscountsMeta,
TotalsWrapper,
} from '@woocommerce/blocks-checkout';
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
const DiscountSlotFill = (): JSX.Element => {
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';

View File

@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-checkout';
import { TotalsTaxes, TotalsWrapper } from '@woocommerce/blocks-components';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';

View File

@@ -4,10 +4,12 @@
import classnames from 'classnames';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/blocks-components';
import {
FormStep,
StoreNoticesContainer,
} from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { noticeContexts } from '@woocommerce/base-context';
/**

View File

@@ -1,66 +1,68 @@
.wc-block-checkout__pickup-options,
.wp-block-woocommerce-checkout-pickup-options-block {
.wc-block-components-radio-control__option {
@include with-translucent-border(0 0 1px);
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
}
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;
}
.wc-block-components-radio-control .wc-block-components-radio-control__input {
top: auto;
transform: none;
margin-top: 1px;
}
.wc-block-components-radio-control__option-layout {
display: block;
}
.wc-block-components-radio-control__label-group {
width: 100%;
display: flex;
> :last-child {
margin-left: auto;
.wc-block-components-local-pickup-rates-control {
.wc-block-components-radio-control__option {
@include with-translucent-border(0 0 1px);
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
}
}
.wc-block-components-radio-control__description-group {
display: none;
}
.wc-block-components-radio-control__option-checked {
.wc-block-components-radio-control__description-group {
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;
}
.wc-block-components-radio-control .wc-block-components-radio-control__input {
top: auto;
transform: none;
margin-top: 1px;
}
.wc-block-components-radio-control__option-layout {
display: block;
}
}
.wc-block-components-radio-control__label-group {
em {
text-transform: uppercase;
font-style: inherit;
}
}
.wc-block-components-radio-control__description-group {
width: 100%;
box-sizing: border-box;
background-color: $gray-100;
border-radius: $universal-border-radius;
padding: 1px em($gap-small);
margin-top: em($gap-smaller);
@include font-size(regular);
}
.wc-block-components-radio-control__description,
.wc-block-components-radio-control__secondary-description {
width: 100%;
text-align: left;
margin: em($gap-small) 0;
display: block;
}
.wc-block-components-radio-control__secondary-description {
color: $gray-700;
.wc-block-components-radio-control__label-group {
width: 100%;
display: flex;
> svg {
vertical-align: middle;
margin-top: -4px;
fill: currentColor;
> :last-child {
margin-left: auto;
}
}
.wc-block-components-radio-control__description-group {
display: none;
}
.wc-block-components-radio-control__option-checked {
.wc-block-components-radio-control__description-group {
display: block;
}
}
.wc-block-components-radio-control__label-group {
em {
text-transform: uppercase;
font-style: inherit;
}
}
.wc-block-components-radio-control__description-group {
width: 100%;
box-sizing: border-box;
background-color: $gray-100;
border-radius: $universal-border-radius;
padding: 1px em($gap-small);
margin-top: em($gap-smaller);
@include font-size(regular);
}
.wc-block-components-radio-control__description,
.wc-block-components-radio-control__secondary-description {
width: 100%;
text-align: left;
margin: em($gap-small) 0;
display: block;
}
.wc-block-components-radio-control__secondary-description {
color: $gray-700;
> svg {
vertical-align: middle;
margin-top: -4px;
fill: currentColor;
}
}
}
}

View File

@@ -9,10 +9,8 @@ import {
useEditorContext,
noticeContexts,
} from '@woocommerce/base-context';
import {
CheckboxControl,
StoreNoticesContainer,
} from '@woocommerce/blocks-checkout';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
import Noninteractive from '@woocommerce/base-components/noninteractive';
import type {
BillingAddress,
@@ -82,11 +80,17 @@ const Block = ( {
address_2: {
hidden: ! showApartmentField,
},
phone: {
hidden: ! showPhoneField,
required: requirePhoneField,
},
};
}, [
showCompanyField,
requireCompanyField,
showApartmentField,
showPhoneField,
requirePhoneField,
] ) as Record< keyof AddressFields, Partial< AddressField > >;
const WrapperComponent = isEditor ? Noninteractive : Fragment;
@@ -105,6 +109,9 @@ const Block = ( {
};
} );
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
const defaultEditingAddress = isEditor || ! hasAddress;
return (
<>
<StoreNoticesContainer context={ noticeContext } />
@@ -112,27 +119,24 @@ const Block = ( {
{ cartDataLoaded ? (
<CustomerAddress
addressFieldsConfig={ addressFieldsConfig }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
defaultEditing={ defaultEditingAddress }
/>
) : null }
</WrapperComponent>
{ hasAddress && (
<CheckboxControl
className="wc-block-checkout__use-address-for-billing"
label={ __(
'Use same address for billing',
'woo-gutenberg-products-block'
) }
checked={ useShippingAsBilling }
onChange={ ( checked: boolean ) => {
setUseShippingAsBilling( checked );
if ( checked ) {
syncBillingWithShipping();
}
} }
/>
) }
<CheckboxControl
className="wc-block-checkout__use-address-for-billing"
label={ __(
'Use same address for billing',
'woo-gutenberg-products-block'
) }
checked={ useShippingAsBilling }
onChange={ ( checked: boolean ) => {
setUseShippingAsBilling( checked );
if ( checked ) {
syncBillingWithShipping();
}
} }
/>
</>
);
};

View File

@@ -3,11 +3,7 @@
*/
import { useState, useCallback, useEffect } from '@wordpress/element';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutAddress,
useStoreEvents,
useEditorContext,
} from '@woocommerce/base-context';
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
import type {
ShippingAddress,
AddressField,
@@ -20,34 +16,24 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
* Internal dependencies
*/
import AddressWrapper from '../../address-wrapper';
import PhoneNumber from '../../phone-number';
import AddressCard from '../../address-card';
const CustomerAddress = ( {
addressFieldsConfig,
showPhoneField,
requirePhoneField,
defaultEditing = false,
}: {
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
showPhoneField: boolean;
requirePhoneField: boolean;
defaultEditing?: boolean;
} ) => {
const {
defaultAddressFields,
shippingAddress,
setShippingAddress,
setBillingAddress,
setShippingPhone,
setBillingPhone,
useShippingAsBilling,
} = useCheckoutAddress();
const { dispatchCheckoutEvent } = useStoreEvents();
const { isEditor } = useEditorContext();
const hasAddress = !! (
shippingAddress.address_1 &&
( shippingAddress.first_name || shippingAddress.last_name )
);
const [ editing, setEditing ] = useState( ! hasAddress || isEditor );
const [ editing, setEditing ] = useState( defaultEditing );
// Forces editing state if store has errors.
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
@@ -74,19 +60,11 @@ const CustomerAddress = ( {
const addressFieldKeys = Object.keys(
defaultAddressFields
) as ( keyof AddressFields )[];
const onChangeAddress = useCallback(
( values: Partial< ShippingAddress > ) => {
setShippingAddress( values );
if ( useShippingAsBilling ) {
// Sync billing with shipping. Ensure unwanted properties are omitted.
const { ...syncBilling } = values;
if ( ! showPhoneField ) {
delete syncBilling.phone;
}
setBillingAddress( syncBilling );
setBillingAddress( values );
dispatchCheckoutEvent( 'set-billing-address' );
}
dispatchCheckoutEvent( 'set-shipping-address' );
@@ -96,7 +74,6 @@ const CustomerAddress = ( {
setBillingAddress,
setShippingAddress,
useShippingAsBilling,
showPhoneField,
]
);
@@ -108,56 +85,28 @@ const CustomerAddress = ( {
onEdit={ () => {
setEditing( true );
} }
showPhoneField={ showPhoneField }
fieldConfig={ addressFieldsConfig }
/>
),
[ shippingAddress, showPhoneField ]
[ shippingAddress, addressFieldsConfig ]
);
const renderAddressFormComponent = useCallback(
() => (
<>
<AddressForm
id="shipping"
type="shipping"
onChange={ onChangeAddress }
values={ shippingAddress }
fields={ addressFieldKeys }
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id="shipping-phone"
errorId={ 'shipping_phone' }
isRequired={ requirePhoneField }
value={ shippingAddress.phone }
onChange={ ( value ) => {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'shipping',
} );
if ( useShippingAsBilling ) {
setBillingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
}
} }
/>
) }
</>
<AddressForm
id="shipping"
type="shipping"
onChange={ onChangeAddress }
values={ shippingAddress }
fields={ addressFieldKeys }
fieldConfig={ addressFieldsConfig }
/>
),
[
addressFieldKeys,
addressFieldsConfig,
dispatchCheckoutEvent,
onChangeAddress,
requirePhoneField,
setBillingPhone,
setShippingPhone,
shippingAddress,
showPhoneField,
useShippingAsBilling,
]
);

View File

@@ -13,9 +13,11 @@ import {
isAddressComplete,
} from '@woocommerce/base-utils';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
import {
FormattedMonetaryAmount,
StoreNoticesContainer,
} from '@woocommerce/blocks-components';
import { useEditorContext, noticeContexts } from '@woocommerce/base-context';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { decodeEntities } from '@wordpress/html-entities';
import { getSetting } from '@woocommerce/settings';
import type {

View File

@@ -3,7 +3,7 @@
*/
import classnames from 'classnames';
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
const FrontendBlock = ( {
children,

View File

@@ -4,7 +4,7 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import { Textarea } from '@woocommerce/base-components/textarea';
import { Textarea } from '@woocommerce/blocks-components';
interface CheckoutOrderNotesProps {
disabled: boolean;

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ValidatedTextInput } from '@woocommerce/blocks-checkout';
import { ValidatedTextInput } from '@woocommerce/blocks-components';
/**
* Renders a phone number input.

View File

@@ -1,37 +0,0 @@
/**
* External dependencies
*/
import { render, findByLabelText } from '@testing-library/react';
/**
* Internal dependencies
*/
import PhoneNumber from '../index';
describe( 'Phone number', () => {
it( 'Renders an input field with type tel', async () => {
const { container } = render(
<PhoneNumber
id={ 'shipping-phone' }
isRequired={ true }
onChange={ () => null }
value={ '' }
/>
);
const input = await findByLabelText( container, 'Phone' );
expect( input.getAttribute( 'type' ) ).toEqual( 'tel' );
} );
it( 'Renders (optional) in the label if the field is not marked as required', async () => {
const { container } = render(
<PhoneNumber
id={ 'shipping-phone' }
isRequired={ false }
onChange={ () => null }
value={ '' }
/>
);
const input = await findByLabelText( container, 'Phone (optional)' );
expect( input ).toBeTruthy();
} );
} );

View File

@@ -4,14 +4,30 @@
"title": "Collection Filters",
"description": "A block that adds product filters to the product collection.",
"category": "woocommerce",
"keywords": [ "WooCommerce", "Filters" ],
"keywords": [
"WooCommerce",
"Filters"
],
"textdomain": "woocommerce",
"supports": {
"html": false,
"reusable": false
},
"usesContext": [ "query" ],
"ancestor": [ "woocommerce/product-collection" ],
"usesContext": [
"query"
],
"providesContext": {
"collectionData": "collectionData"
},
"ancestor": [
"woocommerce/product-collection"
],
"attributes": {
"collectionData": {
"type": "object",
"default": {}
}
},
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

Some files were not shown because too many files have changed in this diff Show More