Task list - add a shortcut back to store setup (https://github.com/woocommerce/woocommerce-admin/pull/4853)
Fixes woocommerce/woocommerce-admin#4592 This adds the functionality to support a store setup tab displayed in ActivityPanel. The tab currently just redirects to the home page, rather than implementing the nice-to-haves of displaying the task list in the panel. The reason for this, is that I found it would require significant refactoring of the task list to support this currently. As part of this I have also refactored rendering of tabs inside into 2 new functional components: <Tabs> and <Tab>. The reason for this is to decompose large untestable components, test them and by virtue of being decomposed they will also be more reusable in future. Ideally would have been refactored to a functional component as well, but it would have been too large a task.
This commit is contained in:
parent
17d79a2d67
commit
0d95facf92
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { isWCAdmin } from '../utils';
|
||||
|
||||
describe( 'isWCAdmin', () => {
|
||||
it( 'correctly identifies WC admin urls', () => {
|
||||
[
|
||||
'https://example.com/wp-admin/admin.php?page=wc-admin',
|
||||
'https://example.com/wp-admin/admin.php?page=wc-admin&foo=bar',
|
||||
'/admin.php?page=wc-admin',
|
||||
'/admin.php?page=wc-admin&foo=bar',
|
||||
].forEach( ( url ) => {
|
||||
expect( isWCAdmin( url ) ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'rejects URLs that are not WC admin urls', () => {
|
||||
[
|
||||
'https://example.com/wp-admin/edit.php?page=wc-admin',
|
||||
'https://example.com/wp-admin/admin.php?page=other',
|
||||
'/edit.php?page=wc-admin',
|
||||
'/admin.php?page=other',
|
||||
].forEach( ( url ) => {
|
||||
expect( isWCAdmin( url ) ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -182,3 +182,13 @@ export function isOnboardingEnabled() {
|
|||
|
||||
return getSetting( 'onboardingEnabled', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL is a WC admin url.
|
||||
*
|
||||
* @param {*} url - the url to test
|
||||
* @return {boolean} true if the url is a wc-admin URL
|
||||
*/
|
||||
export function isWCAdmin( url ) {
|
||||
return /admin.php\?page=wc-admin/.test( url );
|
||||
}
|
||||
|
|
|
@ -2,23 +2,24 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import clickOutside from 'react-click-outside';
|
||||
import { Component, lazy, Suspense } from '@wordpress/element';
|
||||
import { Button, NavigableMenu } from '@wordpress/components';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { partial, uniqueId, find } from 'lodash';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
import { uniqueId, find } from 'lodash';
|
||||
import PagesIcon from 'gridicons/dist/pages';
|
||||
import CrossIcon from 'gridicons/dist/cross-small';
|
||||
import classnames from 'classnames';
|
||||
import { Icon, lifesaver } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/wc-admin-settings';
|
||||
import { getSetting, getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||
import { H, Section, Spinner } from '@woocommerce/components';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { getHistory } from '@woocommerce/navigation';
|
||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -31,7 +32,8 @@ import {
|
|||
getUnapprovedReviews,
|
||||
getUnreadStock,
|
||||
} from './unread-indicators';
|
||||
import { isOnboardingEnabled } from 'dashboard/utils';
|
||||
import { isOnboardingEnabled, isWCAdmin } from 'dashboard/utils';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
const HelpPanel = lazy( () =>
|
||||
import( /* webpackChunkName: "activity-panels-help" */ './panels/help' )
|
||||
|
@ -50,19 +52,14 @@ const ReviewsPanel = lazy( () =>
|
|||
import( /* webpackChunkName: "activity-panels-inbox" */ './panels/reviews' )
|
||||
);
|
||||
|
||||
import { recordEvent } from 'lib/tracks';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
import { Tabs } from './tabs';
|
||||
import { SetupProgress } from './setup-progress';
|
||||
|
||||
const manageStock = getSetting( 'manageStock', 'no' );
|
||||
const reviewsEnabled = getSetting( 'reviewsEnabled', 'no' );
|
||||
|
||||
export class ActivityPanel extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.togglePanel = this.togglePanel.bind( this );
|
||||
this.clearPanel = this.clearPanel.bind( this );
|
||||
this.toggleMobile = this.toggleMobile.bind( this );
|
||||
this.renderTab = this.renderTab.bind( this );
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
isPanelOpen: false,
|
||||
mobileOpen: false,
|
||||
|
@ -71,30 +68,34 @@ export class ActivityPanel extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
togglePanel( tabName ) {
|
||||
const { isPanelOpen, currentTab } = this.state;
|
||||
|
||||
// If a panel is being opened, or if an existing panel is already open and a different one is being opened, record a track.
|
||||
if ( ! isPanelOpen || tabName !== currentTab ) {
|
||||
recordEvent( 'activity_panel_open', { tab: tabName } );
|
||||
}
|
||||
|
||||
togglePanel( { name: tabName }, isTabOpen ) {
|
||||
this.setState( ( state ) => {
|
||||
if ( tabName === state.currentTab || state.currentTab === '' ) {
|
||||
return {
|
||||
isPanelOpen: ! state.isPanelOpen,
|
||||
currentTab: tabName,
|
||||
mobileOpen: ! state.isPanelOpen,
|
||||
};
|
||||
}
|
||||
return { currentTab: tabName, isPanelSwitching: true };
|
||||
const isPanelSwitching =
|
||||
tabName !== state.currentTab &&
|
||||
state.currentTab !== '' &&
|
||||
isTabOpen &&
|
||||
state.isPanelOpen;
|
||||
|
||||
return {
|
||||
isPanelOpen: isTabOpen,
|
||||
mobileOpen: isTabOpen,
|
||||
currentTab: tabName,
|
||||
isPanelSwitching,
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
closePanel() {
|
||||
this.setState( () => ( {
|
||||
isPanelOpen: false,
|
||||
currentTab: '',
|
||||
} ) );
|
||||
}
|
||||
|
||||
clearPanel() {
|
||||
this.setState( ( { isPanelOpen } ) =>
|
||||
isPanelOpen ? { isPanelSwitching: false } : { currentTab: '' }
|
||||
);
|
||||
this.setState( () => ( {
|
||||
isPanelSwitching: false,
|
||||
} ) );
|
||||
}
|
||||
|
||||
// On smaller screen, the panel buttons are hidden behind a toggle.
|
||||
|
@ -108,14 +109,14 @@ export class ActivityPanel extends Component {
|
|||
}
|
||||
|
||||
handleClickOutside( event ) {
|
||||
const { isPanelOpen, currentTab } = this.state;
|
||||
const { isPanelOpen } = this.state;
|
||||
const isClickOnModalOrSnackbar =
|
||||
event.target.closest(
|
||||
'.woocommerce-inbox-dismiss-confirmation_modal'
|
||||
) || event.target.closest( '.components-snackbar__action' );
|
||||
|
||||
if ( isPanelOpen && ! isClickOnModalOrSnackbar ) {
|
||||
this.togglePanel( currentTab );
|
||||
this.closePanel();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,8 +129,10 @@ export class ActivityPanel extends Component {
|
|||
hasUnreadStock,
|
||||
isEmbedded,
|
||||
requestingTaskListOptions,
|
||||
taskListEnabledResolving,
|
||||
taskListComplete,
|
||||
taskListHidden,
|
||||
taskListEnabled,
|
||||
query,
|
||||
} = this.props;
|
||||
|
||||
|
@ -145,6 +148,32 @@ export class ActivityPanel extends Component {
|
|||
( requestingTaskListOptions === true ||
|
||||
( taskListHidden === false && taskListComplete === false ) );
|
||||
|
||||
// To prevent a flicker between 2 different tab groups, while this option resolves just display no tabs.
|
||||
if ( taskListEnabledResolving ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! taskListComplete && taskListEnabled && showInbox ) {
|
||||
return [
|
||||
{
|
||||
name: 'inbox',
|
||||
title: __( 'Inbox', 'woocommerce-admin' ),
|
||||
icon: <i className="material-icons-outlined">inbox</i>,
|
||||
unread: hasUnreadNotes,
|
||||
},
|
||||
{
|
||||
name: 'setup',
|
||||
title: __( 'Store Setup', 'woocommerce-admin' ),
|
||||
icon: <SetupProgress />,
|
||||
},
|
||||
isPerformingSetupTask && {
|
||||
name: 'help',
|
||||
title: __( 'Help', 'woocommerce-admin' ),
|
||||
icon: <i className="material-icons-outlined">support</i>,
|
||||
},
|
||||
].filter( Boolean );
|
||||
}
|
||||
|
||||
return [
|
||||
! isPerformingSetupTask && showInbox
|
||||
? {
|
||||
|
@ -191,24 +220,23 @@ export class ActivityPanel extends Component {
|
|||
}
|
||||
|
||||
getPanelContent( tab ) {
|
||||
const { hasUnreadOrders, query, hasUnapprovedReviews } = this.props;
|
||||
const { task } = query;
|
||||
|
||||
switch ( tab ) {
|
||||
case 'inbox':
|
||||
return <InboxPanel />;
|
||||
case 'orders':
|
||||
const { hasUnreadOrders } = this.props;
|
||||
return <OrdersPanel hasActionableOrders={ hasUnreadOrders } />;
|
||||
case 'stock':
|
||||
return <StockPanel />;
|
||||
case 'reviews':
|
||||
const { hasUnapprovedReviews } = this.props;
|
||||
return (
|
||||
<ReviewsPanel
|
||||
hasUnapprovedReviews={ hasUnapprovedReviews }
|
||||
/>
|
||||
);
|
||||
case 'help':
|
||||
const { query } = this.props;
|
||||
const { task } = query;
|
||||
return <HelpPanel taskName={ task } />;
|
||||
default:
|
||||
return null;
|
||||
|
@ -216,15 +244,42 @@ export class ActivityPanel extends Component {
|
|||
}
|
||||
|
||||
renderPanel() {
|
||||
const { updateOptions, taskListHidden } = this.props;
|
||||
const { isPanelOpen, currentTab, isPanelSwitching } = this.state;
|
||||
|
||||
const tab = find( this.getTabs(), { name: currentTab } );
|
||||
|
||||
if ( ! tab ) {
|
||||
return (
|
||||
<div className="woocommerce-layout__activity-panel-wrapper" />
|
||||
);
|
||||
}
|
||||
|
||||
const clearPanel = () => {
|
||||
this.clearPanel();
|
||||
};
|
||||
|
||||
if ( currentTab === 'setup' ) {
|
||||
const currentLocation = window.location.href;
|
||||
const homescreenLocation = getAdminLink(
|
||||
'admin.php?page=wc-admin'
|
||||
);
|
||||
|
||||
// Don't navigate if we're already on the homescreen, this will cause an infinite loop
|
||||
if ( currentLocation !== homescreenLocation ) {
|
||||
// Ensure that if the user is trying to get to the task list they can see it even if
|
||||
// it was dismissed.
|
||||
if ( taskListHidden === 'no' ) {
|
||||
this.redirectToHomeScreen();
|
||||
} else {
|
||||
updateOptions( {
|
||||
woocommerce_task_list_hidden: 'no',
|
||||
} ).then( this.redirectToHomeScreen );
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const classNames = classnames(
|
||||
'woocommerce-layout__activity-panel-wrapper',
|
||||
{
|
||||
|
@ -239,8 +294,8 @@ export class ActivityPanel extends Component {
|
|||
tabIndex={ 0 }
|
||||
role="tabpanel"
|
||||
aria-label={ tab.title }
|
||||
onTransitionEnd={ this.clearPanel }
|
||||
onAnimationEnd={ this.clearPanel }
|
||||
onTransitionEnd={ clearPanel }
|
||||
onAnimationEnd={ clearPanel }
|
||||
>
|
||||
<div
|
||||
className="woocommerce-layout__activity-panel-content"
|
||||
|
@ -255,49 +310,17 @@ export class ActivityPanel extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderTab( tab, i ) {
|
||||
const { currentTab, isPanelOpen } = this.state;
|
||||
const className = classnames(
|
||||
'woocommerce-layout__activity-panel-tab',
|
||||
{
|
||||
'is-active': isPanelOpen && tab.name === currentTab,
|
||||
'has-unread': tab.unread,
|
||||
}
|
||||
);
|
||||
|
||||
const selected = tab.name === currentTab;
|
||||
let tabIndex = -1;
|
||||
|
||||
// Only make this item tabbable if it is the currently selected item, or the panel is closed and the item is the first item.
|
||||
if ( selected || ( ! isPanelOpen && i === 0 ) ) {
|
||||
tabIndex = null;
|
||||
redirectToHomeScreen() {
|
||||
if ( isWCAdmin( window.location.href ) ) {
|
||||
getHistory().push( getNewPath( {}, '/', {} ) );
|
||||
} else {
|
||||
window.location.href = getAdminLink( 'admin.php?page=wc-admin' );
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
role="tab"
|
||||
className={ className }
|
||||
tabIndex={ tabIndex }
|
||||
aria-selected={ selected }
|
||||
aria-controls={ 'activity-panel-' + tab.name }
|
||||
key={ 'activity-panel-tab-' + tab.name }
|
||||
id={ 'activity-panel-tab-' + tab.name }
|
||||
onClick={ partial( this.togglePanel, tab.name ) }
|
||||
>
|
||||
{ tab.icon }
|
||||
{ tab.title }{ ' ' }
|
||||
{ tab.unread && (
|
||||
<span className="screen-reader-text">
|
||||
{ __( 'unread activity', 'woocommerce-admin' ) }
|
||||
</span>
|
||||
) }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const tabs = this.getTabs();
|
||||
const { mobileOpen } = this.state;
|
||||
const { mobileOpen, currentTab, isPanelOpen } = this.state;
|
||||
const headerId = uniqueId( 'activity-panel-header_' );
|
||||
const panelClasses = classnames( 'woocommerce-layout__activity-panel', {
|
||||
'is-mobile-open': this.state.mobileOpen,
|
||||
|
@ -322,7 +345,9 @@ export class ActivityPanel extends Component {
|
|||
aria-labelledby={ headerId }
|
||||
>
|
||||
<Button
|
||||
onClick={ this.toggleMobile }
|
||||
onClick={ () => {
|
||||
this.toggleMobile();
|
||||
} }
|
||||
label={
|
||||
mobileOpen
|
||||
? __(
|
||||
|
@ -343,13 +368,14 @@ export class ActivityPanel extends Component {
|
|||
) }
|
||||
</Button>
|
||||
<div className={ panelClasses }>
|
||||
<NavigableMenu
|
||||
role="tablist"
|
||||
orientation="horizontal"
|
||||
className="woocommerce-layout__activity-panel-tabs"
|
||||
>
|
||||
{ tabs && tabs.map( this.renderTab ) }
|
||||
</NavigableMenu>
|
||||
<Tabs
|
||||
tabs={ tabs }
|
||||
tabOpen={ isPanelOpen }
|
||||
selectedTab={ currentTab }
|
||||
onTabClick={ ( tab, tabOpen ) => {
|
||||
this.togglePanel( tab, tabOpen );
|
||||
} }
|
||||
/>
|
||||
{ this.renderPanel() }
|
||||
</div>
|
||||
</Section>
|
||||
|
@ -370,6 +396,13 @@ export default compose(
|
|||
const hasUnapprovedReviews = getUnapprovedReviews( select );
|
||||
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
|
||||
|
||||
const taskListEnabledResolving = isResolving( 'getOption', [
|
||||
'woocommerce_homescreen_enabled',
|
||||
] );
|
||||
|
||||
// This indicates the task list is in progress, but not if it has been hidden or not
|
||||
const taskListEnabled = isOnboardingEnabled();
|
||||
|
||||
let requestingTaskListOptions, taskListComplete, taskListHidden;
|
||||
|
||||
if ( isOnboardingEnabled() ) {
|
||||
|
@ -390,9 +423,14 @@ export default compose(
|
|||
hasUnreadStock,
|
||||
hasUnapprovedReviews,
|
||||
requestingTaskListOptions,
|
||||
taskListEnabledResolving,
|
||||
taskListComplete,
|
||||
taskListHidden,
|
||||
taskListEnabled,
|
||||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => ( {
|
||||
updateOptions: dispatch( OPTIONS_STORE_NAME ).updateOptions,
|
||||
} ) ),
|
||||
clickOutside
|
||||
)( ActivityPanel );
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export const SetupProgress = () => (
|
||||
<svg
|
||||
className="setup-progress"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z"
|
||||
stroke="#DCDCDE"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M4 12V12C4 16.4183 7.58172 20 12 20V20C16.4183 20 20 16.4183 20 12V12C20 7.58172 16.4183 4 12 4V4"
|
||||
// stroke="#1E1E1E"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -44,6 +44,15 @@
|
|||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
// custom progress icon, requires specific coloring
|
||||
&.setup-progress {
|
||||
fill: none;
|
||||
|
||||
path:last-child {
|
||||
stroke: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint( '>960px' ) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
@ -77,6 +86,7 @@
|
|||
width: 100%;
|
||||
height: $header-height;
|
||||
color: $gray-700;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
background-color: var(--wp-admin-theme-color);
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export const Tab = ( {
|
||||
icon,
|
||||
title,
|
||||
name,
|
||||
unread,
|
||||
selected,
|
||||
isPanelOpen,
|
||||
onTabClick,
|
||||
index,
|
||||
} ) => {
|
||||
const className = classnames( 'woocommerce-layout__activity-panel-tab', {
|
||||
'is-active': isPanelOpen && selected,
|
||||
'has-unread': unread,
|
||||
} );
|
||||
|
||||
let tabIndex = -1;
|
||||
|
||||
// Only make this item tabbable if it is the currently selected item, or the panel is closed and the item is the first item.
|
||||
if ( selected || ( ! isPanelOpen && index === 0 ) ) {
|
||||
tabIndex = null;
|
||||
}
|
||||
|
||||
const tabKey = `activity-panel-tab-${ name }`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
role="tab"
|
||||
className={ className }
|
||||
tabIndex={ tabIndex }
|
||||
aria-selected={ selected }
|
||||
aria-controls={ `activity-panel-${ name }` }
|
||||
key={ tabKey }
|
||||
id={ tabKey }
|
||||
onClick={ () => {
|
||||
onTabClick( name );
|
||||
} }
|
||||
>
|
||||
{ icon }
|
||||
{ title }{ ' ' }
|
||||
{ unread && (
|
||||
<span className="screen-reader-text">
|
||||
{ __( 'unread activity', 'woocommerce-admin' ) }
|
||||
</span>
|
||||
) }
|
||||
</Button>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,247 @@
|
|||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import PagesIcon from 'gridicons/dist/pages';
|
||||
|
||||
import { Tab } from '../';
|
||||
|
||||
const renderTab = () =>
|
||||
render(
|
||||
<Tab
|
||||
icon={ null }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected
|
||||
isPanelOpen
|
||||
index={ 0 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
describe( 'ActivityPanel Tab', () => {
|
||||
it( 'displays a title and unread status based on props', () => {
|
||||
const { getByText } = render(
|
||||
<Tab
|
||||
icon={ null }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread
|
||||
selected
|
||||
isPanelOpen
|
||||
index={ 0 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getByText( 'Hello World' ) ).not.toBeNull();
|
||||
expect( getByText( 'unread activity' ) ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'renders the node passed to icon', () => {
|
||||
const { getByText } = render(
|
||||
<Tab
|
||||
icon={ <div>Fake icon</div> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread
|
||||
selected
|
||||
isPanelOpen
|
||||
index={ 0 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getByText( 'Fake icon' ) ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'does not display unread status if unread is false', () => {
|
||||
const { queryByText } = render(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected
|
||||
isPanelOpen
|
||||
index={ 0 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( queryByText( 'unread activity' ) ).toBeNull();
|
||||
} );
|
||||
|
||||
it( 'is tabbable if its selected or if its closed and the first item', () => {
|
||||
const { getByRole, rerender } = render(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected
|
||||
isPanelOpen={ false }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
let tab = getByRole( 'tab' );
|
||||
|
||||
// Tab index is set to null if its the currently selected item, or the panel is closed and the item is the first item.
|
||||
expect( tab ).not.toHaveAttribute( 'tabindex' );
|
||||
|
||||
rerender(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ false }
|
||||
index={ 0 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
tab = getByRole( 'tab' );
|
||||
|
||||
expect( tab ).not.toHaveAttribute( 'tabindex' );
|
||||
} );
|
||||
|
||||
it( 'is not tabbable if its not selected, or its open', () => {
|
||||
const { getByRole, rerender } = render(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ false }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
let tab = getByRole( 'tab' );
|
||||
|
||||
// Tab index is not set if its the currently selected item, or the panel is closed and the item is the first item.
|
||||
expect( tab ).toHaveAttribute( 'tabindex', '-1' );
|
||||
|
||||
rerender(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ true }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
tab = getByRole( 'tab' );
|
||||
|
||||
expect( tab ).toHaveAttribute( 'tabindex', '-1' );
|
||||
} );
|
||||
|
||||
it( 'calls the onTabClick handler if a tab is clicked', () => {
|
||||
const onTabClickSpy = jest.fn();
|
||||
|
||||
const { getByRole } = render(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ true }
|
||||
index={ 1 }
|
||||
onTabClick={ onTabClickSpy }
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click( getByRole( 'tab' ) );
|
||||
|
||||
expect( onTabClickSpy ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'derives aria-controls and id from the name prop', () => {
|
||||
const nameProp = 'some-name';
|
||||
const { getByRole } = render(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ nameProp }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ true }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
const tab = getByRole( 'tab' );
|
||||
|
||||
expect( tab ).toHaveAttribute(
|
||||
'aria-controls',
|
||||
`activity-panel-${ nameProp }`
|
||||
);
|
||||
|
||||
expect( tab ).toHaveAttribute(
|
||||
'id',
|
||||
`activity-panel-tab-${ nameProp }`
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'has an is-active class if isPanelOpen is true', () => {
|
||||
const { getByRole, rerender } = renderTab();
|
||||
expect( getByRole( 'tab' ) ).toHaveClass( 'is-active' );
|
||||
|
||||
rerender(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ false }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getByRole( 'tab' ) ).not.toHaveClass( 'is-active' );
|
||||
} );
|
||||
|
||||
it( 'has an has-unread class if unread is true', () => {
|
||||
const { getByRole, rerender } = render(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ true }
|
||||
selected={ false }
|
||||
isPanelOpen={ false }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getByRole( 'tab' ) ).toHaveClass( 'has-unread' );
|
||||
|
||||
rerender(
|
||||
<Tab
|
||||
icon={ <PagesIcon /> }
|
||||
title={ 'Hello World' }
|
||||
name={ 'overview' }
|
||||
unread={ false }
|
||||
selected={ false }
|
||||
isPanelOpen={ false }
|
||||
index={ 1 }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getByRole( 'tab' ) ).not.toHaveClass( 'has-unread' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,63 @@
|
|||
import { NavigableMenu } from '@wordpress/components';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
|
||||
import { Tab } from '../tab';
|
||||
import { recordEvent } from 'lib/tracks';
|
||||
|
||||
export const Tabs = ( {
|
||||
tabs,
|
||||
onTabClick,
|
||||
selectedTab: selectedTabName,
|
||||
tabOpen = false,
|
||||
} ) => {
|
||||
const [ { tabOpen: tabIsOpenState, currentTab }, setTabState ] = useState( {
|
||||
tabOpen,
|
||||
currentTab: selectedTabName,
|
||||
} );
|
||||
|
||||
// Keep state synced with props
|
||||
useEffect( () => {
|
||||
setTabState( {
|
||||
tabOpen,
|
||||
currentTab: selectedTabName,
|
||||
} );
|
||||
}, [ tabOpen, selectedTabName ] );
|
||||
|
||||
return (
|
||||
<NavigableMenu
|
||||
role="tablist"
|
||||
orientation="horizontal"
|
||||
className="woocommerce-layout__activity-panel-tabs"
|
||||
>
|
||||
{ tabs &&
|
||||
tabs.map( ( tab, i ) => (
|
||||
<Tab
|
||||
key={ i }
|
||||
index={ i }
|
||||
isPanelOpen={ tabIsOpenState }
|
||||
selected={ currentTab === tab.name }
|
||||
{ ...tab }
|
||||
onTabClick={ () => {
|
||||
const isTabOpen =
|
||||
currentTab === tab.name || currentTab === ''
|
||||
? ! tabIsOpenState
|
||||
: true;
|
||||
|
||||
// If a panel is being opened, or if an existing panel is already open and a different one is being opened, record a track.
|
||||
if ( ! isTabOpen || currentTab !== tab.name ) {
|
||||
recordEvent( 'activity_panel_open', {
|
||||
tab: tab.name,
|
||||
} );
|
||||
}
|
||||
|
||||
setTabState( {
|
||||
tabOpen: isTabOpen,
|
||||
currentTab: tab.name,
|
||||
} );
|
||||
onTabClick( tab, isTabOpen );
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
</NavigableMenu>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
jest.mock( 'lib/tracks', () => ( { recordEvent: jest.fn() } ) );
|
||||
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { Tabs } from '../';
|
||||
import { recordEvent } from 'lib/tracks';
|
||||
|
||||
const generateTabs = () => {
|
||||
return [ '0', '1', '2', '3' ].map( ( name ) => ( {
|
||||
name,
|
||||
title: `Tab ${ name }`,
|
||||
icon: <span>icon</span>,
|
||||
unread: false,
|
||||
} ) );
|
||||
};
|
||||
|
||||
describe( 'Activity Panel Tabs', () => {
|
||||
it( 'correctly tracks the selected tab', () => {
|
||||
const { getAllByRole } = render(
|
||||
<Tabs
|
||||
selectedTab={ '3' }
|
||||
tabs={ generateTabs() }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = getAllByRole( 'tab' );
|
||||
|
||||
fireEvent.click( tabs[ 2 ] );
|
||||
|
||||
expect( tabs[ 2 ] ).toHaveClass( 'is-active' );
|
||||
|
||||
fireEvent.click( tabs[ 3 ] );
|
||||
|
||||
expect( tabs[ 2 ] ).not.toHaveClass( 'is-active' );
|
||||
expect( tabs[ 3 ] ).toHaveClass( 'is-active' );
|
||||
} );
|
||||
|
||||
it( 'closes a tab if its the same one last opened', () => {
|
||||
const { getAllByRole } = render(
|
||||
<Tabs
|
||||
selectedTab={ '3' }
|
||||
tabs={ generateTabs() }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = getAllByRole( 'tab' );
|
||||
|
||||
fireEvent.click( tabs[ 2 ] );
|
||||
expect( tabs[ 2 ] ).toHaveClass( 'is-active' );
|
||||
fireEvent.click( tabs[ 2 ] );
|
||||
expect( tabs[ 2 ] ).not.toHaveClass( 'is-active' );
|
||||
} );
|
||||
|
||||
it( 'triggers onTabClick with the selected when a tab is clicked', () => {
|
||||
const tabClickSpy = jest.fn();
|
||||
const generatedTabs = generateTabs();
|
||||
|
||||
const { getAllByRole } = render(
|
||||
<Tabs
|
||||
selectedTab={ '3' }
|
||||
tabs={ generatedTabs }
|
||||
onTabClick={ tabClickSpy }
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = getAllByRole( 'tab' );
|
||||
|
||||
fireEvent.click( tabs[ 3 ] );
|
||||
|
||||
expect( tabClickSpy ).toHaveBeenCalledWith( generatedTabs[ 3 ], true );
|
||||
} );
|
||||
|
||||
it( 'records an event when panels are being opened and when the open panel changes', () => {
|
||||
const generatedTabs = generateTabs();
|
||||
|
||||
const { getAllByRole } = render(
|
||||
<Tabs
|
||||
selectedTab={ '3' }
|
||||
tabs={ generatedTabs }
|
||||
onTabClick={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = getAllByRole( 'tab' );
|
||||
|
||||
fireEvent.click( tabs[ 3 ] );
|
||||
|
||||
expect( recordEvent ).toHaveBeenCalledWith( 'activity_panel_open', {
|
||||
tab: generatedTabs[ 3 ].name,
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -22,8 +22,8 @@ import ActivityPanel from './activity-panel';
|
|||
import { recordEvent } from 'lib/tracks';
|
||||
|
||||
class Header extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
isScrolled: false,
|
||||
};
|
||||
|
|
|
@ -44,6 +44,7 @@ const ProfileWizard = lazy( () =>
|
|||
import( /* webpackChunkName: "profile-wizard" */ 'profile-wizard' )
|
||||
);
|
||||
import getReports from 'analytics/report/get-reports';
|
||||
import { isWCAdmin } from 'dashboard/utils';
|
||||
|
||||
const TIME_EXCLUDED_SCREENS_FILTER = 'woocommerce_admin_time_excluded_screens';
|
||||
|
||||
|
@ -252,9 +253,7 @@ export class Controller extends Component {
|
|||
* @param {Array} excludedScreens - wc-admin screens to avoid updating.
|
||||
*/
|
||||
export function updateLinkHref( item, nextQuery, excludedScreens ) {
|
||||
const isWCAdmin = /admin.php\?page=wc-admin/.test( item.href );
|
||||
|
||||
if ( isWCAdmin ) {
|
||||
if ( isWCAdmin( item.href ) ) {
|
||||
const search = last( item.href.split( '?' ) );
|
||||
const query = parse( search );
|
||||
const defaultPath = window.wcAdminFeatures.homescreen
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"baseUrl": "./client",
|
||||
"paths": {
|
||||
"@woocommerce/*": [ "../packages/*/src" ]
|
||||
},
|
||||
"exclude": [ "node_modules", "build" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"exclude": [ "node_modules", "build" ]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue