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 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
9 changes: 9 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 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.
55 changes: 55 additions & 0 deletions .github/prompts/accessibility_issue.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Accessibility Issue Fixing Prompt (Generic)

**Context:**
You are tasked with fixing an accessibility issue in the given project and validating it thoroughly.

---

## Steps

### 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.

---

### Step 2: FIX IMPLEMENTATION

1. Explain the root cause and target component of the bug to the user.
2. Implement the fix in the **target package or module** (provided by the user or derived from context).
3. Don't validate the issue at this stage; focus solely on implementing the fix.
4. Focus only on fixing the given bug:
- Do not introduce unrelated changes.
- Ensure no new bugs or syntax errors are introduced.
- Ensure the fix adheres to **accessibility standards** (e.g., ARIA roles, aria-labels, keyboard navigation, screen reader support).

---

### Step 3: VALIDATION USING PLAYWRIGHT MCP AND A11Y-ACCESSIBILITY MCP

1. Take repo specific context of development commands from `fluent_charts.prompt.md` file to build and run the storybook server.
2. **Wait for the server to start**. Only after the server is up and running, use the localhost server running in terminal to validate the bug using Playwright MCP.
3. **Use the same terminal session** to:
- Launch Playwright MCP.
- Perform interactive accessibility validation.
- Use A11Y-accessibility MCP for ARIA compliance.

---

### Step 4: SCREENSHOT CAPTURE AND ACCESSIBILITY SNAPSHOT

1. Capture **screenshots of the entire interactive component area**.
2. Ensure focus reaches the **interactive elements** (e.g., pressing Tab until focus reaches chart/controls).
3. Enumerate test cases before execution:
- Include actions (e.g., tab navigation, screen reader attribute check).
- Include validation steps (e.g., expected aria-label values).
4. Capture screenshots of:
- Every tab stop within the interactive area.
- Accessibility snapshots of ARIA attributes and labels.
5. Confirm no new accessibility regressions are introduced.

---

**Goal:** Fix and validate accessibility issues reliably across any repository or package.
30 changes: 30 additions & 0 deletions .github/prompts/fluent_charts.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Fluent UI Charts Accessibility Prompt Add-on

This file provides **Fluent UI charts-specific context** to be used in conjunction with `accessibility_fix.prompt.md`.

---

### Repository Structure

- **Monorepo Structure:**
Fluent UI is a monorepo.
- **Charts Packages:**
- `packages/charts/react-charting` → v8 charts implementation (reference only).
- `packages/charts/react-charts/library` → v9 charts implementation (fixes should be made here).

---

### Development Commands

**Don't use && as it runs on yarn workspace in powershell**

- Build package -

```bash
yarn --cwd packages/charts/react-charts/library nx run react-charts:build
```

- Start Storybook server for development -
```bash
yarn --cwd packages/charts/react-charts/library nx run react-charts:start
```
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
Loading