SummaryNumber: Switch to dropdown display on small screens (https://github.com/woocommerce/woocommerce-admin/pull/265)

* Move isMobileViewport to a helper function in lib

* Switch SummaryList to use navigable menu to support up/down (or left/right) arrow key navigation

* Switch to a dropdown menu/button combo when on a smaller screen

* Ensure aria role & href are only added if the item is a link

* Wrap the entire SummaryNumber in a link to match non-mobile use

* Update card content to be single line on mobile

* Add label to the popover title

* Make SummaryNumbers edge-to-edge on smaller screens

* Switch to the collapsed/dropdown view on screens <1100px

* Adjust offset of arrow icon
This commit is contained in:
Kelly Dwan 2018-08-02 18:20:48 -04:00 committed by GitHub
parent 0496d51e34
commit 6a82db326e
6 changed files with 137 additions and 22 deletions

View File

@ -18,15 +18,12 @@ import 'react-dates/lib/css/_datepicker.css';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { validateDateInputForRange } from 'lib/date';
import DateInput from './input'; import DateInput from './input';
import { isMobileViewport } from 'lib/ui';
import phrases from './phrases'; import phrases from './phrases';
import { validateDateInputForRange } from 'lib/date';
import './style.scss'; import './style.scss';
// 782px is the width designated by Gutenberg's `</ Popover>` component.
// * https://github.com/WordPress/gutenberg/blob/c8f8806d4465a83c1a0bc62d5c61377b56fa7214/components/popover/utils.js#L6
const isMobileViewport = () => window.innerWidth < 782;
class DateRange extends Component { class DateRange extends Component {
constructor( props ) { constructor( props ) {
super( props ); super( props );

View File

@ -30,6 +30,7 @@ render: function() {
* `value` (required): A string or number value to display - a string is allowed so we can accept currency formatting. * `value` (required): A string or number value to display - a string is allowed so we can accept currency formatting.
* `href` (required): An internal link to the report focused on this number. * `href` (required): An internal link to the report focused on this number.
* `delta`: A number to represent the percentage change since the last comparison period - positive numbers will show a green up arrow, negative numbers will show a red down arrow. If omitted, no change value will display. * `delta`: A number to represent the percentage change since the last comparison period - positive numbers will show a green up arrow, negative numbers will show a red down arrow. If omitted, no change value will display.
* `onToggle`: A function used to switch the given SummaryNumber to a button, and called on click.
* `prevLabel`: A string description of the previous value's timeframe, ex "Previous Year:". Defaults to "Previous Period:". * `prevLabel`: A string description of the previous value's timeframe, ex "Previous Year:". Defaults to "Previous Period:".
* `prevValue`: A string or number value to display - a string is allowed so we can accept currency formatting. If omitted, this section won't display. * `prevValue`: A string or number value to display - a string is allowed so we can accept currency formatting. If omitted, this section won't display.
* `selected`: A boolean used to show a highlight style on this number. Defaults to false. * `selected`: A boolean used to show a highlight style on this number. Defaults to false.

View File

@ -4,35 +4,69 @@
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import classnames from 'classnames'; import classnames from 'classnames';
import { Children, cloneElement } from '@wordpress/element';
import { Dropdown, NavigableMenu } from '@wordpress/components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { isMobileViewport, isTabletViewport } from 'lib/ui';
import './style.scss'; import './style.scss';
const SummaryList = ( { children, label } ) => { const SummaryList = ( { children, label } ) => {
if ( ! label ) { if ( ! label ) {
label = __( 'Performance Indicators', 'wc-admin' ); label = __( 'Performance Indicators', 'wc-admin' );
} }
// We default to "one" because we can't have empty children. If `children` is just one item, const isDropdownBreakpoint = isTabletViewport() || isMobileViewport();
// it's not an array and .length is undefined.
let hasItemsClass = 'has-one-item'; // We default to "one" because we can't have empty children.
if ( children && children.length ) { const itemCount = Children.count( children ) || 1;
const length = children.filter( Boolean ).length; const hasItemsClass = itemCount < 10 ? `has-${ itemCount }-items` : 'has-10-items';
hasItemsClass = length < 10 ? `has-${ length }-items` : 'has-10-items'; const classes = classnames( 'woocommerce-summary', {
} [ hasItemsClass ]: ! isDropdownBreakpoint,
const classes = classnames( 'woocommerce-summary', hasItemsClass ); } );
const instanceId = uniqueId( 'woocommerce-summary-helptext-' ); const instanceId = uniqueId( 'woocommerce-summary-helptext-' );
return ( const menu = (
<nav aria-label={ label } aria-describedby={ instanceId }> <NavigableMenu
aria-label={ label }
aria-describedby={ instanceId }
orientation={ isDropdownBreakpoint ? 'vertical' : 'horizontal' }
stopNavigationEvents
>
<p id={ instanceId } className="screen-reader-text"> <p id={ instanceId } className="screen-reader-text">
{ __( 'View the report for the selected data point.', 'wc-admin' ) } { __(
'List of data points available for filtering. Use arrow keys to cycle through ' +
'the list. Click a data point for a detailed report.',
'wc-admin'
) }
</p> </p>
<ul className={ classes }>{ children }</ul> <ul className={ classes }>{ children }</ul>
</nav> </NavigableMenu>
);
// On large screens, or if there are not multiple SummaryNumbers, we'll display the plain list.
if ( ! isDropdownBreakpoint || itemCount < 2 ) {
return menu;
}
const items = Children.toArray( children );
const selected = items.find( item => !! item.props.selected );
if ( ! selected ) {
return menu;
}
return (
<Dropdown
className="woocommerce-summary"
position="bottom"
headerTitle={ label }
expandOnMobile
renderToggle={ ( { onToggle } ) => cloneElement( selected, { onToggle } ) }
renderContent={ () => menu }
/>
); );
}; };

View File

@ -3,6 +3,7 @@
* External dependencies * External dependencies
*/ */
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import classnames from 'classnames'; import classnames from 'classnames';
import Gridicon from 'gridicons'; import Gridicon from 'gridicons';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
@ -17,12 +18,16 @@ const SummaryNumber = ( {
delta, delta,
href, href,
label, label,
onToggle,
prevLabel, prevLabel,
prevValue, prevValue,
reverseTrend, reverseTrend,
selected, selected,
value, value,
} ) => { } ) => {
const liClasses = classnames( 'woocommerce-summary__item-container', {
'is-dropdown-button': onToggle,
} );
const classes = classnames( 'woocommerce-summary__item', { const classes = classnames( 'woocommerce-summary__item', {
'is-selected': selected, 'is-selected': selected,
'is-good-trend': reverseTrend ? delta < 0 : delta > 0, 'is-good-trend': reverseTrend ? delta < 0 : delta > 0,
@ -40,9 +45,21 @@ const SummaryNumber = ( {
screenReaderLabel = sprintf( __( 'No change from %s', 'wc-admin' ), prevLabel ); screenReaderLabel = sprintf( __( 'No change from %s', 'wc-admin' ), prevLabel );
} }
const Container = onToggle ? Button : Link;
const containerProps = {
className: classes,
'aria-current': selected ? 'page' : null,
};
if ( ! onToggle ) {
containerProps.href = href;
containerProps.role = 'menuitem';
} else {
containerProps.onClick = onToggle;
}
return ( return (
<li className="woocommerce-summary__item-container"> <li className={ liClasses }>
<Link className={ classes } href={ href }> <Container { ...containerProps }>
<span className="woocommerce-summary__item-label">{ label }</span> <span className="woocommerce-summary__item-label">{ label }</span>
<span className="woocommerce-summary__item-data"> <span className="woocommerce-summary__item-data">
@ -65,7 +82,11 @@ const SummaryNumber = ( {
<span className="woocommerce-summary__item-prev-value"> <span className="woocommerce-summary__item-prev-value">
{ ! isUndefined( prevValue ) ? prevValue : __( 'N/A', 'wc-admin' ) } { ! isUndefined( prevValue ) ? prevValue : __( 'N/A', 'wc-admin' ) }
</span> </span>
</Link>
{ onToggle ? (
<Gridicon className="woocommerce-summary__toggle" icon="chevron-down" size={ 24 } />
) : null }
</Container>
</li> </li>
); );
}; };
@ -74,6 +95,7 @@ SummaryNumber.propTypes = {
delta: PropTypes.number, delta: PropTypes.number,
href: PropTypes.string.isRequired, href: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
onToggle: PropTypes.func,
prevLabel: PropTypes.string, prevLabel: PropTypes.string,
prevValue: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ), prevValue: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ),
reverseTrend: PropTypes.bool, reverseTrend: PropTypes.bool,

View File

@ -38,6 +38,7 @@ $inner-border: $core-grey-light-500;
} }
.woocommerce-summary { .woocommerce-summary {
margin: $gap 0;
display: grid; display: grid;
border-width: 1px 0 0 1px; border-width: 1px 0 0 1px;
border-style: solid; border-style: solid;
@ -45,6 +46,14 @@ $inner-border: $core-grey-light-500;
background-color: $core-grey-light-300; background-color: $core-grey-light-300;
box-shadow: inset -1px -1px 0 $outer-border; box-shadow: inset -1px -1px 0 $outer-border;
.components-popover__content & {
max-height: 100%;
margin-top: 0;
margin-bottom: 0;
overflow-y: scroll;
border: none;
}
.woocommerce-summary__item-data { .woocommerce-summary__item-data {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -116,6 +125,19 @@ $inner-border: $core-grey-light-500;
} }
} }
} }
@include breakpoint( '<782px' ) {
.woocommerce-summary__item-container:only-child {
margin-left: -16px;
margin-right: -16px;
width: auto;
& .woocommerce-summary__item {
// Remove the border when the button is edge-to-edge
border-right: none;
}
}
}
} }
.woocommerce-summary__item-container { .woocommerce-summary__item-container {
@ -126,6 +148,15 @@ $inner-border: $core-grey-light-500;
// Make sure the last item always uses the outer-border color. // Make sure the last item always uses the outer-border color.
border-right-color: $outer-border !important; border-right-color: $outer-border !important;
} }
&.is-dropdown-button {
padding: 0;
list-style: none;
.components-button {
text-align: left;
}
}
} }
.woocommerce-summary__item { .woocommerce-summary__item {
@ -146,7 +177,8 @@ $inner-border: $core-grey-light-500;
} }
&:focus { &:focus {
box-shadow: inset -2px -2px 0 $black, inset 2px 2px 0 $black; // !important to override button styles
box-shadow: inset -2px -2px 0 $black, inset 2px 2px 0 $black !important;
} }
&.is-selected { &.is-selected {
@ -154,7 +186,18 @@ $inner-border: $core-grey-light-500;
height: calc(100% + 1px); // Hack to avoid double border height: calc(100% + 1px); // Hack to avoid double border
&:focus { &:focus {
box-shadow: inset -2px -2px 0 $black, inset 2px 2px 0 $black, inset 0 4px 0 $woocommerce; // !important to override button styles
box-shadow: inset -2px -2px 0 $black, inset 2px 2px 0 $black, inset 0 4px 0 $woocommerce !important;
}
}
.is-dropdown-button & {
position: relative;
width: 100%;
padding-right: 2 * $gap + 24px;
@include breakpoint( '<782px' ) {
border-right: none;
} }
} }
@ -220,6 +263,12 @@ $inner-border: $core-grey-light-500;
@include font-size( 13 ); @include font-size( 13 );
color: $core-grey-dark-500; color: $core-grey-dark-500;
} }
.woocommerce-summary__toggle {
position: absolute;
top: 44px;
right: $gap;
}
} }
.woocommerce-card { .woocommerce-card {

View File

@ -0,0 +1,12 @@
/** @format */
// 782px is the width designated by Gutenberg's `</ Popover>` component.
// * https://github.com/WordPress/gutenberg/blob/c8f8806d4465a83c1a0bc62d5c61377b56fa7214/components/popover/utils.js#L6
export function isMobileViewport() {
return window.innerWidth <= 782;
}
// Most screens at 1100px or lower are tablets
export function isTabletViewport() {
return window.innerWidth > 782 && window.innerWidth <= 1100;
}