Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
779a3bd
fix(web): test friendliness
eirikbacker Mar 26, 2026
9550639
Create sharp-bulldogs-run.md
eirikbacker Mar 26, 2026
b422e16
chore: update lock file
eirikbacker Mar 26, 2026
8f66d77
fix: remove redundant aria-invalid="false"
eirikbacker Mar 27, 2026
89e2c44
fix: speed up tooltip, dialog buttons and errorsummary and fix react …
eirikbacker Mar 27, 2026
1978a21
fix field allow manually setting aria-invalid without vaildation message
eirikbacker Mar 27, 2026
9276eea
fix: clarify field validation logic
eirikbacker Mar 27, 2026
54c7225
chore: add comment
eirikbacker Mar 27, 2026
ceac39e
chore: update commment
eirikbacker Mar 27, 2026
22345a7
fix: update tooltip and link styling
eirikbacker Mar 27, 2026
370f78d
fix: add document check in mutationobserver
eirikbacker Mar 27, 2026
67d0eb7
fix: respect external aria-describedby
eirikbacker Mar 27, 2026
991f2dd
fix: simplify onMutation
eirikbacker Mar 28, 2026
ae18651
fix(field): sync for with id
eirikbacker Mar 30, 2026
69e2f06
fix: pr review clarifying bundled dependencies
eirikbacker Mar 31, 2026
b616b54
chore: rename bundled_dependencies to _vendors
eirikbacker Mar 31, 2026
248e18d
fix: use entryfilenames also in cjs
eirikbacker Mar 31, 2026
a95cc7e
chore: cleanup
eirikbacker Mar 31, 2026
869d19b
Merge branch 'main' into fix/test-friendliness
eirikbacker Mar 31, 2026
bf5dadf
chore: run pnpm i
eirikbacker Mar 31, 2026
a82226d
fix(togglegroup): support aria-labelledby
eirikbacker Mar 31, 2026
a20ed89
fix: typescript types for togglegroup aria-labelledby
eirikbacker Mar 31, 2026
06edc8c
fix(field): play nicer with virtual dom
eirikbacker Apr 1, 2026
c001eb0
chore: remove deprecated comment
eirikbacker Apr 1, 2026
86710f4
Merge branch 'main' into fix/test-friendliness
eirikbacker Apr 1, 2026
35ffe06
add fieldset tests and update readme
mimarz Apr 1, 2026
1bc322b
fix: avoid event.isTrusted and prepare for jsdom testing
eirikbacker Apr 1, 2026
36a8d5f
chore: lint
eirikbacker Apr 1, 2026
656844d
chore: clean react tests
eirikbacker Apr 1, 2026
8925473
chore(breadcrumbs): remove redundant async
eirikbacker Apr 1, 2026
1765b34
chore: re-implement act
eirikbacker Apr 1, 2026
718f05b
fix(textfield): update test to work without userEvent
eirikbacker Apr 1, 2026
87498b2
chore: consistent queryBy vs getBy usage in tests
eirikbacker Apr 1, 2026
04cce54
chore: cleanup tooltip tests
eirikbacker Apr 1, 2026
3c31d10
chore: test cleanup
eirikbacker Apr 1, 2026
e9bedd1
fix: warn with use console.log instead of console.warn to avoid stopp…
eirikbacker Apr 1, 2026
5ab92e2
Merge branch 'main' into fix/test-friendliness
mimarz Apr 7, 2026
f62053d
fix lock file
mimarz Apr 7, 2026
50459c3
add some changesets
mimarz Apr 7, 2026
a62ea3b
tweak wording in changelogs
mimarz Apr 7, 2026
4ea7e88
Merge branch 'main' into fix/test-friendliness
mimarz Apr 8, 2026
f298cc3
rollback link css fix to separate PR
mimarz Apr 8, 2026
cea4b21
try to fix lockfile
mimarz Apr 8, 2026
b517ab3
fix: revert to data-toggle-group, fix breadcrumbs rendering, fix dial…
eirikbacker Apr 9, 2026
ac4ebb7
Merge branch 'main' into fix/test-friendliness
mimarz Apr 9, 2026
43c1e83
fix: revert data-toggle-group more
eirikbacker Apr 9, 2026
c721a62
fix lockfile
mimarz Apr 9, 2026
ca26607
fix: togglegroup test
eirikbacker Apr 9, 2026
402f953
Create swift-shrimps-return.md
eirikbacker Apr 9, 2026
fa57206
Create old-boats-work.md
eirikbacker Apr 9, 2026
f737ae4
Create lovely-kids-reflect.md
eirikbacker Apr 9, 2026
3789ed1
add tabs changeset
mimarz Apr 10, 2026
7f7e107
add missing toggle-group
mimarz Apr 10, 2026
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
6 changes: 6 additions & 0 deletions .changeset/chilly-pigs-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@digdir/designsystemet-react": patch
"@digdir/designsystemet-web": patch
---

**Field**: `<ds-field>` should now respect existing `aria-describedby` and `aria-invalid`
5 changes: 5 additions & 0 deletions .changeset/eight-taxes-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-react": patch
---

**Pagination:** fix `PaginationButton` missing some `Button` props
5 changes: 5 additions & 0 deletions .changeset/free-bobcats-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-react": patch
---

**Tabs**: Now supports programmatically triggering click on controlled `Tabs`
5 changes: 5 additions & 0 deletions .changeset/lovely-kids-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-web": patch
---

**Field:** No longer uses `CSS.supports` to play nice with Jest + JSDOM
5 changes: 5 additions & 0 deletions .changeset/nasty-shrimps-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-web": patch
---

fixed some native keystrokes being ignored if readonly fields were focused
5 changes: 5 additions & 0 deletions .changeset/old-boats-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-css": patch
---

**Breadcrumbs:** Renders correcly as `display: block`
5 changes: 5 additions & 0 deletions .changeset/pretty-bugs-enter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-web": patch
---

`invokers-polyfill` is now bundled inline as part of source files for better compatibility with Jest module resolving.
5 changes: 5 additions & 0 deletions .changeset/sharp-bulldogs-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-web": patch
---

**All components:** Renders instantly for easier test setup
5 changes: 5 additions & 0 deletions .changeset/swift-shrimps-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet-react": patch
---

**Dialog:** Use `ref` in `Dialog.TriggerContext` for better performance
4 changes: 4 additions & 0 deletions packages/css/src/breadcrumbs.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
--dsc-breadcrumbs-color: var(--ds-color-text-subtle);
--_ds-aria-label: var(--dsc-breadcrumbs-label); /* "proxy" so attrOrCSS works even if changing --ds- prefix */

&:not([hidden]) {
display: block; /* Needed for <ds-breadcrumbs> element */
}

&:is([lang='nb'], [lang='nn'], [lang='no']),
:is([lang='nb'], [lang='nn'], [lang='no']) & {
--dsc-breadcrumbs-label: 'Du er her:'; /* Only set default label if Norwegian */
Expand Down
27 changes: 9 additions & 18 deletions packages/react/src/components/breadcrumbs/breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import type { BreadcrumbsProps } from '../';
import { Breadcrumbs } from '../';

Expand Down Expand Up @@ -26,34 +26,29 @@ const renderWithRoot = (props?: BreadcrumbsProps) =>
);

describe('Breadcrumbs', () => {
it('should render correctly with default props', async () => {
it('should render correctly with default props', () => {
renderWithRoot();

expect(await screen.findByRole('navigation')).toBeInTheDocument();
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
});

describe('Breadcrumbs.List', () => {
it('should render with aria-current on last item', async () => {
it('should render with aria-current on last item', () => {
renderWithRoot();

await waitFor(() => {
const links = screen.getAllByRole('link');
expect(links.at(-1)).toHaveAttribute('aria-current', 'page');
});
const links = screen.getAllByRole('link');
expect(links.at(-1)).toHaveAttribute('aria-current', 'page');
expect(links.at(0)).not.toHaveAttribute('aria-current', 'page');
expect(links.at(1)).not.toHaveAttribute('aria-current', 'page');
expect(links.at(2)).not.toHaveAttribute('aria-current', 'page');
});

it('should move aria-current to item when re-rendering', async () => {
it('should move aria-current to item when re-rendering', () => {
renderWithRoot();

await waitFor(() => {
const links = screen.getAllByRole('link');
expect(links.at(-1)).toHaveAttribute('aria-current', 'page');
});
const links = screen.getAllByRole('link');
expect(links.at(-1)).toHaveAttribute('aria-current', 'page');

// Re-render with additional level
render(
Expand Down Expand Up @@ -81,11 +76,7 @@ describe('Breadcrumbs.List', () => {
</Breadcrumbs>,
);

await waitFor(() => {
const links = screen.getAllByRole('link');
expect(links.at(-1)).toHaveAttribute('aria-current', 'page');
});
const links = screen.getAllByRole('link');
expect(links.at(-1)).toHaveAttribute('aria-current', 'page');
expect(links.at(-2)).not.toHaveAttribute('aria-current', 'page');
});
});
48 changes: 20 additions & 28 deletions packages/react/src/components/button/button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { render as renderRtl, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act } from 'react';

import type { ButtonProps } from './button';
import { act, render, screen } from '@testing-library/react';
import { Button } from './button';

const user = userEvent.setup();

describe('Button', () => {
beforeAll(() => {
// Spinner for loading state uses animations, which we need to mock
Expand All @@ -16,19 +10,15 @@ describe('Button', () => {
});

it('should render as aria-disabled when aria-disabled is true regardless of variant', () => {
render({
'aria-disabled': true,
});
render(<Button aria-disabled='true' />);

const button = screen.getByRole('button');

expect(button).toHaveAttribute('aria-disabled');
});

it('should render as disabled when disabled is true regardless of variant', () => {
render({
disabled: true,
});
render(<Button disabled />);

const button = screen.getByRole('button');

Expand All @@ -37,41 +27,43 @@ describe('Button', () => {

it('should not call onClick when disabled', async () => {
const fn = vi.fn();
render({
disabled: true,
onClick: fn,
});
render(<Button disabled onClick={fn} />);

const button = screen.getByRole('button');
await act(async () => await user.click(button));
await act(async () => screen.getByRole('button').click());
expect(fn).not.toHaveBeenCalled();
});

it('should render children as button text', () => {
render({ children: 'different button text' });
render(<Button>different button text</Button>);
expect(
screen.getByRole('button', { name: 'different button text' }),
).toBeInTheDocument();
});

it('should handle onClick event', async () => {
const fn = vi.fn();
render({ onClick: fn });
await act(async () => await user.click(screen.getByRole('button')));
render(<Button onClick={fn} />);
await act(async () => screen.getByRole('button').click());
expect(fn).toHaveBeenCalled();
});

it('should not have type attribute when asChild is true', () => {
render({ asChild: true, children: <a href='#'>Link</a> });
render(
<Button asChild>
<a href='#'>Link</a>
</Button>,
);
expect(screen.getByRole('link')).not.toHaveAttribute('type');
expect(screen.queryByRole('button')).toBeNull();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});

it('should not render children when icon-only button is loading', () => {
render({ loading: true, icon: true, children: 'Button text' });
expect(screen.queryByText('Button text')).toBeNull();
render(
<Button loading icon>
Button text
</Button>,
);
expect(screen.queryByText('Button text')).not.toBeInTheDocument();
expect(screen.getByRole('button')).toHaveAttribute('aria-busy');
});
});

const render = (props?: ButtonProps) => renderRtl(<Button {...props} />);
21 changes: 8 additions & 13 deletions packages/react/src/components/card/card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import { render as renderRtl, screen } from '@testing-library/react';

import type { CardProps } from './card';
import { render, screen } from '@testing-library/react';
import { Card } from './card';
import { CardBlock } from './card-block';

const renderCard = (props?: Partial<CardProps>) =>
renderRtl(
<Card title='card' {...props}>
<CardBlock />
</Card>,
);

describe('Card Component', () => {
it('renders Card component', () => {
renderCard();
render(
<Card title='card'>
<CardBlock />
</Card>,
);
expect(screen.getByTitle('card')).toBeInTheDocument();
});

it('renders media image if provided', () => {
const mediaImage = 'some/media/image/path';

renderRtl(
<Card title='card'>
render(
<Card>
<CardBlock>
<img src={mediaImage} alt='cat' />
</CardBlock>
Expand Down
22 changes: 8 additions & 14 deletions packages/react/src/components/checkbox/checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act } from 'react';

import { act, render, screen } from '@testing-library/react';
import { Checkbox } from './checkbox';

describe('Checkbox', () => {
test('has correct value and label', async () => {
test('has correct value and label', () => {
render(<Checkbox label='label' value='test' />);
expect(await screen.findByLabelText('label')).toBeDefined();
expect(screen.getByLabelText('label')).toBeDefined();
expect(screen.getByDisplayValue('test')).toBeDefined();
});

test('has correct description', async () => {
test('has correct description', () => {
render(<Checkbox label='test' value='test' description='description' />);
expect(
await screen.findByRole('checkbox', { description: 'description' }),
screen.getByRole('checkbox', { description: 'description' }),
).toBeDefined();
});
it('calls onChange and onClick when user clicks', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const onClick = vi.fn();

Expand All @@ -37,15 +33,14 @@ describe('Checkbox', () => {

expect(radio.checked).toBeFalsy();

await act(async () => await user.click(radio));
await act(async () => radio.click());

expect(onChange).toHaveBeenCalledTimes(1);
expect(onClick).toHaveBeenCalledTimes(1);
expect(radio.checked).toBeTruthy();
});

it('does not call onChange or onClick when user clicks and the radio is disabled', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const onClick = vi.fn();

Expand All @@ -60,15 +55,14 @@ describe('Checkbox', () => {
);

const radio = screen.getByRole('checkbox');
await act(async () => await user.click(radio));
await act(async () => radio.click());

expect(radio).toBeDisabled();
expect(onClick).not.toHaveBeenCalled();
expect(onChange).not.toHaveBeenCalled();
});

it('does not call onChange when user clicks and the radio is readOnly', async () => {
const user = userEvent.setup();
const onChange = vi.fn();

render(
Expand All @@ -81,7 +75,7 @@ describe('Checkbox', () => {
);

const radio = screen.getByRole('checkbox');
await act(async () => await user.click(radio));
await act(async () => radio.click());

expect(radio).toHaveAttribute('readonly');
expect(onChange).not.toHaveBeenCalled();
Expand Down
8 changes: 3 additions & 5 deletions packages/react/src/components/details/details.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act, render, screen } from '@testing-library/react';
import type { JSX } from 'react';

import { Details, type DetailsProps } from './';

const user = userEvent.setup();
const VOID = () => {};

const TestComponent = (rest: DetailsProps): JSX.Element => {
Expand Down Expand Up @@ -47,7 +45,7 @@ describe('Details', () => {
const detailsExpandButton = screen.getByTestId('summary');
expect(detailsExpandButton.parentElement).toHaveAttribute('open');

await user.click(detailsExpandButton);
expect(detailsExpandButton.parentElement).toHaveAttribute('open');
await act(async () => detailsExpandButton.click());
expect(detailsExpandButton.parentElement).toHaveAttribute('open'); // Let React controll state before checking
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ReactNode } from 'react';
import { createContext, useState } from 'react';
import { createContext, useRef } from 'react';

type DialogContext = { id?: string; modal?: boolean };
export const Context = createContext<
DialogContext & { setContext?: (context: DialogContext) => void }
>({});
export const Context = createContext<React.RefObject<HTMLDialogElement | null>>(
{
current: null,
},
);

export type DialogTriggerContextProps = { children: ReactNode };

Expand All @@ -20,10 +21,9 @@ export type DialogTriggerContextProps = { children: ReactNode };
* </Dialog.TriggerContext>
*/
export const DialogTriggerContext = (rest: DialogTriggerContextProps) => {
const [state, setState] = useState<DialogContext>({});
const setContext = (next: DialogContext) => setState({ ...state, ...next });
const contextRef = useRef<HTMLDialogElement>(null);

return <Context.Provider value={{ ...state, setContext }} {...rest} />;
return <Context.Provider value={contextRef} {...rest} />;
};

DialogTriggerContext.displayName = 'DialogTriggerContext';
Loading
Loading