Skip to content
14 changes: 14 additions & 0 deletions semcore/d3-chart/__tests__/axe-tests/bar-chart.axe-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ test.describe(`@d3-chart @bar-chart ${TAG.ACCESSIBILITY}`, () => {
expect(violations).toEqual([]);
});

test('links with hovered tick', async ({ page }) => {
await loadPage(page, 'stories/components/d3-chart/docs/examples/bar-chart/links.tsx', 'en');

const plotBox = await page.locator('svg[data-ui-name="Plot"]').boundingBox();
const linkBox = await page.locator('[data-ui-name="Axis.Ticks"] a').first().boundingBox();
if (!plotBox || !linkBox) throw new Error('Bounding box not found');

await page.mouse.move(linkBox.x + linkBox.width / 2, plotBox.y + plotBox.height / 2);
await expect(page.locator('foreignObject[data-hovered-tick] a')).toHaveCount(1);

const violations = await getAccessibilityViolations({ page });
expect(violations).toEqual([]);
});

test('negative-values', async ({ page }) => {
await loadPage(page, 'stories/components/d3-chart/docs/examples/bar-chart/negative-values.tsx', 'en');
const violations = await getAccessibilityViolations({ page });
Expand Down
43 changes: 43 additions & 0 deletions semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ const hoverPlotCenter = async (page: Page) => {
return box;
};

/**
* Hovers the plot column under the given tick, so the hovered tick pill shows the same
* content as that tick.
*/
const hoverTickColumn = async (page: Page, tick: ReturnType<Page['locator']>) => {
const plotBox = await locators.plot(page).first().boundingBox();
const tickBox = await tick.boundingBox();
if (!plotBox || !tickBox) throw new Error('Bounding box not found');

await page.mouse.move(tickBox.x + tickBox.width / 2, plotBox.y + plotBox.height / 2);
};

/**
* Dots give the exact position of a data point, so the hovered index needs no
* coordinate math. The first 10 dots belong to the first series, one per point.
Expand Down Expand Up @@ -742,6 +754,37 @@ test.describe(`${TAG.FUNCTIONAL}`, () => {
// The link is styled as a link, but it must not grow or shrink the label.
expect(linkSize).toBe(tickSize);
});

test('Verify the hovered tick hides while a tick link has keyboard focus', {
tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, TAG.MOUSE, '@d3-chart',
'@bar-chart'],
}, async ({ page }) => {
await loadPage(page, LINKS_EXAMPLE, 'en');
await locators.plot(page).waitFor({ state: 'visible' });

const tickLinks = locators.axisTicks(page).locator('a');
const pill = locators.hoveredTick(page);

await hoverTickColumn(page, tickLinks.first());
await expect(pill).toHaveCSS('visibility', 'visible');

await test.step('Focus the link with the keyboard', async () => {
// Focus arrives by Shift+Tab, so the link matches :focus-visible in every browser.
await tickLinks.nth(1).focus();
await page.keyboard.press('Shift+Tab');

await expect(tickLinks.first()).toBeFocused();
await expect(pill).toHaveCSS('visibility', 'hidden');
});

await test.step('Move focus off the tick links', async () => {
// The first link is the first tab stop, so Shift+Tab would leave the page.
await tickLinks.first().blur();

await expect(tickLinks.first()).not.toBeFocused();
await expect(pill).toHaveCSS('visibility', 'visible');
});
});
});

test.describe('Pattern fills, dots and lines', () => {
Expand Down
20 changes: 9 additions & 11 deletions semcore/d3-chart/src/Hover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,23 +238,20 @@ const HOVERED_TICK_PADDING_X = 12;
function HoveredTick(props) {
const { tickFormatter, value, isFirstTick = false, isLastTick = false, styles, x, yRange, size, position } = props;

const contentRef = React.useRef(null);
const [interactive, setInteractive] = React.useState(false);

const formattedValue = tickFormatter(value);

const contentRef = React.useRef(null);

const isRendered = size?.width !== undefined && size?.height !== undefined && position !== null;
const isMightBeInteractive = formattedValue !== null && typeof formattedValue === 'object';

useEnhancedEffect(() => {
if (!isMightBeInteractive || !contentRef.current) {
setInteractive(false);
return;
}
if (!isRendered || !isMightBeInteractive || !contentRef.current) return;

setInteractive(getFocusableIn(contentRef.current).length > 0);
});
getFocusableIn(contentRef.current).forEach((node) => node.setAttribute('tabindex', '-1'));
}, [value, isRendered, isMightBeInteractive]);

if (size?.width === undefined || size?.height === undefined || position === null) return null;
if (!isRendered) return null;

const { width, height } = size;

Expand All @@ -281,7 +278,8 @@ function HoveredTick(props) {
data-is-first={isFirstTick}
data-is-last={isLastTick}
position={position}
aria-hidden={interactive ? undefined : true}
data-hovered-tick
aria-hidden='true'
>
<SHoveredTickContent ref={contentRef}>
<SHoveredTick>{formattedValue}</SHoveredTick>
Expand Down
4 changes: 4 additions & 0 deletions semcore/d3-chart/src/style/plot.shadow.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ foreignObject[data-aria-only] {
pointer-events: none;
}

SPlot:has([data-tick-content] :focus-visible) foreignObject[data-hovered-tick] {
visibility: hidden;
}

SPlot:has(foreignObject[data-aria-only]:focus-within) {
@mixin focus-outline-mixin;
border-radius: var(--intergalactic-surface-rounded, 8px);
Expand Down
Loading