From 1900bb0917b12e374ef32d303b7374af288f809c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 25 Sep 2018 11:42:08 +0200 Subject: [PATCH] Improve charts accessibility (https://github.com/woocommerce/woocommerce-admin/pull/421) * 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 --- .../client/analytics/report/revenue/index.js | 1 + .../client/components/chart/charts.js | 8 ++ .../client/components/chart/index.js | 8 ++ .../client/components/chart/utils.js | 92 +++++++++++-------- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js index 563d4d20472..577395c63ee 100644 --- a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js @@ -332,6 +332,7 @@ export class RevenueReport extends Component { title={ selectedChart.label } interval={ currentInterval } allowedIntervals={ allowedIntervals } + mode="time-comparison" pointLabelFormat={ formats.pointLabelFormat } tooltipTitle={ selectedChart.label } xFormat={ formats.xFormat } diff --git a/plugins/woocommerce-admin/client/components/chart/charts.js b/plugins/woocommerce-admin/client/components/chart/charts.js index 44977e317f7..61db7ba0bfe 100644 --- a/plugins/woocommerce-admin/client/components/chart/charts.js +++ b/plugins/woocommerce-admin/client/components/chart/charts.js @@ -100,6 +100,7 @@ class D3Chart extends Component { height, layout, margin, + mode, orderedKeys, pointLabelFormat, tooltipFormat, @@ -132,6 +133,7 @@ class D3Chart extends Component { line: getLine( xLineScale, yScale ), lineData, margin, + mode, orderedKeys: newOrderedKeys, pointLabelFormat, parseDate, @@ -221,6 +223,11 @@ D3Chart.propTypes = { right: 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. */ @@ -267,6 +274,7 @@ D3Chart.defaultProps = { top: 20, }, layout: 'standard', + mode: 'item-comparison', tooltipFormat: '%B %d, %Y', type: 'line', width: 600, diff --git a/plugins/woocommerce-admin/client/components/chart/index.js b/plugins/woocommerce-admin/client/components/chart/index.js index 0bb8cb1d113..f7c7c20e5b9 100644 --- a/plugins/woocommerce-admin/client/components/chart/index.js +++ b/plugins/woocommerce-admin/client/components/chart/index.js @@ -188,6 +188,7 @@ class Chart extends Component { const { dateParser, layout, + mode, pointLabelFormat, title, tooltipFormat, @@ -263,6 +264,7 @@ class Chart extends Component { dateParser={ dateParser } height={ 300 } margin={ margin } + mode={ mode } orderedKeys={ orderedKeys } pointLabelFormat={ pointLabelFormat } tooltipFormat={ tooltipFormat } @@ -320,6 +322,11 @@ Chart.propTypes = { * to the left or 'compact' has the legend below */ 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. */ @@ -350,6 +357,7 @@ Chart.defaultProps = { x2Format: '%b %Y', yFormat: '$.3s', layout: 'standard', + mode: 'item-comparison', type: 'line', interval: 'day', }; diff --git a/plugins/woocommerce-admin/client/components/chart/utils.js b/plugins/woocommerce-admin/client/components/chart/utils.js index b5b31db149e..676982423e4 100644 --- a/plugins/woocommerce-admin/client/components/chart/utils.js +++ b/plugins/woocommerce-admin/client/components/chart/utils.js @@ -3,7 +3,6 @@ /** * External dependencies */ - import { findIndex, get } from 'lodash'; import { max as d3Max } from 'd3-array'; 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 { line as d3Line } from 'd3-shape'; import { format as formatDate } from '@wordpress/date'; + /** * Internal dependencies */ @@ -84,6 +84,7 @@ export const getLineData = ( data, orderedKeys ) => values: data.map( d => ( { date: d.date, focus: row.focus, + label: get( d, [ row.key, 'label' ], '' ), value: get( d, [ row.key, 'value' ], 0 ), visible: row.visible, } ) ), @@ -314,6 +315,7 @@ export const drawAxis = ( node, params ) => { node .append( 'g' ) .attr( 'class', 'axis' ) + .attr( 'aria-hidden', 'true' ) .attr( 'transform', `translate(0,${ params.height })` ) .call( d3AxisBottom( xScale ) @@ -324,6 +326,7 @@ export const drawAxis = ( node, params ) => { node .append( 'g' ) .attr( 'class', 'axis axis-month' ) + .attr( 'aria-hidden', 'true' ) .attr( 'transform', `translate(3, ${ params.height + 20 })` ) .call( d3AxisBottom( xScale ) @@ -371,6 +374,7 @@ export const drawAxis = ( node, params ) => { node .append( 'g' ) .attr( 'class', 'axis y-axis' ) + .attr( 'aria-hidden', 'true' ) .attr( 'transform', 'translate(-50, 0)' ) .attr( 'text-anchor', 'start' ) .call( @@ -428,29 +432,29 @@ const showTooltip = ( node, params, d, position ) => { ` ); }; -const handleMouseOverBarChart = ( d, i, nodes, node, data, params, position ) => { - d3Select( nodes[ i ].parentNode ) +const handleMouseOverBarChart = ( date, parentNode, node, data, params, position ) => { + d3Select( parentNode ) .select( '.barfocus' ) .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 ) => { - d3Select( nodes[ i ].parentNode ) +const handleMouseOutBarChart = ( parentNode, params ) => { + d3Select( parentNode ) .select( '.barfocus' ) .attr( 'opacity', '0' ); params.tooltip.style( 'display', 'none' ); }; -const handleMouseOverLineChart = ( d, i, nodes, node, data, params, position ) => { - d3Select( nodes[ i ].parentNode ) +const handleMouseOverLineChart = ( date, parentNode, node, data, params, position ) => { + d3Select( parentNode ) .select( '.focus-grid' ) .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 ) => { - d3Select( nodes[ i ].parentNode ) +const handleMouseOutLineChart = ( parentNode, params ) => { + d3Select( parentNode ) .select( '.focus-grid' ) .attr( 'opacity', '0' ); params.tooltip.style( 'display', 'none' ); @@ -470,7 +474,9 @@ export const drawLines = ( node, data, params ) => { .data( params.lineData.filter( d => d.visible ) ) .enter() .append( 'g' ) - .attr( 'class', 'line-g' ); + .attr( 'class', 'line-g' ) + .attr( 'role', 'region' ) + .attr( 'aria-label', d => d.key ); series .append( 'path' ) @@ -501,10 +507,18 @@ export const drawLines = ( node, data, params ) => { } ) .attr( 'cx', d => params.xLineScale( new Date( d.date ) ) ) .attr( 'cy', d => params.yScale( d.value ) ) - .on( 'mouseover', ( d, i, nodes ) => - handleMouseOverLineChart( d, i, nodes, node, data, params ) - ) - .on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) ); + .attr( 'tabindex', '0' ) + .attr( 'aria-label', d => { + const label = d.label + ? 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 .append( 'g' ) @@ -532,15 +546,10 @@ export const drawLines = ( node, data, params ) => { .attr( 'width', d => d.width ) .attr( 'height', params.height ) .attr( 'opacity', 0 ) - .attr( 'tabindex', '0' ) .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 ) => { - 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 ) ); + .on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( nodes[ i ].parentNode, params ) ); }; export const drawBars = ( node, data, params ) => { @@ -552,7 +561,15 @@ export const drawBars = ( node, data, params ) => { .enter() .append( 'g' ) .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 .append( 'rect' ) @@ -569,8 +586,10 @@ export const drawBars = ( node, data, params ) => { params.orderedKeys.filter( row => row.visible ).map( row => ( { key: row.key, focus: row.focus, - value: d[ row.key ].value, + value: get( d, [ row.key, 'value' ], 0 ), + label: get( d, [ row.key, 'label' ], '' ), visible: row.visible, + date: d.date, } ) ) ) .enter() @@ -581,14 +600,20 @@ export const drawBars = ( node, data, params ) => { .attr( 'width', params.xGroupScale.bandwidth() ) .attr( 'height', d => params.height - params.yScale( d.value ) ) .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 => { const opacity = d.focus ? 1 : 0.1; return d.visible ? opacity : 0; } ) - .on( 'mouseover', ( d, i, nodes ) => - handleMouseOverBarChart( d, i, nodes, node, data, params ) - ) - .on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, params ) ); + .on( 'focus', ( d, i, nodes ) => { + const position = calculatePositionInChart( d3Event.target, node.node() ); + handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position ); + } ) + .on( 'blur', ( d, i, nodes ) => handleMouseOutBarChart( nodes[ i ].parentNode, params ) ); barGroup .append( 'rect' ) @@ -598,13 +623,8 @@ export const drawBars = ( node, data, params ) => { .attr( 'width', params.xGroupScale.range()[ 1 ] ) .attr( 'height', params.height ) .attr( 'opacity', '0' ) - .attr( 'tabindex', '0' ) .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 ) => { - 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 ) ); + .on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( nodes[ i ].parentNode, params ) ); };