@scottish-government/designsystem-react 0.0.0
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/.editorconfig +12 -0
- package/.github/workflows/release-package.yml +96 -0
- package/@types/common/ConditionalWrapper.d.ts +6 -0
- package/@types/common/HintText.d.ts +6 -0
- package/@types/common/Icon.d.ts +11 -0
- package/@types/common/ScreenReaderText.d.ts +4 -0
- package/@types/common/WrapperTag.d.ts +5 -0
- package/@types/components/Accordion.d.ts +15 -0
- package/@types/components/AspectBox.d.ts +5 -0
- package/@types/components/BackToTop.d.ts +5 -0
- package/@types/components/Breadcrumbs.d.ts +14 -0
- package/@types/components/Button.d.ts +17 -0
- package/@types/components/Checkbox.d.ts +13 -0
- package/@types/components/ConfirmationMessage.d.ts +7 -0
- package/@types/components/ContentsNav.d.ts +15 -0
- package/@types/components/DatePicker.d.ts +19 -0
- package/@types/components/Details.d.ts +6 -0
- package/@types/components/ErrorMessage.d.ts +6 -0
- package/@types/components/Metadata.d.ts +11 -0
- package/@types/components/NotificationBanner.d.ts +9 -0
- package/@types/components/NotificationPanel.d.ts +7 -0
- package/@types/components/PageHeader.d.ts +6 -0
- package/@types/components/PhaseBanner.d.ts +5 -0
- package/@types/components/Question.d.ts +11 -0
- package/@types/components/RadioButton.d.ts +15 -0
- package/@types/components/Select.d.ts +14 -0
- package/@types/components/SequentialNavigation.d.ts +14 -0
- package/@types/components/SideNavigation.d.ts +19 -0
- package/@types/components/SiteNavigation.d.ts +13 -0
- package/@types/components/SiteSearch.d.ts +14 -0
- package/@types/components/SkipLinks.d.ts +14 -0
- package/@types/components/Tag.d.ts +7 -0
- package/@types/components/TaskList.d.ts +21 -0
- package/@types/components/TextInput.d.ts +12 -0
- package/@types/components/Textarea.d.ts +4 -0
- package/@types/global.d.ts +1 -0
- package/@types/sgds.d.ts +35 -0
- package/package.json +36 -0
- package/src/common/conditional-wrapper.test.tsx +36 -0
- package/src/common/conditional-wrapper.tsx +9 -0
- package/src/common/hint-text.test.tsx +47 -0
- package/src/common/hint-text.tsx +21 -0
- package/src/common/icon.test.tsx +100 -0
- package/src/common/icon.tsx +28 -0
- package/src/common/screen-reader-text.test.tsx +31 -0
- package/src/common/screen-reader-text.tsx +17 -0
- package/src/common/wrapper-tag.test.tsx +42 -0
- package/src/common/wrapper-tag.tsx +15 -0
- package/src/components/accordion/accordion.test.tsx +212 -0
- package/src/components/accordion/accordion.tsx +108 -0
- package/src/components/aspect-box/aspect-box.test.tsx +81 -0
- package/src/components/aspect-box/aspect-box.tsx +57 -0
- package/src/components/back-to-top/back-to-top.test.tsx +45 -0
- package/src/components/back-to-top/back-to-top.tsx +33 -0
- package/src/components/breadcrumbs/breadcrumbs.test.tsx +77 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +53 -0
- package/src/components/button/button.test.tsx +125 -0
- package/src/components/button/button.tsx +48 -0
- package/src/components/checkbox/checkbox.test.tsx +180 -0
- package/src/components/checkbox/checkbox.tsx +107 -0
- package/src/components/confirmation-message/confirmation-message.test.tsx +46 -0
- package/src/components/confirmation-message/confirmation-message.tsx +32 -0
- package/src/components/contents-nav/contents-nav.test.tsx +136 -0
- package/src/components/contents-nav/contents-nav.tsx +54 -0
- package/src/components/date-picker/date-picker.test.tsx +209 -0
- package/src/components/date-picker/date-picker.tsx +129 -0
- package/src/components/details/details.test.tsx +38 -0
- package/src/components/details/details.tsx +25 -0
- package/src/components/error-message/error-message.test.tsx +40 -0
- package/src/components/error-message/error-message.tsx +23 -0
- package/src/components/inset-text/inset-text.test.tsx +33 -0
- package/src/components/inset-text/inset-text.tsx +19 -0
- package/src/components/notification-banner/notification-banner.test.tsx +93 -0
- package/src/components/notification-banner/notification-banner.tsx +70 -0
- package/src/components/notification-panel/notification-panel.test.tsx +77 -0
- package/src/components/notification-panel/notification-panel.tsx +31 -0
- package/src/components/page-header/page-header.test.tsx +48 -0
- package/src/components/page-header/page-header.tsx +22 -0
- package/src/components/page-metadata/page-metadata.test.tsx +56 -0
- package/src/components/page-metadata/page-metadata.tsx +39 -0
- package/src/components/phase-banner/phase-banner.test.tsx +67 -0
- package/src/components/phase-banner/phase-banner.tsx +27 -0
- package/src/components/question/question.test.tsx +69 -0
- package/src/components/question/question.tsx +33 -0
- package/src/components/radio-button/radio-button.test.tsx +190 -0
- package/src/components/radio-button/radio-button.tsx +88 -0
- package/src/components/select/select.test.tsx +208 -0
- package/src/components/select/select.tsx +86 -0
- package/src/components/sequential-navigation/sequential-navigation.test.tsx +67 -0
- package/src/components/sequential-navigation/sequential-navigation.tsx +55 -0
- package/src/components/side-navigation/side-navigation.test.tsx +156 -0
- package/src/components/side-navigation/side-navigation.tsx +85 -0
- package/src/components/site-navigation/site-navigation.test.tsx +63 -0
- package/src/components/site-navigation/site-navigation.tsx +40 -0
- package/src/components/site-search/site-search.test.tsx +153 -0
- package/src/components/site-search/site-search.tsx +97 -0
- package/src/components/skip-links/skip-links.test.tsx +84 -0
- package/src/components/skip-links/skip-links.tsx +39 -0
- package/src/components/tag/tag.test.tsx +45 -0
- package/src/components/tag/tag.tsx +23 -0
- package/src/components/task-list/task-list.test.tsx +409 -0
- package/src/components/task-list/task-list.tsx +132 -0
- package/src/components/text-input/text-input.test.tsx +307 -0
- package/src/components/text-input/text-input.tsx +98 -0
- package/src/components/textarea/textarea.test.tsx +212 -0
- package/src/components/textarea/textarea.tsx +82 -0
- package/src/components/warning-text/warning-text.test.tsx +40 -0
- package/src/components/warning-text/warning-text.tsx +21 -0
- package/tsconfig.json +45 -0
- package/vite.config.ts +12 -0
- package/vitest-setup.ts +13 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import AspectBox from './aspect-box';
|
|
4
|
+
|
|
5
|
+
test('aspect box renders correctly', () => {
|
|
6
|
+
render(
|
|
7
|
+
<AspectBox>
|
|
8
|
+
<img src="./highland-cow.jpg" alt="" />
|
|
9
|
+
</AspectBox>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const image = document.querySelector('img');
|
|
13
|
+
const imageContainer = image?.parentNode;
|
|
14
|
+
|
|
15
|
+
expect(image).toHaveClass('ds_aspect-box__inner');
|
|
16
|
+
expect(imageContainer).toHaveClass('ds_aspect-box');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('1:1 ratio', () => {
|
|
20
|
+
render(
|
|
21
|
+
<AspectBox ratio="1:1">
|
|
22
|
+
<img src="./highland-cow.jpg" alt="" />
|
|
23
|
+
</AspectBox>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const image = document.querySelector('img');
|
|
27
|
+
const imageContainer = image?.parentNode;
|
|
28
|
+
|
|
29
|
+
expect(imageContainer).toHaveClass('ds_aspect-box--square');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('square ratio', () => {
|
|
33
|
+
render(
|
|
34
|
+
<AspectBox ratio="square">
|
|
35
|
+
<img src="./highland-cow.jpg" alt="" />
|
|
36
|
+
</AspectBox>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const image = document.querySelector('img');
|
|
40
|
+
const imageContainer = image?.parentNode;
|
|
41
|
+
|
|
42
|
+
expect(imageContainer).toHaveClass('ds_aspect-box--square');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('4:3 ratio', () => {
|
|
46
|
+
render(
|
|
47
|
+
<AspectBox ratio="4:3">
|
|
48
|
+
<img src="./highland-cow.jpg" alt="" />
|
|
49
|
+
</AspectBox>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const image = document.querySelector('img');
|
|
53
|
+
const imageContainer = image?.parentNode;
|
|
54
|
+
|
|
55
|
+
expect(imageContainer).toHaveClass('ds_aspect-box--43');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('21:9 ratio', () => {
|
|
59
|
+
render(
|
|
60
|
+
<AspectBox ratio="21:9">
|
|
61
|
+
<img src="./highland-cow.jpg" alt="" />
|
|
62
|
+
</AspectBox>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const image = document.querySelector('img');
|
|
66
|
+
const imageContainer = image?.parentNode;
|
|
67
|
+
|
|
68
|
+
expect(imageContainer).toHaveClass('ds_aspect-box--219');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('passing additional props', () => {
|
|
72
|
+
render(
|
|
73
|
+
<AspectBox data-test="foo">
|
|
74
|
+
<img src="./highland-cow.jpg" alt="" />
|
|
75
|
+
</AspectBox>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const image = document.querySelector('img');
|
|
79
|
+
const imageContainer = image?.parentNode;
|
|
80
|
+
expect(imageContainer?.dataset.test).toEqual('foo');
|
|
81
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { Children, useEffect, useRef } from 'react';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import DSAspectBox from '@scottish-government/design-system/src/components/aspect-box/aspect-box-fallback';
|
|
4
|
+
|
|
5
|
+
const AspectBox: React.FC<SGDS.Component.AspectBox> = ({
|
|
6
|
+
children,
|
|
7
|
+
ratio,
|
|
8
|
+
...props
|
|
9
|
+
}) => {
|
|
10
|
+
const ref = useRef(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (ref.current) {
|
|
14
|
+
new DSAspectBox(ref.current).init();
|
|
15
|
+
}
|
|
16
|
+
}, [ref]);
|
|
17
|
+
|
|
18
|
+
function processChild(child: any) {
|
|
19
|
+
if (['img', 'svg', 'picture'].includes(child.type)) {
|
|
20
|
+
return React.cloneElement(child, { className: 'ds_aspect-box__inner' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let ratioClassName;
|
|
25
|
+
switch (ratio) {
|
|
26
|
+
case '1:1':
|
|
27
|
+
case 'square':
|
|
28
|
+
ratioClassName = 'ds_aspect-box--square'
|
|
29
|
+
break;
|
|
30
|
+
case '4:3':
|
|
31
|
+
ratioClassName = 'ds_aspect-box--43'
|
|
32
|
+
break;
|
|
33
|
+
case '21:9':
|
|
34
|
+
ratioClassName = 'ds_aspect-box--219'
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
ratioClassName = ''
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={[
|
|
44
|
+
'ds_aspect-box',
|
|
45
|
+
`${ratioClassName}`
|
|
46
|
+
].join(' ')}
|
|
47
|
+
ref={ref}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{Children.map(children, child => processChild(child))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
AspectBox.displayName = 'AspectBox';
|
|
56
|
+
|
|
57
|
+
export default AspectBox;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import BackToTop from './back-to-top';
|
|
4
|
+
|
|
5
|
+
test('back to top renders correctly', () => {
|
|
6
|
+
render(
|
|
7
|
+
<BackToTop data-test="foo" />
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const button = screen.getByRole('link');
|
|
11
|
+
const container = button.parentElement;
|
|
12
|
+
const icon = within(button).getByRole('img', {hidden: true});
|
|
13
|
+
|
|
14
|
+
// check button
|
|
15
|
+
expect(button).toHaveClass('ds_back-to-top__button');
|
|
16
|
+
expect(button).toHaveAttribute('href', '#page-top');
|
|
17
|
+
expect(button.tagName).toEqual('A');
|
|
18
|
+
|
|
19
|
+
// check container
|
|
20
|
+
expect(container).toHaveClass('ds_back-to-top');
|
|
21
|
+
|
|
22
|
+
// check icon
|
|
23
|
+
expect(icon).toHaveClass('ds_icon', 'ds_back-to-top__icon');
|
|
24
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('renders with custom href', () => {
|
|
28
|
+
render(
|
|
29
|
+
<BackToTop href="#foo" />
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// check href
|
|
33
|
+
const button = screen.getByRole('link');
|
|
34
|
+
expect(button).toHaveAttribute('href', '#foo');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('passing additional props', () => {
|
|
38
|
+
render(
|
|
39
|
+
<BackToTop data-test="foo" />
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const button = screen.getByRole('link');
|
|
43
|
+
const container = button.parentElement;
|
|
44
|
+
expect(container?.dataset.test).toEqual('foo');
|
|
45
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import DSBackToTop from '@scottish-government/design-system/src/components/back-to-top/back-to-top';
|
|
4
|
+
import Icon from '../../common/icon';
|
|
5
|
+
|
|
6
|
+
const BackToTop: React.FC<SGDS.Component.BackToTop> = ({
|
|
7
|
+
href = '#page-top',
|
|
8
|
+
...props
|
|
9
|
+
}) => {
|
|
10
|
+
const ref = useRef(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (ref.current) {
|
|
14
|
+
new DSBackToTop(ref.current).init();
|
|
15
|
+
}
|
|
16
|
+
}, [ref]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className='ds_back-to-top'
|
|
21
|
+
ref={ref}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<a href={href} className="ds_back-to-top__button">Back to top
|
|
25
|
+
<Icon className="ds_back-to-top__icon" icon="arrow_upward"/>
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
BackToTop.displayName = 'BackToTop';
|
|
32
|
+
|
|
33
|
+
export default BackToTop;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import Breadcrumbs from './breadcrumbs';
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ href: 'home', title: 'Home' },
|
|
7
|
+
{ href: 'category', title: 'Category' },
|
|
8
|
+
{ title: 'Page' }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
test('renders correctly', () => {
|
|
12
|
+
render(
|
|
13
|
+
<Breadcrumbs items={items} />
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const nav = screen.getByRole('navigation');
|
|
17
|
+
const list = within(nav).getByRole('list');
|
|
18
|
+
const listItems = within(list).getAllByRole('listitem');
|
|
19
|
+
|
|
20
|
+
// check nav
|
|
21
|
+
expect(nav).toHaveAttribute('aria-label', 'Breadcrumb');
|
|
22
|
+
|
|
23
|
+
// check list
|
|
24
|
+
expect(list.tagName).toEqual('OL');
|
|
25
|
+
expect(list).toHaveClass('ds_breadcrumbs');
|
|
26
|
+
|
|
27
|
+
// check items
|
|
28
|
+
expect(listItems.length).toEqual(items.length);
|
|
29
|
+
|
|
30
|
+
listItems.forEach((item, index) => {
|
|
31
|
+
expect(item).toHaveClass('ds_breadcrumbs__item');
|
|
32
|
+
|
|
33
|
+
const link = within(item).queryByRole('link');
|
|
34
|
+
|
|
35
|
+
if (index + 1 < listItems.length) {
|
|
36
|
+
expect(link).toBeDefined();
|
|
37
|
+
expect(link).toHaveClass('ds_breadcrumbs__link');
|
|
38
|
+
} else {
|
|
39
|
+
expect(link).toBeNull();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// check href matches correct item
|
|
44
|
+
const categoryLink = within(list).getByRole('link', { name: 'Category' });
|
|
45
|
+
expect(categoryLink).toHaveAttribute('href', 'category');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('renders with last item hidden', () => {
|
|
49
|
+
render(
|
|
50
|
+
<Breadcrumbs
|
|
51
|
+
hideLastItem
|
|
52
|
+
items={items}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// check still 3 items
|
|
57
|
+
const list = screen.getByRole('list');
|
|
58
|
+
const listItems = within(list).getAllByRole('listitem');
|
|
59
|
+
expect(listItems.length).toEqual(3);
|
|
60
|
+
|
|
61
|
+
// check last item is hidden
|
|
62
|
+
const pageCrumb = within(list).getByText('Page');
|
|
63
|
+
expect(pageCrumb).toHaveClass('visually-hidden');
|
|
64
|
+
expect(pageCrumb.tagName).toEqual('LI');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('passing additional props', () => {
|
|
68
|
+
render(
|
|
69
|
+
<Breadcrumbs
|
|
70
|
+
items={items}
|
|
71
|
+
data-test="foo"
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const nav = screen.getByRole('navigation');
|
|
76
|
+
expect(nav.dataset.test).toEqual('foo');
|
|
77
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const Breadcrumb: React.FC<SGDS.Component.Breadcrumbs.Item> = ({
|
|
2
|
+
hidden,
|
|
3
|
+
href,
|
|
4
|
+
title
|
|
5
|
+
}) => {
|
|
6
|
+
return (
|
|
7
|
+
<li
|
|
8
|
+
className={[
|
|
9
|
+
'ds_breadcrumbs__item',
|
|
10
|
+
hidden && 'visually-hidden'
|
|
11
|
+
].join(' ')}
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
{href ? (<a className="ds_breadcrumbs__link" href={href}>
|
|
15
|
+
{title}
|
|
16
|
+
</a>) : (title)}
|
|
17
|
+
</li>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {boolean} hideLastItem
|
|
23
|
+
* @param {Array} items
|
|
24
|
+
* @param {Object} props - Properties for the element
|
|
25
|
+
* @returns {JSX.Element} - The element
|
|
26
|
+
*/
|
|
27
|
+
const Breadcrumbs: React.FC<SGDS.Component.Breadcrumbs> = ({
|
|
28
|
+
hideLastItem,
|
|
29
|
+
items,
|
|
30
|
+
...props
|
|
31
|
+
}) => {
|
|
32
|
+
return (
|
|
33
|
+
<nav
|
|
34
|
+
aria-label="Breadcrumb"
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
<ol className="ds_breadcrumbs">
|
|
38
|
+
{items && items.map((item, index: number) => (
|
|
39
|
+
<Breadcrumb
|
|
40
|
+
title={item.title}
|
|
41
|
+
href={item.href}
|
|
42
|
+
hidden={(hideLastItem) && index + 1 === items.length}
|
|
43
|
+
key={'breadcrumb' + index}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</ol>
|
|
47
|
+
</nav>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
Breadcrumbs.displayName = 'Breadcrumbs';
|
|
52
|
+
|
|
53
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import Button from './button';
|
|
4
|
+
|
|
5
|
+
test('renders correctly', () => {
|
|
6
|
+
render(
|
|
7
|
+
<Button>Button text</Button>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const button = screen.getByRole('button');
|
|
11
|
+
expect(button).toHaveClass('ds_button');
|
|
12
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
13
|
+
expect(button.tagName).toEqual('BUTTON');
|
|
14
|
+
expect(button.textContent).toEqual('Button text');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('secondary button', () => {
|
|
18
|
+
render(
|
|
19
|
+
<Button buttonStyle="secondary">Button text</Button>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const button = screen.getByRole('button');
|
|
23
|
+
expect(button).toHaveClass('ds_button--secondary');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('disabled button', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Button disabled>Button text</Button>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const button = screen.getByRole('button');
|
|
32
|
+
expect(button).toHaveAttribute('disabled');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('fixed-width button', () => {
|
|
36
|
+
render(
|
|
37
|
+
<Button width="fixed">Button text</Button>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const button = screen.getByRole('button');
|
|
41
|
+
expect(button).toHaveClass('ds_button--fixed');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('max-width button', () => {
|
|
45
|
+
render(
|
|
46
|
+
<Button width="max">Button text</Button>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const button = screen.getByRole('button');
|
|
50
|
+
expect(button).toHaveClass('ds_button--max');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('small button', () => {
|
|
54
|
+
render(
|
|
55
|
+
<Button small>Button text</Button>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const button = screen.getByRole('button');
|
|
59
|
+
expect(button).toHaveClass('ds_button--small');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('button with icon', () => {
|
|
63
|
+
render(
|
|
64
|
+
<Button icon="chevron_right">Button text</Button>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const button = screen.getByRole('button');
|
|
68
|
+
const icon = within(button).getByRole('img', { hidden: true });
|
|
69
|
+
|
|
70
|
+
expect(button).toHaveClass('ds_button--has-icon');
|
|
71
|
+
expect(icon).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('button with icon (left)', () => {
|
|
75
|
+
render(
|
|
76
|
+
<Button icon="chevron_left" iconLeft>Button text</Button>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const button = screen.getByRole('button');
|
|
80
|
+
|
|
81
|
+
expect(button).toHaveClass('ds_button--has-icon', 'ds_button--has-icon--left');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('button only icon', () => {
|
|
85
|
+
render(
|
|
86
|
+
<Button icon="search" iconOnly>Button text</Button>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const button = screen.getByRole('button');
|
|
90
|
+
const buttonText = within(button).getByText('Button text');
|
|
91
|
+
|
|
92
|
+
expect(button).not.toHaveClass('ds_button--has-icon');
|
|
93
|
+
expect(buttonText).toHaveClass('visually-hidden');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('link styled as button', () => {
|
|
97
|
+
render(
|
|
98
|
+
<Button href="#foo">Button text</Button>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const button = screen.getByRole('link');
|
|
102
|
+
expect(button.tagName).toEqual('A');
|
|
103
|
+
expect(button).toHaveAttribute('href', '#foo');
|
|
104
|
+
expect(button).not.toHaveAttribute('type');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('button styled as link', () => {
|
|
108
|
+
render(
|
|
109
|
+
<Button styleAsLink>Button text</Button>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const button = screen.getByRole('button');
|
|
113
|
+
expect(button).toHaveClass('ds_link');
|
|
114
|
+
expect(button).not.toHaveClass('ds_button');
|
|
115
|
+
expect(button.tagName).toEqual('BUTTON');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('passing additional props', () => {
|
|
119
|
+
render(
|
|
120
|
+
<Button data-test="foo">Button text</Button>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const button = screen.getByRole('button');
|
|
124
|
+
expect(button.dataset.test).toEqual('foo');
|
|
125
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Icon from '../../common/icon';
|
|
2
|
+
import ScreenReaderText from '../../common/screen-reader-text';
|
|
3
|
+
import WrapperTag from '../../common/wrapper-tag';
|
|
4
|
+
|
|
5
|
+
const Button: React.FC<SGDS.Component.Button> = ({
|
|
6
|
+
children,
|
|
7
|
+
buttonStyle,
|
|
8
|
+
icon,
|
|
9
|
+
iconLeft,
|
|
10
|
+
iconOnly = false,
|
|
11
|
+
href,
|
|
12
|
+
small,
|
|
13
|
+
styleAsLink,
|
|
14
|
+
type = 'button',
|
|
15
|
+
width,
|
|
16
|
+
...props
|
|
17
|
+
}) => {
|
|
18
|
+
// determine which HTML tag to use
|
|
19
|
+
let tagName = 'button';
|
|
20
|
+
if (href) {
|
|
21
|
+
tagName = 'a';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<WrapperTag
|
|
26
|
+
tagName={tagName}
|
|
27
|
+
className={[
|
|
28
|
+
!styleAsLink ? 'ds_button' : 'ds_link',
|
|
29
|
+
width && `ds_button--${width}`,
|
|
30
|
+
buttonStyle && `ds_button--${buttonStyle}`,
|
|
31
|
+
small && 'ds_button--small',
|
|
32
|
+
(icon && !iconOnly) ? 'ds_button--has-icon' : undefined,
|
|
33
|
+
iconLeft && 'ds_button--has-icon--left'
|
|
34
|
+
].join(' ')}
|
|
35
|
+
href={href}
|
|
36
|
+
{...(tagName === 'button' ? { type: type } : {})}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{iconOnly ? <ScreenReaderText>{children}</ScreenReaderText> : children}
|
|
40
|
+
|
|
41
|
+
{icon && <Icon icon={icon}/>}
|
|
42
|
+
</WrapperTag>
|
|
43
|
+
)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
Button.displayName = 'Button';
|
|
47
|
+
|
|
48
|
+
export default Button;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import CheckboxGroup, { Checkbox } from './checkbox';
|
|
4
|
+
|
|
5
|
+
test('checkbox group renders correct children', () => {
|
|
6
|
+
const items = [
|
|
7
|
+
{
|
|
8
|
+
id: 'universal-credit',
|
|
9
|
+
label: 'Universal Credit',
|
|
10
|
+
checked: true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'pensioncredit',
|
|
14
|
+
label: 'Pension Credit'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'jsa',
|
|
18
|
+
label: 'Income-based Job Seeker\'s Allowance',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
exclusive: true,
|
|
22
|
+
id: 'none',
|
|
23
|
+
label: 'No, I do not receive any of these benefits',
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<CheckboxGroup items={items} />
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const checkboxes = screen.getAllByRole('checkbox');
|
|
32
|
+
const groupContainer = checkboxes[0].parentNode.parentNode;
|
|
33
|
+
expect(checkboxes.length).toEqual(items.length);
|
|
34
|
+
expect(groupContainer).toHaveClass('ds_checkboxes', 'ds_field-group');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('checkbox group passes all expected item params', () => {
|
|
38
|
+
const onBlurFn = vi.fn();
|
|
39
|
+
const onChangeFn = vi.fn();
|
|
40
|
+
|
|
41
|
+
render(
|
|
42
|
+
<CheckboxGroup small items={[
|
|
43
|
+
{
|
|
44
|
+
checked: true,
|
|
45
|
+
exclusive: true,
|
|
46
|
+
hintText: 'hint text',
|
|
47
|
+
id: 'myid',
|
|
48
|
+
label: 'label text',
|
|
49
|
+
onBlur: {onBlurFn},
|
|
50
|
+
onChange: {onChangeFn},
|
|
51
|
+
small: true
|
|
52
|
+
}
|
|
53
|
+
]}/>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const checkbox = screen.getByRole('checkbox');
|
|
57
|
+
const checkboxContainer = checkbox.parentNode;
|
|
58
|
+
const hintText = screen.getByText('hint text');
|
|
59
|
+
|
|
60
|
+
expect(checkbox).toHaveAttribute('data-behaviour', 'exclusive');
|
|
61
|
+
expect(checkbox).toHaveAttribute('checked');
|
|
62
|
+
expect(checkbox.id).toEqual('myid');
|
|
63
|
+
expect(checkboxContainer).toHaveClass('ds_checkbox--small');
|
|
64
|
+
expect(hintText).toBeInTheDocument();
|
|
65
|
+
expect(checkbox).toHaveAttribute('aria-describedby', hintText.id);
|
|
66
|
+
|
|
67
|
+
// fireEvent.blur(checkbox);
|
|
68
|
+
// expect(onBlurFn).toHaveBeenCalled();
|
|
69
|
+
|
|
70
|
+
// fireEvent.click(checkbox);
|
|
71
|
+
// expect(onChangeFn).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('individual checkbox renders correctly', () => {
|
|
75
|
+
render(
|
|
76
|
+
<Checkbox label="Pension Credit" id="pensioncredit" />
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const checkbox = screen.getByRole('checkbox');
|
|
80
|
+
const checkboxContainer = checkbox.parentNode;
|
|
81
|
+
const label = screen.getByText('Pension Credit');
|
|
82
|
+
|
|
83
|
+
expect(checkboxContainer).toHaveClass('ds_checkbox');
|
|
84
|
+
expect(checkbox.tagName).toEqual('INPUT');
|
|
85
|
+
expect(checkbox).toHaveAttribute('type', 'checkbox');
|
|
86
|
+
expect(checkbox.id).toEqual('pensioncredit');
|
|
87
|
+
expect(checkbox).toHaveAttribute('name', checkbox.id);
|
|
88
|
+
expect(checkbox).toHaveClass('ds_checkbox__input');
|
|
89
|
+
expect(label).toHaveAttribute('for', checkbox.id);
|
|
90
|
+
expect(checkbox).not.toHaveAttribute('aria-describedby');
|
|
91
|
+
expect(label).toHaveClass('ds_checkbox__label');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('checked checkbox', () => {
|
|
95
|
+
render(
|
|
96
|
+
<Checkbox checked label="Pension Credit" id="pensioncredit" />
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const checkbox = screen.getByRole('checkbox');
|
|
100
|
+
|
|
101
|
+
expect(checkbox).toHaveAttribute('checked')
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('exclusive checkbox', () => {
|
|
105
|
+
render(
|
|
106
|
+
<Checkbox exclusive label="Pension Credit" id="pensioncredit" />
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const checkbox = screen.getByRole('checkbox');
|
|
110
|
+
const separator = checkbox.parentNode?.previousSibling;
|
|
111
|
+
|
|
112
|
+
expect(checkbox).toHaveAttribute('data-behaviour', 'exclusive');
|
|
113
|
+
expect(separator).toBeInTheDocument();
|
|
114
|
+
expect(separator).toHaveClass('ds_checkbox-separator');
|
|
115
|
+
expect(separator.textContent).toEqual('or');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('checkbox with blur fn', () => {
|
|
119
|
+
const onBlurFn = vi.fn();
|
|
120
|
+
|
|
121
|
+
render(
|
|
122
|
+
<Checkbox onBlur={onBlurFn} label="Pension Credit" id="pensioncredit" />
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const checkbox = screen.getByRole('checkbox');
|
|
126
|
+
|
|
127
|
+
fireEvent.blur(checkbox);
|
|
128
|
+
|
|
129
|
+
expect(onBlurFn).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('checkbox with change fn', () => {
|
|
133
|
+
const onChangeFn = vi.fn();
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<Checkbox onChange={onChangeFn} label="Pension Credit" id="pensioncredit" />
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const checkbox = screen.getByRole('checkbox');
|
|
140
|
+
|
|
141
|
+
fireEvent.click(checkbox);
|
|
142
|
+
|
|
143
|
+
expect(onChangeFn).toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('checkbox with hint text', () => {
|
|
147
|
+
render(
|
|
148
|
+
<Checkbox hintText="hint text" label="Pension Credit" id="pensioncredit" />
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const hintText = screen.getByText('hint text');
|
|
152
|
+
const checkbox = screen.getByRole('checkbox');
|
|
153
|
+
|
|
154
|
+
expect(hintText).toBeInTheDocument();
|
|
155
|
+
expect(checkbox).toHaveAttribute('aria-describedby', hintText.id);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('small checkbox', () => {
|
|
159
|
+
render(
|
|
160
|
+
<Checkbox small label="Pension Credit" id="pensioncredit" />
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const checkbox = screen.getByRole('checkbox');
|
|
164
|
+
const checkboxContainer = checkbox.parentNode;
|
|
165
|
+
|
|
166
|
+
expect(checkboxContainer).toHaveClass('ds_checkbox--small');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('passing additional props', () => {
|
|
170
|
+
render(
|
|
171
|
+
<CheckboxGroup data-test="foo" items={[{
|
|
172
|
+
id: 'universal-credit',
|
|
173
|
+
label: 'Universal Credit'
|
|
174
|
+
}]} />
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const checkboxes = screen.getAllByRole('checkbox');
|
|
178
|
+
const groupContainer = checkboxes[0]?.parentNode?.parentNode;
|
|
179
|
+
expect(groupContainer?.dataset.test).toEqual('foo');
|
|
180
|
+
});
|