tharaday 0.7.2 → 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 +873 -805
- 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/package.json +8 -1
- package/src/components/Accordion/Accordion.test.tsx +82 -0
- 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 +5 -5
- package/src/components/Modal/Modal.test.tsx +86 -0
- 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/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/styles/semantic.css +3 -0
- package/src/styles/tokens.css +15 -0
|
@@ -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
|
+
});
|
|
@@ -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;
|
|
@@ -70,16 +70,16 @@
|
|
|
70
70
|
|
|
71
71
|
/* Sizes */
|
|
72
72
|
.sm {
|
|
73
|
-
max-width:
|
|
73
|
+
max-width: var(--ds-content-width-sm);
|
|
74
74
|
}
|
|
75
75
|
.md {
|
|
76
|
-
max-width:
|
|
76
|
+
max-width: var(--ds-content-width-md);
|
|
77
77
|
}
|
|
78
78
|
.lg {
|
|
79
|
-
max-width:
|
|
79
|
+
max-width: var(--ds-content-width-lg);
|
|
80
80
|
}
|
|
81
81
|
.xl {
|
|
82
|
-
max-width:
|
|
82
|
+
max-width: var(--ds-content-width-xl);
|
|
83
83
|
}
|
|
84
84
|
.full {
|
|
85
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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|