diff --git a/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx b/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx index 108fb33f25a..204596bffd0 100644 --- a/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx +++ b/assets/js/base/components/cart-checkout/shipping-calculator/index.tsx @@ -30,10 +30,9 @@ const ShippingCalculator = ( { addressFields = [ 'country', 'state', 'city', 'postcode' ], }: ShippingCalculatorProps ): JSX.Element => { const { shippingAddress } = useCustomerData(); - const noticeContext = 'wc/cart/shipping-calculator'; return (
- + - - + { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx index 3d42b8ebc6a..1d372046da0 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx @@ -88,10 +88,13 @@ const Block = ( { ] ) as Record< keyof AddressFields, Partial< AddressField > >; const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment; + const noticeContext = useBillingAsShipping + ? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ] + : [ noticeContexts.BILLING_ADDRESS ]; return ( - + >; const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment; + const noticeContext = useShippingAsBilling + ? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ] + : [ noticeContexts.SHIPPING_ADDRESS ]; return ( <> - + { }; ``` +#### Multiple contexts + +```jsx +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; + +const AddressForm = () => { + return ( + + ); +}; +``` + ## Snackbar notices in WooCommerce Blocks WooCommerce Blocks also shows snackbar notices, to add a snackbar notice you need to create a notice with `type:snackbar` in the options object. @@ -53,17 +73,3 @@ dispatch( 'core/notices' ).createNotice( 'snackbar-notice-id' ); ``` - -### `SnackbarNoticesContainer` - -To display snackbar notices, use the `SnackbarNoticesContainer` component. This component is rendered with the Cart and Checkout blocks, so there is no need to add another. The context it displays notices for is `default`. If, for some reason you do need to show snackbar messages for a different context, you can render this component again and pass the context as a prop to the component. - -```jsx -import { SnackbarNoticesContainer } from '@woocommerce/base-components/snackbar-notices-container'; - -const AlternativeSnackbarNotices = () => { - return ( - - ); -}; -``` diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index 08178df360d..65ce1caa7d4 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -38,13 +38,14 @@ const StoreNoticesContainer = ( { ).getRegisteredContainers(), } ) ); - + const contexts = Array.isArray( context ) ? context : [ context ]; // Find sub-contexts that have not been registered. We will show notices from those contexts here too. const allContexts = getNoticeContexts(); const unregisteredSubContexts = allContexts.filter( ( subContext: string ) => - subContext.includes( context + '/' ) && - ! registeredContainers.includes( subContext ) + contexts.some( ( _context: string ) => + subContext.includes( _context + '/' ) + ) && ! registeredContainers.includes( subContext ) ); // Get notices from the current context and any sub-contexts and append the name of the context to the notice @@ -56,13 +57,14 @@ const StoreNoticesContainer = ( { ...unregisteredSubContexts.flatMap( ( subContext: string ) => formatNotices( getNotices( subContext ), subContext ) ), - ...formatNotices( - getNotices( context ).concat( additionalNotices ), - context + ...contexts.flatMap( ( subContext: string ) => + formatNotices( + getNotices( subContext ).concat( additionalNotices ), + subContext + ) ), ].filter( Boolean ) as StoreNotice[]; } ); - if ( suppressNotices || ! notices.length ) { return null; } @@ -71,7 +73,7 @@ const StoreNoticesContainer = ( { <> notice.type === 'default' ) } diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index 8262ff18764..7957e1d3822 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -21,7 +21,7 @@ const StoreNotices = ( { className, notices, }: { - context: string; + context: string | string[]; className: string; notices: StoreNotice[]; } ): JSX.Element => { @@ -64,12 +64,12 @@ const StoreNotices = ( { } ); } }, [ noticeIds, previousNoticeIds, ref ] ); - // Register the container context with the parent. useEffect( () => { - registerContainer( context ); + const contexts = Array.isArray( context ) ? context : [ context ]; + contexts.map( ( _context ) => registerContainer( _context ) ); return () => { - unregisterContainer( context ); + contexts.map( ( _context ) => unregisterContainer( _context ) ); }; }, [ context, registerContainer, unregisterContainer ] ); @@ -117,6 +117,17 @@ const StoreNotices = ( { if ( ! noticeGroup.length ) { return null; } + const uniqueNotices = noticeGroup.filter( + ( + notice: Notice, + noticeIndex: number, + noticesArray: Notice[] + ) => + noticesArray.findIndex( + ( _notice: Notice ) => + _notice.content === notice.content + ) === noticeIndex + ); return ( - { noticeGroup.length === 1 ? ( + { uniqueNotices.length === 1 ? ( <> { sanitizeHTML( decodeEntities( @@ -140,7 +151,7 @@ const StoreNotices = ( { ) : (
    - { noticeGroup.map( ( notice ) => ( + { uniqueNotices.map( ( notice ) => (
  • { ) ); } ); + + it( 'Shows notices from several contexts', async () => { + dispatch( noticesStore ).createErrorNotice( 'Custom shipping error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/shipping-address', + } ); + dispatch( noticesStore ).createErrorNotice( 'Custom billing error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/billing-address', + } ); + render( + + ); + // This should match against 4 elements; A written and spoken message for each error. + expect( screen.getAllByText( /Custom shipping error/i ) ).toHaveLength( + 2 + ); + expect( screen.getAllByText( /Custom billing error/i ) ).toHaveLength( + 2 + ); + // Clean up notices. + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/shipping-address' + ) + ); + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/billing-address' + ) + ); + } ); + + it( 'Combine same notices from several contexts', async () => { + dispatch( noticesStore ).createErrorNotice( 'Custom generic error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/shipping-address', + } ); + dispatch( noticesStore ).createErrorNotice( 'Custom generic error', { + id: 'custom-subcontext-test-error', + context: 'wc/checkout/billing-address', + } ); + render( + + ); + // This should match against 2 elements; A written and spoken message. + expect( screen.getAllByText( /Custom generic error/i ) ).toHaveLength( + 2 + ); + // Clean up notices. + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/shipping-address' + ) + ); + await act( () => + dispatch( noticesStore ).removeNotice( + 'custom-subcontext-test-error', + 'wc/checkout/billing-address' + ) + ); + } ); } ); diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts index 332ea079f5d..63e1db419b7 100644 --- a/packages/checkout/components/store-notices-container/types.ts +++ b/packages/checkout/components/store-notices-container/types.ts @@ -8,7 +8,7 @@ import type { export interface StoreNoticesContainerProps { className?: string | undefined; - context: string; + context: string | string[]; // List of additional notices that were added inline and not stored in the `core/notices` store. additionalNotices?: ( NoticeType & NoticeOptions )[]; }