Skip to content

Commit

Permalink
Make line chart screen reader accessible (#27632)
Browse files Browse the repository at this point in the history
* make line chart screen reader accessible

* add change file

* add function
  • Loading branch information
krkshitij authored Apr 24, 2023
1 parent 860d570 commit 948b5bf
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Make line chart screen reader accessible",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
61 changes: 46 additions & 15 deletions packages/react-charting/src/components/LineChart/LineChart.base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,15 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
onMouseOut={this._handleMouseOut}
strokeWidth={activePoint === circleId ? DEFAULT_LINE_STROKE_SIZE : 0}
stroke={activePoint === circleId ? lineColor : ''}
role="img"
aria-label={this._getAriaLabel(i, 0)}
data-is-focusable={true}
ref={(e: SVGCircleElement | null) => {
this._refCallback(e!, circleId);
}}
onFocus={() => this._handleFocus(circleId, x1, xAxisCalloutData, circleId, xAxisCalloutAccessibilityData)}
onBlur={this._handleMouseOut}
{...this._getClickHandler(this._points[i].data[0].onDataPointClick)}
/>,
);
}
Expand All @@ -576,7 +585,7 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
const gaps = this._points[i].gaps?.sort((a, b) => a.startIndex - b.startIndex) ?? [];

// Use path rendering technique for larger datasets to optimize performance.
if (this.props.optimizeLargeData!) {
if (this.props.optimizeLargeData && this._points[i].data.length > 1) {
const line = d3Line()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.x((d: any) => this._xAxisScale(d[0]))
Expand Down Expand Up @@ -634,8 +643,12 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
onMouseMove={this._onMouseOverLargeDataset.bind(this, i, verticaLineHeight)}
onMouseOver={this._onMouseOverLargeDataset.bind(this, i, verticaLineHeight)}
onMouseOut={this._handleMouseOut}
onClick={this._onLineClick.bind(this, this._points[i].onLineClick)}
{...this._getClickHandler(this._points[i].onLineClick)}
opacity={1}
role="img"
aria-label={`${legendVal}, series ${i + 1} of ${this._points.length} with ${
this._points[i].data.length
} data points.`}
/>,
);
} else {
Expand All @@ -650,6 +663,10 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
strokeWidth={strokeWidth}
strokeLinecap={this._points[i].lineOptions?.strokeLinecap ?? 'round'}
opacity={0.1}
role="img"
aria-label={`${legendVal}, series ${i + 1} of ${this._points.length} with ${
this._points[i].data.length
} data points.`}
/>,
);
}
Expand All @@ -664,7 +681,6 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
cx={0}
cy={0}
fill={theme!.palette.white}
data-is-focusable={true}
strokeWidth={DEFAULT_LINE_STROKE_SIZE}
stroke={lineColor}
visibility={isPointHighlighted ? 'visibility' : 'hidden'}
Expand All @@ -673,7 +689,7 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
onMouseOut={this._handleMouseOut}
/>,
);
} else {
} else if (!this.props.optimizeLargeData) {
for (let j = 1; j < this._points[i].data.length; j++) {
const gapResult = this._checkInGap(j, gaps, gapIndex);
const isInGap = gapResult.isInGap;
Expand Down Expand Up @@ -726,11 +742,13 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
onMouseOut={this._handleMouseOut}
onFocus={() => this._handleFocus(lineId, x1, xAxisCalloutData, circleId, xAxisCalloutAccessibilityData)}
onBlur={this._handleMouseOut}
onClick={this._onDataPointClick.bind(this, this._points[i].data[j - 1].onDataPointClick)}
{...this._getClickHandler(this._points[i].data[j - 1].onDataPointClick)}
opacity={isLegendSelected && !currentPointHidden ? 1 : 0.01}
fill={this._getPointFill(lineColor, circleId, j, false)}
stroke={lineColor}
strokeWidth={strokeWidth}
role="img"
aria-label={this._getAriaLabel(i, j - 1)}
/>,
);
if (j + 1 === this._points[i].data.length) {
Expand Down Expand Up @@ -778,11 +796,13 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
this._handleFocus(lineId, x2, lastCirlceXCallout, lastCircleId, lastCirlceXCalloutAccessibilityData)
}
onBlur={this._handleMouseOut}
onClick={this._onDataPointClick.bind(this, this._points[i].data[j].onDataPointClick)}
{...this._getClickHandler(this._points[i].data[j].onDataPointClick)}
opacity={isLegendSelected && !lastPointHidden ? 1 : 0.01}
fill={this._getPointFill(lineColor, lastCircleId, j, true)}
stroke={lineColor}
strokeWidth={strokeWidth}
role="img"
aria-label={this._getAriaLabel(i, j)}
/>,
);
/* eslint-enable react/jsx-no-bind */
Expand Down Expand Up @@ -847,7 +867,7 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
strokeDasharray={this._points[i].lineOptions?.strokeDasharray}
strokeDashoffset={this._points[i].lineOptions?.strokeDashoffset}
opacity={1}
onClick={this._onLineClick.bind(this, this._points[i].onLineClick)}
{...this._getClickHandler(this._points[i].onLineClick)}
/>,
);
}
Expand Down Expand Up @@ -1103,7 +1123,6 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt

if (found) {
const _this = this;
d3Select('#' + circleId).attr('aria-labelledby', `toolTip${this._uniqueCallOutID}`);
d3Select(`#${this._verticalLine}`)
.attr('transform', () => `translate(${_this._xAxisScale(x)}, 0)`)
.attr('visibility', 'visibility');
Expand Down Expand Up @@ -1173,16 +1192,18 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
}
};

private _onLineClick = (func: () => void) => {
/**
* Screen readers announce an element as clickable if the onClick attribute is set.
* This function sets the attribute only when a click event handler is provided.
*/
private _getClickHandler = (func?: () => void): { onClick?: () => void } => {
if (func) {
func();
return {
onClick: func,
};
}
};

private _onDataPointClick = (func: () => void) => {
if (func) {
func();
}
return {};
};

private _handleMouseOut = () => {
Expand Down Expand Up @@ -1326,4 +1347,14 @@ export class LineChartBase extends React.Component<ILineChartProps, ILineChartSt
private _getColorFillBarOpacity = (colorFillBar: IColorFillBarsProps) => {
return colorFillBar.applyPattern ? 1 : 0.4;
};

private _getAriaLabel = (lineIndex: number, pointIndex: number): string => {
const line = this._points[lineIndex];
const point = line.data[pointIndex];
const formattedDate = point.x instanceof Date ? point.x.toLocaleString() : point.x;
const xValue = point.xAxisCalloutData || formattedDate;
const legend = line.legend;
const yValue = point.yAxisCalloutData || point.y;
return point.callOutAccessibilityData?.ariaLabel || `${xValue}. ${legend}, ${yValue}.`;
};
}
Loading

0 comments on commit 948b5bf

Please sign in to comment.