tharaday 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +1 -1
- package/.storybook/preview.ts +0 -2
- package/.storybook/vitest.setup.ts +2 -0
- package/dist/ds.css +1 -1
- package/dist/ds.js +889 -814
- package/dist/ds.umd.cjs +1 -1
- package/dist/src/components/Tree/Tree.d.ts +1 -1
- package/dist/src/components/Tree/Tree.stories.d.ts +1 -1
- package/dist/src/components/Tree/TreeItem.d.ts +1 -1
- package/dist/src/components/Tree/TreeItem.types.d.ts +6 -0
- package/dist/src/layouts/AppLayout/AppLayout.d.ts +1 -7
- package/dist/src/layouts/AppLayout/AppLayout.stories.d.ts +1 -9
- package/dist/src/layouts/AppLayout/AppLayout.types.d.ts +7 -52
- package/dist/src/layouts/DashboardLayout/DashboardLayout.d.ts +1 -1
- package/dist/src/layouts/DashboardLayout/DashboardLayout.stories.d.ts +1 -7
- package/dist/src/layouts/DashboardLayout/DashboardLayout.types.d.ts +2 -9
- package/dist/src/layouts/SettingsLayout/SettingsLayout.d.ts +1 -1
- package/dist/src/layouts/SettingsLayout/SettingsLayout.stories.d.ts +1 -7
- package/dist/src/layouts/SettingsLayout/SettingsLayout.types.d.ts +2 -9
- package/package.json +8 -1
- package/src/components/Accordion/Accordion.module.css +1 -1
- package/src/components/Accordion/Accordion.test.tsx +82 -0
- package/src/components/Accordion/Accordion.tsx +14 -1
- package/src/components/Avatar/Avatar.test.tsx +36 -0
- package/src/components/Badge/Badge.test.tsx +15 -0
- package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +96 -0
- package/src/components/Checkbox/Checkbox.module.css +7 -7
- package/src/components/Checkbox/Checkbox.test.tsx +68 -0
- package/src/components/Dropdown/Dropdown.test.tsx +104 -0
- package/src/components/Input/Input.test.tsx +61 -0
- package/src/components/List/List.module.css +12 -12
- package/src/components/List/List.test.tsx +46 -0
- package/src/components/Modal/Modal.module.css +12 -13
- package/src/components/Modal/Modal.test.tsx +86 -0
- package/src/components/Modal/Modal.tsx +10 -4
- package/src/components/NavBar/NavBar.module.css +3 -3
- package/src/components/Notification/Notification.module.css +6 -6
- package/src/components/Notification/Notification.test.tsx +38 -0
- package/src/components/Notification/Notification.tsx +8 -1
- package/src/components/Pagination/Pagination.test.tsx +70 -0
- package/src/components/ProgressBar/ProgressBar.test.tsx +58 -0
- package/src/components/RadioButton/RadioButton.test.tsx +51 -0
- package/src/components/Select/Select.test.tsx +64 -0
- package/src/components/Slider/Slider.test.tsx +49 -0
- package/src/components/Stepper/Step.module.css +2 -2
- package/src/components/Stepper/Stepper.test.tsx +51 -0
- package/src/components/Switch/Switch.test.tsx +53 -0
- package/src/components/Table/Table.test.tsx +78 -0
- package/src/components/Tabs/Tabs.test.tsx +83 -0
- package/src/components/Textarea/Textarea.test.tsx +56 -0
- package/src/components/Tree/Tree.test.tsx +116 -0
- package/src/components/Tree/Tree.tsx +65 -1
- package/src/components/Tree/TreeItem.module.css +20 -26
- package/src/components/Tree/TreeItem.tsx +144 -79
- package/src/components/Tree/TreeItem.types.ts +6 -0
- package/src/layouts/AppLayout/AppLayout.stories.tsx +48 -36
- package/src/layouts/AppLayout/AppLayout.tsx +4 -34
- package/src/layouts/AppLayout/AppLayout.types.ts +7 -51
- package/src/layouts/DashboardLayout/DashboardLayout.stories.tsx +4 -8
- package/src/layouts/DashboardLayout/DashboardLayout.tsx +2 -17
- package/src/layouts/DashboardLayout/DashboardLayout.types.tsx +2 -7
- package/src/layouts/SettingsLayout/SettingsLayout.stories.tsx +16 -7
- package/src/layouts/SettingsLayout/SettingsLayout.tsx +2 -17
- package/src/layouts/SettingsLayout/SettingsLayout.types.tsx +2 -7
- package/src/styles/semantic.css +3 -0
- package/src/styles/tokens.css +15 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Accordion } from './Accordion.tsx';
|
|
5
|
+
|
|
6
|
+
const items = [
|
|
7
|
+
{ id: 'a', title: 'First', content: 'Content A' },
|
|
8
|
+
{ id: 'b', title: 'Second', content: 'Content B' },
|
|
9
|
+
{ id: 'c', title: 'Third', content: 'Content C', isDisabled: true },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe('Accordion', () => {
|
|
13
|
+
it('renders all item headers', () => {
|
|
14
|
+
render(<Accordion items={items} />);
|
|
15
|
+
expect(screen.getByRole('button', { name: 'First' })).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByRole('button', { name: 'Second' })).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByRole('button', { name: 'Third' })).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('collapses all items by default', () => {
|
|
21
|
+
render(<Accordion items={items} />);
|
|
22
|
+
expect(screen.getByRole('button', { name: 'First' })).toHaveAttribute('aria-expanded', 'false');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('expands defaultExpanded items on mount', () => {
|
|
26
|
+
render(<Accordion items={items} defaultExpanded={['a']} />);
|
|
27
|
+
expect(screen.getByRole('button', { name: 'First' })).toHaveAttribute('aria-expanded', 'true');
|
|
28
|
+
expect(screen.getByRole('button', { name: 'Second' })).toHaveAttribute(
|
|
29
|
+
'aria-expanded',
|
|
30
|
+
'false'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('expands an item on click', async () => {
|
|
35
|
+
render(<Accordion items={items} />);
|
|
36
|
+
await userEvent.click(screen.getByRole('button', { name: 'First' }));
|
|
37
|
+
expect(screen.getByRole('button', { name: 'First' })).toHaveAttribute('aria-expanded', 'true');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('collapses an expanded item on click', async () => {
|
|
41
|
+
render(<Accordion items={items} defaultExpanded={['a']} />);
|
|
42
|
+
await userEvent.click(screen.getByRole('button', { name: 'First' }));
|
|
43
|
+
expect(screen.getByRole('button', { name: 'First' })).toHaveAttribute('aria-expanded', 'false');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('closes previously open item when allowMultiple is false', async () => {
|
|
47
|
+
render(<Accordion items={items} />);
|
|
48
|
+
await userEvent.click(screen.getByRole('button', { name: 'First' }));
|
|
49
|
+
await userEvent.click(screen.getByRole('button', { name: 'Second' }));
|
|
50
|
+
expect(screen.getByRole('button', { name: 'First' })).toHaveAttribute('aria-expanded', 'false');
|
|
51
|
+
expect(screen.getByRole('button', { name: 'Second' })).toHaveAttribute('aria-expanded', 'true');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('allows multiple expanded items when allowMultiple is true', async () => {
|
|
55
|
+
render(<Accordion items={items} allowMultiple />);
|
|
56
|
+
await userEvent.click(screen.getByRole('button', { name: 'First' }));
|
|
57
|
+
await userEvent.click(screen.getByRole('button', { name: 'Second' }));
|
|
58
|
+
expect(screen.getByRole('button', { name: 'First' })).toHaveAttribute('aria-expanded', 'true');
|
|
59
|
+
expect(screen.getByRole('button', { name: 'Second' })).toHaveAttribute('aria-expanded', 'true');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not expand a disabled item', async () => {
|
|
63
|
+
render(<Accordion items={items} />);
|
|
64
|
+
expect(screen.getByRole('button', { name: 'Third' })).toBeDisabled();
|
|
65
|
+
await userEvent.click(screen.getByRole('button', { name: 'Third' }));
|
|
66
|
+
expect(screen.getByRole('button', { name: 'Third' })).toHaveAttribute('aria-expanded', 'false');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('sets aria-controls pointing to the content region', () => {
|
|
70
|
+
render(<Accordion items={items} id="acc" />);
|
|
71
|
+
const btn = screen.getByRole('button', { name: 'First' });
|
|
72
|
+
const contentId = btn.getAttribute('aria-controls');
|
|
73
|
+
expect(document.getElementById(contentId!)).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('sets aria-labelledby on the content region pointing to the button', () => {
|
|
77
|
+
render(<Accordion items={items} id="acc" />);
|
|
78
|
+
const btn = screen.getByRole('button', { name: 'First' });
|
|
79
|
+
const region = document.getElementById(btn.getAttribute('aria-controls')!);
|
|
80
|
+
expect(region).toHaveAttribute('aria-labelledby', btn.id);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -46,7 +46,20 @@ export const Accordion = ({
|
|
|
46
46
|
aria-controls={`${componentId}-content-${item.id}`}
|
|
47
47
|
>
|
|
48
48
|
<span>{item.title}</span>
|
|
49
|
-
<span
|
|
49
|
+
<span
|
|
50
|
+
className={clsx(styles.icon, isExpanded && styles.iconExpanded)}
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
>
|
|
53
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
54
|
+
<path
|
|
55
|
+
d="M2.5 4.5L6 8L9.5 4.5"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
strokeWidth="1.5"
|
|
58
|
+
strokeLinecap="round"
|
|
59
|
+
strokeLinejoin="round"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
</span>
|
|
50
63
|
</button>
|
|
51
64
|
<div
|
|
52
65
|
id={`${componentId}-content-${item.id}`}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Avatar } from './Avatar.tsx';
|
|
4
|
+
|
|
5
|
+
describe('Avatar', () => {
|
|
6
|
+
it('renders an image when src is provided', () => {
|
|
7
|
+
render(<Avatar src="photo.jpg" alt="Alice" />);
|
|
8
|
+
expect(screen.getByRole('img', { name: 'Alice' })).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('shows initials from name when no src', () => {
|
|
12
|
+
render(<Avatar name="Alice Doe" />);
|
|
13
|
+
expect(screen.getByText('AD')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('shows only two initials for multi-word names', () => {
|
|
17
|
+
render(<Avatar name="First Middle Last" />);
|
|
18
|
+
expect(screen.getByText('FM')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders custom fallback when provided', () => {
|
|
22
|
+
render(<Avatar fallback="AB" />);
|
|
23
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders a default icon svg when no src, name, or fallback', () => {
|
|
27
|
+
const { container } = render(<Avatar />);
|
|
28
|
+
expect(container.querySelector('svg')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('falls back to initials when image fails to load', () => {
|
|
32
|
+
render(<Avatar src="broken.jpg" name="Bob Smith" />);
|
|
33
|
+
fireEvent.error(screen.getByRole('img'));
|
|
34
|
+
expect(screen.getByText('BS')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Badge } from './Badge.tsx';
|
|
4
|
+
|
|
5
|
+
describe('Badge', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(<Badge>New</Badge>);
|
|
8
|
+
expect(screen.getByText('New')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('forwards extra props', () => {
|
|
12
|
+
render(<Badge data-testid="badge">Label</Badge>);
|
|
13
|
+
expect(screen.getByTestId('badge')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs.tsx';
|
|
4
|
+
|
|
5
|
+
describe('Breadcrumbs', () => {
|
|
6
|
+
it('renders a navigation landmark with label "Breadcrumbs"', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Breadcrumbs>
|
|
9
|
+
<BreadcrumbItem href="/home">Home</BreadcrumbItem>
|
|
10
|
+
</Breadcrumbs>
|
|
11
|
+
);
|
|
12
|
+
expect(screen.getByRole('navigation', { name: 'Breadcrumbs' })).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('renders an ordered list', () => {
|
|
16
|
+
render(
|
|
17
|
+
<Breadcrumbs>
|
|
18
|
+
<BreadcrumbItem>Home</BreadcrumbItem>
|
|
19
|
+
</Breadcrumbs>
|
|
20
|
+
);
|
|
21
|
+
expect(screen.getByRole('list')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders all items', () => {
|
|
25
|
+
render(
|
|
26
|
+
<Breadcrumbs>
|
|
27
|
+
<BreadcrumbItem href="/home">Home</BreadcrumbItem>
|
|
28
|
+
<BreadcrumbItem href="/blog">Blog</BreadcrumbItem>
|
|
29
|
+
<BreadcrumbItem isCurrent>Post</BreadcrumbItem>
|
|
30
|
+
</Breadcrumbs>
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText('Blog')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Post')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders a link for items with href', () => {
|
|
38
|
+
render(
|
|
39
|
+
<Breadcrumbs>
|
|
40
|
+
<BreadcrumbItem href="/home">Home</BreadcrumbItem>
|
|
41
|
+
</Breadcrumbs>
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/home');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders a span (not a link) for the current item', () => {
|
|
47
|
+
render(
|
|
48
|
+
<Breadcrumbs>
|
|
49
|
+
<BreadcrumbItem href="/page" isCurrent>
|
|
50
|
+
Current
|
|
51
|
+
</BreadcrumbItem>
|
|
52
|
+
</Breadcrumbs>
|
|
53
|
+
);
|
|
54
|
+
expect(screen.queryByRole('link', { name: 'Current' })).not.toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText('Current')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('sets aria-current="page" on the current item', () => {
|
|
59
|
+
render(
|
|
60
|
+
<Breadcrumbs>
|
|
61
|
+
<BreadcrumbItem isCurrent>Now</BreadcrumbItem>
|
|
62
|
+
</Breadcrumbs>
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getByText('Now')).toHaveAttribute('aria-current', 'page');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders default separator between items', () => {
|
|
68
|
+
render(
|
|
69
|
+
<Breadcrumbs>
|
|
70
|
+
<BreadcrumbItem>A</BreadcrumbItem>
|
|
71
|
+
<BreadcrumbItem>B</BreadcrumbItem>
|
|
72
|
+
</Breadcrumbs>
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByText('/')).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('renders custom separator', () => {
|
|
78
|
+
render(
|
|
79
|
+
<Breadcrumbs separator=">">
|
|
80
|
+
<BreadcrumbItem>A</BreadcrumbItem>
|
|
81
|
+
<BreadcrumbItem>B</BreadcrumbItem>
|
|
82
|
+
</Breadcrumbs>
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByText('>')).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not render a separator after the last item', () => {
|
|
88
|
+
render(
|
|
89
|
+
<Breadcrumbs>
|
|
90
|
+
<BreadcrumbItem>A</BreadcrumbItem>
|
|
91
|
+
<BreadcrumbItem>B</BreadcrumbItem>
|
|
92
|
+
</Breadcrumbs>
|
|
93
|
+
);
|
|
94
|
+
expect(screen.getAllByText('/')).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
|
|
28
28
|
.control {
|
|
29
29
|
flex-shrink: 0;
|
|
30
|
-
height:
|
|
31
|
-
width:
|
|
30
|
+
height: var(--ds-checkbox-size);
|
|
31
|
+
width: var(--ds-checkbox-size);
|
|
32
32
|
background-color: var(--ds-surface-0);
|
|
33
|
-
border:
|
|
33
|
+
border: var(--ds-border-width-2) solid var(--ds-border-1);
|
|
34
34
|
border-radius: var(--ds-radius-sm);
|
|
35
35
|
transition: all var(--ds-transition-fast);
|
|
36
36
|
display: flex;
|
|
37
37
|
align-items: center;
|
|
38
38
|
justify-content: center;
|
|
39
|
-
margin-top:
|
|
39
|
+
margin-top: var(--ds-border-width-2);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.input:checked ~ .control {
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
|
|
57
57
|
.checkmark {
|
|
58
58
|
color: var(--ds-text-on-brand);
|
|
59
|
-
width:
|
|
60
|
-
height:
|
|
59
|
+
width: var(--ds-space-3);
|
|
60
|
+
height: var(--ds-space-3);
|
|
61
61
|
display: none;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
font-family: var(--ds-font-family-base);
|
|
81
81
|
font-size: var(--ds-font-size-xs);
|
|
82
82
|
color: var(--ds-text-2);
|
|
83
|
-
margin-left: calc(
|
|
83
|
+
margin-left: calc(var(--ds-checkbox-size) + var(--ds-space-2));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
.errorText {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Checkbox } from './Checkbox.tsx';
|
|
5
|
+
|
|
6
|
+
describe('Checkbox', () => {
|
|
7
|
+
it('renders a checkbox input', () => {
|
|
8
|
+
render(<Checkbox />);
|
|
9
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders label text', () => {
|
|
13
|
+
render(<Checkbox label="Accept terms" />);
|
|
14
|
+
expect(screen.getByLabelText('Accept terms')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders helper text', () => {
|
|
18
|
+
render(<Checkbox helperText="Required field" />);
|
|
19
|
+
expect(screen.getByText('Required field')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('links helper text via aria-describedby', () => {
|
|
23
|
+
render(<Checkbox label="Accept" helperText="Required" />);
|
|
24
|
+
const input = screen.getByRole('checkbox');
|
|
25
|
+
const helperId = input.getAttribute('aria-describedby');
|
|
26
|
+
expect(helperId).toBeTruthy();
|
|
27
|
+
expect(document.getElementById(helperId!)).toHaveTextContent('Required');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not set aria-describedby when no helperText', () => {
|
|
31
|
+
render(<Checkbox label="Accept" />);
|
|
32
|
+
expect(screen.getByRole('checkbox')).not.toHaveAttribute('aria-describedby');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('is disabled when disabled prop is set', () => {
|
|
36
|
+
render(<Checkbox disabled />);
|
|
37
|
+
expect(screen.getByRole('checkbox')).toBeDisabled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is checked when defaultChecked', () => {
|
|
41
|
+
render(<Checkbox defaultChecked />);
|
|
42
|
+
expect(screen.getByRole('checkbox')).toBeChecked();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('calls onChange when clicked', async () => {
|
|
46
|
+
const onChange = vi.fn();
|
|
47
|
+
render(<Checkbox onChange={onChange} />);
|
|
48
|
+
await userEvent.click(screen.getByRole('checkbox'));
|
|
49
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not call onChange when disabled', async () => {
|
|
53
|
+
const onChange = vi.fn();
|
|
54
|
+
render(<Checkbox disabled onChange={onChange} />);
|
|
55
|
+
await userEvent.click(screen.getByRole('checkbox'));
|
|
56
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('sets aria-invalid when error is true', () => {
|
|
60
|
+
render(<Checkbox error />);
|
|
61
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-invalid');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('does not set aria-invalid without error', () => {
|
|
65
|
+
render(<Checkbox />);
|
|
66
|
+
expect(screen.getByRole('checkbox')).not.toHaveAttribute('aria-invalid');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Dropdown } from './Dropdown.tsx';
|
|
5
|
+
|
|
6
|
+
// jsdom doesn't implement scrollIntoView
|
|
7
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
8
|
+
|
|
9
|
+
const options = [
|
|
10
|
+
{ value: 'a', label: 'Apple' },
|
|
11
|
+
{ value: 'b', label: 'Banana' },
|
|
12
|
+
{ value: 'c', label: 'Cherry', disabled: true },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
describe('Dropdown', () => {
|
|
16
|
+
it('renders the trigger button', () => {
|
|
17
|
+
render(<Dropdown options={options} />);
|
|
18
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('shows placeholder when no value is selected', () => {
|
|
22
|
+
render(<Dropdown options={options} placeholder="Pick a fruit" />);
|
|
23
|
+
expect(screen.getByRole('button')).toHaveTextContent('Pick a fruit');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders label and links it to the trigger', () => {
|
|
27
|
+
render(<Dropdown options={options} label="Fruit" />);
|
|
28
|
+
expect(screen.getByLabelText('Fruit')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('is closed by default', () => {
|
|
32
|
+
render(<Dropdown options={options} />);
|
|
33
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
|
|
34
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('opens listbox on click', async () => {
|
|
38
|
+
render(<Dropdown options={options} />);
|
|
39
|
+
await userEvent.click(screen.getByRole('button'));
|
|
40
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('shows all options when open', async () => {
|
|
45
|
+
render(<Dropdown options={options} />);
|
|
46
|
+
await userEvent.click(screen.getByRole('button'));
|
|
47
|
+
expect(screen.getAllByRole('option')).toHaveLength(3);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('selects an option on click and closes', async () => {
|
|
51
|
+
render(<Dropdown options={options} />);
|
|
52
|
+
await userEvent.click(screen.getByRole('button'));
|
|
53
|
+
await userEvent.click(screen.getByRole('option', { name: 'Banana' }));
|
|
54
|
+
expect(screen.getByRole('button')).toHaveTextContent('Banana');
|
|
55
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('calls onChange when an option is selected', async () => {
|
|
59
|
+
const onChange = vi.fn();
|
|
60
|
+
render(<Dropdown options={options} onChange={onChange} />);
|
|
61
|
+
await userEvent.click(screen.getByRole('button'));
|
|
62
|
+
await userEvent.click(screen.getByRole('option', { name: 'Apple' }));
|
|
63
|
+
expect(onChange).toHaveBeenCalledWith('a');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not select a disabled option', async () => {
|
|
67
|
+
const onChange = vi.fn();
|
|
68
|
+
render(<Dropdown options={options} onChange={onChange} />);
|
|
69
|
+
await userEvent.click(screen.getByRole('button'));
|
|
70
|
+
await userEvent.click(screen.getByRole('option', { name: 'Cherry' }));
|
|
71
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
72
|
+
expect(screen.getByRole('button')).not.toHaveTextContent('Cherry');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('closes on Escape key', async () => {
|
|
76
|
+
render(<Dropdown options={options} />);
|
|
77
|
+
await userEvent.click(screen.getByRole('button'));
|
|
78
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
79
|
+
await userEvent.keyboard('{Escape}');
|
|
80
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('opens and navigates with ArrowDown', async () => {
|
|
84
|
+
render(<Dropdown options={options} />);
|
|
85
|
+
screen.getByRole('button').focus();
|
|
86
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
87
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('is disabled when disabled prop is set', () => {
|
|
91
|
+
render(<Dropdown options={options} disabled />);
|
|
92
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('renders helper text', () => {
|
|
96
|
+
render(<Dropdown options={options} helperText="Choose one" />);
|
|
97
|
+
expect(screen.getByText('Choose one')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('sets aria-invalid when error is true', () => {
|
|
101
|
+
render(<Dropdown options={options} error />);
|
|
102
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-invalid');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Input } from './Input.tsx';
|
|
5
|
+
|
|
6
|
+
describe('Input', () => {
|
|
7
|
+
it('renders a text input', () => {
|
|
8
|
+
render(<Input />);
|
|
9
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders label and links it to the input', () => {
|
|
13
|
+
render(<Input label="Email" />);
|
|
14
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders helper text', () => {
|
|
18
|
+
render(<Input helperText="Enter your email" />);
|
|
19
|
+
expect(screen.getByText('Enter your email')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('links helper text via aria-describedby', () => {
|
|
23
|
+
render(<Input helperText="Hint" />);
|
|
24
|
+
const input = screen.getByRole('textbox');
|
|
25
|
+
const helperId = input.getAttribute('aria-describedby');
|
|
26
|
+
expect(helperId).toBeTruthy();
|
|
27
|
+
expect(document.getElementById(helperId!)).toHaveTextContent('Hint');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not set aria-describedby when no helperText', () => {
|
|
31
|
+
render(<Input />);
|
|
32
|
+
expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-describedby');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('passes placeholder through', () => {
|
|
36
|
+
render(<Input placeholder="Search..." />);
|
|
37
|
+
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is disabled when disabled prop is set', () => {
|
|
41
|
+
render(<Input disabled />);
|
|
42
|
+
expect(screen.getByRole('textbox')).toBeDisabled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('calls onChange when typing', async () => {
|
|
46
|
+
const onChange = vi.fn();
|
|
47
|
+
render(<Input onChange={onChange} />);
|
|
48
|
+
await userEvent.type(screen.getByRole('textbox'), 'hello');
|
|
49
|
+
expect(onChange).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('sets aria-invalid when error is true', () => {
|
|
53
|
+
render(<Input error />);
|
|
54
|
+
expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not set aria-invalid without error', () => {
|
|
58
|
+
render(<Input />);
|
|
59
|
+
expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-invalid');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
.unordered {
|
|
7
7
|
list-style-type: disc;
|
|
8
|
-
padding-left:
|
|
8
|
+
padding-left: var(--ds-space-6);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
.ordered {
|
|
12
12
|
list-style-type: decimal;
|
|
13
|
-
padding-left:
|
|
13
|
+
padding-left: var(--ds-space-6);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.none {
|
|
@@ -22,34 +22,34 @@
|
|
|
22
22
|
gap: 0;
|
|
23
23
|
}
|
|
24
24
|
.gap-1 {
|
|
25
|
-
gap:
|
|
25
|
+
gap: var(--ds-space-1);
|
|
26
26
|
}
|
|
27
27
|
.gap-2 {
|
|
28
|
-
gap:
|
|
28
|
+
gap: var(--ds-space-2);
|
|
29
29
|
}
|
|
30
30
|
.gap-3 {
|
|
31
|
-
gap:
|
|
31
|
+
gap: var(--ds-space-3);
|
|
32
32
|
}
|
|
33
33
|
.gap-4 {
|
|
34
|
-
gap:
|
|
34
|
+
gap: var(--ds-space-4);
|
|
35
35
|
}
|
|
36
36
|
.gap-5 {
|
|
37
|
-
gap:
|
|
37
|
+
gap: var(--ds-space-5);
|
|
38
38
|
}
|
|
39
39
|
.gap-6 {
|
|
40
|
-
gap:
|
|
40
|
+
gap: var(--ds-space-6);
|
|
41
41
|
}
|
|
42
42
|
.gap-8 {
|
|
43
|
-
gap:
|
|
43
|
+
gap: var(--ds-space-8);
|
|
44
44
|
}
|
|
45
45
|
.gap-10 {
|
|
46
|
-
gap:
|
|
46
|
+
gap: var(--ds-space-10);
|
|
47
47
|
}
|
|
48
48
|
.gap-12 {
|
|
49
|
-
gap:
|
|
49
|
+
gap: var(--ds-space-12);
|
|
50
50
|
}
|
|
51
51
|
.gap-14 {
|
|
52
|
-
gap:
|
|
52
|
+
gap: var(--ds-space-14);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/* Margin and Padding Utility classes - simplified for List if needed,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { List } from './List.tsx';
|
|
4
|
+
|
|
5
|
+
describe('List', () => {
|
|
6
|
+
it('renders an unordered list by default', () => {
|
|
7
|
+
render(
|
|
8
|
+
<List>
|
|
9
|
+
<List.Item>Item</List.Item>
|
|
10
|
+
</List>
|
|
11
|
+
);
|
|
12
|
+
expect(screen.getByRole('list')).toBeInTheDocument();
|
|
13
|
+
// ul doesn't have a specific role query distinguishing ul/ol, check tagName
|
|
14
|
+
expect(screen.getByRole('list').tagName).toBe('UL');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders an ordered list when variant is "ordered"', () => {
|
|
18
|
+
render(
|
|
19
|
+
<List variant="ordered">
|
|
20
|
+
<List.Item>Item</List.Item>
|
|
21
|
+
</List>
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByRole('list').tagName).toBe('OL');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders children', () => {
|
|
27
|
+
render(
|
|
28
|
+
<List>
|
|
29
|
+
<List.Item>Alpha</List.Item>
|
|
30
|
+
<List.Item>Beta</List.Item>
|
|
31
|
+
</List>
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Beta')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders list items', () => {
|
|
38
|
+
render(
|
|
39
|
+
<List>
|
|
40
|
+
<List.Item>One</List.Item>
|
|
41
|
+
<List.Item>Two</List.Item>
|
|
42
|
+
</List>
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
|
45
|
+
});
|
|
46
|
+
});
|