Skip to content

A11Y testing using AI #34924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
26 changes: 26 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Repository Structure

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Avatar Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Avatar Converged.badgeMask - RTL.normal.chromium.png 1 Changed
vr-tests-react-components/Avatar Converged.badgeMask.normal.chromium.png 5 Changed
vr-tests-react-components/Charts-DonutChart 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png 27053 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png 30793 Changed
vr-tests-react-components/Charts-HorizontalBarChart 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-HorizontalBarChart.Basic - Dark Mode.hover.chromium.png 530 Changed
vr-tests-react-components/Charts-HorizontalBarChart.Basic.hover.chromium.png 1652 Changed
vr-tests-react-components/Drawer 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Drawer.overlay drawer full.chromium.png 3277 Changed
vr-tests-react-components/Drawer.overlay drawer full - High Contrast.chromium.png 2292 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 16 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 721 Changed
vr-tests-react-components/Skeleton converged 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Skeleton converged.Opaque Skeleton with square - Dark Mode.default.chromium.png 2 Changed
vr-tests-react-components/TagPicker 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png 635 Changed

There were 1 duplicate changes discarded. Check the build logs for more information.


Fluent UI is a **monorepo** managed with Yarn workspace.
It contains multiple packages, each with its own purpose and versioning strategy. The main packages of interest for charting components are:

Key structure:

- **`react-charting` (v8)** → Legacy charting components built on Fluent UI v8 styling & patterns.
- **`react-charts` (v9)** → Next-generation chart components based on Fluent UI v9, focusing on modern React patterns, hooks, and convergence goals.

## Build & Setup

### Step 1:

Install dependencies (from root):
run `yarn install`

### Step 2:

Go to the `packages/charts/react-charts/library` directory:
run `yarn build` to build the v9 charting components.

### Step 3:

Run the local Storybook server:
run `yarn start` from `packages/charts/react-charts/library` to run the local Storybook server for the v9 charting components.
63 changes: 63 additions & 0 deletions .github/prompts/accessibility_issue.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Accessibility Issue Fixing Prompt

**Context:**
You are tasked with fixing an accessibility issue in the charts component of the Fluent UI library and validate it.

**Instructions:**

### Step 1: Bug Context Gathering

- Take the concerned project name and bug id from the user.

- Use the ADO MCP Server to gather all relevant context for the bug/work item with the given ID.

- Ensure you understand the nature of the bug, its impact on accessibility, and any related user stories or acceptance criteria.

### Step 2: Fix Implementation

- Explain the root cause and target component of the bug to the user.

- fix the bug and focus only on the solution of given bug. don't add extra things.

- Don't introduce any extra bugs. fix all syntax errors before proceeding.

- Ensure that the fix adheres to accessibility standards, such as WCAG 2.1.

- Implement the fix in `packages/charts/react-charts/library` package. This package represents the v9 version of charts.

- For reference, you can refer the `packages/charts/react-charting/src` package which holds the v8 version of charts. Don't change any code in this folder.

### Step 3: Validation using Playwright MCP and a11y-accessibility MCP

validate the bug using playwright and a11y-accessibility MCP. Follow the following steps for this validation:

- Go to directory `packages/charts/react-charts/library` from root `fluentui` if not already there.

**Run all yarn commands from `packages/charts/react-charts/library` directory.**

- Refer package.json in `packages/charts/react-charts/library` to build the package. Solve build errors if any before proceeding.

- Refer package.json in `packages/charts/react-charts/library` to run the storybook localhost server and ensure it is running.

- **Wait for the server to start.** Only after the storybook is up and running, Use the **localhost server link running in terminal** to validate the bug using playwright MCP in the same terminal session. **Don't start a new terminal session.**

- Don't open a simple browser window. Use the Playwright MCP to run the tests.

- Wait for the chart to fully render.

### Step 4: Screenshot Capture and Accessibility Testing

**Capture every screenshot of entire page**

- **Click on the example story and in the interactive chart canvas area immediately** to ensure focus reaches to the chart controls. We don't want to test anything outside chart controls. Wait till control reaches interactive chart elements.

- First enumerate all the test cases that you will run to validate the fix. Also tell me the action and validation steps for each test case.

- Start validating the fix by capturing screenshots of the interactive chart elements using Playwright MCP. **Start capturing screenshots after focus reaches the interactive chart elements. Don't take screenshots before that of other components.**

- **Capture screenshots of entire chart canvas after each step** on pressing tab keys. Continue this process till control reaches outside the interactive chart elements.

- Check for any ARIA attributes that may need to be added or modified to enhance accessibility.
**At the end, capture accessibility snapshot of the chart canvas to validate references of aria-labels**.

- Ensure that the fix does not introduce any new issues or regressions in the charting components.
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,6 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
opacity={layerOpacity}
fillOpacity={_getOpacity(points[index]!.legend)}
onMouseMove={event => _onRectMouseMove(event)}
onFocus={event => _handleFocus(event, index, 0, `${_circleId}_${index}`)}
onMouseOut={_onRectMouseOut}
onMouseOver={event => _onRectMouseMove(event)}
/>
Expand Down Expand Up @@ -702,7 +701,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
onMouseOut={_onRectMouseOut}
onMouseOver={event => _onRectMouseMove(event)}
onClick={() => _onDataPointClick(points[index]!.data[pointIndex].onDataPointClick!)}
onFocus={event => _handleFocus(event, index, pointIndex, circleId)}
onFocus={() => _handleFocus(index, pointIndex, circleId)}
onBlur={_handleBlur}
{...getSecureProps(pointOptions)}
r={_getCircleRadius(xDataPoint, circleRadius, circleId, legend)}
Expand Down Expand Up @@ -732,8 +731,9 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
fill={_updateCircleFillColor(xDataPoint, lineColor, circleId)}
onMouseOut={_onRectMouseOut}
onMouseOver={event => _onRectMouseMove(event)}
onFocus={event => _handleFocus(event, index, pointIndex, circleId)}
onClick={() => _onDataPointClick(points[index]!.data[pointIndex].onDataPointClick!)}
onFocus={() => _handleFocus(index, pointIndex, circleId)}
onBlur={_handleBlur}
{...getSecureProps(pointOptions)}
r={_getCircleRadius(xDataPoint, circleRadius, circleId, legend)}
/>,
Expand Down Expand Up @@ -836,20 +836,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
: [];
}

function _handleFocus(
event: React.FocusEvent<SVGCircleElement, Element>,
lineIndex: number,
pointIndex: number,
circleId: string,
) {
let cx = 0;
let cy = 0;

const targetRect = (event.target as SVGCircleElement).getBoundingClientRect();
cx = targetRect.left + targetRect.width / 2;
cy = targetRect.top + targetRect.height / 2;
_updatePosition(cx, cy);

function _handleFocus(lineIndex: number, pointIndex: number, circleId: string) {
const { x, y, xAxisCalloutData } = props.data.lineChartData![lineIndex].data[pointIndex];
const formattedDate = x instanceof Date ? formatDate(x, props.useUTC) : x;
const modifiedXVal = x instanceof Date ? x.getTime() : x;
Expand Down Expand Up @@ -991,7 +978,9 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
onMouseOver={event => _onRectMouseMove(event)}
/>
</g>
<g>{_chart}</g>
<g role="application" aria-label="Area chart data points">
{_chart}
</g>
</>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl
_updateChart(props);
}, [props]);

function _onFocus(data: ChartDataPoint, id: string, event: React.FocusEvent<SVGPathElement, Element>): void {
props.onFocusCallback!(data, id, event, currentRef.current);
function _onFocus(data: ChartDataPoint, id: string): void {
props.onFocusCallback!(data, id, currentRef.current);
}

function _hoverOn(data: ChartDataPoint, mouseEvent: React.MouseEvent<SVGPathElement>): void {
Expand All @@ -46,12 +46,6 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl
return point.callOutAccessibilityData?.ariaLabel || (legend ? `${legend}, ` : '') + `${yValue}.`;
}

function _shouldHighlightArc(legend?: string): boolean {
const { activeArc } = props;
// If no activeArc is provided, highlight all arcs. Otherwise, only highlight the arcs that are active.
return !activeArc || activeArc.length === 0 || legend === undefined || activeArc.includes(legend);
}

function _renderArcLabel(className: string) {
const { data, innerRadius, outerRadius, showLabelsInPercent, totalValue, hideLabels, activeArc } = props;

Expand Down Expand Up @@ -99,41 +93,27 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl
(typeof props.data!.data.legend === 'string' ? props.data!.data.legend.replace(/\s+/g, '') : '') +
props.data!.data.data;
const opacity: number = props.activeArc === props.data!.data.legend || props.activeArc === '' ? 1 : 0.1;
const cornerRadius = props.roundCorners ? 3 : 0;
return (
<g ref={currentRef}>
{!!focusedArcId && focusedArcId === id && (
// TODO innerradius and outerradius were absent
<path
id={id + 'focusRing'}
d={
arc.cornerRadius(cornerRadius)({
...props.data!,
innerRadius: props.innerRadius,
outerRadius: props.outerRadius,
})!
}
d={arc({ ...props.focusData!, innerRadius: props.innerRadius, outerRadius: props.outerRadius })!}
className={classes.focusRing}
/>
)}
<path
// TODO innerradius and outerradius were absent
id={id}
d={
arc.cornerRadius(cornerRadius)({
...props.data!,
innerRadius: props.innerRadius,
outerRadius: props.outerRadius,
})!
}
d={arc({ ...props.data!, innerRadius: props.innerRadius, outerRadius: props.outerRadius })!}
className={classes.root}
style={{ fill: props.color, cursor: href ? 'pointer' : 'default' }}
onFocus={event => _onFocus(props.data!.data, id, event)}
onFocus={_onFocus.bind(this, props.data!.data, id)}
data-is-focusable={props.activeArc === props.data!.data.legend || props.activeArc === ''}
onMouseOver={event => _hoverOn(props.data!.data, event)}
onMouseMove={event => _hoverOn(props.data!.data, event)}
onMouseOver={_hoverOn.bind(this, props.data!.data)}
onMouseMove={_hoverOn.bind(this, props.data!.data)}
onMouseLeave={_hoverOff}
tabIndex={_shouldHighlightArc(props.data!.data.legend!) ? 0 : undefined}
onBlur={_onBlur}
opacity={opacity}
onClick={props.data?.data.onClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,6 @@ export interface ArcProps {
* Additional CSS class(es) to apply to the Chart.
*/
className?: string;

/**
* Prop to enable the round corners in the chart
* @default false
*/
roundCorners?: boolean;
}

export interface ArcData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const donutArcClassNames: SlotClassNames<ArcStyles> = {
const useStyles = makeStyles({
root: {
cursor: 'default',
outline: 'transparent',
...shorthands.outline('transparent'),
stroke: tokens.colorNeutralBackground1,
'& selectors': {
'::-moz-focus-inner': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,7 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
return legends;
}

function _focusCallback(data: ChartDataPoint, id: string, e: React.FocusEvent<SVGPathElement>): void {
let cx = 0;
let cy = 0;

const targetRect = (e.target as SVGPathElement).getBoundingClientRect();
cx = targetRect.left + targetRect.width / 2;
cy = targetRect.top + targetRect.height / 2;
updatePosition(cx, cy);
function _focusCallback(data: ChartDataPoint, id: string, element: SVGPathElement): void {
setPopoverOpen(selectedLegend === '' || selectedLegend === data.legend);
setValue(data.data!.toString());
setLegend(data.legend);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const Pie: React.FunctionComponent<PieProps> = React.forwardRef<HTMLDivEl
.value((d: any) => d.data)
.padAngle(0);

function _focusCallback(data: ChartDataPoint, id: string, e: React.FocusEvent<SVGPathElement>): void {
function _focusCallback(data: ChartDataPoint, id: string, e: SVGPathElement): void {
props.onFocusCallback!(data, id, e);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,19 +389,11 @@ export const GroupedVerticalBarChart: React.FC<GroupedVerticalBarChartProps> = R
};

const onBarFocus = (
event: React.FocusEvent<SVGRectElement, Element>,
pointData: GVBarChartSeriesPoint,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
groupData: any,
refArrayIndexNumber: number,
): void => {
let x = 0;
let y = 0;

const targetRect = (event.target as SVGRectElement).getBoundingClientRect();
x = targetRect.left + targetRect.width / 2;
y = targetRect.top + targetRect.height / 2;
updatePosition(x, y);
_refArray.forEach((obj: RefArrayData, index: number) => {
if (obj.index === pointData.legend && refArrayIndexNumber === index) {
setPopoverOpen(_noLegendHighlighted() || _legendHighlighted(pointData.legend));
Expand Down Expand Up @@ -472,16 +464,17 @@ export const GroupedVerticalBarChart: React.FC<GroupedVerticalBarChartProps> = R
width={_barWidth}
x={xPoint}
y={yPoint}
data-is-focusable={!props.hideTooltip && (_legendHighlighted(pointData.legend) || _noLegendHighlighted())}
opacity={_getOpacity(pointData.legend)}
ref={(e: SVGRectElement | null) => {
_refCallback(e!, pointData.legend, refIndexNumber);
}}
fill={startColor}
rx={0}
onMouseOver={event => onBarHover(pointData, singleSet, event)}
onMouseMove={event => onBarHover(pointData, singleSet, event)}
onMouseOver={onBarHover.bind(null, pointData, singleSet)}
onMouseMove={onBarHover.bind(null, pointData, singleSet)}
onMouseOut={_onBarLeave}
onFocus={event => onBarFocus(event, pointData, singleSet, refIndexNumber)}
onFocus={onBarFocus.bind(null, pointData, singleSet, refIndexNumber)}
onBlur={_onBarLeave}
onClick={pointData.onClick}
aria-label={getAriaLabel(pointData, singleSet.xAxisPoint)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const HorizontalBarChart: React.FunctionComponent<HorizontalBarChartProps
}

function _hoverOn(
event: React.FocusEvent<SVGRectElement> | React.MouseEvent<SVGRectElement>,
event: React.MouseEvent<SVGRectElement, MouseEvent>,
hoverVal: string | number | Date,
point: ChartDataPoint,
): void {
Expand All @@ -57,21 +57,7 @@ export const HorizontalBarChart: React.FunctionComponent<HorizontalBarChartProps
(_legendHighlighted(point.legend) || _noLegendHighlighted())
) {
_calloutAnchorPoint = point;
let x = 0;
let y = 0;

if ('clientX' in event && event.clientX && event.clientY) {
// Mouse event
x = event.clientX;
y = event.clientY;
} else {
// Focus event
const targetRect = (event.target as SVGRectElement).getBoundingClientRect();
x = targetRect.left + targetRect.width / 2;
y = targetRect.top + targetRect.height / 2;
}

updatePosition(x, y);
updatePosition(event.clientX, event.clientY);
setHoverValue(hoverVal);
setLineColor(point.color!);
setLegend(point.legend!);
Expand Down Expand Up @@ -309,11 +295,12 @@ export const HorizontalBarChart: React.FunctionComponent<HorizontalBarChartProps
: startingPoint[index] + index * barSpacingInPercent
}%`}
y={0}
data-is-focusable={point.legend !== '' ? true : false}
width={value + '%'}
height={_barHeight}
fill={color}
onMouseOver={point.legend !== '' ? event => _hoverOn(event, xValue, point) : undefined}
onFocus={point.legend !== '' ? event => _hoverOn(event, xValue, point) : undefined}
onFocus={point.legend !== '' ? event => _hoverOn.bind(event, xValue, point) : undefined}
role="img"
aria-label={_getAriaLabel(point)}
onBlur={_hoverOff}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,19 +271,7 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent<HorizontalBarCh
}

// eslint-disable-next-line @typescript-eslint/no-shadow
function _onBarFocus(
event: React.FocusEvent<SVGRectElement, Element>,
point: HorizontalBarChartWithAxisDataPoint,
refArrayIndexNumber: number,
color: string,
): void {
let cx = 0;
let cy = 0;

const targetRect = (event.target as SVGRectElement).getBoundingClientRect();
cx = targetRect.left + targetRect.width / 2;
cy = targetRect.top + targetRect.height / 2;
_updatePosition(cx, cy);
function _onBarFocus(point: HorizontalBarChartWithAxisDataPoint, refArrayIndexNumber: number, color: string): void {
if ((isLegendSelected === false || _isLegendHighlighted(point.legend)) && _calloutAnchorPoint !== point) {
// eslint-disable-next-line @typescript-eslint/no-shadow
_refArray.forEach((obj: RefArrayData, index: number) => {
Expand Down Expand Up @@ -444,7 +432,7 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent<HorizontalBarCh
role="img"
aria-labelledby={`toolTip${_calloutId}`}
onMouseLeave={_onBarLeave}
onFocus={event => _onBarFocus(event, point, index, startColor)}
onFocus={() => _onBarFocus(point, index, startColor)}
onBlur={_onBarLeave}
fill={startColor}
opacity={shouldHighlight ? 1 : 0.1}
Expand Down Expand Up @@ -611,7 +599,7 @@ export const HorizontalBarChartWithAxis: React.FunctionComponent<HorizontalBarCh
onBlur={_onBarLeave}
data-is-focusable={shouldHighlight}
opacity={shouldHighlight ? 1 : 0.1}
onFocus={event => _onBarFocus(event, point, index, startColor)}
onFocus={() => _onBarFocus(point, index, startColor)}
fill={startColor}
tabIndex={point.legend !== '' ? 0 : undefined}
/>
Expand Down
Loading
Loading