Add support for List item tags and link types (https://github.com/woocommerce/woocommerce-admin/pull/4287)

* Add Storybook console addon.
* Add Link stories for all link types.
* Add unit tests for Link component.
* Add unit tests for List component.
* Add support for List item `listItemTag`.
This commit is contained in:
Matt Sherman 2020-05-07 11:52:49 -04:00 committed by GitHub
parent 015fc89dbd
commit 61a643655a
7 changed files with 378 additions and 8 deletions

View File

@ -3646,6 +3646,15 @@
} }
} }
}, },
"@storybook/addon-console": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-console/-/addon-console-1.2.1.tgz",
"integrity": "sha512-2iDbDTipWonvRpIqLLntfhCGvawFFvoG1xyErpyL7K/HRdQ1zzIvR1Qm83S7TK8Vg+RzZWm4wcDbxx7WOsFCNg==",
"dev": true,
"requires": {
"global": "^4.3.2"
}
},
"@storybook/addon-docs": { "@storybook/addon-docs": {
"version": "5.3.18", "version": "5.3.18",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-5.3.18.tgz", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-5.3.18.tgz",
@ -5313,6 +5322,12 @@
"@types/testing-library__react": "^10.0.0" "@types/testing-library__react": "^10.0.0"
} }
}, },
"@testing-library/user-event": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-10.1.0.tgz",
"integrity": "sha512-qutUm/2lWAD8IiKrss2Cg6Hf8AkcMeylKm09bSMtYC39Ug68aXWkcbc0H/NVD5R1zOHguTjkR/Ppuns6bWksGQ==",
"dev": true
},
"@types/anymatch": { "@types/anymatch": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
@ -22203,7 +22218,7 @@
"dev": true "dev": true
}, },
"prettier": { "prettier": {
"version": "npm:wp-prettier@1.19.1", "version": "npm:prettier@1.19.1",
"resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz", "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz",
"integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==", "integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==",
"dev": true "dev": true

View File

@ -145,6 +145,7 @@
"@octokit/graphql": "4.3.1", "@octokit/graphql": "4.3.1",
"@storybook/addon-a11y": "5.3.18", "@storybook/addon-a11y": "5.3.18",
"@storybook/addon-actions": "5.3.18", "@storybook/addon-actions": "5.3.18",
"@storybook/addon-console": "1.2.1",
"@storybook/addon-docs": "5.3.18", "@storybook/addon-docs": "5.3.18",
"@storybook/addon-knobs": "5.3.18", "@storybook/addon-knobs": "5.3.18",
"@storybook/addon-links": "5.3.18", "@storybook/addon-links": "5.3.18",
@ -153,6 +154,7 @@
"@storybook/addons": "5.3.18", "@storybook/addons": "5.3.18",
"@storybook/react": "5.3.18", "@storybook/react": "5.3.18",
"@testing-library/react": "^10.0.3", "@testing-library/react": "^10.0.3",
"@testing-library/user-event": "^10.1.0",
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
"@wordpress/babel-plugin-makepot": "2.1.3", "@wordpress/babel-plugin-makepot": "2.1.3",
"@wordpress/babel-preset-default": "3.0.2", "@wordpress/babel-preset-default": "3.0.2",

View File

@ -1,17 +1,62 @@
/* /**
* External dependencies
*/
import { withConsole } from '@storybook/addon-console';
/**
* Internal dependencies * Internal dependencies
*/ */
import Link from '../'; import Link from '../';
function logLinkClick( event ) {
const a = event.currentTarget;
const logMessage = `[${ a.textContent }](${ a.href }) ${ a.dataset.linkType } link clicked`;
// eslint-disable-next-line no-console
console.log( logMessage );
event.preventDefault();
return false;
}
export default { export default {
title: 'WooCommerce Admin/components/Link', title: 'WooCommerce Admin/components/Link',
component: Link, component: Link,
decorators: [ ( storyFn, context ) => withConsole()( storyFn )( context ) ],
}; };
export const External = () => { export const External = () => {
return ( return (
<Link href="https://woocommerce.com" type="external"> <Link
href="https://woocommerce.com"
type="external"
onClick={ logLinkClick }
>
WooCommerce.com WooCommerce.com
</Link> </Link>
); );
}; };
export const WCAdmin = () => {
return (
<Link
href="admin.php?page=wc-admin&path=%2Fanalytics%2Forders"
type="wc-admin"
onClick={ logLinkClick }
>
Analytics: Orders
</Link>
);
};
export const WPAdmin = () => {
return (
<Link
href="post-new.php?post_type=product"
type="wp-admin"
onClick={ logLinkClick }
>
New Product
</Link>
);
};

View File

@ -0,0 +1,124 @@
/**
* External dependencies
*/
import { fireEvent, render } from '@testing-library/react';
/**
* Internal dependencies
*/
import Link from '../index';
describe( 'Link', () => {
it( 'should render `external` links', () => {
const { container } = render(
<Link href="https://woocommerce.com" type="external">
WooCommerce.com
</Link>
);
expect( container.firstChild ).toMatchInlineSnapshot( `
<a
data-link-type="external"
href="https://woocommerce.com"
>
WooCommerce.com
</a>
` );
} );
it( 'should render `wp-admin` links', () => {
const { container } = render(
<Link href="post-new.php?post_type=product" type="wp-admin">
New Post
</Link>
);
expect( container.firstChild ).toMatchInlineSnapshot( `
<a
data-link-type="wp-admin"
href="post-new.php?post_type=product"
>
New Post
</a>
` );
} );
it( 'should render `wc-admin` links', () => {
const { container } = render(
<Link
href="admin.php?page=wc-admin&path=%2Fanalytics%2Forders"
type="wc-admin"
>
Analytics: Orders
</Link>
);
expect( container.firstChild ).toMatchInlineSnapshot( `
<a
data-link-type="wc-admin"
href="admin.php?page=wc-admin&path=%2Fanalytics%2Forders"
>
Analytics: Orders
</a>
` );
} );
it( 'should render links without a type as `wc-admin`', () => {
const { container } = render(
<Link href="admin.php?page=wc-admin&path=%2Fanalytics%2Forders">
Analytics: Orders
</Link>
);
expect( container.firstChild ).toMatchInlineSnapshot( `
<a
data-link-type="wc-admin"
href="admin.php?page=wc-admin&path=%2Fanalytics%2Forders"
>
Analytics: Orders
</a>
` );
} );
it( 'should allow custom props to be passed through', () => {
const { container } = render(
<Link
href="https://woocommerce.com"
type="external"
className="foo"
target="bar"
>
WooCommerce.com
</Link>
);
expect( container.firstChild ).toMatchInlineSnapshot( `
<a
class="foo"
data-link-type="external"
href="https://woocommerce.com"
target="bar"
>
WooCommerce.com
</a>
` );
} );
it( 'should support `onClick`', () => {
const clickHandler = jest.fn();
const { container } = render(
<Link
href="https://woocommerce.com"
type="external"
onClick={ clickHandler }
>
WooCommerce.com
</Link>
);
fireEvent.click( container.firstChild );
expect( clickHandler ).toHaveBeenCalled();
} );
} );

View File

@ -21,6 +21,16 @@ class List extends Component {
} }
} }
getItemLinkType( item ) {
const { href, linkType } = item;
if ( linkType ) {
return linkType;
}
return href ? 'external' : null;
}
render() { render() {
const { className, items } = this.props; const { className, items } = this.props;
const listClassName = classnames( 'woocommerce-list', className ); const listClassName = classnames( 'woocommerce-list', className );
@ -34,6 +44,7 @@ class List extends Component {
className: itemClasses, className: itemClasses,
content, content,
href, href,
listItemTag,
onClick, onClick,
target, target,
title, title,
@ -57,8 +68,9 @@ class List extends Component {
onKeyDown: ( e ) => onKeyDown: ( e ) =>
hasAction ? this.handleKeyDown( e, onClick ) : null, hasAction ? this.handleKeyDown( e, onClick ) : null,
target: href ? target : null, target: href ? target : null,
type: href ? 'external' : null, type: this.getItemLinkType( item ),
href, href,
'data-list-item-tag': listItemTag,
}; };
return ( return (

View File

@ -2,6 +2,7 @@
* External dependencies * External dependencies
*/ */
import Gridicon from 'gridicons'; import Gridicon from 'gridicons';
import { withConsole } from '@storybook/addon-console';
/** /**
* Internal dependencies * Internal dependencies
@ -9,9 +10,27 @@ import Gridicon from 'gridicons';
import List from '../'; import List from '../';
import './style.scss'; import './style.scss';
function logItemClick( event ) {
const a = event.currentTarget;
const itemDescription = a.href
? `[${ a.textContent }](${ a.href }) ${ a.dataset.linkType }`
: `[${ a.textContent }]`;
const itemTag = a.dataset.listItemTag
? `'${ a.dataset.listItemTag }'`
: 'not set';
const logMessage = `[${ itemDescription } item clicked (tag: ${ itemTag })`;
// eslint-disable-next-line no-console
console.log( logMessage );
event.preventDefault();
return false;
}
export default { export default {
title: 'WooCommerce Admin/components/List', title: 'WooCommerce Admin/components/List',
component: List, component: List,
decorators: [ ( storyFn, context ) => withConsole()( storyFn )( context ) ],
}; };
export const Default = () => { export const Default = () => {
@ -19,10 +38,12 @@ export const Default = () => {
{ {
title: 'WooCommerce.com', title: 'WooCommerce.com',
href: 'https://woocommerce.com', href: 'https://woocommerce.com',
onClick: logItemClick,
}, },
{ {
title: 'WordPress.org', title: 'WordPress.org',
href: 'https://wordpress.org', href: 'https://wordpress.org',
onClick: logItemClick,
}, },
{ {
title: 'A list item with no action', title: 'A list item with no action',
@ -30,9 +51,10 @@ export const Default = () => {
{ {
title: 'Click me!', title: 'Click me!',
content: 'An alert will be triggered.', content: 'An alert will be triggered.',
onClick: () => { onClick: ( event ) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
window.alert( 'List item clicked' ); window.alert( 'List item clicked' );
return logItemClick( event );
}, },
}, },
]; ];
@ -47,12 +69,14 @@ export const BeforeAndAfter = () => {
after: <Gridicon icon="chevron-right" />, after: <Gridicon icon="chevron-right" />,
title: 'WooCommerce.com', title: 'WooCommerce.com',
href: 'https://woocommerce.com', href: 'https://woocommerce.com',
onClick: logItemClick,
}, },
{ {
before: <Gridicon icon="my-sites" />, before: <Gridicon icon="my-sites" />,
after: <Gridicon icon="chevron-right" />, after: <Gridicon icon="chevron-right" />,
title: 'WordPress.org', title: 'WordPress.org',
href: 'https://wordpress.org', href: 'https://wordpress.org',
onClick: logItemClick,
}, },
{ {
before: <Gridicon icon="link-break" />, before: <Gridicon icon="link-break" />,
@ -63,9 +87,10 @@ export const BeforeAndAfter = () => {
before: <Gridicon icon="notice" />, before: <Gridicon icon="notice" />,
title: 'Click me!', title: 'Click me!',
content: 'An alert will be triggered.', content: 'An alert will be triggered.',
onClick: () => { onClick: ( event ) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
window.alert( 'List item clicked' ); window.alert( 'List item clicked' );
return logItemClick( event );
}, },
}, },
]; ];
@ -73,19 +98,23 @@ export const BeforeAndAfter = () => {
return <List items={ listItems } />; return <List items={ listItems } />;
}; };
export const CustomStyle = () => { export const CustomStyleAndTags = () => {
const listItems = [ const listItems = [
{ {
before: <Gridicon icon="cart" />, before: <Gridicon icon="cart" />,
after: <Gridicon icon="chevron-right" />, after: <Gridicon icon="chevron-right" />,
title: 'WooCommerce.com', title: 'WooCommerce.com',
href: 'https://woocommerce.com', href: 'https://woocommerce.com',
onClick: logItemClick,
listItemTag: 'woocommerce.com-link',
}, },
{ {
before: <Gridicon icon="my-sites" />, before: <Gridicon icon="my-sites" />,
after: <Gridicon icon="chevron-right" />, after: <Gridicon icon="chevron-right" />,
title: 'WordPress.org', title: 'WordPress.org',
href: 'https://wordpress.org', href: 'https://wordpress.org',
onClick: logItemClick,
listItemTag: 'wordpress.org-link',
}, },
{ {
before: <Gridicon icon="link-break" />, before: <Gridicon icon="link-break" />,
@ -95,10 +124,12 @@ export const CustomStyle = () => {
before: <Gridicon icon="notice" />, before: <Gridicon icon="notice" />,
title: 'Click me!', title: 'Click me!',
content: 'An alert will be triggered.', content: 'An alert will be triggered.',
onClick: () => { onClick: ( event ) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
window.alert( 'List item clicked' ); window.alert( 'List item clicked' );
return logItemClick( event );
}, },
listItemTag: 'click-me',
}, },
]; ];

View File

@ -0,0 +1,141 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import List from '../index';
describe( 'List', () => {
it( 'should have aria roles for items', () => {
const clickHandler = jest.fn();
const listItems = [
{
title: 'WooCommerce.com',
href: 'https://woocommerce.com',
},
{
title: 'Click me!',
onClick: clickHandler,
},
];
render( <List items={ listItems } /> );
expect( screen.getAllByRole( 'menuitem' ) ).toHaveLength( 2 );
} );
it( 'should support `onClick` for items', () => {
const clickHandler = jest.fn();
const listItems = [
{
title: 'WooCommerce.com',
href: 'https://woocommerce.com',
},
{
title: 'Click me!',
onClick: clickHandler,
},
];
render( <List items={ listItems } /> );
userEvent.click(
screen.getByRole( 'menuitem', { name: 'Click me!' } )
);
expect( clickHandler ).toHaveBeenCalled();
} );
it( 'should set `data-link-type` on items', () => {
const listItems = [
{
title: 'Add products',
href: '/post-new.php?post_type=product',
linkType: 'wp-admin',
},
{
title: 'Market my store',
href: '/admin.php?page=wc-admin&path=%2Fmarketing',
linkType: 'wc-admin',
},
{
title: 'WooCommerce.com',
href: 'https://woocommerce.com',
linkType: 'external',
},
{
title: 'WordPress.org',
href: 'https://wordpress.org',
},
];
render( <List items={ listItems } /> );
expect(
screen.getByRole( 'menuitem', { name: 'Add products' } ).dataset
.linkType
).toBe( 'wp-admin' );
expect(
screen.getByRole( 'menuitem', { name: 'Market my store' } ).dataset
.linkType
).toBe( 'wc-admin' );
expect(
screen.getByRole( 'menuitem', { name: 'WooCommerce.com' } ).dataset
.linkType
).toBe( 'external' );
expect(
screen.getByRole( 'menuitem', { name: 'WordPress.org' } ).dataset
.linkType
).toBe( 'external' );
} );
it( 'should set `data-list-item-tag` on items', () => {
const listItems = [
{
title: 'Add products',
href: '/post-new.php?post_type=product',
linkType: 'wp-admin',
listItemTag: 'add-product',
},
{
title: 'Market my store',
href: '/admin.php?page=wc-admin&path=%2Fmarketing',
linkType: 'wc-admin',
listItemTag: 'marketing',
},
{
title: 'WooCommerce.com',
href: 'https://woocommerce.com',
linkType: 'external',
listItemTag: 'woocommerce.com-site',
},
{
title: 'WordPress.org',
href: 'https://wordpress.org',
},
];
render( <List items={ listItems } /> );
expect(
screen.getByRole( 'menuitem', { name: 'Add products' } ).dataset
.listItemTag
).toBe( 'add-product' );
expect(
screen.getByRole( 'menuitem', { name: 'Market my store' } ).dataset
.listItemTag
).toBe( 'marketing' );
expect(
screen.getByRole( 'menuitem', { name: 'WooCommerce.com' } ).dataset
.listItemTag
).toBe( 'woocommerce.com-site' );
expect(
screen.getByRole( 'menuitem', { name: 'WordPress.org' } ).dataset
.listItemTag
).toBeUndefined();
} );
} );