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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
left: 0;
|
|
5
5
|
right: 0;
|
|
6
6
|
bottom: 0;
|
|
7
|
-
background-color:
|
|
7
|
+
background-color: var(--ds-overlay);
|
|
8
8
|
display: flex;
|
|
9
9
|
align-items: center;
|
|
10
10
|
justify-content: center;
|
|
@@ -39,15 +39,14 @@
|
|
|
39
39
|
color: var(--ds-text-1);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
padding: 0
|
|
45
|
-
width: var(--ds-space-8)
|
|
46
|
-
height: var(--ds-space-8)
|
|
47
|
-
|
|
48
|
-
font-size: var(--ds-font-size-lg) !important;
|
|
42
|
+
/* Double class increases specificity to (0,2,0) — wins over Button's single-class size rules */
|
|
43
|
+
.closeButton.closeButton {
|
|
44
|
+
padding: 0;
|
|
45
|
+
width: var(--ds-space-8);
|
|
46
|
+
height: var(--ds-space-8);
|
|
47
|
+
font-size: var(--ds-font-size-lg);
|
|
49
48
|
font-weight: var(--ds-font-weight-semibold);
|
|
50
|
-
line-height: 1
|
|
49
|
+
line-height: 1;
|
|
51
50
|
display: flex;
|
|
52
51
|
align-items: center;
|
|
53
52
|
justify-content: center;
|
|
@@ -71,16 +70,16 @@
|
|
|
71
70
|
|
|
72
71
|
/* Sizes */
|
|
73
72
|
.sm {
|
|
74
|
-
max-width:
|
|
73
|
+
max-width: var(--ds-content-width-sm);
|
|
75
74
|
}
|
|
76
75
|
.md {
|
|
77
|
-
max-width:
|
|
76
|
+
max-width: var(--ds-content-width-md);
|
|
78
77
|
}
|
|
79
78
|
.lg {
|
|
80
|
-
max-width:
|
|
79
|
+
max-width: var(--ds-content-width-lg);
|
|
81
80
|
}
|
|
82
81
|
.xl {
|
|
83
|
-
max-width:
|
|
82
|
+
max-width: var(--ds-content-width-xl);
|
|
84
83
|
}
|
|
85
84
|
.full {
|
|
86
85
|
max-width: 100%;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Modal } from './Modal.tsx';
|
|
5
|
+
|
|
6
|
+
describe('Modal', () => {
|
|
7
|
+
it('renders nothing when closed', () => {
|
|
8
|
+
render(<Modal isOpen={false} onClose={vi.fn()} title="Test" />);
|
|
9
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders a dialog when open', () => {
|
|
13
|
+
render(<Modal isOpen onClose={vi.fn()} title="Test" />);
|
|
14
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders the title', () => {
|
|
18
|
+
render(<Modal isOpen onClose={vi.fn()} title="My Modal" />);
|
|
19
|
+
expect(screen.getByText('My Modal')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('sets aria-labelledby pointing to the title', () => {
|
|
23
|
+
render(<Modal isOpen onClose={vi.fn()} title="My Modal" />);
|
|
24
|
+
const dialog = screen.getByRole('dialog');
|
|
25
|
+
const titleId = dialog.getAttribute('aria-labelledby');
|
|
26
|
+
expect(titleId).toBeTruthy();
|
|
27
|
+
expect(document.getElementById(titleId!)).toHaveTextContent('My Modal');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders children', () => {
|
|
31
|
+
render(
|
|
32
|
+
<Modal isOpen onClose={vi.fn()} title="Test">
|
|
33
|
+
<p>Modal body</p>
|
|
34
|
+
</Modal>
|
|
35
|
+
);
|
|
36
|
+
expect(screen.getByText('Modal body')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders footer when provided', () => {
|
|
40
|
+
render(
|
|
41
|
+
<Modal isOpen onClose={vi.fn()} title="Test" footer={<button>Confirm</button>}>
|
|
42
|
+
Body
|
|
43
|
+
</Modal>
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('calls onClose when close button is clicked', async () => {
|
|
49
|
+
const onClose = vi.fn();
|
|
50
|
+
render(<Modal isOpen onClose={onClose} title="Test" />);
|
|
51
|
+
await userEvent.click(screen.getByRole('button', { name: 'Close modal' }));
|
|
52
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('calls onClose when overlay is clicked', async () => {
|
|
56
|
+
const onClose = vi.fn();
|
|
57
|
+
const { container } = render(<Modal isOpen onClose={onClose} title="Test" />);
|
|
58
|
+
const overlay = container.ownerDocument.body.querySelector('[class*="overlay"]') as HTMLElement;
|
|
59
|
+
await userEvent.click(overlay);
|
|
60
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('calls onClose when Escape key is pressed', async () => {
|
|
64
|
+
const onClose = vi.fn();
|
|
65
|
+
render(<Modal isOpen onClose={onClose} title="Test" />);
|
|
66
|
+
await userEvent.keyboard('{Escape}');
|
|
67
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not call onClose on Escape when isLoading', async () => {
|
|
71
|
+
const onClose = vi.fn();
|
|
72
|
+
render(<Modal isOpen isLoading onClose={onClose} title="Test" />);
|
|
73
|
+
await userEvent.keyboard('{Escape}');
|
|
74
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('disables close button when isLoading', () => {
|
|
78
|
+
render(<Modal isOpen isLoading onClose={vi.fn()} title="Test" />);
|
|
79
|
+
expect(screen.getByRole('button', { name: 'Close modal' })).toBeDisabled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('has aria-modal="true"', () => {
|
|
83
|
+
render(<Modal isOpen onClose={vi.fn()} title="Test" />);
|
|
84
|
+
expect(screen.getByRole('dialog')).toHaveAttribute('aria-modal', 'true');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -61,15 +61,14 @@ export const Modal = ({
|
|
|
61
61
|
document.body.style.overflow = 'hidden';
|
|
62
62
|
window.addEventListener('keydown', handleKeyDown);
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
setTimeout(() => {
|
|
64
|
+
requestAnimationFrame(() => {
|
|
66
65
|
const focusableElements = modalRef.current?.querySelectorAll<HTMLElement>(
|
|
67
66
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
68
67
|
);
|
|
69
68
|
if (focusableElements && focusableElements.length > 0) {
|
|
70
69
|
focusableElements[0].focus();
|
|
71
70
|
}
|
|
72
|
-
}
|
|
71
|
+
});
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
return () => {
|
|
@@ -108,7 +107,14 @@ export const Modal = ({
|
|
|
108
107
|
aria-label="Close modal"
|
|
109
108
|
disabled={isLoading}
|
|
110
109
|
>
|
|
111
|
-
|
|
110
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
|
111
|
+
<path
|
|
112
|
+
d="M1 1L9 9M9 1L1 9"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth="1.5"
|
|
115
|
+
strokeLinecap="round"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
112
118
|
</Button>
|
|
113
119
|
</div>
|
|
114
120
|
<div className={styles.content}>{children}</div>
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
.container {
|
|
8
|
-
max-width:
|
|
8
|
+
max-width: var(--ds-content-width-2xl);
|
|
9
9
|
margin: 0 auto;
|
|
10
10
|
padding: 0 var(--ds-space-4);
|
|
11
11
|
display: flex;
|
|
12
12
|
align-items: stretch;
|
|
13
|
-
height:
|
|
13
|
+
height: var(--ds-space-16);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.leftSection {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
text-decoration: none;
|
|
48
48
|
font-size: var(--ds-font-size-sm);
|
|
49
49
|
font-weight: var(--ds-font-weight-medium);
|
|
50
|
-
border-bottom:
|
|
50
|
+
border-bottom: var(--ds-border-width-2) solid transparent;
|
|
51
51
|
background: none;
|
|
52
52
|
border-top: none;
|
|
53
53
|
border-left: none;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
font-family: var(--ds-font-family-base);
|
|
8
8
|
gap: var(--ds-space-3);
|
|
9
9
|
box-shadow: var(--ds-shadow-sm);
|
|
10
|
-
max-width:
|
|
10
|
+
max-width: var(--ds-content-width-sm);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.content {
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
line-height: var(--ds-line-height-base);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/* Double class increases specificity to (0,2,0) — wins over Button's single-class size rules */
|
|
29
|
+
.closeButton.closeButton {
|
|
29
30
|
flex-shrink: 0;
|
|
30
31
|
margin-top: calc(var(--ds-space-1) * -1);
|
|
31
32
|
margin-right: calc(var(--ds-space-2) * -1);
|
|
32
|
-
padding: 0
|
|
33
|
-
width: var(--ds-space-6)
|
|
34
|
-
height: var(--ds-space-6)
|
|
35
|
-
min-width: unset !important;
|
|
33
|
+
padding: 0;
|
|
34
|
+
width: var(--ds-space-6);
|
|
35
|
+
height: var(--ds-space-6);
|
|
36
36
|
opacity: 0.6;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Notification } from './Notification.tsx';
|
|
5
|
+
|
|
6
|
+
describe('Notification', () => {
|
|
7
|
+
it('renders with role="alert"', () => {
|
|
8
|
+
render(<Notification>Something happened</Notification>);
|
|
9
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders children as the message', () => {
|
|
13
|
+
render(<Notification>File saved successfully</Notification>);
|
|
14
|
+
expect(screen.getByText('File saved successfully')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders a title when provided', () => {
|
|
18
|
+
render(<Notification title="Success">Done</Notification>);
|
|
19
|
+
expect(screen.getByText('Success')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('does not render a close button without onClose', () => {
|
|
23
|
+
render(<Notification>Message</Notification>);
|
|
24
|
+
expect(screen.queryByRole('button', { name: 'Close notification' })).not.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders a close button when onClose is provided', () => {
|
|
28
|
+
render(<Notification onClose={vi.fn()}>Message</Notification>);
|
|
29
|
+
expect(screen.getByRole('button', { name: 'Close notification' })).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('calls onClose when close button is clicked', async () => {
|
|
33
|
+
const onClose = vi.fn();
|
|
34
|
+
render(<Notification onClose={onClose}>Message</Notification>);
|
|
35
|
+
await userEvent.click(screen.getByRole('button', { name: 'Close notification' }));
|
|
36
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -26,7 +26,14 @@ export const Notification = ({
|
|
|
26
26
|
className={styles.closeButton}
|
|
27
27
|
aria-label="Close notification"
|
|
28
28
|
>
|
|
29
|
-
|
|
29
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
|
30
|
+
<path
|
|
31
|
+
d="M1 1L9 9M9 1L1 9"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
strokeWidth="1.5"
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
30
37
|
</Button>
|
|
31
38
|
)}
|
|
32
39
|
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Pagination } from './Pagination.tsx';
|
|
5
|
+
|
|
6
|
+
describe('Pagination', () => {
|
|
7
|
+
it('renders nothing when count is 0', () => {
|
|
8
|
+
const { container } = render(<Pagination count={0} onPageChange={vi.fn()} />);
|
|
9
|
+
expect(container.firstChild).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders a nav landmark', () => {
|
|
13
|
+
render(<Pagination count={5} onPageChange={vi.fn()} />);
|
|
14
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders page buttons', () => {
|
|
18
|
+
render(<Pagination count={3} onPageChange={vi.fn()} />);
|
|
19
|
+
expect(screen.getByRole('button', { name: 'Page 1, current page' })).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByRole('button', { name: 'Page 2' })).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByRole('button', { name: 'Page 3' })).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('marks the current page with aria-current="page"', () => {
|
|
25
|
+
render(<Pagination count={5} defaultPage={2} onPageChange={vi.fn()} />);
|
|
26
|
+
expect(screen.getByRole('button', { name: 'Page 2, current page' })).toHaveAttribute(
|
|
27
|
+
'aria-current',
|
|
28
|
+
'page'
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('calls onPageChange when a page is clicked', async () => {
|
|
33
|
+
const onPageChange = vi.fn();
|
|
34
|
+
render(<Pagination count={5} onPageChange={onPageChange} />);
|
|
35
|
+
await userEvent.click(screen.getByRole('button', { name: 'Page 3' }));
|
|
36
|
+
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('disables First and Prev buttons on first page', () => {
|
|
40
|
+
render(<Pagination count={5} defaultPage={1} onPageChange={vi.fn()} />);
|
|
41
|
+
expect(screen.getByRole('button', { name: 'Go to first page' })).toBeDisabled();
|
|
42
|
+
expect(screen.getByRole('button', { name: 'Go to previous page' })).toBeDisabled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('disables Next and Last buttons on last page', () => {
|
|
46
|
+
render(<Pagination count={5} defaultPage={5} onPageChange={vi.fn()} />);
|
|
47
|
+
expect(screen.getByRole('button', { name: 'Go to next page' })).toBeDisabled();
|
|
48
|
+
expect(screen.getByRole('button', { name: 'Go to last page' })).toBeDisabled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('advances page with Next button', async () => {
|
|
52
|
+
const onPageChange = vi.fn();
|
|
53
|
+
render(<Pagination count={5} defaultPage={2} onPageChange={onPageChange} />);
|
|
54
|
+
await userEvent.click(screen.getByRole('button', { name: 'Go to next page' }));
|
|
55
|
+
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('goes back with Prev button', async () => {
|
|
59
|
+
const onPageChange = vi.fn();
|
|
60
|
+
render(<Pagination count={5} defaultPage={3} onPageChange={onPageChange} />);
|
|
61
|
+
await userEvent.click(screen.getByRole('button', { name: 'Go to previous page' }));
|
|
62
|
+
expect(onPageChange).toHaveBeenCalledWith(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('disables all buttons when disabled prop is set', () => {
|
|
66
|
+
render(<Pagination count={5} defaultPage={3} disabled onPageChange={vi.fn()} />);
|
|
67
|
+
const buttons = screen.getAllByRole('button');
|
|
68
|
+
buttons.forEach((btn) => expect(btn).toBeDisabled());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { ProgressBar } from './ProgressBar.tsx';
|
|
4
|
+
|
|
5
|
+
describe('ProgressBar', () => {
|
|
6
|
+
it('renders a progressbar', () => {
|
|
7
|
+
render(<ProgressBar value={50} />);
|
|
8
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('sets aria-valuenow to the current value', () => {
|
|
12
|
+
render(<ProgressBar value={40} />);
|
|
13
|
+
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '40');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('sets aria-valuemin to 0', () => {
|
|
17
|
+
render(<ProgressBar value={50} />);
|
|
18
|
+
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemin', '0');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('sets aria-valuemax to the max prop', () => {
|
|
22
|
+
render(<ProgressBar value={5} max={10} />);
|
|
23
|
+
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuemax', '10');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('clamps value to max', () => {
|
|
27
|
+
render(<ProgressBar value={150} max={100} />);
|
|
28
|
+
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '100');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('clamps value to 0', () => {
|
|
32
|
+
render(<ProgressBar value={-10} />);
|
|
33
|
+
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders label text', () => {
|
|
37
|
+
render(<ProgressBar value={50} label="Uploading" />);
|
|
38
|
+
expect(screen.getByText('Uploading')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders percentage text when showLabel is true', () => {
|
|
42
|
+
render(<ProgressBar value={75} showLabel />);
|
|
43
|
+
expect(screen.getByText('75%')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('uses aria-label="Progress" when no label is provided', () => {
|
|
47
|
+
render(<ProgressBar value={50} />);
|
|
48
|
+
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-label', 'Progress');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('uses aria-labelledby pointing to the label when label is provided', () => {
|
|
52
|
+
render(<ProgressBar value={50} label="Loading" />);
|
|
53
|
+
const pb = screen.getByRole('progressbar');
|
|
54
|
+
const labelId = pb.getAttribute('aria-labelledby');
|
|
55
|
+
expect(labelId).toBeTruthy();
|
|
56
|
+
expect(document.getElementById(labelId!)).toHaveTextContent('Loading');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { RadioButton } from './RadioButton.tsx';
|
|
5
|
+
|
|
6
|
+
describe('RadioButton', () => {
|
|
7
|
+
it('renders a radio input', () => {
|
|
8
|
+
render(<RadioButton />);
|
|
9
|
+
expect(screen.getByRole('radio')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders label and links it to the input', () => {
|
|
13
|
+
render(<RadioButton label="Option A" />);
|
|
14
|
+
expect(screen.getByLabelText('Option A')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders helper text', () => {
|
|
18
|
+
render(<RadioButton helperText="Recommended" />);
|
|
19
|
+
expect(screen.getByText('Recommended')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('links helper text via aria-describedby', () => {
|
|
23
|
+
render(<RadioButton helperText="Hint" />);
|
|
24
|
+
const input = screen.getByRole('radio');
|
|
25
|
+
const helperId = input.getAttribute('aria-describedby');
|
|
26
|
+
expect(helperId).toBeTruthy();
|
|
27
|
+
expect(document.getElementById(helperId!)).toHaveTextContent('Hint');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is disabled when disabled prop is set', () => {
|
|
31
|
+
render(<RadioButton disabled />);
|
|
32
|
+
expect(screen.getByRole('radio')).toBeDisabled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('is checked when defaultChecked', () => {
|
|
36
|
+
render(<RadioButton defaultChecked />);
|
|
37
|
+
expect(screen.getByRole('radio')).toBeChecked();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('calls onChange when selected', async () => {
|
|
41
|
+
const onChange = vi.fn();
|
|
42
|
+
render(<RadioButton onChange={onChange} />);
|
|
43
|
+
await userEvent.click(screen.getByRole('radio'));
|
|
44
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('sets aria-invalid when error is true', () => {
|
|
48
|
+
render(<RadioButton error />);
|
|
49
|
+
expect(screen.getByRole('radio')).toHaveAttribute('aria-invalid');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Select } from './Select.tsx';
|
|
5
|
+
|
|
6
|
+
const options = [
|
|
7
|
+
{ value: 'a', label: 'Option A' },
|
|
8
|
+
{ value: 'b', label: 'Option B' },
|
|
9
|
+
{ value: 'c', label: 'Option C', disabled: true },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe('Select', () => {
|
|
13
|
+
it('renders a combobox', () => {
|
|
14
|
+
render(<Select options={options} />);
|
|
15
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders label and links it to the select', () => {
|
|
19
|
+
render(<Select options={options} label="Country" />);
|
|
20
|
+
expect(screen.getByLabelText('Country')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders options from the options prop', () => {
|
|
24
|
+
render(<Select options={options} />);
|
|
25
|
+
expect(screen.getByRole('option', { name: 'Option A' })).toBeInTheDocument();
|
|
26
|
+
expect(screen.getByRole('option', { name: 'Option B' })).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders children when options prop is not provided', () => {
|
|
30
|
+
render(
|
|
31
|
+
<Select>
|
|
32
|
+
<option value="x">Choice X</option>
|
|
33
|
+
</Select>
|
|
34
|
+
);
|
|
35
|
+
expect(screen.getByRole('option', { name: 'Choice X' })).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders disabled options', () => {
|
|
39
|
+
render(<Select options={options} />);
|
|
40
|
+
expect(screen.getByRole('option', { name: 'Option C' })).toBeDisabled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders helper text', () => {
|
|
44
|
+
render(<Select options={options} helperText="Pick one" />);
|
|
45
|
+
expect(screen.getByText('Pick one')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('is disabled when disabled prop is set', () => {
|
|
49
|
+
render(<Select options={options} disabled />);
|
|
50
|
+
expect(screen.getByRole('combobox')).toBeDisabled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('calls onChange when selection changes', async () => {
|
|
54
|
+
const onChange = vi.fn();
|
|
55
|
+
render(<Select options={options} onChange={onChange} />);
|
|
56
|
+
await userEvent.selectOptions(screen.getByRole('combobox'), 'Option B');
|
|
57
|
+
expect(onChange).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('sets aria-invalid when error is true', () => {
|
|
61
|
+
render(<Select options={options} error />);
|
|
62
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Slider } from './Slider.tsx';
|
|
4
|
+
|
|
5
|
+
describe('Slider', () => {
|
|
6
|
+
it('renders a range input', () => {
|
|
7
|
+
render(<Slider />);
|
|
8
|
+
expect(screen.getByRole('slider')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('renders label and links it to the slider', () => {
|
|
12
|
+
render(<Slider label="Volume" />);
|
|
13
|
+
expect(screen.getByLabelText('Volume')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('uses default min=0 and max=100', () => {
|
|
17
|
+
render(<Slider />);
|
|
18
|
+
const slider = screen.getByRole('slider');
|
|
19
|
+
expect(slider).toHaveAttribute('min', '0');
|
|
20
|
+
expect(slider).toHaveAttribute('max', '100');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('forwards min and max props', () => {
|
|
24
|
+
render(<Slider min={10} max={50} />);
|
|
25
|
+
const slider = screen.getByRole('slider');
|
|
26
|
+
expect(slider).toHaveAttribute('min', '10');
|
|
27
|
+
expect(slider).toHaveAttribute('max', '50');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders helper text', () => {
|
|
31
|
+
render(<Slider helperText="Adjust the level" />);
|
|
32
|
+
expect(screen.getByText('Adjust the level')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows current value when showValue is true', () => {
|
|
36
|
+
render(<Slider defaultValue={30} showValue />);
|
|
37
|
+
expect(screen.getByText('30')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is disabled when disabled prop is set', () => {
|
|
41
|
+
render(<Slider disabled />);
|
|
42
|
+
expect(screen.getByRole('slider')).toBeDisabled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders two sliders for a range (dual) value', () => {
|
|
46
|
+
render(<Slider defaultValue={[20, 80]} />);
|
|
47
|
+
expect(screen.getAllByRole('slider')).toHaveLength(2);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -95,13 +95,13 @@
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
[data-orientation='horizontal'] .connector {
|
|
98
|
-
height:
|
|
98
|
+
height: var(--ds-border-width-2);
|
|
99
99
|
flex: 1;
|
|
100
100
|
margin-left: var(--ds-space-3);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
[data-orientation='vertical'] .connector {
|
|
104
|
-
width:
|
|
104
|
+
width: var(--ds-border-width-2);
|
|
105
105
|
height: var(--ds-space-8);
|
|
106
106
|
margin-left: calc(var(--stepper-marker-size) / 2 - 1px);
|
|
107
107
|
margin-top: var(--ds-space-2);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Stepper } from './Stepper.tsx';
|
|
5
|
+
|
|
6
|
+
const steps = [
|
|
7
|
+
{ id: 'account', label: 'Account' },
|
|
8
|
+
{ id: 'profile', label: 'Profile' },
|
|
9
|
+
{ id: 'review', label: 'Review' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe('Stepper', () => {
|
|
13
|
+
it('renders an ordered list', () => {
|
|
14
|
+
render(<Stepper steps={steps} currentStep="account" />);
|
|
15
|
+
expect(screen.getByRole('list')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders all step labels', () => {
|
|
19
|
+
render(<Stepper steps={steps} currentStep="account" />);
|
|
20
|
+
expect(screen.getByText('Account')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('Profile')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText('Review')).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('uses "Progress" as default aria-label', () => {
|
|
26
|
+
render(<Stepper steps={steps} currentStep="account" />);
|
|
27
|
+
expect(screen.getByRole('list')).toHaveAttribute('aria-label', 'Progress');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses custom ariaLabel when provided', () => {
|
|
31
|
+
render(<Stepper steps={steps} currentStep="account" ariaLabel="Checkout steps" />);
|
|
32
|
+
expect(screen.getByRole('list')).toHaveAttribute('aria-label', 'Checkout steps');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not render interactive buttons without onStepClick', () => {
|
|
36
|
+
render(<Stepper steps={steps} currentStep="account" />);
|
|
37
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders interactive buttons when onStepClick is provided', () => {
|
|
41
|
+
render(<Stepper steps={steps} currentStep="account" onStepClick={vi.fn()} />);
|
|
42
|
+
expect(screen.getAllByRole('button')).toHaveLength(steps.length);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('calls onStepClick with the step when clicked', async () => {
|
|
46
|
+
const onStepClick = vi.fn();
|
|
47
|
+
render(<Stepper steps={steps} currentStep="account" onStepClick={onStepClick} />);
|
|
48
|
+
await userEvent.click(screen.getByRole('button', { name: /Profile/i }));
|
|
49
|
+
expect(onStepClick).toHaveBeenCalledWith(steps[1], 1);
|
|
50
|
+
});
|
|
51
|
+
});
|