Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fast-cats-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-react": patch
---

**Tooltip**: deprecate `type` prop, as it no longer does anything
5 changes: 5 additions & 0 deletions .changeset/fuzzy-cooks-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-react": patch
---

**Tooltip**: the React component will no longer override existing accessible text. It now correctly sets `aria-description` in that case. If there is no accessible text, `aria-label` will be used as before.
101 changes: 75 additions & 26 deletions packages/react/src/components/tooltip/tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { FilesIcon } from '@navikt/aksel-icons';
import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite';
import { useEffect, useRef, useState } from 'react';
import { expect, within } from 'storybook/test';
import { Button } from '../../';
import { expect, fireEvent, userEvent, waitFor } from 'storybook/test';
import { Button, Link } from '../../';
import { Tooltip } from './tooltip';

type Story = StoryObj<typeof Tooltip>;
type FnStory = StoryFn<typeof Tooltip>;

function isInViewport(el: Element) {
const { height, width } = el.getBoundingClientRect();
return height > 1 && width > 1;
}

export default {
title: 'Komponenter/Tooltip',
Expand All @@ -17,17 +23,34 @@ export default {
},
},
play: async (ctx) => {
document.querySelector('.ds-tooltip')?.remove(); // Reset to run next test without waiting for tooltip to disappear // <== Må "nullstille"/fjerne tooltip mellom hver test
const button =
ctx.canvasElement.querySelector<HTMLButtonElement>('[data-tooltip]');

await new Promise((resolve) => {
document.addEventListener('animationend', resolve, true); // <== Merk at vi binder event-listener før vi gjør hover
button?.focus();
});

const tooltip = await within(document.body).findByText(ctx.args.content); // <== trenger ikke sjekke toBeInDocument siden denne testen krever det
expect(tooltip).toBeVisible();
const tooltips =
ctx.canvasElement.querySelectorAll<HTMLElement>('[data-tooltip]');
const fakeFocus = (e: HTMLElement) => {
fireEvent.focus(e); // shows up in interaction log in Storybook
e.focus({ focusVisible: true } as Record<string, unknown>); // necessary to get focusVisible styling, but doesn't show up in interaction log
};
for (const event of [userEvent.hover, fakeFocus])
for (const tooltipTrigger of tooltips) {
await event(tooltipTrigger);
await waitFor(async () => {
const text = tooltipTrigger.getAttribute('data-tooltip');
if (!text) {
throw new Error('Tooltip trigger has no data-tooltip attribute');
}
const tooltipRenderer = document.body.querySelector('.ds-tooltip');
await expect(tooltipRenderer).toBeVisible();
await expect(tooltipRenderer).toSatisfy(isInViewport); // toBeVisible() doesn't check if the element is in the viewport
await expect(tooltipRenderer).toHaveTextContent(text);
if (tooltipTrigger.textContent.trim()) {
await expect(tooltipTrigger).toHaveAttribute(
'aria-description',
text,
);
} else {
await expect(tooltipTrigger).toHaveAttribute('aria-label', text);
}
});
}
},
} satisfies Meta;

Expand All @@ -44,6 +67,30 @@ Preview.args = {
placement: 'top',
};

export const WithLink: FnStory = () => {
return (
<Tooltip content='Gå til en annen side...' placement='top'>
<Link href='#'>En lenke</Link>
</Tooltip>
);
};

export const WithSpan: FnStory = () => {
return (
<Tooltip content='Innholdet i tooltipen' placement='top'>
<span>Tekst med tooltip</span>
</Tooltip>
);
};

export const WithPlainText: FnStory = () => {
return (
<Tooltip content='Innholdet i tooltipen' placement='top'>
Tekst med tooltip
</Tooltip>
);
};

export const WithString: Story = {
args: {
content: 'Organisasjonsnummer',
Expand All @@ -64,12 +111,18 @@ export const Placement: Story = {
},
};

export const Aria: StoryFn<typeof Tooltip> = () => {
export const Aria: FnStory = () => {
return (
<>
<Tooltip content='Eg er aria-description'>
<Tooltip content='Beskrivelse for aria-description'>
<Button>Eg er aria-description</Button>
</Tooltip>
<Tooltip content='Beskrivelse for aria-description'>
<Button>
<FilesIcon aria-hidden />
<span>Eg er også aria-description</span>
</Button>
</Tooltip>
<Tooltip content='Eg er aria-label'>
<Button icon>
<FilesIcon aria-hidden />
Expand All @@ -79,17 +132,13 @@ export const Aria: StoryFn<typeof Tooltip> = () => {
);
};

Aria.decorators = [
(Story) => (
<div
style={{ display: 'flex', gap: 'var(--ds-size-2)', alignItems: 'center' }}
>
<Story />
</div>
),
];

Aria.play = async () => {};
Aria.parameters = {
customStyles: {
display: 'flex',
gap: 'var(--ds-size-2)',
alignItems: 'center',
},
};

export const WithDynamicTooltipText: Story = {
args: {
Expand Down
10 changes: 7 additions & 3 deletions packages/react/src/components/tooltip/tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { act } from 'react';
import { Tooltip } from './tooltip';

Expand Down Expand Up @@ -64,7 +64,9 @@ describe('Tooltip', () => {
</Tooltip>,
);
const trigger = screen.getByRole('button');
expect(trigger.getAttribute('aria-describedby')).toBeDefined();
waitFor(() =>
expect(trigger.getAttribute('aria-description')).not.toBeNullable(),
);
});

it('should be aria-labelledby when there is no text in the trigger', () => {
Expand All @@ -74,6 +76,8 @@ describe('Tooltip', () => {
</Tooltip>,
);
const trigger = screen.getByRole('button');
expect(trigger.getAttribute('aria-labelledby')).toBeDefined();
waitFor(() =>
expect(trigger.getAttribute('aria-label')).not.toBeNullable(),
);
});
});
5 changes: 2 additions & 3 deletions packages/react/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export type TooltipProps = MergeRight<
*/
open?: boolean;
/**
* Override if `aria-describedby` or `aria-labelledby` is used.
* By default, if the trigger element has no inner text, `aria-labelledby` is used.
* @deprecated This prop has no effect. The tooltip will be set as `aria-description`
* if the component already contains accessible text, and `aria-label` otherwise.
*/
type?: 'describedby' | 'labelledby';
}
Expand Down Expand Up @@ -68,7 +68,6 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(

return (
<Slot
aria-label={content || undefined} // designsystemet-web will re-evaulate if this should be an aria-label or aria-description, but kept here for better SSR
data-tooltip={content}
data-placement={placement}
data-autoplacement={autoPlacement}
Expand Down
Loading