* ActivityCard: Update to match new activity panel designs

* Add spacing rhythm system, add the rest of the base colors

* Add styles for the ActivityCard

* Use the new ActivityCard in the orders panel

* Update text color & size

* Disable timestamped test (string changes based on date)

* Use moment to generate a relative timestamp to match “3 days ago” snapshot

* Rename spacing vars

The difference between gap and gap-med is not intuitive, so let’s switch to -small, and change -small to -smallest.
This commit is contained in:
Kelly Dwan 2018-07-16 09:53:38 -04:00 committed by GitHub
parent d353706f1b
commit 9fca535c67
11 changed files with 504 additions and 107 deletions

View File

@ -11,11 +11,10 @@ import ActivityCard from 'components/activity-card';
render: function() {
return (
<ActivityCard
label="Insight"
icon={ <Dashicon icon="search" /> }
date="30 minutes ago"
title="Insight"
icon={ <Gridicon icon="search" /> }
date="2018-07-10T00:00:00Z"
actions={ [ <a href="/">Action link</a>, <a href="/">Action link 2</a> ] }
image={ <Dashicon icon="palmtree" /> }
>
Insight content goes in this area here. It will probably be a couple of lines long and may
include an accompanying image. We might consider color-coding the icon for quicker
@ -27,10 +26,10 @@ render: function() {
## Props
* `label`: A title for this card (required).
* `title`: A title for this card (required).
* `subtitle`: An element rendered right under the title.
* `children`: Content used in the body of the action card (required).
* `actions`: A list of links or buttons shown in the footer of the card.
* `date`: The timestamp associated with this activity.
* `icon`: An icon used to label this activity. Defaults to "warning".
* `image`: An image show in this card. Can be `img` or `Dashicon` (or any renderable element).
* `menu`: A dropdown menu (EllipsisMenu) shown at the top-right of the card.
* `icon`: An icon or avatar used to identify this activity. Defaults to Gridicon "notice-outline".
* `unread`: If this prop is present, the card has a small red bubble indicating an "unread" item. Defaults to false.

View File

@ -4,7 +4,8 @@
*/
import classnames from 'classnames';
import { cloneElement, Component } from '@wordpress/element';
import { Dashicon } from '@wordpress/components';
import Gridicon from 'gridicons';
import { isArray } from 'lodash';
import { moment } from '@wordpress/date';
import PropTypes from 'prop-types';
@ -12,35 +13,31 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import './style.scss';
import { EllipsisMenu } from 'components/ellipsis-menu';
import { H, Section } from 'layout/section';
class ActivityCard extends Component {
render() {
const { actions, className, date, icon, image, label, menu, children } = this.props;
const { actions, className, children, date, icon, subtitle, title, unread } = this.props;
const cardClassName = classnames( 'woocommerce-activity-card', className );
const actionsList = isArray( actions ) ? actions : [ actions ];
return (
<section className={ cardClassName }>
{ unread && <span className="woocommerce-activity-card__unread" /> }
<span className="woocommerce-activity-card__icon" aria-hidden>
{ icon }
</span>
<header className="woocommerce-activity-card__header">
<span className="woocommerce-activity-card__icon">{ icon }</span>
<H className="woocommerce-activity-card__label">
{ label }
{ date && (
<span className="woocommerce-activity-card__date">
{ moment( date ).fromNow() }
</span>
) }
</H>
{ menu && <div className="woocommerce-activity-card__menu">{ menu }</div> }
<H className="woocommerce-activity-card__title">{ title }</H>
{ subtitle && <div className="woocommerce-activity-card__subtitle">{ subtitle }</div> }
{ date && (
<span className="woocommerce-activity-card__date">{ moment( date ).fromNow() }</span>
) }
</header>
<Section className="woocommerce-activity-card__body">
<div className="woocommerce-activity-card__content">{ children }</div>
{ image && <div className="woocommerce-activity-card__image">{ image }</div> }
</Section>
<Section className="woocommerce-activity-card__body">{ children }</Section>
{ actions && (
<footer className="woocommerce-activity-card__actions">
{ actions.map( ( item, i ) => cloneElement( item, { key: i } ) ) }
{ actionsList.map( ( item, i ) => cloneElement( item, { key: i } ) ) }
</footer>
) }
</section>
@ -49,20 +46,19 @@ class ActivityCard extends Component {
}
ActivityCard.propTypes = {
actions: PropTypes.oneOfType( [ PropTypes.array, PropTypes.element ] ),
actions: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.element ), PropTypes.element ] ),
className: PropTypes.string,
children: PropTypes.node.isRequired,
date: PropTypes.string,
icon: PropTypes.node,
image: PropTypes.node,
label: PropTypes.string.isRequired,
menu: PropTypes.shape( {
type: PropTypes.oneOf( [ EllipsisMenu ] ),
} ),
subtitle: PropTypes.node,
title: PropTypes.string.isRequired,
unread: PropTypes.bool,
};
ActivityCard.defaultProps = {
icon: <Dashicon icon="warning" />,
icon: <Gridicon icon="notice-outline" size={ 48 } />,
unread: false,
};
export default ActivityCard;

View File

@ -1,72 +1,73 @@
/** @format */
.woocommerce-activity-card {
position: relative;
padding: $gutter;
background: $white;
margin-bottom: $spacing * 2;
border-bottom: 1px solid $core-grey-light-400;
color: $gray-text;
@include font-size( 13 );
display: grid;
grid-template-columns: 84px 1fr;
grid-template-areas:
'icon header'
'icon body'
'icon actions';
@include breakpoint( '<782px' ) {
grid-template-columns: 76px 1fr;
}
}
.woocommerce-activity-card__unread {
position: absolute;
top: calc(#{ $gutter } - 6px);
right: calc(#{ $gutter } - 6px);
width: 6px;
height: 6px;
border-radius: 50%;
background: $core-orange;
}
.woocommerce-activity-card__icon {
grid-area: icon;
fill: $core-grey-light-600;
}
.woocommerce-activity-card__header {
display: flex;
align-items: center;
margin-bottom: $spacing * 0.5;
padding: $spacing $spacing 0;
grid-area: header;
margin-bottom: $gap;
display: grid;
grid-template-areas:
'title date'
'subtitle date';
.woocommerce-activity-card__icon {
margin-right: $spacing;
.dashicon {
fill: $gray-text;
}
}
.woocommerce-activity-card__label {
flex: 1;
.woocommerce-activity-card__title {
grid-area: title;
margin: 0;
@include font-size( 14 );
color: $gray-text;
font-weight: normal;
@include font-size( 13 );
}
.woocommerce-activity-card__date {
margin-left: 0.5em;
grid-area: date;
justify-self: end;
color: $core-grey-dark-300;
text-transform: uppercase;
@include font-size( 11 );
}
.woocommerce-activity-card__menu {
// Pull the menu up/out slightly
margin-right: $spacing * -0.5;
margin-top: $spacing * -0.5;
.woocommerce-activity-card__subtitle {
grid-area: subtitle;
}
}
.woocommerce-activity-card__body {
padding: 0 $spacing $spacing;
display: flex;
.woocommerce-activity-card__content {
flex: 1;
}
.woocommerce-activity-card__image {
border-radius: 50%;
overflow: hidden;
width: 60px;
align-self: flex-end;
border: 1px solid $gray;
background: #f8f8f8;
img,
.dashicon {
vertical-align: middle;
width: 100%;
height: auto;
}
}
grid-area: body;
}
.woocommerce-activity-card__actions {
padding: $spacing;
border-top: 1px solid $gray;
background: #eee;
grid-area: actions;
margin-top: $gap;
// Ensures any immediate child with a sibling has space between the items
& > * + * {

View File

@ -0,0 +1,248 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActivityCard should render a basic card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<t
icon="notice-outline"
size={48}
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
</section>
`;
exports[`ActivityCard should render a custom icon on a card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<t
icon="customize"
size={24}
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
</section>
`;
exports[`ActivityCard should render a gravatar on a card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<Gravatar
size={60}
user="admin@local.test"
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
</section>
`;
exports[`ActivityCard should render a timestamp on a card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<t
icon="notice-outline"
size={48}
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
<span
className="woocommerce-activity-card__date"
>
3 days ago
</span>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
</section>
`;
exports[`ActivityCard should render an action on a card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<t
icon="notice-outline"
size={48}
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
<footer
className="woocommerce-activity-card__actions"
>
<[object Object]
isDefault={true}
key="0"
onClick={[Function]}
>
Action
</[object Object]>
</footer>
</section>
`;
exports[`ActivityCard should render an unread bubble on a card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
className="woocommerce-activity-card__unread"
/>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<t
icon="notice-outline"
size={48}
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
</section>
`;
exports[`ActivityCard should render multiple actions on a card 1`] = `
<section
className="woocommerce-activity-card"
>
<span
aria-hidden={true}
className="woocommerce-activity-card__icon"
>
<t
icon="notice-outline"
size={48}
/>
</span>
<header
className="woocommerce-activity-card__header"
>
<H
className="woocommerce-activity-card__title"
>
Inbox message
</H>
</header>
<Section
className="woocommerce-activity-card__body"
>
This card has some content
</Section>
<footer
className="woocommerce-activity-card__actions"
>
<[object Object]
isPrimary={true}
key="0"
onClick={[Function]}
>
Action 1
</[object Object]>
<[object Object]
isDefault={true}
key="1"
onClick={[Function]}
>
Action 2
</[object Object]>
</footer>
</section>
`;

View File

@ -0,0 +1,105 @@
/** @format */
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import Gridicon from 'gridicons';
import { shallow } from 'enzyme';
/**
* Internal dependencies
*/
import ActivityCard from '../';
import Gravatar from 'components/gravatar';
describe( 'ActivityCard', () => {
test( 'should have correct title', () => {
const card = <ActivityCard title="Inbox message">This card has some content</ActivityCard>;
expect( card.props.title ).toBe( 'Inbox message' );
} );
test( 'should render a basic card', () => {
const card = shallow(
<ActivityCard title="Inbox message">This card has some content</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
test( 'should render an unread bubble on a card', () => {
const card = shallow(
<ActivityCard title="Inbox message" unread>
This card has some content
</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
test( 'should render a custom icon on a card', () => {
const card = shallow(
<ActivityCard title="Inbox message" icon={ <Gridicon icon="customize" /> }>
This card has some content
</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
test( 'should render a gravatar on a card', () => {
const card = shallow(
<ActivityCard title="Inbox message" icon={ <Gravatar user="admin@local.test" /> }>
This card has some content
</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
test( 'should render a timestamp on a card', () => {
// We're generating this via moment to ensure it's always "3 days ago".
const threeDaysAgo = wp.date
.moment()
.subtract( 3, 'days' )
.format();
const card = shallow(
<ActivityCard title="Inbox message" date={ threeDaysAgo }>
This card has some content
</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
test( 'should render an action on a card', () => {
const noop = () => {};
const card = shallow(
<ActivityCard
title="Inbox message"
actions={
<Button isDefault onClick={ noop }>
Action
</Button>
}
>
This card has some content
</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
test( 'should render multiple actions on a card', () => {
const noop = () => {};
const card = shallow(
<ActivityCard
title="Inbox message"
actions={ [
<Button isPrimary onClick={ noop }>
Action 1
</Button>,
<Button isDefault onClick={ noop }>
Action 2
</Button>,
] }
>
This card has some content
</ActivityCard>
);
expect( card ).toMatchSnapshot();
} );
} );

View File

@ -4,7 +4,7 @@
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { compose, Fragment } from '@wordpress/element';
import { Dashicon, withAPIData } from '@wordpress/components';
import { Button, withAPIData } from '@wordpress/components';
import PropTypes from 'prop-types';
import { noop } from 'lodash';
@ -52,28 +52,35 @@ function OrdersPanel( { orders } ) {
return (
<ActivityCard
key={ i }
label={ __( 'Order', 'wc-admin' ) }
icon={ <Dashicon icon="format-aside" /> }
className="woocommerce-order-activity-card"
title={ sprintf( __( '%s placed order #%d', 'wc-admin' ), name, order.id ) }
icon={ <Gravatar user={ address.email } /> }
date={ order.date_created }
>
<Gravatar user={ address.email } />
<div>{ sprintf( __( '%s placed order #%d', 'wc-admin' ), name, order.id ) }</div>
<div>
<span>
{ sprintf(
_n( '%d product', '%d products', productsCount, 'wc-admin' ),
productsCount
) }
</span>{' '}
{ refundValue ? (
subtitle={
<div>
<span>
<s>{ formatCurrency( total, order.currency ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency ) }
{ sprintf(
_n( '%d product', '%d products', productsCount, 'wc-admin' ),
productsCount
) }
</span>
) : (
<span>{ formatCurrency( total, order.currency ) }</span>
) }
</div>
{ refundValue ? (
<span>
<s>{ formatCurrency( total, order.currency ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency ) }
</span>
) : (
<span>{ formatCurrency( total, order.currency ) }</span>
) }
</div>
}
actions={
<Button isDefault onClick={ noop }>
Begin fulfillment
</Button>
}
>
Pending
</ActivityCard>
);
} )

View File

@ -211,3 +211,15 @@
}
}
}
.woocommerce-order-activity-card {
.woocommerce-activity-card__title {
font-weight: normal;
}
.woocommerce-activity-card__subtitle {
span + span:before {
content: ' \2022 ';
}
}
}

View File

@ -8,17 +8,29 @@ $gray-darken-10: darken($gray, 10%);
$gray-darken-20: darken($gray, 20%);
$gray-darken-30: darken($gray, 30%);
$gray-darken-40: darken($gray, 40%);
$gray-text: $gray-darken-40;
$gray-light-10: lighten($gray, 10%);
// Greys
$core-grey-light-100: #f8f9f9;
$core-grey-light-200: #f3f4f5;
$core-grey-light-300: #edeff0;
$core-grey-light-400: #e8eaeb;
$core-grey-light-500: #e2e4e7;
$core-grey-light-600: #d7dade;
$core-grey-light-700: #ccd0d4;
$core-grey-light-800: #b5bcc2;
$core-grey-light-900: #a2aab2;
$core-grey-dark-100: #86909b;
$core-grey-dark-200: #78848f;
$core-grey-dark-300: #6c7781; // This & below have 4.5+ contrast against white
$core-grey-dark-400: #606a73;
$core-grey-dark-500: #555d66;
$core-grey-dark-600: #40464d;
$core-grey-dark-700: #32373c;
$core-grey-dark-800: #23282d;
$core-grey-dark-900: #191e23;
$gray-text: $core-grey-dark-500;
// wp-admin
$wp-admin-background: #f1f1f1;
@ -30,8 +42,5 @@ $error-red: #d94f4f;
$woocommerce: #95588a;
$core-orange: #ca4a1f;
// Until we create a separate variables partial
$spacing: 16px;
$button: #f0f2f4;
$button-border: darken($button, 20%);

View File

@ -0,0 +1,11 @@
/** @format */
$gutter: var(--main-gap);
$gap-large: 24px;
$gap: 16px;
$gap-small: 12px;
$gap-smallest: 4px;
// @todo remove this spacing variable
$spacing: 16px;

View File

@ -1,7 +1,16 @@
/** @format */
// css resets some wp-admin specific rules so that the app fits better in the extension container
// By using CSS variables, we can switch the spacing rhythm using a single media query
:root {
--main-gap: 24px;
}
@media (max-width: 782px) {
:root {
--main-gap: 16px;
}
}
// css resets some wp-admin specific rules so that the app fits better in the extension container
.woocommerce-page {
.wrap {
margin: 0;

View File

@ -103,7 +103,7 @@ const webpackConfig = {
loader: 'sass-loader',
query: {
includePaths: [ 'client/stylesheets' ],
data: '@import "_colors"; @import "_breakpoints"; @import "_mixins";',
data: '@import "_variables"; @import "_colors"; @import "_breakpoints"; @import "_mixins";',
},
},
],