File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/store-notices-container.tar
index.tsx 0000644 00000005757 15155513253 0006436 0 ustar 00 /**
* External dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import {
PAYMENT_STORE_KEY,
STORE_NOTICES_STORE_KEY,
} from '@woocommerce/block-data';
import { getNoticeContexts } from '@woocommerce/base-utils';
import type { Notice } from '@wordpress/notices';
import { useMemo, useEffect } from '@wordpress/element';
import type { NoticeType } from '@woocommerce/types';
/**
* Internal dependencies
*/
import './style.scss';
import StoreNotices from './store-notices';
import SnackbarNotices from './snackbar-notices';
import type { StoreNoticesContainerProps } from './types';
const formatNotices = ( notices: Notice[], context: string ): NoticeType[] => {
return notices.map( ( notice ) => ( {
...notice,
context,
} ) ) as NoticeType[];
};
const StoreNoticesContainer = ( {
className = '',
context = '',
additionalNotices = [],
}: StoreNoticesContainerProps ): JSX.Element | null => {
const { registerContainer, unregisterContainer } = useDispatch(
STORE_NOTICES_STORE_KEY
);
const { suppressNotices, registeredContainers } = useSelect(
( select ) => ( {
suppressNotices:
select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive(),
registeredContainers: select(
STORE_NOTICES_STORE_KEY
).getRegisteredContainers(),
} )
);
const contexts = useMemo< string[] >(
() => ( Array.isArray( context ) ? 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 ) =>
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
// objects for later reference.
const notices = useSelect< NoticeType[] >( ( select ) => {
const { getNotices } = select( 'core/notices' );
return [
...unregisteredSubContexts.flatMap( ( subContext: string ) =>
formatNotices( getNotices( subContext ), subContext )
),
...contexts.flatMap( ( subContext: string ) =>
formatNotices(
getNotices( subContext ).concat( additionalNotices ),
subContext
)
),
].filter( Boolean ) as NoticeType[];
} );
// Register the container context with the parent.
useEffect( () => {
contexts.map( ( _context ) => registerContainer( _context ) );
return () => {
contexts.map( ( _context ) => unregisterContainer( _context ) );
};
}, [ contexts, registerContainer, unregisterContainer ] );
if ( suppressNotices ) {
return null;
}
return (
<>
<StoreNotices
className={ className }
notices={ notices.filter(
( notice ) => notice.type === 'default'
) }
/>
<SnackbarNotices
className={ className }
notices={ notices.filter(
( notice ) => notice.type === 'snackbar'
) }
/>
</>
);
};
export default StoreNoticesContainer;
snackbar-notices.tsx 0000644 00000001633 15155513253 0010542 0 ustar 00 /**
* External dependencies
*/
import classnames from 'classnames';
import SnackbarList from '@woocommerce/base-components/snackbar-list';
import { useDispatch } from '@wordpress/data';
import type { NoticeType } from '@woocommerce/types';
const SnackbarNotices = ( {
className,
notices,
}: {
className: string;
notices: NoticeType[];
} ): JSX.Element | null => {
const { removeNotice } = useDispatch( 'core/notices' );
return (
<SnackbarList
className={ classnames(
className,
'wc-block-components-notices__snackbar'
) }
notices={ notices }
onRemove={ ( noticeId: string ) => {
notices.forEach( ( notice ) => {
if ( notice.explicitDismiss && notice.id === noticeId ) {
removeNotice( notice.id, notice.context );
} else if ( ! notice.explicitDismiss ) {
removeNotice( notice.id, notice.context );
}
} );
} }
/>
);
};
export default SnackbarNotices;
store-notices.tsx 0000644 00000010713 15155513253 0010111 0 ustar 00 /**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { useRef, useEffect, RawHTML } from '@wordpress/element';
import { sanitizeHTML } from '@woocommerce/utils';
import { useDispatch } from '@wordpress/data';
import { usePrevious } from '@woocommerce/base-hooks';
import { decodeEntities } from '@wordpress/html-entities';
import type { NoticeType } from '@woocommerce/types';
import type { NoticeBannerProps } from '@woocommerce/base-components/notice-banner';
/**
* Internal dependencies
*/
import StoreNotice from '../store-notice';
const StoreNotices = ( {
className,
notices,
}: {
className: string;
notices: NoticeType[];
} ): JSX.Element => {
const ref = useRef< HTMLDivElement >( null );
const { removeNotice } = useDispatch( 'core/notices' );
const noticeIds = notices.map( ( notice ) => notice.id );
const previousNoticeIds = usePrevious( noticeIds );
useEffect( () => {
// Scroll to container when an error is added here.
const containerRef = ref.current;
if ( ! containerRef ) {
return;
}
// Do not scroll if input has focus.
const activeElement = containerRef.ownerDocument.activeElement;
const inputs = [ 'input', 'select', 'button', 'textarea' ];
if (
activeElement &&
inputs.indexOf( activeElement.tagName.toLowerCase() ) !== -1 &&
activeElement.getAttribute( 'type' ) !== 'radio'
) {
return;
}
const newNoticeIds = noticeIds.filter(
( value ) =>
! previousNoticeIds || ! previousNoticeIds.includes( value )
);
if ( newNoticeIds.length && containerRef?.scrollIntoView ) {
containerRef.scrollIntoView( {
behavior: 'smooth',
} );
}
}, [ noticeIds, previousNoticeIds, ref ] );
// Group notices by whether or not they are dismissible. Dismissible notices can be grouped.
const dismissibleNotices = notices.filter(
( { isDismissible } ) => !! isDismissible
);
const nonDismissibleNotices = notices.filter(
( { isDismissible } ) => ! isDismissible
);
// Group dismissibleNotices by status. They will be combined into a single notice.
const dismissibleNoticeGroups = {
error: dismissibleNotices.filter(
( { status } ) => status === 'error'
),
success: dismissibleNotices.filter(
( { status } ) => status === 'success'
),
warning: dismissibleNotices.filter(
( { status } ) => status === 'warning'
),
info: dismissibleNotices.filter( ( { status } ) => status === 'info' ),
default: dismissibleNotices.filter(
( { status } ) => status === 'default'
),
};
return (
<div
ref={ ref }
className={ classnames( className, 'wc-block-components-notices' ) }
>
{ nonDismissibleNotices.map( ( notice ) => (
<StoreNotice
key={ notice.id + '-' + notice.context }
{ ...notice }
>
<RawHTML>
{ sanitizeHTML( decodeEntities( notice.content ) ) }
</RawHTML>
</StoreNotice>
) ) }
{ Object.entries( dismissibleNoticeGroups ).map(
( [ status, noticeGroup ] ) => {
if ( ! noticeGroup.length ) {
return null;
}
const uniqueNotices = noticeGroup
.filter(
(
notice: NoticeType,
noticeIndex: number,
noticesArray: NoticeType[]
) =>
noticesArray.findIndex(
( _notice: NoticeType ) =>
_notice.content === notice.content
) === noticeIndex
)
.map( ( notice ) => ( {
...notice,
content: sanitizeHTML(
decodeEntities( notice.content )
),
} ) );
const noticeProps: Omit< NoticeBannerProps, 'children' > & {
key: string;
} = {
key: `store-notice-${ status }`,
status: 'error',
onRemove: () => {
noticeGroup.forEach( ( notice ) => {
removeNotice( notice.id, notice.context );
} );
},
};
return uniqueNotices.length === 1 ? (
<StoreNotice { ...noticeProps }>
<RawHTML>{ noticeGroup[ 0 ].content }</RawHTML>
</StoreNotice>
) : (
<StoreNotice
{ ...noticeProps }
summary={
status === 'error'
? __(
'Please fix the following errors before continuing',
'woo-gutenberg-products-block'
)
: ''
}
>
<ul>
{ uniqueNotices.map( ( notice ) => (
<li
key={ notice.id + '-' + notice.context }
>
<RawHTML>{ notice.content }</RawHTML>
</li>
) ) }
</ul>
</StoreNotice>
);
}
) }
</div>
);
};
export default StoreNotices;
style.scss 0000644 00000002676 15155513253 0006621 0 ustar 00 .wc-block-components-notices {
display: block;
margin: 1.5em 0;
&:first-child {
margin-top: 0;
}
&:empty {
margin: 0;
}
.wc-block-components-notices__notice {
margin: 0;
display: flex;
flex-wrap: nowrap;
a {
text-decoration: underline;
}
.components-notice__dismiss {
background: transparent none;
padding: 0;
margin: 0 0 0 auto;
border: 0;
outline: 0;
color: currentColor;
svg {
fill: currentColor;
vertical-align: text-top;
}
}
.components-notice__content > div:not(.components-notice__actions) {
*:first-child {
margin-top: 0;
}
*:last-child {
margin-bottom: 0;
}
}
.components-notice__content {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li + li {
margin: 0.25em 0 0 0;
}
}
}
.wc-block-components-notices__notice + .wc-block-components-notices__notice {
margin-top: 1em;
}
}
// @todo Either move notice style fixes to Woo core, or take full control over notice component styling in blocks.
.theme-twentytwentyone,
.theme-twentytwenty {
.wc-block-components-notices__notice {
padding: 1.5rem 3rem;
}
}
.wc-block-components-notices__snackbar {
position: fixed;
bottom: 20px;
left: 16px;
width: auto;
@include breakpoint("<782px") {
position: fixed;
top: 10px;
left: 0;
bottom: auto;
}
.components-snackbar-list__notice-container {
@include breakpoint("<782px") {
margin-left: 10px;
margin-right: 10px;
}
}
}
test/index.tsx 0000644 00000013340 15155513253 0007400 0 ustar 00 /**
* External dependencies
*/
import { store as noticesStore } from '@wordpress/notices';
import { dispatch, select } from '@wordpress/data';
import { act, render, screen, waitFor } from '@testing-library/react';
/**
* Internal dependencies
*/
import StoreNoticesContainer from '../index';
describe( 'StoreNoticesContainer', () => {
it( 'Shows notices from the correct context', async () => {
dispatch( noticesStore ).createErrorNotice( 'Custom test error', {
id: 'custom-test-error',
context: 'test-context',
} );
render( <StoreNoticesContainer context="test-context" /> );
expect( screen.getAllByText( /Custom test error/i ) ).toHaveLength( 2 );
// Clean up notices.
await act( () =>
dispatch( noticesStore ).removeNotice(
'custom-test-error',
'test-context'
)
);
await waitFor( () => {
return (
select( noticesStore ).getNotices( 'test-context' ).length === 0
);
} );
} );
it( 'Does not show notices from other contexts', async () => {
dispatch( noticesStore ).createErrorNotice( 'Custom test error 2', {
id: 'custom-test-error-2',
context: 'test-context',
} );
render( <StoreNoticesContainer context="other-context" /> );
expect( screen.queryAllByText( /Custom test error 2/i ) ).toHaveLength(
0
);
// Clean up notices.
await act( () =>
dispatch( noticesStore ).removeNotice(
'custom-test-error-2',
'test-context'
)
);
await waitFor( () => {
return (
select( noticesStore ).getNotices( 'test-context' ).length === 0
);
} );
} );
it( 'Does not show snackbar notices', async () => {
dispatch( noticesStore ).createErrorNotice( 'Custom test error 2', {
id: 'custom-test-error-2',
context: 'test-context',
type: 'snackbar',
} );
render( <StoreNoticesContainer context="other-context" /> );
expect( screen.queryAllByText( /Custom test error 2/i ) ).toHaveLength(
0
);
// Clean up notices.
await act( () =>
dispatch( noticesStore ).removeNotice(
'custom-test-error-2',
'test-context'
)
);
await waitFor( () => {
return (
select( noticesStore ).getNotices( 'test-context' ).length === 0
);
} );
} );
it( 'Shows additional notices', () => {
render(
<StoreNoticesContainer
additionalNotices={ [
{
id: 'additional-test-error',
status: 'error',
spokenMessage: 'Additional test error',
isDismissible: false,
content: 'Additional test error',
actions: [],
speak: false,
__unstableHTML: '',
type: 'default',
},
] }
/>
);
// Also counts the spokenMessage.
expect( screen.getAllByText( /Additional test error/i ) ).toHaveLength(
2
);
} );
it( 'Shows notices from unregistered sub-contexts', async () => {
dispatch( noticesStore ).createErrorNotice(
'Custom first sub-context error',
{
id: 'custom-subcontext-test-error',
context: 'wc/checkout/shipping-address',
}
);
dispatch( noticesStore ).createErrorNotice(
'Custom second sub-context error',
{
id: 'custom-subcontext-test-error',
context: 'wc/checkout/billing-address',
}
);
render( <StoreNoticesContainer context="wc/checkout" /> );
// This should match against 2 messages, one for each sub-context.
expect(
screen.getAllByText( /Custom first sub-context error/i )
).toHaveLength( 2 );
expect(
screen.getAllByText( /Custom second sub-context 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( '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(
<StoreNoticesContainer
context={ [
'wc/checkout/billing-address',
'wc/checkout/shipping-address',
] }
/>
);
// 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(
<StoreNoticesContainer
context={ [
'wc/checkout/billing-address',
'wc/checkout/shipping-address',
] }
/>
);
// 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'
)
);
} );
} );
types.ts 0000644 00000000514 15155513253 0006265 0 ustar 00 /**
* External dependencies
*/
import type { NoticeType } from '@woocommerce/types';
export interface StoreNoticesContainerProps {
className?: string | undefined;
context?: string | string[];
// List of additional notices that were added inline and not stored in the `core/notices` store.
additionalNotices?: NoticeType[];
}