Skip to content

Commit e869d28

Browse files
Anush2303Anush
andauthored
fix(react-charts): fix a11y bugs in line chart (#35550)
Co-authored-by: Anush <anushgupta@microsoft.com>
1 parent c9f3537 commit e869d28

File tree

3 files changed

+117
-180
lines changed

3 files changed

+117
-180
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix a11y bugs in line chart",
4+
"packageName": "@fluentui/react-charts",
5+
"email": "anushgupta@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/react-charts/library/src/components/LineChart/LineChart.tsx

Lines changed: 110 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,6 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
627627
stroke={activePoint === circleId ? lineColor : ''}
628628
role="img"
629629
aria-label={_points[i].data[0].text ?? _getAriaLabel(i, 0)}
630-
data-is-focusable={isLegendSelected}
631630
ref={(e: SVGCircleElement | null) => {
632631
_refCallback(e!, circleId);
633632
}}
@@ -724,7 +723,6 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
724723
key={lineId}
725724
d={line(lineData)!}
726725
fill="transparent"
727-
data-is-focusable={true}
728726
stroke={lineColor}
729727
strokeWidth={strokeWidth}
730728
strokeLinecap={_points[i].lineOptions?.strokeLinecap ?? 'round'}
@@ -744,7 +742,7 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
744742
key={lineId}
745743
d={line(lineData)!}
746744
fill="transparent"
747-
data-is-focusable={false}
745+
tabIndex={-1}
748746
stroke={lineColor}
749747
strokeWidth={strokeWidth}
750748
strokeLinecap={_points[i].lineOptions?.strokeLinecap ?? 'round'}
@@ -764,6 +762,7 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
764762
cy={0}
765763
fill={tokens.colorNeutralBackground1}
766764
strokeWidth={DEFAULT_LINE_STROKE_SIZE}
765+
tabIndex={isLegendSelected ? 0 : undefined}
767766
stroke={lineColor}
768767
visibility={'hidden'}
769768
onMouseMove={event => _onMouseOverLargeDataset(i, verticaLineHeight, event, yScale)}
@@ -800,11 +799,13 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
800799
? tokens.colorNeutralBackground1
801800
: perPointColor || _points[i]?.color || lineColor
802801
}
802+
tabIndex={isLegendSelected ? 0 : undefined}
803803
stroke={perPointColor || lineColor}
804804
strokeWidth={1}
805805
opacity={isLegendSelected ? 1 : 0.1}
806-
onMouseMove={_onMouseOverLargeDataset.bind(i, verticaLineHeight, yScale)}
807-
onMouseOver={_onMouseOverLargeDataset.bind(i, verticaLineHeight, yScale)}
806+
onMouseMove={event => _onMouseOverLargeDataset(i, verticaLineHeight, event, yScale)}
807+
onMouseOver={event => _onMouseOverLargeDataset(i, verticaLineHeight, event, yScale)}
808+
onFocus={event => _onFocusLargeDataset(i, verticaLineHeight, event, yScale, k)}
808809
onMouseOut={_handleMouseOut}
809810
/>,
810811
);
@@ -852,7 +853,7 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
852853
r={currentMarkerSize ? (currentMarkerSize! * extraMaxPixels) / maxMarkerSize : 4}
853854
cx={xPoint1}
854855
cy={yPoint1}
855-
data-is-focusable={isLegendSelected}
856+
tabIndex={isLegendSelected ? 0 : undefined}
856857
onMouseOver={event =>
857858
_handleHover(
858859
x1,
@@ -925,7 +926,6 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
925926
id={circleId}
926927
key={circleId}
927928
d={path}
928-
data-is-focusable={isLegendSelected}
929929
onMouseOver={(event: React.MouseEvent<SVGElement>) =>
930930
_handleHover(
931931
x1,
@@ -1006,7 +1006,7 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
10061006
r={currentMarkerSize ? (currentMarkerSize! * extraMaxPixels) / maxMarkerSize : 4}
10071007
cx={xPoint2}
10081008
cy={yPoint2}
1009-
data-is-focusable={isLegendSelected}
1009+
tabIndex={isLegendSelected ? 0 : undefined}
10101010
onMouseOver={event =>
10111011
_handleHover(
10121012
x2,
@@ -1081,7 +1081,6 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
10811081
id={lastCircleId}
10821082
key={lastCircleId}
10831083
d={path}
1084-
data-is-focusable={isLegendSelected}
10851084
onMouseOver={(event: React.MouseEvent<SVGElement>) =>
10861085
_handleHover(
10871086
x2,
@@ -1445,6 +1444,98 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
14451444
_refArray.push({ index: legendTitle, refElement: element });
14461445
}
14471446

1447+
// Helper function to update highlight circle, vertical line, and callout for large datasets
1448+
const _updateLargeDatasetHighlightAndCallout = (
1449+
linenumber: number,
1450+
lineHeight: number,
1451+
pointToHighlight: LineChartDataPoint,
1452+
xAxisCalloutData: string | undefined,
1453+
formattedDate: string | number,
1454+
yScale: ScaleLinear<number, number>,
1455+
) => {
1456+
// Check if this point is plottable. If not, close the popover and return.
1457+
const xPoint = _xAxisScale(pointToHighlight.x);
1458+
const yPoint = yScale(pointToHighlight.y);
1459+
if (!isPlottable(xPoint, yPoint)) {
1460+
return;
1461+
}
1462+
1463+
const found = findCalloutPoints(calloutPointsRef.current, pointToHighlight.x) as
1464+
| CustomizedCalloutData
1465+
| undefined;
1466+
1467+
const pointToHighlightUpdated =
1468+
nearestCircleToHighlight === null ||
1469+
(nearestCircleToHighlight !== null &&
1470+
pointToHighlight !== null &&
1471+
(nearestCircleToHighlight.x !== pointToHighlight.x || nearestCircleToHighlight.y !== pointToHighlight.y));
1472+
1473+
// if no points need to be called out then don't show vertical line and callout card
1474+
if (found && pointToHighlightUpdated) {
1475+
_uniqueCallOutID = `#${_staticHighlightCircle}_${linenumber}`;
1476+
1477+
d3Select(`#${_staticHighlightCircle}_${linenumber}`)
1478+
.attr('cx', `${xPoint}`)
1479+
.attr('cy', `${yPoint}`)
1480+
.attr('visibility', 'visibility');
1481+
1482+
d3Select(`#${_verticalLine}`)
1483+
.attr('transform', () => `translate(${xPoint}, ${yPoint})`)
1484+
.attr('visibility', 'visibility')
1485+
.attr('y2', `${lineHeight - 5 - yPoint}`);
1486+
1487+
const targetElement = document.getElementById(`${_staticHighlightCircle}_${linenumber}`);
1488+
const rect = targetElement!.getBoundingClientRect();
1489+
setNearestCircleToHighlight(pointToHighlight);
1490+
updatePosition(rect.x, rect.y);
1491+
setStackCalloutProps(found!);
1492+
setYValueHover(found.values);
1493+
setDataPointCalloutProps(found!);
1494+
xAxisCalloutData ? setHoverXValue(xAxisCalloutData) : setHoverXValue(formattedDate);
1495+
setActivePoint('');
1496+
}
1497+
1498+
if (!found) {
1499+
setPopoverOpen(false);
1500+
setNearestCircleToHighlight(pointToHighlight);
1501+
setActivePoint('');
1502+
}
1503+
};
1504+
1505+
const _onFocusLargeDataset = (
1506+
linenumber: number,
1507+
lineHeight: number,
1508+
focusEvent: React.FocusEvent<SVGRectElement | SVGPathElement | SVGCircleElement>,
1509+
yScale: ScaleLinear<number, number>,
1510+
pointIndex: number,
1511+
) => {
1512+
focusEvent.persist();
1513+
const { data } = props;
1514+
const { lineChartData } = data;
1515+
1516+
// For focus events, we use the provided point index directly
1517+
const pointToHighlight: LineChartDataPoint = lineChartData![linenumber].data[pointIndex] as LineChartDataPoint;
1518+
1519+
if (!pointToHighlight) {
1520+
return;
1521+
}
1522+
1523+
const { xAxisCalloutData } = pointToHighlight;
1524+
const formattedDate: string | number =
1525+
pointToHighlight.x instanceof Date
1526+
? formatDateToLocaleString(pointToHighlight.x, props.culture, props.useUTC as boolean)
1527+
: pointToHighlight.x;
1528+
1529+
_updateLargeDatasetHighlightAndCallout(
1530+
linenumber,
1531+
lineHeight,
1532+
pointToHighlight,
1533+
xAxisCalloutData,
1534+
formattedDate,
1535+
yScale,
1536+
);
1537+
};
1538+
14481539
const _onMouseOverLargeDataset = (
14491540
linenumber: number,
14501541
lineHeight: number,
@@ -1495,55 +1586,20 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
14951586
}
14961587

14971588
const { xAxisCalloutData } = lineChartData![linenumber].data[index as number];
1498-
const formattedDate =
1589+
const formattedDate: string | number =
14991590
xPointToHighlight instanceof Date
15001591
? formatDateToLocaleString(xPointToHighlight, props.culture, props.useUTC as boolean)
15011592
: xPointToHighlight;
1502-
const found = findCalloutPoints(calloutPointsRef.current, xPointToHighlight) as CustomizedCalloutData | undefined;
15031593
const pointToHighlight: LineChartDataPoint = lineChartData![linenumber].data[index!] as LineChartDataPoint;
15041594

1505-
// Check if this point is plottable. If not, close the popover and return.
1506-
const xPoint = _xAxisScale(pointToHighlight.x);
1507-
const yPoint = yScale(pointToHighlight.y);
1508-
if (!isPlottable(xPoint, yPoint)) {
1509-
return;
1510-
}
1511-
1512-
const pointToHighlightUpdated =
1513-
nearestCircleToHighlight === null ||
1514-
(nearestCircleToHighlight !== null &&
1515-
pointToHighlight !== null &&
1516-
(nearestCircleToHighlight.x !== pointToHighlight.x || nearestCircleToHighlight.y !== pointToHighlight.y));
1517-
// if no points need to be called out then don't show vertical line and callout card
1518-
if (found && pointToHighlightUpdated) {
1519-
_uniqueCallOutID = `#${_staticHighlightCircle}_${linenumber}`;
1520-
1521-
d3Select(`#${_staticHighlightCircle}_${linenumber}`)
1522-
.attr('cx', `${xPoint}`)
1523-
.attr('cy', `${yPoint}`)
1524-
.attr('visibility', 'visibility');
1525-
1526-
d3Select(`#${_verticalLine}`)
1527-
.attr('transform', () => `translate(${xPoint}, ${yPoint})`)
1528-
.attr('visibility', 'visibility')
1529-
.attr('y2', `${lineHeight - 5 - yPoint}`);
1530-
1531-
const targetElement = document.getElementById(`${_staticHighlightCircle}_${linenumber}`);
1532-
const rect = targetElement!.getBoundingClientRect();
1533-
setNearestCircleToHighlight(pointToHighlight);
1534-
updatePosition(rect.x, rect.y);
1535-
setStackCalloutProps(found!);
1536-
setYValueHover(found.values);
1537-
setDataPointCalloutProps(found!);
1538-
xAxisCalloutData ? setHoverXValue(xAxisCalloutData) : setHoverXValue(formattedDate);
1539-
setActivePoint('');
1540-
}
1541-
1542-
if (!found) {
1543-
setPopoverOpen(false);
1544-
setNearestCircleToHighlight(pointToHighlight);
1545-
setActivePoint('');
1546-
}
1595+
_updateLargeDatasetHighlightAndCallout(
1596+
linenumber,
1597+
lineHeight,
1598+
pointToHighlight,
1599+
xAxisCalloutData,
1600+
formattedDate,
1601+
yScale,
1602+
);
15471603
};
15481604

15491605
function _handleFocus(
@@ -1818,7 +1874,7 @@ export const LineChart: React.FunctionComponent<LineChartProps> = React.forwardR
18181874
props.getCalloutDescriptionMessage && stackCalloutProps
18191875
? props.getCalloutDescriptionMessage(stackCalloutProps)
18201876
: undefined,
1821-
'data-is-focusable': true,
1877+
tabIndex: 0,
18221878
xAxisCalloutAccessibilityData: xAxisCalloutAccessibilityData,
18231879
...props.calloutProps,
18241880
isPopoverOpen: isPopoverOpen,

0 commit comments

Comments
 (0)