Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
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