Use IDs instead of labels to identify chart legend items (https://github.com/woocommerce/woocommerce-admin/pull/1730)

* Use instanceId to generate unique ids for chart legend items

* Decouple item key and label in Chart components

* Remove more duplicate IDs in the chart legend

* Use double underscores in element IDs following BEM rules

* Move 'withInstanceId' inside D3Legend component

* Simplify screen reader labels logic

* Add CHANGELOG message

* Use 'primary' and 'secondary' as items keys in time-comparison charts
This commit is contained in:
Albert Juhé Lluveras 2019-03-05 19:27:20 +01:00 committed by GitHub
parent 3de2bd1953
commit 5d3aa58a5d
7 changed files with 43 additions and 38 deletions

View File

@ -42,7 +42,8 @@ export class ReportChart extends Component {
const label = intervalData[ segment.segment_label ] const label = intervalData[ segment.segment_label ]
? segment.segment_label + ' (#' + segment.segment_id + ')' ? segment.segment_label + ' (#' + segment.segment_id + ')'
: segment.segment_label; : segment.segment_label;
intervalData[ label ] = { intervalData[ segment.segment_id ] = {
label,
value: segment.subtotals[ selectedChart.key ] || 0, value: segment.subtotals[ selectedChart.key ] || 0,
}; };
} }
@ -59,8 +60,6 @@ export class ReportChart extends Component {
const { query, primaryData, secondaryData, selectedChart } = this.props; const { query, primaryData, secondaryData, selectedChart } = this.props;
const currentInterval = getIntervalForQuery( query ); const currentInterval = getIntervalForQuery( query );
const { primary, secondary } = getCurrentDates( query ); const { primary, secondary } = getCurrentDates( query );
const primaryKey = `${ primary.label } (${ primary.range })`;
const secondaryKey = `${ secondary.label } (${ secondary.range })`;
const chartData = primaryData.data.intervals.map( function( interval, index ) { const chartData = primaryData.data.intervals.map( function( interval, index ) {
const secondaryDate = getPreviousDate( const secondaryDate = getPreviousDate(
@ -74,11 +73,13 @@ export class ReportChart extends Component {
const secondaryInterval = secondaryData.data.intervals[ index ]; const secondaryInterval = secondaryData.data.intervals[ index ];
return { return {
date: formatDate( 'Y-m-d\\TH:i:s', interval.date_start ), date: formatDate( 'Y-m-d\\TH:i:s', interval.date_start ),
[ primaryKey ]: { primary: {
label: `${ primary.label } (${ primary.range })`,
labelDate: interval.date_start, labelDate: interval.date_start,
value: interval.subtotals[ selectedChart.key ] || 0, value: interval.subtotals[ selectedChart.key ] || 0,
}, },
[ secondaryKey ]: { secondary: {
label: `${ secondary.label } (${ secondary.range })`,
labelDate: secondaryDate.format( 'YYYY-MM-DD HH:mm:ss' ), labelDate: secondaryDate.format( 'YYYY-MM-DD HH:mm:ss' ),
value: ( secondaryInterval && secondaryInterval.subtotals[ selectedChart.key ] ) || 0, value: ( secondaryInterval && secondaryInterval.subtotals[ selectedChart.key ] ) || 0,
}, },

View File

@ -1,3 +1,6 @@
# (unreleased)
- Chart legend component now uses withInstanceId HOC so the ids used in several HTML elements are unique.
# 1.6.0 # 1.6.0
- Chart component: new props `emptyMessage` and `baseValue`. When an empty message is provided, it will be displayed on top of the chart if there are no values different than `baseValue`. - Chart component: new props `emptyMessage` and `baseValue`. When an empty message is provided, it will be displayed on top of the chart if there are no values different than `baseValue`.
- Chart component: remove d3-array dependency. - Chart component: remove d3-array dependency.

View File

@ -5,6 +5,7 @@
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames'; import classNames from 'classnames';
import { Component, createRef } from '@wordpress/element'; import { Component, createRef } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
/** /**
@ -57,6 +58,7 @@ class D3Legend extends Component {
interactive, interactive,
legendDirection, legendDirection,
legendValueFormat, legendValueFormat,
instanceId,
totalLabel, totalLabel,
} = this.props; } = this.props;
const { isScrollable } = this.state; const { isScrollable } = this.state;
@ -89,7 +91,7 @@ class D3Legend extends Component {
'woocommerce-legend__item-checked': row.visible, 'woocommerce-legend__item-checked': row.visible,
} ) } } ) }
key={ row.key } key={ row.key }
id={ row.key } id={ `woocommerce-legend-${ instanceId }__item__${ row.key }` }
onMouseEnter={ handleLegendHover } onMouseEnter={ handleLegendHover }
onMouseLeave={ handleLegendHover } onMouseLeave={ handleLegendHover }
onBlur={ handleLegendHover } onBlur={ handleLegendHover }
@ -97,7 +99,7 @@ class D3Legend extends Component {
> >
<button <button
onClick={ handleLegendToggle } onClick={ handleLegendToggle }
id={ row.key } id={ `woocommerce-legend-${ instanceId }__item-button__${ row.key }` }
disabled={ disabled={
( row.visible && numberOfRowsVisible <= 1 ) || ( row.visible && numberOfRowsVisible <= 1 ) ||
( ! row.visible && numberOfRowsVisible >= selectionLimit ) || ( ! row.visible && numberOfRowsVisible >= selectionLimit ) ||
@ -108,18 +110,17 @@ class D3Legend extends Component {
: '' : ''
} }
> >
<div className="woocommerce-legend__item-container" id={ row.key }> <div className="woocommerce-legend__item-container">
<span <span
className={ classNames( 'woocommerce-legend__item-checkmark', { className={ classNames( 'woocommerce-legend__item-checkmark', {
'woocommerce-legend__item-checkmark-checked': row.visible, 'woocommerce-legend__item-checkmark-checked': row.visible,
} ) } } ) }
id={ row.key }
style={ row.visible ? { color: getColor( keys, colorScheme )( row.key ) } : null } style={ row.visible ? { color: getColor( keys, colorScheme )( row.key ) } : null }
/> />
<span className="woocommerce-legend__item-title" id={ row.key }> <span className="woocommerce-legend__item-title">
{ row.key } { row.label }
</span> </span>
<span className="woocommerce-legend__item-total" id={ row.key }> <span className="woocommerce-legend__item-total">
{ getFormatter( legendValueFormat )( row.total ) } { getFormatter( legendValueFormat )( row.total ) }
</span> </span>
</div> </div>
@ -173,6 +174,8 @@ D3Legend.propTypes = {
* comparison charts when there are many. * comparison charts when there are many.
*/ */
totalLabel: PropTypes.string, totalLabel: PropTypes.string,
// from withInstanceId
instanceId: PropTypes.number,
}; };
D3Legend.defaultProps = { D3Legend.defaultProps = {
@ -181,4 +184,4 @@ D3Legend.defaultProps = {
legendValueFormat: ',', legendValueFormat: ',',
}; };
export default D3Legend; export default withInstanceId( D3Legend );

View File

@ -47,7 +47,7 @@ export const drawBars = ( node, data, params, scales, formats, tooltip ) => {
key: row.key, key: row.key,
focus: row.focus, focus: row.focus,
value: get( d, [ row.key, 'value' ], 0 ), value: get( d, [ row.key, 'value' ], 0 ),
label: get( d, [ row.key, 'label' ], '' ), label: row.label,
visible: row.visible, visible: row.visible,
date: d.date, date: d.date,
} ) ) } ) )
@ -63,14 +63,10 @@ export const drawBars = ( node, data, params, scales, formats, tooltip ) => {
.attr( 'pointer-events', 'none' ) .attr( 'pointer-events', 'none' )
.attr( 'tabindex', '0' ) .attr( 'tabindex', '0' )
.attr( 'aria-label', d => { .attr( 'aria-label', d => {
let label = d.key; let label = d.label || d.key;
if ( params.mode === 'time-comparison' ) { if ( params.mode === 'time-comparison' ) {
if ( d.label ) { const dayData = data.find( e => e.date === d.date );
label = d.label; label = formats.screenReaderFormat( moment( dayData[ d.key ].labelDate ).toDate() );
} else {
const dayData = data.find( e => e.date === d.date );
label = formats.screenReaderFormat( moment( dayData[ d.key ].labelDate ).toDate() );
}
} }
return `${ label } ${ tooltip.valueFormat( d.value ) }`; return `${ label } ${ tooltip.valueFormat( d.value ) }`;
} ) } )

View File

@ -74,10 +74,10 @@ export const getLineData = ( data, orderedKeys ) =>
key: row.key, key: row.key,
focus: row.focus, focus: row.focus,
visible: row.visible, visible: row.visible,
label: row.label,
values: data.map( d => ( { values: data.map( d => ( {
date: d.date, date: d.date,
focus: row.focus, focus: row.focus,
label: get( d, [ row.key, 'label' ], '' ),
value: get( d, [ row.key, 'value' ], 0 ), value: get( d, [ row.key, 'value' ], 0 ),
visible: row.visible, visible: row.visible,
} ) ), } ) ),
@ -97,7 +97,7 @@ export const drawLines = ( node, data, params, scales, formats, tooltip ) => {
.append( 'g' ) .append( 'g' )
.attr( 'class', 'line-g' ) .attr( 'class', 'line-g' )
.attr( 'role', 'region' ) .attr( 'role', 'region' )
.attr( 'aria-label', d => d.key ); .attr( 'aria-label', d => d.label || d.key );
const dateSpaces = getDateSpaces( data, params.uniqueDates, width, scales.xScale ); const dateSpaces = getDateSpaces( data, params.uniqueDates, width, scales.xScale );
let lineStroke = width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3; let lineStroke = width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
@ -138,9 +138,7 @@ export const drawLines = ( node, data, params, scales, formats, tooltip ) => {
.attr( 'cy', d => scales.yScale( d.value ) ) .attr( 'cy', d => scales.yScale( d.value ) )
.attr( 'tabindex', '0' ) .attr( 'tabindex', '0' )
.attr( 'aria-label', d => { .attr( 'aria-label', d => {
const label = d.label const label = formats.screenReaderFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
? d.label
: formats.screenReaderFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
return `${ label } ${ tooltip.valueFormat( d.value ) }`; return `${ label } ${ tooltip.valueFormat( d.value ) }`;
} ) } )
.on( 'focus', ( d, i, nodes ) => { .on( 'focus', ( d, i, nodes ) => {

View File

@ -105,7 +105,7 @@ class ChartTooltip {
if ( d[ row.key ].labelDate ) { if ( d[ row.key ].labelDate ) {
return this.labelFormat( moment( d[ row.key ].labelDate ).toDate() ); return this.labelFormat( moment( d[ row.key ].labelDate ).toDate() );
} }
return row.key; return row.label || row.key;
} }
show( d, triggerElement, parentNode, elementWidthRatio = 1 ) { show( d, triggerElement, parentNode, elementWidthRatio = 1 ) {

View File

@ -51,18 +51,20 @@ d3FormatDefaultLocale( {
} ); } );
function getOrderedKeys( props, previousOrderedKeys = [] ) { function getOrderedKeys( props, previousOrderedKeys = [] ) {
const updatedKeys = [ const uniqueKeys = props.data.reduce( ( accum, curr ) => {
...new Set( Object.entries( curr ).forEach( ( [ key, value ] ) => {
props.data.reduce( ( accum, curr ) => { if ( key !== 'date' && ! accum[ key ] ) {
Object.keys( curr ).forEach( key => key !== 'date' && accum.push( key ) ); accum[ key ] = value.label;
return accum; }
}, [] ) } );
), return accum;
].map( ( key ) => { }, {} );
const updatedKeys = Object.entries( uniqueKeys ).map( ( [ key, label ] ) => {
const previousKey = previousOrderedKeys.find( item => key === item.key ); const previousKey = previousOrderedKeys.find( item => key === item.key );
const defaultVisibleStatus = 'item-comparison' === props.mode ? false : true; const defaultVisibleStatus = 'item-comparison' === props.mode ? false : true;
return { return {
key, key,
label,
total: props.data.reduce( ( a, c ) => a + c[ key ].value, 0 ), total: props.data.reduce( ( a, c ) => a + c[ key ].value, 0 ),
visible: previousKey ? previousKey.visible : defaultVisibleStatus, visible: previousKey ? previousKey.visible : defaultVisibleStatus,
focus: true, focus: true,
@ -155,9 +157,10 @@ class Chart extends Component {
if ( ! interactiveLegend ) { if ( ! interactiveLegend ) {
return; return;
} }
const key = event.currentTarget.id.split( '_' ).pop();
const orderedKeys = this.state.orderedKeys.map( d => ( { const orderedKeys = this.state.orderedKeys.map( d => ( {
...d, ...d,
visible: d.key === event.target.id ? ! d.visible : d.visible, visible: d.key === key ? ! d.visible : d.visible,
} ) ); } ) );
const copyEvent = { ...event }; // can't pass a synthetic event into the hover handler const copyEvent = { ...event }; // can't pass a synthetic event into the hover handler
this.setState( this.setState(
@ -172,10 +175,11 @@ class Chart extends Component {
} }
handleLegendHover( event ) { handleLegendHover( event ) {
const hoverTarget = this.state.orderedKeys.filter( d => d.key === event.target.id )[ 0 ]; const key = event.currentTarget.id.split( '__' ).pop();
const hoverTarget = this.state.orderedKeys.filter( d => d.key === key )[ 0 ];
this.setState( { this.setState( {
orderedKeys: this.state.orderedKeys.map( d => { orderedKeys: this.state.orderedKeys.map( d => {
let enterFocus = d.key === event.target.id ? true : false; let enterFocus = d.key === key ? true : false;
enterFocus = ! hoverTarget.visible ? true : enterFocus; enterFocus = ! hoverTarget.visible ? true : enterFocus;
return { return {
...d, ...d,