Skip to content
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

accessibilityLayer should respect a reversed XAxis #4225

Merged
merged 13 commits into from
Feb 29, 2024
14 changes: 11 additions & 3 deletions src/chart/AccessibilityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface InitiableOptions {
container?: HTMLElement;
layout?: LayoutType;
offset?: ContainerOffset;
/* Is the chart oriented left-to-right? true = left-to-right, false = right-to-left */
ltr?: boolean;
}

export class AccessibilityManager {
Expand All @@ -26,18 +28,22 @@ export class AccessibilityManager {

private offset: InitiableOptions['offset'];

private ltr = true;

public setDetails({
coordinateList = null,
container = null,
layout = null,
offset = null,
mouseHandlerCallback = null,
ltr = null,
}: InitiableOptions) {
this.coordinateList = coordinateList ?? this.coordinateList ?? [];
this.container = container ?? this.container;
this.layout = layout ?? this.layout;
this.offset = offset ?? this.offset;
this.mouseHandlerCallback = mouseHandlerCallback ?? this.mouseHandlerCallback;
this.ltr = ltr ?? this.ltr;

// Keep activeIndex in the bounds between 0 and the last coordinate index
this.activeIndex = Math.min(Math.max(this.activeIndex, 0), this.coordinateList.length - 1);
Expand All @@ -55,16 +61,18 @@ export class AccessibilityManager {
return;
}

switch (e.key) {
case 'ArrowRight': {
switch (`${this.ltr ? 'ltr' : 'rtl'}-${e.key}`) {
case 'ltr-ArrowRight':
case 'rtl-ArrowLeft': {
if (this.layout !== 'horizontal') {
return;
}
this.activeIndex = Math.min(this.activeIndex + 1, this.coordinateList.length - 1);
this.spoofMouse();
break;
}
case 'ArrowLeft': {
case 'ltr-ArrowLeft':
case 'rtl-ArrowRight': {
if (this.layout !== 'horizontal') {
return;
}
Expand Down
9 changes: 9 additions & 0 deletions src/chart/generateCategoricalChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
getStackGroupsByAxisId,
getTicksOfAxis,
getTooltipItem,
isAxisLTR,
isCategoricalAxis,
parseDomainOfCategoryAxis,
parseErrorBarsOfAxis,
Expand Down Expand Up @@ -1089,6 +1090,8 @@ export const generateCategoricalChart = ({
coordinateList: this.state.tooltipTicks,
mouseHandlerCallback: this.triggeredAfterMouseMove,
layout: this.props.layout,
// Check all (0+) <XAxis /> elements to see if ANY have reversed={true}. If so, this will be treated as an RTL chart
ltr: isAxisLTR(this.state.xAxisMap),
});
this.displayDefaultTooltip();
}
Expand Down Expand Up @@ -1170,6 +1173,12 @@ export const generateCategoricalChart = ({
});
}

if (this.state.xAxisMap !== prevState.xAxisMap) {
this.accessibilityManager.setDetails({
ltr: isAxisLTR(this.state.xAxisMap),
});
}

if (this.props.layout !== prevProps.layout) {
this.accessibilityManager.setDetails({
layout: this.props.layout,
Expand Down
12 changes: 12 additions & 0 deletions src/util/ChartUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
StackOffsetType,
Margin,
ChartOffset,
XAxisMap,
} from './types';
import { getLegendProps } from './getLegendProps';

Expand Down Expand Up @@ -1332,3 +1333,14 @@ export const getTooltipItem = (graphicalItem: ReactElement, payload: any) => {
hide,
};
};

export const isAxisLTR = (axisMap: XAxisMap) => {
const axes = Object.values(axisMap ?? {});
// If there are no <XAxis /> elements in the chart, then the chart is left-to-right (returning true).
if (axes.length === 0) {
return true;
}
// If there are any cases of reversed=true, then the chart is right-to-left (returning false).
// Otherwise, the chart is left-to-right (returning true)
return !axes.some(({ reversed }) => reversed);
};
148 changes: 148 additions & 0 deletions test/chart/AccessibilityLayer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ describe('AccessibilityLayer', () => {
expect(tooltip).toHaveTextContent('Page A');
});

test('accessibilityLayer works, even without *Axis elements', () => {
const { container } = render(
<AreaChart width={100} height={50} data={data} accessibilityLayer>
<Area type="monotone" dataKey="uv" stroke="#ff7300" fill="#ff7300" />
<Tooltip />
</AreaChart>,
);

// Confirm that the tooltip container exists, but isn't displaying anything
const tooltip = container.querySelector('.recharts-tooltip-wrapper');
expect(tooltip?.textContent).toBe('');

// Once the chart receives focus, the tooltip should display
container.querySelector('svg')?.focus();
expect(tooltip).toHaveTextContent('uv : 400');

// Use keyboard to move around
fireEvent.keyDown(document.querySelector('svg') as SVGSVGElement, {
key: 'ArrowRight',
});
expect(tooltip).toHaveTextContent('uv : 300');
});

test('Chart updates when it receives left/right arrow keystrokes', () => {
const mockMouseMovements = vi.fn();

Expand Down Expand Up @@ -172,6 +195,83 @@ describe('AccessibilityLayer', () => {
expect(mockMouseMovements.mock.instances).toHaveLength(0);
});

test('Left/right arrow pays attention to if the XAxis is reversed', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

<3

const mockMouseMovements = vi.fn();

const { container } = render(
<AreaChart width={100} height={50} data={data} accessibilityLayer onMouseMove={mockMouseMovements}>
<Area type="monotone" dataKey="uv" stroke="#ff7300" fill="#ff7300" />
<Tooltip />
<Legend />
<XAxis dataKey="name" reversed />
<YAxis />
</AreaChart>,
);

const svg = container.querySelector('svg');
assertNotNull(svg);
const tooltip = container.querySelector('.recharts-tooltip-wrapper');

expect(tooltip?.textContent).toBe('');
expect(mockMouseMovements).toHaveBeenCalledTimes(0);

// Once the chart receives focus, the tooltip should display
svg.focus();
expect(tooltip).toHaveTextContent('Page A');
expect(mockMouseMovements).toHaveBeenCalledTimes(1);

// Ignore right arrow when you're already at the right
fireEvent.keyDown(svg, {
key: 'ArrowRight',
});
expect(tooltip).toHaveTextContent('Page A');
expect(mockMouseMovements).toHaveBeenCalledTimes(2);

// Respect left arrow when there's something to the left
fireEvent.keyDown(svg, {
key: 'ArrowLeft',
});
expect(tooltip).toHaveTextContent('Page B');
expect(mockMouseMovements).toHaveBeenCalledTimes(3);

// Page C
fireEvent.keyDown(svg, {
key: 'ArrowLeft',
});

// Page D
fireEvent.keyDown(svg, {
key: 'ArrowLeft',
});

fireEvent.keyDown(svg, {
key: 'ArrowLeft',
});
expect(tooltip).toHaveTextContent('Page E');
expect(mockMouseMovements).toHaveBeenCalledTimes(6);

// Ignore left arrow when you're already at the left
fireEvent.keyDown(svg, {
key: 'ArrowLeft',
});
expect(tooltip).toHaveTextContent('Page F');
expect(mockMouseMovements).toHaveBeenCalledTimes(7);

// Respect right arrow when there's something to the right
fireEvent.keyDown(svg, {
key: 'ArrowRight',
});
expect(tooltip).toHaveTextContent('Page E');
expect(mockMouseMovements).toHaveBeenCalledTimes(8);

// Chart ignores non-arrow keys
fireEvent.keyDown(svg, {
key: 'a',
});
expect(tooltip).toHaveTextContent('Page E');
expect(mockMouseMovements).toHaveBeenCalledTimes(8);
});

const Expand = () => {
const [width, setWidth] = useState(6);
const myData = data.slice(0, width);
Expand Down Expand Up @@ -233,6 +333,7 @@ describe('AccessibilityLayer', () => {
key: 'ArrowRight',
});

// Page F
fireEvent.keyDown(svg, {
key: 'ArrowRight',
});
Expand Down Expand Up @@ -394,4 +495,51 @@ describe('AccessibilityLayer', () => {
});
}).not.toThrowError();
});

const DirectionSwitcher = () => {
const [reversed, setReversed] = useState(false);

return (
<div>
<button type="button" onClick={() => setReversed(!reversed)}>
Change directions
</button>

<AreaChart width={100} height={50} data={data} accessibilityLayer>
<Area type="monotone" dataKey="uv" stroke="#ff7300" fill="#ff7300" />
<Tooltip />
<Legend />
<XAxis dataKey="name" reversed={reversed} />
<YAxis orientation={reversed ? 'right' : 'left'} />
</AreaChart>
</div>
);
};

test('AccessibilityLayer respects dynamic changes to the XAxis orientation', () => {
const { container } = render(<DirectionSwitcher />);

const svg = container.querySelector('svg');
assertNotNull(svg);
const tooltip = container.querySelector('.recharts-tooltip-wrapper');

expect(tooltip?.textContent).toBe('');

svg.focus();
expect(tooltip).toHaveTextContent('Page A');

fireEvent.keyDown(svg, {
key: 'ArrowRight',
});
expect(tooltip).toHaveTextContent('Page B');

const button = container.querySelector('BUTTON') as HTMLButtonElement;
fireEvent.click(button);

// Key events should now go in reverse
fireEvent.keyDown(svg, {
key: 'ArrowRight',
});
expect(tooltip).toHaveTextContent('Page A');
});
});
51 changes: 51 additions & 0 deletions test/util/ChartUtils.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
parseSpecifiedDomain,
getTicksOfAxis,
getLegendProps,
isAxisLTR,
} from '../../src/util/ChartUtils';
import { BaseAxisProps, DataKey } from '../../src/util/types';

Expand Down Expand Up @@ -682,3 +683,53 @@ describe('exports for backwards-compatibility', () => {
expect(getLegendProps).toBeInstanceOf(Function);
});
});

test('isLTR', () => {
// Axis with reversed=false
expect(
isAxisLTR({
0: { reversed: false },
}),
).toBeTruthy();
// Axis with reversed=true
expect(
isAxisLTR({
0: { reversed: true },
}),
).toBeFalsy();
// Custom XAxisId, reversed=false
expect(
isAxisLTR({
custom: { reversed: false },
}),
).toBeTruthy();
// Custom XAxisId, reversed=true
expect(
isAxisLTR({
custom: { reversed: true },
}),
).toBeFalsy();
// Multiple axes, both reversed=true
expect(
isAxisLTR({
0: { reversed: true },
1: { reversed: true },
}),
).toBeFalsy();
// Multiple axes, both reversed=false
expect(
isAxisLTR({
0: { reversed: false },
1: { reversed: false },
}),
).toBeTruthy();
// Multiple axes, different reversed values
expect(
isAxisLTR({
0: { reversed: true },
1: { reversed: false },
}),
).toBeFalsy();
// Empty set of axes
expect(isAxisLTR({})).toBeTruthy();
});
Loading