* Refactored the inbox note cards and moved to experimental package

* Update experimental dependencies

* Add tests for inbox-note components, and updated naming

* Add changelog

* Update readme and fix dismiss all

* Fixed lint errors

* Refactor dismiss logic in inbox-panel

* Add hook for handling inner link callbacks

* Export updates and a minor TS update

* Fix lint error
This commit is contained in:
louwie17 2021-06-02 15:25:41 -03:00 committed by GitHub
parent 1b022ae4de
commit 65de4bff00
24 changed files with 1073 additions and 807 deletions

View File

@ -1,184 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
import PropTypes from 'prop-types';
import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings';
import { NOTES_STORE_NAME } from '@woocommerce/data';
class InboxNoteAction extends Component {
constructor( props ) {
super( props );
this.state = {
inAction: false,
};
this.handleActionClick = this.handleActionClick.bind( this );
}
handleActionClick( event ) {
const {
action,
actionCallback,
batchUpdateNotes,
createNotice,
noteId,
triggerNoteAction,
removeAllNotes,
removeNote,
onClick,
updateNote,
} = this.props;
const href = event.target.href || '';
let inAction = true;
if ( href.length && ! href.startsWith( adminUrl ) ) {
event.preventDefault();
inAction = false; // link buttons shouldn't be "busy".
window.open( href, '_blank' );
}
if ( ! action ) {
if ( noteId ) {
removeNote( noteId )
.then( () => {
createNotice(
'success',
__( 'Message dismissed', 'woocommerce-admin' ),
{
actions: [
{
label: __(
'Undo',
'woocommerce-admin'
),
onClick: () => {
updateNote( noteId, {
is_deleted: 0,
} );
},
},
],
}
);
} )
.catch( () => {
createNotice(
'error',
__(
'Message could not be dismissed',
'woocommerce-admin'
)
);
} );
} else {
removeAllNotes()
.then( ( notes ) => {
createNotice(
'success',
__( 'All messages dismissed', 'woocommerce-admin' ),
{
actions: [
{
label: __(
'Undo',
'woocommerce-admin'
),
onClick: () => {
batchUpdateNotes(
notes.map(
( note ) => note.id
),
{
is_deleted: 0,
}
);
},
},
],
}
);
} )
.catch( () => {
createNotice(
'error',
__(
'Message could not be dismissed',
'woocommerce-admin'
)
);
} );
}
actionCallback( true );
} else {
this.setState( { inAction }, () => {
triggerNoteAction( noteId, action.id );
if ( !! onClick ) {
onClick();
}
} );
}
}
render() {
const { action, dismiss, label } = this.props;
return (
<Button
isSecondary
isBusy={ this.state.inAction }
disabled={ this.state.inAction }
href={
action && action.url && action.url.length
? action.url
: undefined
}
onClick={ this.handleActionClick }
>
{ dismiss ? label : action.label }
</Button>
);
}
}
InboxNoteAction.propTypes = {
noteId: PropTypes.number,
label: PropTypes.string,
dismiss: PropTypes.bool,
actionCallback: PropTypes.func,
action: PropTypes.shape( {
id: PropTypes.number.isRequired,
url: PropTypes.string,
label: PropTypes.string.isRequired,
primary: PropTypes.bool.isRequired,
} ),
onClick: PropTypes.func,
};
export default compose(
withDispatch( ( dispatch ) => {
const { createNotice } = dispatch( 'core/notices' );
const {
batchUpdateNotes,
removeAllNotes,
removeNote,
updateNote,
triggerNoteAction,
} = dispatch( NOTES_STORE_NAME );
return {
batchUpdateNotes,
createNotice,
removeAllNotes,
removeNote,
triggerNoteAction,
updateNote,
};
} )
)( InboxNoteAction );

View File

@ -1,371 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, createRef } from '@wordpress/element';
import { Button, Dropdown, Modal } from '@wordpress/components';
import PropTypes from 'prop-types';
import VisibilitySensor from 'react-visibility-sensor';
import moment from 'moment';
import classnames from 'classnames';
import { H, Section } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import NoteAction from './action';
import sanitizeHTML from '../lib/sanitize-html';
import './style.scss';
import { getScreenName } from '../utils';
class InboxNoteCard extends Component {
constructor( props ) {
super( props );
this.onVisible = this.onVisible.bind( this );
this.hasBeenSeen = false;
this.state = {
isDismissModalOpen: false,
dismissType: null,
clickedActionText: null,
};
this.openDismissModal = this.openDismissModal.bind( this );
this.closeDismissModal = this.closeDismissModal.bind( this );
this.bodyNotificationRef = createRef();
this.toggleButtonRef = createRef();
this.screen = getScreenName();
}
componentDidMount() {
if ( this.bodyNotificationRef.current ) {
this.bodyNotificationRef.current.addEventListener(
'click',
( event ) => this.handleBodyClick( event, this.props )
);
}
}
componentWillUnmount() {
if ( this.bodyNotificationRef.current ) {
this.bodyNotificationRef.current.removeEventListener(
'click',
( event ) => this.handleBodyClick( event, this.props )
);
}
}
handleBodyClick( event, props ) {
const innerLink = event.target.href;
if ( innerLink ) {
const { note } = props;
recordEvent( 'wcadmin_inbox_action_click', {
note_name: note.name,
note_title: note.title,
note_content_inner_link: innerLink,
} );
}
}
// Trigger a view Tracks event when the note is seen.
onVisible( isVisible ) {
if ( isVisible && ! this.hasBeenSeen ) {
const { note } = this.props;
recordEvent( 'inbox_note_view', {
note_content: note.content,
note_name: note.name,
note_title: note.title,
note_type: note.type,
screen: this.screen,
} );
this.hasBeenSeen = true;
}
}
openDismissModal( type, onToggle ) {
this.setState( {
isDismissModalOpen: true,
dismissType: type,
} );
onToggle();
}
closeDismissModal( noteNameDismissConfirmation ) {
const { dismissType } = this.state;
const { note } = this.props;
const noteNameDismissAll = dismissType === 'all' ? true : false;
recordEvent( 'inbox_action_dismiss', {
note_name: note.name,
note_title: note.title,
note_name_dismiss_all: noteNameDismissAll,
note_name_dismiss_confirmation:
noteNameDismissConfirmation || false,
screen: this.screen,
} );
this.setState( {
isDismissModalOpen: false,
} );
}
handleBlur( event, onClose ) {
const dropdownClasses = [
'woocommerce-admin-dismiss-notification',
'components-popover__content',
];
// This line is for IE compatibility.
let relatedTarget;
if ( event.relatedTarget ) {
relatedTarget = event.relatedTarget;
} else if ( this.toggleButtonRef.current ) {
const ownerDoc = this.toggleButtonRef.current.ownerDocument;
relatedTarget = ownerDoc ? ownerDoc.activeElement : null;
}
const isClickOutsideDropdown = relatedTarget
? dropdownClasses.some( ( className ) =>
relatedTarget.className.includes( className )
)
: false;
if ( isClickOutsideDropdown ) {
event.preventDefault();
} else {
onClose();
}
}
renderDismissButton() {
const { clickedActionText } = this.state;
if ( clickedActionText ) {
return null;
}
return (
<Dropdown
contentClassName="woocommerce-admin-dismiss-dropdown"
position="bottom right"
renderToggle={ ( { onClose, onToggle } ) => (
<Button
isTertiary
onClick={ onToggle }
ref={ this.toggleButtonRef }
onBlur={ ( event ) =>
this.handleBlur( event, onClose )
}
>
{ __( 'Dismiss', 'woocommerce-admin' ) }
</Button>
) }
focusOnMount={ false }
popoverProps={ { noArrow: true } }
renderContent={ ( { onToggle } ) => (
<ul>
<li>
<Button
className="woocommerce-admin-dismiss-notification"
onClick={ () =>
this.openDismissModal( 'this', onToggle )
}
>
{ __(
'Dismiss this message',
'woocommerce-admin'
) }
</Button>
</li>
<li>
<Button
className="woocommerce-admin-dismiss-notification"
onClick={ () =>
this.openDismissModal( 'all', onToggle )
}
>
{ __(
'Dismiss all messages',
'woocommerce-admin'
) }
</Button>
</li>
</ul>
) }
/>
);
}
getDismissConfirmationButton() {
const { note } = this.props;
const { dismissType } = this.state;
return (
<NoteAction
key={ note.id }
noteId={ dismissType === 'all' ? null : note.id }
label={ __( "Yes, I'm sure", 'woocommerce-admin' ) }
actionCallback={ this.closeDismissModal }
dismiss={ true }
screen={ this.screen }
/>
);
}
renderDismissConfirmationModal() {
return (
<Modal
title={ <>{ __( 'Are you sure?', 'woocommerce-admin' ) }</> }
onRequestClose={ () => this.closeDismissModal() }
className="woocommerce-inbox-dismiss-confirmation_modal"
>
<div className="woocommerce-inbox-dismiss-confirmation_wrapper">
<p>
{ __(
'Dismissed messages cannot be viewed again',
'woocommerce-admin'
) }
</p>
<div className="woocommerce-inbox-dismiss-confirmation_buttons">
<Button
isSecondary
onClick={ () => this.closeDismissModal() }
>
{ __( 'Cancel', 'woocommerce-admin' ) }
</Button>
{ this.getDismissConfirmationButton() }
</div>
</div>
</Modal>
);
}
renderActions( note ) {
const { actions: noteActions, id: noteId } = note;
const { clickedActionText } = this.state;
if ( !! clickedActionText ) {
return clickedActionText;
}
if ( ! noteActions ) {
return;
}
return (
<>
{ noteActions.map( ( action, index ) => (
<NoteAction
key={ index }
noteId={ noteId }
action={ action }
onClick={ () => this.onActionClicked( action ) }
/>
) ) }
</>
);
}
onActionClicked = ( action ) => {
if ( ! action.actioned_text ) {
return;
}
this.setState( {
clickedActionText: action.actioned_text,
} );
};
render() {
const { lastRead, note } = this.props;
const { isDismissModalOpen } = this.state;
const {
content,
date_created: dateCreated,
date_created_gmt: dateCreatedGmt,
image,
is_deleted: isDeleted,
layout,
status,
title,
} = note;
if ( isDeleted ) {
return null;
}
const unread =
! lastRead ||
! dateCreatedGmt ||
new Date( dateCreatedGmt + 'Z' ).getTime() > lastRead;
const date = dateCreated;
const hasImage = layout !== 'plain' && layout !== '';
const cardClassName = classnames( 'woocommerce-inbox-message', layout, {
'message-is-unread': unread && status === 'unactioned',
} );
return (
<VisibilitySensor onChange={ this.onVisible }>
<section className={ cardClassName }>
{ hasImage && (
<div className="woocommerce-inbox-message__image">
<img src={ image } alt="" />
</div>
) }
<div className="woocommerce-inbox-message__wrapper">
<div className="woocommerce-inbox-message__content">
{ unread && (
<div className="woocommerce-inbox-message__unread-indicator" />
) }
{ date && (
<span className="woocommerce-inbox-message__date">
{ moment.utc( date ).fromNow() }
</span>
) }
<H className="woocommerce-inbox-message__title">
{ title }
</H>
<Section className="woocommerce-inbox-message__text">
<span
dangerouslySetInnerHTML={ sanitizeHTML(
content
) }
ref={ this.bodyNotificationRef }
/>
</Section>
</div>
<div className="woocommerce-inbox-message__actions">
{ this.renderActions( note ) }
{ this.renderDismissButton() }
</div>
</div>
{ isDismissModalOpen &&
this.renderDismissConfirmationModal() }
</section>
</VisibilitySensor>
);
}
}
InboxNoteCard.propTypes = {
note: PropTypes.shape( {
id: PropTypes.number,
status: PropTypes.string,
title: PropTypes.string,
content: PropTypes.string,
date_created: PropTypes.string,
date_created_gmt: PropTypes.string,
actions: PropTypes.arrayOf(
PropTypes.shape( {
id: PropTypes.number.isRequired,
url: PropTypes.string,
label: PropTypes.string.isRequired,
primary: PropTypes.bool.isRequired,
} )
),
layout: PropTypes.string,
image: PropTypes.string,
is_deleted: PropTypes.bool,
} ),
lastRead: PropTypes.number,
};
export default InboxNoteCard;

View File

@ -1,25 +1,30 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, _n } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { EmptyContent, Section } from '@woocommerce/components';
import {
NOTES_STORE_NAME,
useUserPreferences,
QUERY_DEFAULTS,
} from '@woocommerce/data';
import { withSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import {
InboxNoteCard,
InboxDismissConfirmationModal,
InboxNotePlaceholder,
} from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import { ActivityCard } from '../header/activity-panel/activity-card';
import InboxNotePlaceholder from './placeholder';
import InboxNoteCard from './card';
import { hasValidNotes } from './utils';
import { getScreenName } from '../utils';
import './index.scss';
const renderEmptyCard = () => (
<ActivityCard
@ -35,7 +40,22 @@ const renderEmptyCard = () => (
</ActivityCard>
);
const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
const onBodyLinkClick = ( note, innerLink ) => {
recordEvent( 'inbox_action_click', {
note_name: note.name,
note_title: note.title,
note_content_inner_link: innerLink,
} );
};
const renderNotes = ( {
hasNotes,
isBatchUpdating,
lastRead,
notes,
onDismiss,
onNoteActionClick,
} ) => {
if ( isBatchUpdating ) {
return;
}
@ -44,6 +64,17 @@ const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
return renderEmptyCard();
}
const screen = getScreenName();
const onNoteVisible = ( note ) => {
recordEvent( 'inbox_note_view', {
note_content: note.content,
note_name: note.name,
note_title: note.title,
note_type: note.type,
screen,
} );
};
const notesArray = Object.keys( notes ).map( ( key ) => notes[ key ] );
return (
@ -63,6 +94,10 @@ const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
key={ noteId }
note={ note }
lastRead={ lastRead }
onDismiss={ onDismiss }
onNoteActionClick={ onNoteActionClick }
onBodyLinkClick={ onBodyLinkClick }
onNoteVisible={ onNoteVisible }
/>
</CSSTransition>
);
@ -71,10 +106,60 @@ const renderNotes = ( { hasNotes, isBatchUpdating, lastRead, notes } ) => {
);
};
const InboxPanel = ( props ) => {
const { isError, isResolving, isBatchUpdating, notes } = props;
const INBOX_QUERY = {
page: 1,
per_page: QUERY_DEFAULTS.pageSize,
status: 'unactioned',
type: QUERY_DEFAULTS.noteTypes,
orderby: 'date',
order: 'desc',
_fields: [
'id',
'name',
'title',
'content',
'type',
'status',
'actions',
'date_created',
'date_created_gmt',
'layout',
'image',
'is_deleted',
],
};
const InboxPanel = () => {
const { createNotice } = useDispatch( 'core/notices' );
const {
batchUpdateNotes,
removeAllNotes,
removeNote,
updateNote,
triggerNoteAction,
} = useDispatch( NOTES_STORE_NAME );
const { isError, isResolvingNotes, isBatchUpdating, notes } = useSelect(
( select ) => {
const {
getNotes,
getNotesError,
isResolving,
isNotesRequesting,
} = select( NOTES_STORE_NAME );
return {
notes: getNotes( INBOX_QUERY ),
isError: Boolean(
getNotesError( 'getNotes', [ INBOX_QUERY ] )
),
isResolvingNotes: isResolving( 'getNotes', [ INBOX_QUERY ] ),
isBatchUpdating: isNotesRequesting( 'batchUpdateNotes' ),
};
}
);
const { updateUserPreferences, ...userPrefs } = useUserPreferences();
const [ lastRead ] = useState( userPrefs.activity_panel_inbox_last_read );
const [ dismiss, setDismiss ] = useState();
useEffect( () => {
const mountTime = Date.now();
@ -85,6 +170,83 @@ const InboxPanel = ( props ) => {
updateUserPreferences( userDataFields );
}, [] );
const onDismiss = ( note, type ) => {
setDismiss( { note, type } );
};
const closeDismissModal = async ( confirmed = false ) => {
const noteNameDismissAll = dismiss.type === 'all' ? true : false;
const screen = getScreenName();
recordEvent( 'inbox_action_dismiss', {
note_name: dismiss.note.name,
note_title: dismiss.note.title,
note_name_dismiss_all: noteNameDismissAll,
note_name_dismiss_confirmation: confirmed,
screen,
} );
if ( confirmed ) {
const noteId = dismiss.note.id;
const removeAll = ! noteId || noteNameDismissAll;
try {
let notesRemoved = [];
if ( removeAll ) {
notesRemoved = await removeAllNotes();
} else {
const noteRemoved = await removeNote( noteId );
notesRemoved = [ noteRemoved ];
}
setDismiss( undefined );
createNotice(
'success',
notesRemoved.length > 1
? __( 'All messages dismissed', 'woocommerce-admin' )
: __( 'Message dismissed', 'woocommerce-admin' ),
{
actions: [
{
label: __( 'Undo', 'woocommerce-admin' ),
onClick: () => {
if ( notesRemoved.length > 1 ) {
batchUpdateNotes(
notesRemoved.map(
( note ) => note.id
),
{
is_deleted: 0,
}
);
} else {
updateNote( noteId, {
is_deleted: 0,
} );
}
},
},
],
}
);
} catch ( e ) {
const numberOfNotes = removeAll ? notes.length : 1;
createNotice(
'error',
_n(
'Message could not be dismissed',
'Messages could not be dismissed',
numberOfNotes,
'woocommerce-admin'
)
);
setDismiss( undefined );
}
}
};
const onNoteActionClick = ( note, action ) => {
triggerNoteAction( note.id, action.id );
};
if ( isError ) {
const title = __(
'There was an error getting your inbox. Please try again.',
@ -113,63 +275,32 @@ const InboxPanel = ( props ) => {
return (
<>
<div className="woocommerce-homepage-notes-wrapper">
{ ( isResolving || isBatchUpdating ) && (
{ ( isResolvingNotes || isBatchUpdating ) && (
<Section>
<InboxNotePlaceholder className="banner message-is-unread" />
</Section>
) }
<Section>
{ ! isResolving &&
{ ! isResolvingNotes &&
! isBatchUpdating &&
renderNotes( {
hasNotes,
isBatchUpdating,
lastRead,
notes,
onDismiss,
onNoteActionClick,
} ) }
</Section>
{ dismiss && (
<InboxDismissConfirmationModal
onClose={ closeDismissModal }
onDismiss={ () => closeDismissModal( true ) }
/>
) }
</div>
</>
);
};
const INBOX_QUERY = {
page: 1,
per_page: QUERY_DEFAULTS.pageSize,
status: 'unactioned',
type: QUERY_DEFAULTS.noteTypes,
orderby: 'date',
order: 'desc',
_fields: [
'id',
'name',
'title',
'content',
'type',
'status',
'actions',
'date_created',
'date_created_gmt',
'layout',
'image',
'is_deleted',
],
};
export default compose(
withSelect( ( select ) => {
const {
getNotes,
getNotesError,
isResolving,
isNotesRequesting,
} = select( NOTES_STORE_NAME );
return {
notes: getNotes( INBOX_QUERY ),
isError: Boolean( getNotesError( 'getNotes', [ INBOX_QUERY ] ) ),
isResolving: isResolving( 'getNotes', [ INBOX_QUERY ] ),
isBatchUpdating: isNotesRequesting( 'batchUpdateNotes' ),
};
} )
)( InboxPanel );
export default InboxPanel;

View File

@ -0,0 +1,40 @@
.woocommerce-homepage-notes-wrapper {
padding-top: $gap-large;
}
#activity-panel-inbox {
margin: 0 $gap-large;
}
.woocommerce-layout__inbox-panel-header {
padding: $gap-large;
.woocommerce-homepage-column & {
padding: 0 24px;
}
}
.woocommerce-inbox-message-enter {
opacity: 0;
max-height: 0;
transform: translateX(50%);
}
.woocommerce-inbox-message-enter-active {
opacity: 1;
max-height: 100vh;
transform: translateX(0%);
transition: opacity 500ms, transform 500ms, max-height 500ms;
}
.woocommerce-inbox-message-exit {
opacity: 1;
max-height: 100vh;
transform: translateX(0%);
}
.woocommerce-inbox-message-exit-active {
opacity: 0;
max-height: 0;
transform: translateX(50%);
transition: opacity 500ms, transform 500ms, max-height 500ms;
}

View File

@ -1,43 +0,0 @@
/**
* External dependencies
*/
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types';
class InboxNotePlaceholder extends Component {
render() {
const { className } = this.props;
return (
<div
className={ `woocommerce-inbox-message is-placeholder ${ className }` }
aria-hidden
>
<div className="woocommerce-inbox-message__wrapper">
<div className="woocommerce-inbox-message__content">
<div className="woocommerce-inbox-message__date">
<div className="sixth-line" />
</div>
<div className="woocommerce-inbox-message__title">
<div className="line" />
<div className="line" />
</div>
<div className="woocommerce-inbox-message__text">
<div className="line" />
<div className="third-line" />
</div>
</div>
<div className="woocommerce-inbox-message__actions">
<div className="fifth-line" />
<div className="fifth-line" />
</div>
</div>
</div>
);
}
}
InboxNotePlaceholder.propTypes = {
className: PropTypes.string,
};
export default InboxNotePlaceholder;

View File

@ -1,118 +1,8 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
/**
* Internal dependencies
*/
import InboxNoteCard from '../card';
import { getUnreadNotesCount, hasValidNotes } from '../utils';
jest.mock( '../action', () =>
jest.fn().mockImplementation( () => <button>mocked button</button> )
);
describe( 'InboxNoteCard', () => {
const note = {
id: 1,
name: 'wc-admin-wc-helper-connection',
type: 'info',
title: 'Connect to WooCommerce.com',
content: 'Connect to get important product notifications and updates.',
status: 'unactioned',
date_created: '2020-05-10T16:57:31',
actions: [
{
id: 1,
name: 'connect',
label: 'Connect',
query: '',
status: 'unactioned',
primary: false,
url: 'http://test.com',
},
],
layout: 'plain',
image: '',
date_created_gmt: '2020-05-10T16:57:31',
is_deleted: false,
};
const lastRead = 1589285995243;
test( 'should render a notification type banner', () => {
note.layout = 'banner';
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
/>
);
const listNoteWithBanner = container.querySelector( '.banner' );
expect( listNoteWithBanner ).not.toBeNull();
} );
test( 'should render a notification type thumbnail', () => {
note.layout = 'thumbnail';
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
/>
);
const listNoteWithThumbnail = container.querySelector( '.thumbnail' );
expect( listNoteWithThumbnail ).not.toBeNull();
} );
test( 'should render a read notification', () => {
note.actions = [];
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
/>
);
const unreadNote = container.querySelector( '.message-is-unread' );
const readNote = container.querySelector(
'.woocommerce-inbox-message'
);
expect( unreadNote ).toBeNull();
expect( readNote ).not.toBeNull();
} );
test( 'should render an unread notification', () => {
const olderLastRead = 1584015595000;
note.actions = [];
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ olderLastRead }
/>
);
const unreadNote = container.querySelector( '.message-is-unread' );
expect( unreadNote ).not.toBeNull();
} );
test( 'should not render any notification', () => {
note.is_deleted = true;
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
/>
);
const unreadNote = container.querySelector(
'.woocommerce-inbox-message'
);
expect( unreadNote ).toBeNull();
} );
} );
const notes = [
{
id: 1,

View File

@ -10923,6 +10923,7 @@
"dev": true,
"requires": {
"@babel/runtime": "7.14.0",
"@woocommerce/components": "file:packages/components",
"@wordpress/components": "10.2.0",
"@wordpress/element": "2.19.0",
"@wordpress/i18n": "3.17.0",
@ -10931,7 +10932,9 @@
"classnames": "^2.3.1",
"dompurify": "2.2.8",
"gridicons": "3.3.1",
"react-transition-group": "4.4.1"
"moment": "2.29.1",
"react-transition-group": "4.4.1",
"react-visibility-sensor": "5.1.1"
},
"dependencies": {
"@wordpress/components": {

View File

@ -13,8 +13,7 @@ import { Level } from './context';
* (`h2`, `h3`, ) you can use `<H />` to create "section headings", which look to the parent `<Section />`s for the appropriate
* heading level.
*
* @param {Object} props -
* @return {Object} -
* @type {HTMLElement}
*/
export function H( props ) {
const level = useContext( Level );

View File

@ -12,9 +12,10 @@ import { Level } from './context';
* The section wrapper, used to indicate a sub-section (and change the header level context).
*
* @param {Object} props
* @param {string} props.component
* @param {Node} props.children
* @return {Object} -
* @param {import('react').ComponentType=} props.component
* @param {import('react').ReactNode} props.children Children to render in the tip.
* @param {string=} props.className
* @return {JSX.Element} -
*/
export function Section( { component, children, ...props } ) {
const Component = component || 'div';
@ -47,4 +48,8 @@ Section.propTypes = {
* The children inside this section, rendered in the `component`. This increases the context level for the next heading used.
*/
children: PropTypes.node,
/**
* Optional classname
*/
className: PropTypes.string,
};

View File

@ -2,6 +2,7 @@
- Remove the use of Dashicons and replace with @wordpress/icons or gridicons #7020
- Add expanded item text and CTA button. #6956
- Add inbox note components (InboxNoteCard, InboxNotePlaceholder, and InboxDismissConfirmationModal). #7006
# 1.2.0

View File

@ -22,6 +22,7 @@
"react-native": "src/index",
"dependencies": {
"@babel/runtime": "7.14.0",
"@woocommerce/components": "file:../components",
"@wordpress/components": "10.2.0",
"@wordpress/element": "2.19.0",
"@wordpress/i18n": "3.17.0",
@ -30,7 +31,9 @@
"classnames": "^2.3.1",
"dompurify": "2.2.8",
"gridicons": "3.3.1",
"react-transition-group": "4.4.1"
"moment": "2.29.1",
"react-transition-group": "4.4.1",
"react-visibility-sensor": "5.1.1"
},
"publishConfig": {
"access": "public"

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { ADMIN_URL as adminUrl } from '@woocommerce/wc-admin-settings';
type InboxNoteActionProps = {
onClick: () => void;
label: string;
href?: string;
};
/**
* Renders a secondary button that can also be a link. If href is provided it will
* automatically open it in a new tab/window.
*/
export const InboxNoteActionButton: React.FC< InboxNoteActionProps > = ( {
label,
onClick,
href,
} ) => {
const [ inAction, setInAction ] = useState( false );
const handleActionClick: React.MouseEventHandler< HTMLAnchorElement > = (
event
) => {
const targetHref = event.currentTarget.href || '';
let isActionable = true;
if ( targetHref.length && ! targetHref.startsWith( adminUrl ) ) {
event.preventDefault();
isActionable = false; // link buttons shouldn't be "busy".
window.open( targetHref, '_blank' );
}
setInAction( isActionable );
onClick();
};
return (
<Button
isSecondary
isBusy={ inAction }
disabled={ inAction }
href={ href }
onClick={ handleActionClick }
>
{ label }
</Button>
);
};

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Modal } from '@wordpress/components';
import { useState } from '@wordpress/element';
type ConfirmationModalProps = {
onClose: () => void;
onDismiss: () => void;
buttonLabel?: string;
};
export const InboxDismissConfirmationModal: React.FC< ConfirmationModalProps > = ( {
onClose,
onDismiss,
buttonLabel = __( "Yes, I'm sure", 'woocommerce-admin' ),
} ) => {
const [ inAction, setInAction ] = useState( false );
return (
<Modal
title={ __( 'Are you sure?', 'woocommerce-admin' ) }
onRequestClose={ () => onClose() }
className="woocommerce-inbox-dismiss-confirmation_modal"
>
<div className="woocommerce-inbox-dismiss-confirmation_wrapper">
<p>
{ __(
'Dismissed messages cannot be viewed again',
'woocommerce-admin'
) }
</p>
<div className="woocommerce-inbox-dismiss-confirmation_buttons">
<Button isSecondary onClick={ () => onClose() }>
{ __( 'Cancel', 'woocommerce-admin' ) }
</Button>
<Button
isSecondary
isBusy={ inAction }
disabled={ inAction }
onClick={ () => {
setInAction( true );
onDismiss();
} }
>
{ buttonLabel }
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1,291 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState, useRef } from '@wordpress/element';
import { Button, Dropdown, Popover } from '@wordpress/components';
import VisibilitySensor from 'react-visibility-sensor';
import moment from 'moment';
import classnames from 'classnames';
import { H, Section } from '@woocommerce/components';
import { sanitize } from 'dompurify';
/**
* Internal dependencies
*/
import { InboxNoteActionButton } from './action';
import { useCallbackOnLinkClick } from './use-callback-on-link-click';
const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ];
const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ];
const sanitizeHTML = ( html: string ) => {
return {
__html: sanitize( html, { ALLOWED_TAGS, ALLOWED_ATTR } ),
};
};
type InboxNoteAction = {
id: number;
url: string;
label: string;
primary: boolean;
actioned_text?: boolean;
};
type InboxNote = {
id: number;
status: string;
title: string;
name: string;
content: string;
date_created: string;
date_created_gmt: string;
actions: InboxNoteAction[];
layout: string;
image: string;
is_deleted: boolean;
type: string;
};
type InboxNoteProps = {
note: InboxNote;
lastRead: number;
onDismiss?: ( note: InboxNote, type: 'all' | 'note' ) => void;
onNoteActionClick?: ( note: InboxNote, action: InboxNoteAction ) => void;
onBodyLinkClick?: ( note: InboxNote, link: string ) => void;
onNoteVisible?: ( note: InboxNote ) => void;
};
const DropdownWithPopoverProps = Dropdown as React.ComponentType<
Dropdown.Props & { popoverProps: Omit< Popover.Props, 'children' > }
>;
const InboxNoteCard: React.FC< InboxNoteProps > = ( {
note,
lastRead,
onDismiss,
onNoteActionClick,
onBodyLinkClick,
onNoteVisible,
} ) => {
const [ clickedActionText, setClickedActionText ] = useState( false );
const hasBeenSeen = useRef( false );
const toggleButtonRef = useRef< HTMLButtonElement >( null );
const linkCallbackRef = useCallbackOnLinkClick( ( innerLink ) => {
if ( onBodyLinkClick ) {
onBodyLinkClick( note, innerLink );
}
} );
// Trigger a view Tracks event when the note is seen.
const onVisible = ( isVisible: boolean ) => {
if ( isVisible && ! hasBeenSeen.current ) {
if ( onNoteVisible ) {
onNoteVisible( note );
}
hasBeenSeen.current = true;
}
};
const handleBlur = ( event: React.FocusEvent, onClose: () => void ) => {
const dropdownClasses = [
'woocommerce-admin-dismiss-notification',
'components-popover__content',
];
// This line is for IE compatibility.
let relatedTarget: EventTarget | Element | null = null;
if ( event.relatedTarget ) {
relatedTarget = event.relatedTarget;
} else if ( toggleButtonRef.current ) {
const ownerDoc = toggleButtonRef.current.ownerDocument;
relatedTarget = ownerDoc ? ownerDoc.activeElement : null;
}
let isClickOutsideDropdown = false;
if ( relatedTarget && 'className' in relatedTarget ) {
const classNames = relatedTarget.className;
isClickOutsideDropdown = dropdownClasses.some( ( className ) =>
classNames.includes( className )
);
}
if ( isClickOutsideDropdown ) {
event.preventDefault();
} else {
onClose();
}
};
const onDropdownDismiss = (
type: 'note' | 'all',
onToggle: () => void
) => {
if ( onDismiss ) {
onDismiss( note, type );
}
onToggle();
};
const renderDismissButton = () => {
if ( clickedActionText ) {
return null;
}
return (
<DropdownWithPopoverProps
contentClassName="woocommerce-admin-dismiss-dropdown"
position="bottom right"
renderToggle={ ( { onClose, onToggle } ) => (
<Button
isTertiary
onClick={ onToggle }
ref={ toggleButtonRef }
onBlur={ ( event: React.FocusEvent ) =>
handleBlur( event, onClose )
}
>
{ __( 'Dismiss', 'woocommerce-admin' ) }
</Button>
) }
focusOnMount={ false }
popoverProps={ { noArrow: true } }
renderContent={ ( { onToggle } ) => (
<ul>
<li>
<Button
className="woocommerce-admin-dismiss-notification"
onClick={ () =>
onDropdownDismiss( 'note', onToggle )
}
>
{ __(
'Dismiss this message',
'woocommerce-admin'
) }
</Button>
</li>
<li>
<Button
className="woocommerce-admin-dismiss-notification"
onClick={ () =>
onDropdownDismiss( 'all', onToggle )
}
>
{ __(
'Dismiss all messages',
'woocommerce-admin'
) }
</Button>
</li>
</ul>
) }
/>
);
};
const onActionClicked = ( action: InboxNoteAction ) => {
if ( onNoteActionClick ) {
onNoteActionClick( note, action );
}
if ( ! action.actioned_text ) {
return;
}
setClickedActionText( action.actioned_text );
};
const renderActions = () => {
const { actions: noteActions, id: noteId } = note;
if ( !! clickedActionText ) {
return clickedActionText;
}
if ( ! noteActions ) {
return;
}
return (
<>
{ noteActions.map( ( action ) => (
<InboxNoteActionButton
key={ action.id }
label={ action.label }
href={
action && action.url && action.url.length
? action.url
: undefined
}
onClick={ () => onActionClicked( action ) }
/>
) ) }
</>
);
};
const {
content,
date_created: dateCreated,
date_created_gmt: dateCreatedGmt,
image,
is_deleted: isDeleted,
layout,
status,
title,
} = note;
if ( isDeleted ) {
return null;
}
const unread =
! lastRead ||
! dateCreatedGmt ||
new Date( dateCreatedGmt + 'Z' ).getTime() > lastRead;
const date = dateCreated;
const hasImage = layout !== 'plain' && layout !== '';
const cardClassName = classnames( 'woocommerce-inbox-message', layout, {
'message-is-unread': unread && status === 'unactioned',
} );
return (
<VisibilitySensor onChange={ onVisible }>
<section className={ cardClassName }>
{ hasImage && (
<div className="woocommerce-inbox-message__image">
<img src={ image } alt="" />
</div>
) }
<div className="woocommerce-inbox-message__wrapper">
<div className="woocommerce-inbox-message__content">
{ unread && (
<div className="woocommerce-inbox-message__unread-indicator" />
) }
{ date && (
<span className="woocommerce-inbox-message__date">
{ moment.utc( date ).fromNow() }
</span>
) }
<H className="woocommerce-inbox-message__title">
{ title }
</H>
<Section className="woocommerce-inbox-message__text">
<span
dangerouslySetInnerHTML={ sanitizeHTML(
content
) }
ref={ linkCallbackRef }
/>
</Section>
</div>
<div className="woocommerce-inbox-message__actions">
{ renderActions() }
{ renderDismissButton() }
</div>
</div>
</section>
</VisibilitySensor>
);
};
export { InboxNoteCard, InboxNote, InboxNoteAction };

View File

@ -0,0 +1,3 @@
export * from './inbox-note';
export * from './inbox-dismiss-confirmation-modal';
export * from './placeholder';

View File

@ -0,0 +1,36 @@
type PlaceholderProps = {
className: string;
};
const InboxNotePlaceholder: React.FC< PlaceholderProps > = ( {
className,
} ) => {
return (
<div
className={ `woocommerce-inbox-message is-placeholder ${ className }` }
aria-hidden
>
<div className="woocommerce-inbox-message__wrapper">
<div className="woocommerce-inbox-message__content">
<div className="woocommerce-inbox-message__date">
<div className="sixth-line" />
</div>
<div className="woocommerce-inbox-message__title">
<div className="line" />
<div className="line" />
</div>
<div className="woocommerce-inbox-message__text">
<div className="line" />
<div className="third-line" />
</div>
</div>
<div className="woocommerce-inbox-message__actions">
<div className="fifth-line" />
<div className="fifth-line" />
</div>
</div>
</div>
);
};
export { InboxNotePlaceholder };

View File

@ -174,22 +174,6 @@
}
}
.woocommerce-homepage-notes-wrapper {
padding-top: $gap-large;
}
#activity-panel-inbox {
margin: 0 $gap-large;
}
.woocommerce-layout__inbox-panel-header {
padding: $gap-large;
.woocommerce-homepage-column & {
padding: 0 24px;
}
}
// Tweak to fix dropdown and placeholder in IE 11
@media all and ( -ms-high-contrast: none ), ( -ms-high-contrast: active ) {
.woocommerce-admin-dismiss-dropdown {
@ -202,29 +186,3 @@
}
}
}
.woocommerce-inbox-message-enter {
opacity: 0;
max-height: 0;
transform: translateX(50%);
}
.woocommerce-inbox-message-enter-active {
opacity: 1;
max-height: 100vh;
transform: translateX(0%);
transition: opacity 500ms, transform 500ms, max-height 500ms;
}
.woocommerce-inbox-message-exit {
opacity: 1;
max-height: 100vh;
transform: translateX(0%);
}
.woocommerce-inbox-message-exit-active {
opacity: 0;
max-height: 0;
transform: translateX(50%);
transition: opacity 500ms, transform 500ms, max-height 500ms;
}

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { InboxDismissConfirmationModal } from '../inbox-dismiss-confirmation-modal';
describe( 'InboxDismissConfirmationModal', () => {
it( "should render with default button label - Yes, I'am sure", () => {
const { queryByText } = render(
<InboxDismissConfirmationModal
onClose={ jest.fn() }
onDismiss={ jest.fn() }
/>
);
expect( queryByText( "Yes, I'm sure" ) ).toBeInTheDocument();
} );
it( 'should render passed in button label if provided', () => {
const { queryByText } = render(
<InboxDismissConfirmationModal
onClose={ jest.fn() }
onDismiss={ jest.fn() }
buttonLabel="Custom button"
/>
);
expect( queryByText( 'Custom button' ) ).toBeInTheDocument();
} );
it( 'should call onClose if Cancel is clicked', () => {
const onClose = jest.fn();
const { getByText } = render(
<InboxDismissConfirmationModal
onClose={ onClose }
onDismiss={ jest.fn() }
/>
);
userEvent.click( getByText( 'Cancel' ) );
expect( onClose ).toHaveBeenCalled();
} );
it( 'should call onDismiss if dismiss button is clicked', () => {
const onDismiss = jest.fn();
const { getByText } = render(
<InboxDismissConfirmationModal
onClose={ jest.fn() }
onDismiss={ onDismiss }
/>
);
userEvent.click( getByText( "Yes, I'm sure" ) );
expect( onDismiss ).toHaveBeenCalled();
} );
} );

View File

@ -0,0 +1,258 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { InboxNoteCard } from '../inbox-note';
jest.mock( 'react-visibility-sensor', () =>
jest.fn().mockImplementation( ( { children, onChange } ) => {
return (
<>
<button onClick={ () => onChange( true ) }>
Trigger change
</button>
{ children }
</>
);
} )
);
describe( 'InboxNoteCard', () => {
const note = {
id: 1,
name: 'wc-admin-wc-helper-connection',
type: 'info',
title: 'Connect to WooCommerce.com',
content: 'Connect to get important product notifications and updates.',
status: 'unactioned',
date_created: '2020-05-10T16:57:31',
actions: [
{
id: 1,
name: 'connect',
label: 'Connect',
query: '',
status: 'unactioned',
primary: false,
url: 'http://test.com',
},
{
id: 2,
name: 'learnmore',
label: 'Learn More',
query: '',
status: 'unactioned',
primary: false,
url: 'http://test.com',
},
],
layout: 'plain',
image: '',
date_created_gmt: '2020-05-10T16:57:31',
is_deleted: false,
};
const lastRead = 1589285995243;
it( 'should render the defined action buttons', () => {
const { queryByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
/>
);
expect( queryByText( 'Connect' ) ).toBeInTheDocument();
expect( queryByText( 'Learn More' ) ).toBeInTheDocument();
} );
it( 'should render a dismiss button', () => {
const { queryByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
/>
);
expect( queryByText( 'Dismiss' ) ).toBeInTheDocument();
} );
it( 'should render a notification type banner', () => {
const bannerNote = { ...note, layout: 'banner' };
const { container } = render(
<InboxNoteCard
key={ bannerNote.id }
note={ bannerNote }
lastRead={ lastRead }
/>
);
const listNoteWithBanner = container.querySelector( '.banner' );
expect( listNoteWithBanner ).not.toBeNull();
} );
it( 'should render a notification type thumbnail', () => {
const thumbnailNote = { ...note, layout: 'thumbnail' };
const { container } = render(
<InboxNoteCard
key={ thumbnailNote.id }
note={ thumbnailNote }
lastRead={ lastRead }
/>
);
const listNoteWithThumbnail = container.querySelector( '.thumbnail' );
expect( listNoteWithThumbnail ).not.toBeNull();
} );
it( 'should render a read notification', () => {
const noteWithoutActions = { ...note, actions: [] };
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ noteWithoutActions }
lastRead={ lastRead }
/>
);
const unreadNote = container.querySelector( '.message-is-unread' );
const readNote = container.querySelector(
'.woocommerce-inbox-message'
);
expect( unreadNote ).toBeNull();
expect( readNote ).not.toBeNull();
} );
it( 'should render an unread notification', () => {
const olderLastRead = 1584015595000;
const noteWithoutActions = { ...note, actions: [] };
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ noteWithoutActions }
lastRead={ olderLastRead }
/>
);
const unreadNote = container.querySelector( '.message-is-unread' );
expect( unreadNote ).not.toBeNull();
} );
it( 'should not render any notification', () => {
const deletedNote = { ...note, is_deleted: true };
const { container } = render(
<InboxNoteCard
key={ note.id }
note={ deletedNote }
lastRead={ lastRead }
/>
);
const unreadNote = container.querySelector(
'.woocommerce-inbox-message'
);
expect( unreadNote ).toBeNull();
} );
describe( 'callbacks', () => {
it( 'should call onDismiss with note type when "Dismiss this message" is clicked', () => {
const onDismiss = jest.fn();
const { getByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
onDismiss={ onDismiss }
/>
);
userEvent.click( getByText( 'Dismiss' ) );
userEvent.click( getByText( 'Dismiss this message' ) );
expect( onDismiss ).toHaveBeenCalledWith( note, 'note' );
} );
it( 'should call onDismiss with all type when "Dismiss all messages" is clicked', () => {
const onDismiss = jest.fn();
const { getByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
onDismiss={ onDismiss }
/>
);
userEvent.click( getByText( 'Dismiss' ) );
userEvent.click( getByText( 'Dismiss all messages' ) );
expect( onDismiss ).toHaveBeenCalledWith( note, 'all' );
} );
it( 'should call onNoteActionClick with specific action when action is clicked', () => {
const onNoteActionClick = jest.fn();
const { getByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
onNoteActionClick={ onNoteActionClick }
/>
);
userEvent.click( getByText( 'Learn More' ) );
expect( onNoteActionClick ).toHaveBeenCalledWith(
note,
note.actions[ 1 ]
);
} );
it( 'should call onBodyLinkClick with innerLink if link within content is clicked', () => {
const onBodyLinkClick = jest.fn();
const noteWithInnerLink = {
...note,
content:
note.content +
' <a href="http://somewhere.com">Somewhere</a>',
};
const { getByText } = render(
<InboxNoteCard
key={ noteWithInnerLink.id }
note={ noteWithInnerLink }
lastRead={ lastRead }
onBodyLinkClick={ onBodyLinkClick }
/>
);
userEvent.click( getByText( 'Somewhere' ) );
expect( onBodyLinkClick ).toHaveBeenCalledWith(
noteWithInnerLink,
'http://somewhere.com/'
);
} );
it( 'should call onVisible when visiblity sensor calls it', () => {
const onVisible = jest.fn();
const { getByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
onNoteVisible={ onVisible }
/>
);
expect( onVisible ).not.toHaveBeenCalled();
userEvent.click( getByText( 'Trigger change' ) );
expect( onVisible ).toHaveBeenCalledWith( note );
} );
it( 'should call onVisible when visiblity sensor calls it, but only once', () => {
const onVisible = jest.fn();
const { getByText } = render(
<InboxNoteCard
key={ note.id }
note={ note }
lastRead={ lastRead }
onNoteVisible={ onVisible }
/>
);
userEvent.click( getByText( 'Trigger change' ) );
userEvent.click( getByText( 'Trigger change' ) );
userEvent.click( getByText( 'Trigger change' ) );
expect( onVisible ).toHaveBeenCalledTimes( 1 );
} );
} );
} );

View File

@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { useCallbackOnLinkClick } from '../use-callback-on-link-click';
const TestComp = ( { callback }: { callback: ( link: string ) => void } ) => {
const containerRef = useCallbackOnLinkClick( ( link ) => {
callback( link );
} );
return (
<span ref={ containerRef }>
Some Text
<br />
<span>
Inner paragraph
<a href="http://tosomewhere.com">Link</a>
</span>
<button>Button</button>
</span>
);
};
describe( 'useCallbackOnLinkClick hook', () => {
it( 'should call callback with link when inner anchor element is clicked', () => {
const callback = jest.fn();
const { getByText } = render( <TestComp callback={ callback } /> );
userEvent.click( getByText( 'Link' ) );
expect( callback ).toHaveBeenCalledWith( 'http://tosomewhere.com/' );
} );
it( 'should not call callback if click event target does not have an href', () => {
const callback = jest.fn();
const { getByText } = render( <TestComp callback={ callback } /> );
userEvent.click( getByText( 'Button' ) );
expect( callback ).not.toHaveBeenCalled();
} );
} );

View File

@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { useCallback } from '@wordpress/element';
export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) {
const onNodeClick = useCallback(
( event: MouseEvent ): void => {
const target = event.target as
| EventTarget
| HTMLAnchorElement
| null;
if ( target && 'href' in target ) {
const innerLink = target.href;
if ( innerLink && onClick ) {
onClick( innerLink );
}
}
},
[ onClick ]
);
return useCallback(
( node: HTMLElement ) => {
if ( node ) {
node.addEventListener( 'click', onNodeClick );
}
return () => {
if ( node ) {
node.removeEventListener( 'click', onNodeClick );
}
};
},
[ onNodeClick ]
);
}

View File

@ -37,3 +37,4 @@ export { ExperimentalListItem as ListItem } from './experimental-list/experiment
export { ExperimentalList as List } from './experimental-list/experimental-list';
export { ExperimentalCollapsibleList as CollapsibleList } from './experimental-list/collapsible-list';
export { TaskItem } from './experimental-list/task-item';
export * from './inbox-note';

View File

@ -4,3 +4,4 @@
@import 'experimental-list/style.scss';
@import 'experimental-list/collapsible-list/style.scss';
@import 'experimental-list/task-item.scss';
@import 'inbox-note/style.scss';

View File

@ -109,6 +109,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
- Dev: Offload remote inbox notifications engine run using action-scheduler. #6995
- Dev: Add source param support for notes query. #6979
- Dev: Remove the use of Dashicons and replace with @wordpress/icons or gridicons. #7020
- Dev: Refactor inbox panel components and moved to experimental package. #7006
- Enhancement: Add expand/collapse to extendable task list. #6910
- Enhancement: Add task hierarchy support to extended task list. #6916
- Fix: Rule Processing Transformer to handle dotNotation default value #7009