* Add aria roles to chart elements

* Make individual points (in line charts) and individual bar (in bar charts) focusable

* Remove methods which are never used

* Reduce the number of parameters required by functions that display/hide the tooltip

* Use tooltipFormat for accessibility dates

* Rename 'formatVoiceDate' function to 'getTooltipDate'

* Use string literals for aria-label

* Remove table role which was no longer needed

* Add aria-hidden to X-axis in charts

* Remove 'key' from points/bar aria-label in charts

* Set different ARIA properties depending on chart mode (time or item comparison)

* Label should default to an empty string instead of a 0

* Use date format from params instead of hardcoded
This commit is contained in:
Albert Juhé Lluveras 2018-09-25 11:42:08 +02:00 committed by GitHub
parent d9e47518f9
commit 1900bb0917
4 changed files with 73 additions and 36 deletions

View File

@ -332,6 +332,7 @@ export class RevenueReport extends Component {
title={ selectedChart.label } title={ selectedChart.label }
interval={ currentInterval } interval={ currentInterval }
allowedIntervals={ allowedIntervals } allowedIntervals={ allowedIntervals }
mode="time-comparison"
pointLabelFormat={ formats.pointLabelFormat } pointLabelFormat={ formats.pointLabelFormat }
tooltipTitle={ selectedChart.label } tooltipTitle={ selectedChart.label }
xFormat={ formats.xFormat } xFormat={ formats.xFormat }

View File

@ -100,6 +100,7 @@ class D3Chart extends Component {
height, height,
layout, layout,
margin, margin,
mode,
orderedKeys, orderedKeys,
pointLabelFormat, pointLabelFormat,
tooltipFormat, tooltipFormat,
@ -132,6 +133,7 @@ class D3Chart extends Component {
line: getLine( xLineScale, yScale ), line: getLine( xLineScale, yScale ),
lineData, lineData,
margin, margin,
mode,
orderedKeys: newOrderedKeys, orderedKeys: newOrderedKeys,
pointLabelFormat, pointLabelFormat,
parseDate, parseDate,
@ -221,6 +223,11 @@ D3Chart.propTypes = {
right: PropTypes.number, right: PropTypes.number,
top: PropTypes.number, top: PropTypes.number,
} ), } ),
/**
* `items-comparison` (default) or `time-comparison`, this is used to generate correct
* ARIA properties.
*/
mode: PropTypes.oneOf( [ 'item-comparison', 'time-comparison' ] ),
/** /**
* The list of labels for this chart. * The list of labels for this chart.
*/ */
@ -267,6 +274,7 @@ D3Chart.defaultProps = {
top: 20, top: 20,
}, },
layout: 'standard', layout: 'standard',
mode: 'item-comparison',
tooltipFormat: '%B %d, %Y', tooltipFormat: '%B %d, %Y',
type: 'line', type: 'line',
width: 600, width: 600,

View File

@ -188,6 +188,7 @@ class Chart extends Component {
const { const {
dateParser, dateParser,
layout, layout,
mode,
pointLabelFormat, pointLabelFormat,
title, title,
tooltipFormat, tooltipFormat,
@ -263,6 +264,7 @@ class Chart extends Component {
dateParser={ dateParser } dateParser={ dateParser }
height={ 300 } height={ 300 }
margin={ margin } margin={ margin }
mode={ mode }
orderedKeys={ orderedKeys } orderedKeys={ orderedKeys }
pointLabelFormat={ pointLabelFormat } pointLabelFormat={ pointLabelFormat }
tooltipFormat={ tooltipFormat } tooltipFormat={ tooltipFormat }
@ -320,6 +322,11 @@ Chart.propTypes = {
* to the left or 'compact' has the legend below * to the left or 'compact' has the legend below
*/ */
layout: PropTypes.oneOf( [ 'standard', 'comparison', 'compact' ] ), layout: PropTypes.oneOf( [ 'standard', 'comparison', 'compact' ] ),
/**
* `item-comparison` (default) or `time-comparison`, this is used to generate correct
* ARIA properties.
*/
mode: PropTypes.oneOf( [ 'item-comparison', 'time-comparison' ] ),
/** /**
* A title describing this chart. * A title describing this chart.
*/ */
@ -350,6 +357,7 @@ Chart.defaultProps = {
x2Format: '%b %Y', x2Format: '%b %Y',
yFormat: '$.3s', yFormat: '$.3s',
layout: 'standard', layout: 'standard',
mode: 'item-comparison',
type: 'line', type: 'line',
interval: 'day', interval: 'day',
}; };

View File

@ -3,7 +3,6 @@
/** /**
* External dependencies * External dependencies
*/ */
import { findIndex, get } from 'lodash'; import { findIndex, get } from 'lodash';
import { max as d3Max } from 'd3-array'; import { max as d3Max } from 'd3-array';
import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis'; import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis';
@ -16,6 +15,7 @@ import {
import { event as d3Event, mouse as d3Mouse, select as d3Select } from 'd3-selection'; import { event as d3Event, mouse as d3Mouse, select as d3Select } from 'd3-selection';
import { line as d3Line } from 'd3-shape'; import { line as d3Line } from 'd3-shape';
import { format as formatDate } from '@wordpress/date'; import { format as formatDate } from '@wordpress/date';
/** /**
* Internal dependencies * Internal dependencies
*/ */
@ -84,6 +84,7 @@ export const getLineData = ( data, orderedKeys ) =>
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,
} ) ), } ) ),
@ -314,6 +315,7 @@ export const drawAxis = ( node, params ) => {
node node
.append( 'g' ) .append( 'g' )
.attr( 'class', 'axis' ) .attr( 'class', 'axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(0,${ params.height })` ) .attr( 'transform', `translate(0,${ params.height })` )
.call( .call(
d3AxisBottom( xScale ) d3AxisBottom( xScale )
@ -324,6 +326,7 @@ export const drawAxis = ( node, params ) => {
node node
.append( 'g' ) .append( 'g' )
.attr( 'class', 'axis axis-month' ) .attr( 'class', 'axis axis-month' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(3, ${ params.height + 20 })` ) .attr( 'transform', `translate(3, ${ params.height + 20 })` )
.call( .call(
d3AxisBottom( xScale ) d3AxisBottom( xScale )
@ -371,6 +374,7 @@ export const drawAxis = ( node, params ) => {
node node
.append( 'g' ) .append( 'g' )
.attr( 'class', 'axis y-axis' ) .attr( 'class', 'axis y-axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', 'translate(-50, 0)' ) .attr( 'transform', 'translate(-50, 0)' )
.attr( 'text-anchor', 'start' ) .attr( 'text-anchor', 'start' )
.call( .call(
@ -428,29 +432,29 @@ const showTooltip = ( node, params, d, position ) => {
` ); ` );
}; };
const handleMouseOverBarChart = ( d, i, nodes, node, data, params, position ) => { const handleMouseOverBarChart = ( date, parentNode, node, data, params, position ) => {
d3Select( nodes[ i ].parentNode ) d3Select( parentNode )
.select( '.barfocus' ) .select( '.barfocus' )
.attr( 'opacity', '0.1' ); .attr( 'opacity', '0.1' );
showTooltip( node, params, d, position ); showTooltip( node, params, data.find( e => e.date === date ), position );
}; };
const handleMouseOutBarChart = ( d, i, nodes, params ) => { const handleMouseOutBarChart = ( parentNode, params ) => {
d3Select( nodes[ i ].parentNode ) d3Select( parentNode )
.select( '.barfocus' ) .select( '.barfocus' )
.attr( 'opacity', '0' ); .attr( 'opacity', '0' );
params.tooltip.style( 'display', 'none' ); params.tooltip.style( 'display', 'none' );
}; };
const handleMouseOverLineChart = ( d, i, nodes, node, data, params, position ) => { const handleMouseOverLineChart = ( date, parentNode, node, data, params, position ) => {
d3Select( nodes[ i ].parentNode ) d3Select( parentNode )
.select( '.focus-grid' ) .select( '.focus-grid' )
.attr( 'opacity', '1' ); .attr( 'opacity', '1' );
showTooltip( node, params, data.find( e => e.date === d.date ), position ); showTooltip( node, params, data.find( e => e.date === date ), position );
}; };
const handleMouseOutLineChart = ( d, i, nodes, params ) => { const handleMouseOutLineChart = ( parentNode, params ) => {
d3Select( nodes[ i ].parentNode ) d3Select( parentNode )
.select( '.focus-grid' ) .select( '.focus-grid' )
.attr( 'opacity', '0' ); .attr( 'opacity', '0' );
params.tooltip.style( 'display', 'none' ); params.tooltip.style( 'display', 'none' );
@ -470,7 +474,9 @@ export const drawLines = ( node, data, params ) => {
.data( params.lineData.filter( d => d.visible ) ) .data( params.lineData.filter( d => d.visible ) )
.enter() .enter()
.append( 'g' ) .append( 'g' )
.attr( 'class', 'line-g' ); .attr( 'class', 'line-g' )
.attr( 'role', 'region' )
.attr( 'aria-label', d => d.key );
series series
.append( 'path' ) .append( 'path' )
@ -501,10 +507,18 @@ export const drawLines = ( node, data, params ) => {
} ) } )
.attr( 'cx', d => params.xLineScale( new Date( d.date ) ) ) .attr( 'cx', d => params.xLineScale( new Date( d.date ) ) )
.attr( 'cy', d => params.yScale( d.value ) ) .attr( 'cy', d => params.yScale( d.value ) )
.on( 'mouseover', ( d, i, nodes ) => .attr( 'tabindex', '0' )
handleMouseOverLineChart( d, i, nodes, node, data, params ) .attr( 'aria-label', d => {
) const label = d.label
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) ); ? d.label
: params.tooltipFormat( d.date instanceof Date ? d.date : new Date( d.date ) );
return `${ label } ${ formatCurrency( d.value ) }`;
} )
.on( 'focus', ( d, i, nodes ) => {
const position = calculatePositionInChart( d3Event.target, node.node() );
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
} )
.on( 'blur', ( d, i, nodes ) => handleMouseOutLineChart( nodes[ i ].parentNode, params ) );
const focus = node const focus = node
.append( 'g' ) .append( 'g' )
@ -532,15 +546,10 @@ export const drawLines = ( node, data, params ) => {
.attr( 'width', d => d.width ) .attr( 'width', d => d.width )
.attr( 'height', params.height ) .attr( 'height', params.height )
.attr( 'opacity', 0 ) .attr( 'opacity', 0 )
.attr( 'tabindex', '0' )
.on( 'mouseover', ( d, i, nodes ) => .on( 'mouseover', ( d, i, nodes ) =>
handleMouseOverLineChart( d, i, nodes, node, data, params ) handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params )
) )
.on( 'focus', ( d, i, nodes ) => { .on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( nodes[ i ].parentNode, params ) );
const position = calculatePositionInChart( d3Event.target, node.node() );
handleMouseOverLineChart( d, i, nodes, node, data, params, position );
} )
.on( 'mouseout blur', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
}; };
export const drawBars = ( node, data, params ) => { export const drawBars = ( node, data, params ) => {
@ -552,7 +561,15 @@ export const drawBars = ( node, data, params ) => {
.enter() .enter()
.append( 'g' ) .append( 'g' )
.attr( 'transform', d => `translate(${ params.xScale( d.date ) },0)` ) .attr( 'transform', d => `translate(${ params.xScale( d.date ) },0)` )
.attr( 'class', 'bargroup' ); .attr( 'class', 'bargroup' )
.attr( 'role', 'region' )
.attr(
'aria-label',
d =>
params.mode === 'item-comparison'
? params.tooltipFormat( d.date instanceof Date ? d.date : new Date( d.date ) )
: null
);
barGroup barGroup
.append( 'rect' ) .append( 'rect' )
@ -569,8 +586,10 @@ export const drawBars = ( node, data, params ) => {
params.orderedKeys.filter( row => row.visible ).map( row => ( { params.orderedKeys.filter( row => row.visible ).map( row => ( {
key: row.key, key: row.key,
focus: row.focus, focus: row.focus,
value: d[ row.key ].value, value: get( d, [ row.key, 'value' ], 0 ),
label: get( d, [ row.key, 'label' ], '' ),
visible: row.visible, visible: row.visible,
date: d.date,
} ) ) } ) )
) )
.enter() .enter()
@ -581,14 +600,20 @@ export const drawBars = ( node, data, params ) => {
.attr( 'width', params.xGroupScale.bandwidth() ) .attr( 'width', params.xGroupScale.bandwidth() )
.attr( 'height', d => params.height - params.yScale( d.value ) ) .attr( 'height', d => params.height - params.yScale( d.value ) )
.attr( 'fill', d => getColor( d.key, params ) ) .attr( 'fill', d => getColor( d.key, params ) )
.attr( 'tabindex', '0' )
.attr( 'aria-label', d => {
const label = params.mode === 'time-comparison' && d.label ? d.label : d.key;
return `${ label } ${ formatCurrency( d.value ) }`;
} )
.style( 'opacity', d => { .style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1; const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0; return d.visible ? opacity : 0;
} ) } )
.on( 'mouseover', ( d, i, nodes ) => .on( 'focus', ( d, i, nodes ) => {
handleMouseOverBarChart( d, i, nodes, node, data, params ) const position = calculatePositionInChart( d3Event.target, node.node() );
) handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, params ) ); } )
.on( 'blur', ( d, i, nodes ) => handleMouseOutBarChart( nodes[ i ].parentNode, params ) );
barGroup barGroup
.append( 'rect' ) .append( 'rect' )
@ -598,13 +623,8 @@ export const drawBars = ( node, data, params ) => {
.attr( 'width', params.xGroupScale.range()[ 1 ] ) .attr( 'width', params.xGroupScale.range()[ 1 ] )
.attr( 'height', params.height ) .attr( 'height', params.height )
.attr( 'opacity', '0' ) .attr( 'opacity', '0' )
.attr( 'tabindex', '0' )
.on( 'mouseover', ( d, i, nodes ) => .on( 'mouseover', ( d, i, nodes ) =>
handleMouseOverBarChart( d, i, nodes, node, data, params ) handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params )
) )
.on( 'focus', ( d, i, nodes ) => { .on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( nodes[ i ].parentNode, params ) );
const position = calculatePositionInChart( d3Event.target, node.node() );
handleMouseOverBarChart( d, i, nodes, node, data, params, position );
} )
.on( 'mouseout blur', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, params ) );
}; };