@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.
Files changed (111) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release-package.yml +96 -0
  3. package/@types/common/ConditionalWrapper.d.ts +6 -0
  4. package/@types/common/HintText.d.ts +6 -0
  5. package/@types/common/Icon.d.ts +11 -0
  6. package/@types/common/ScreenReaderText.d.ts +4 -0
  7. package/@types/common/WrapperTag.d.ts +5 -0
  8. package/@types/components/Accordion.d.ts +15 -0
  9. package/@types/components/AspectBox.d.ts +5 -0
  10. package/@types/components/BackToTop.d.ts +5 -0
  11. package/@types/components/Breadcrumbs.d.ts +14 -0
  12. package/@types/components/Button.d.ts +17 -0
  13. package/@types/components/Checkbox.d.ts +13 -0
  14. package/@types/components/ConfirmationMessage.d.ts +7 -0
  15. package/@types/components/ContentsNav.d.ts +15 -0
  16. package/@types/components/DatePicker.d.ts +19 -0
  17. package/@types/components/Details.d.ts +6 -0
  18. package/@types/components/ErrorMessage.d.ts +6 -0
  19. package/@types/components/Metadata.d.ts +11 -0
  20. package/@types/components/NotificationBanner.d.ts +9 -0
  21. package/@types/components/NotificationPanel.d.ts +7 -0
  22. package/@types/components/PageHeader.d.ts +6 -0
  23. package/@types/components/PhaseBanner.d.ts +5 -0
  24. package/@types/components/Question.d.ts +11 -0
  25. package/@types/components/RadioButton.d.ts +15 -0
  26. package/@types/components/Select.d.ts +14 -0
  27. package/@types/components/SequentialNavigation.d.ts +14 -0
  28. package/@types/components/SideNavigation.d.ts +19 -0
  29. package/@types/components/SiteNavigation.d.ts +13 -0
  30. package/@types/components/SiteSearch.d.ts +14 -0
  31. package/@types/components/SkipLinks.d.ts +14 -0
  32. package/@types/components/Tag.d.ts +7 -0
  33. package/@types/components/TaskList.d.ts +21 -0
  34. package/@types/components/TextInput.d.ts +12 -0
  35. package/@types/components/Textarea.d.ts +4 -0
  36. package/@types/global.d.ts +1 -0
  37. package/@types/sgds.d.ts +35 -0
  38. package/package.json +36 -0
  39. package/src/common/conditional-wrapper.test.tsx +36 -0
  40. package/src/common/conditional-wrapper.tsx +9 -0
  41. package/src/common/hint-text.test.tsx +47 -0
  42. package/src/common/hint-text.tsx +21 -0
  43. package/src/common/icon.test.tsx +100 -0
  44. package/src/common/icon.tsx +28 -0
  45. package/src/common/screen-reader-text.test.tsx +31 -0
  46. package/src/common/screen-reader-text.tsx +17 -0
  47. package/src/common/wrapper-tag.test.tsx +42 -0
  48. package/src/common/wrapper-tag.tsx +15 -0
  49. package/src/components/accordion/accordion.test.tsx +212 -0
  50. package/src/components/accordion/accordion.tsx +108 -0
  51. package/src/components/aspect-box/aspect-box.test.tsx +81 -0
  52. package/src/components/aspect-box/aspect-box.tsx +57 -0
  53. package/src/components/back-to-top/back-to-top.test.tsx +45 -0
  54. package/src/components/back-to-top/back-to-top.tsx +33 -0
  55. package/src/components/breadcrumbs/breadcrumbs.test.tsx +77 -0
  56. package/src/components/breadcrumbs/breadcrumbs.tsx +53 -0
  57. package/src/components/button/button.test.tsx +125 -0
  58. package/src/components/button/button.tsx +48 -0
  59. package/src/components/checkbox/checkbox.test.tsx +180 -0
  60. package/src/components/checkbox/checkbox.tsx +107 -0
  61. package/src/components/confirmation-message/confirmation-message.test.tsx +46 -0
  62. package/src/components/confirmation-message/confirmation-message.tsx +32 -0
  63. package/src/components/contents-nav/contents-nav.test.tsx +136 -0
  64. package/src/components/contents-nav/contents-nav.tsx +54 -0
  65. package/src/components/date-picker/date-picker.test.tsx +209 -0
  66. package/src/components/date-picker/date-picker.tsx +129 -0
  67. package/src/components/details/details.test.tsx +38 -0
  68. package/src/components/details/details.tsx +25 -0
  69. package/src/components/error-message/error-message.test.tsx +40 -0
  70. package/src/components/error-message/error-message.tsx +23 -0
  71. package/src/components/inset-text/inset-text.test.tsx +33 -0
  72. package/src/components/inset-text/inset-text.tsx +19 -0
  73. package/src/components/notification-banner/notification-banner.test.tsx +93 -0
  74. package/src/components/notification-banner/notification-banner.tsx +70 -0
  75. package/src/components/notification-panel/notification-panel.test.tsx +77 -0
  76. package/src/components/notification-panel/notification-panel.tsx +31 -0
  77. package/src/components/page-header/page-header.test.tsx +48 -0
  78. package/src/components/page-header/page-header.tsx +22 -0
  79. package/src/components/page-metadata/page-metadata.test.tsx +56 -0
  80. package/src/components/page-metadata/page-metadata.tsx +39 -0
  81. package/src/components/phase-banner/phase-banner.test.tsx +67 -0
  82. package/src/components/phase-banner/phase-banner.tsx +27 -0
  83. package/src/components/question/question.test.tsx +69 -0
  84. package/src/components/question/question.tsx +33 -0
  85. package/src/components/radio-button/radio-button.test.tsx +190 -0
  86. package/src/components/radio-button/radio-button.tsx +88 -0
  87. package/src/components/select/select.test.tsx +208 -0
  88. package/src/components/select/select.tsx +86 -0
  89. package/src/components/sequential-navigation/sequential-navigation.test.tsx +67 -0
  90. package/src/components/sequential-navigation/sequential-navigation.tsx +55 -0
  91. package/src/components/side-navigation/side-navigation.test.tsx +156 -0
  92. package/src/components/side-navigation/side-navigation.tsx +85 -0
  93. package/src/components/site-navigation/site-navigation.test.tsx +63 -0
  94. package/src/components/site-navigation/site-navigation.tsx +40 -0
  95. package/src/components/site-search/site-search.test.tsx +153 -0
  96. package/src/components/site-search/site-search.tsx +97 -0
  97. package/src/components/skip-links/skip-links.test.tsx +84 -0
  98. package/src/components/skip-links/skip-links.tsx +39 -0
  99. package/src/components/tag/tag.test.tsx +45 -0
  100. package/src/components/tag/tag.tsx +23 -0
  101. package/src/components/task-list/task-list.test.tsx +409 -0
  102. package/src/components/task-list/task-list.tsx +132 -0
  103. package/src/components/text-input/text-input.test.tsx +307 -0
  104. package/src/components/text-input/text-input.tsx +98 -0
  105. package/src/components/textarea/textarea.test.tsx +212 -0
  106. package/src/components/textarea/textarea.tsx +82 -0
  107. package/src/components/warning-text/warning-text.test.tsx +40 -0
  108. package/src/components/warning-text/warning-text.tsx +21 -0
  109. package/tsconfig.json +45 -0
  110. package/vite.config.ts +12 -0
  111. package/vitest-setup.ts +13 -0
@@ -0,0 +1,129 @@
1
+ import { useEffect, useRef } from 'react';
2
+ // @ts-ignore
3
+ import DSDatePicker from '@scottish-government/design-system/src/components/date-picker/date-picker';
4
+ import TextInput from '../text-input/text-input';
5
+
6
+ const DatePicker: React.FC<SGDS.Component.DatePicker> = ({
7
+ width = 'fixed-10',
8
+ disabledDates,
9
+ error,
10
+ errorMessage,
11
+ hintText,
12
+ id,
13
+ iconPath = './',
14
+ label,
15
+ maxDate,
16
+ minDate,
17
+ multiple,
18
+ name,
19
+ onBlur,
20
+ onChange,
21
+ value,
22
+ ...props
23
+ }) => {
24
+ // todo: dateSelectCallback function
25
+
26
+ const ref = useRef(null);
27
+
28
+ useEffect(() => {
29
+ if (ref.current) {
30
+ new DSDatePicker(ref.current, {imagePath: iconPath}).init();
31
+ }
32
+ }, [ref, iconPath]);
33
+
34
+ function handleBlur(event: React.FocusEvent) {
35
+ if (typeof onBlur === 'function') {
36
+ onBlur(event);
37
+ }
38
+ }
39
+
40
+ function handleChange(event: React.ChangeEvent) {
41
+ if (typeof onChange === 'function') {
42
+ onChange(event);
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div
48
+ className={[
49
+ "ds_datepicker",
50
+ multiple && "ds_datepicker--multiple"
51
+ ].join(' ')}
52
+ data-disableddates={disabledDates}
53
+ data-maxdate={maxDate}
54
+ data-mindate={minDate}
55
+ data-module="ds-datepicker"
56
+ ref={ref}
57
+ {...props}
58
+ >
59
+ {(multiple ? (
60
+ <fieldset className="ds_datepicker__input-wrapper">
61
+ <legend>{label}</legend>
62
+ <div>
63
+ <TextInput
64
+ className="js-datepicker-date"
65
+ error={!!error}
66
+ id={id + "-day"}
67
+ hintText={hintText}
68
+ label="Day"
69
+ name={name + "-day"}
70
+ onBlur={handleBlur}
71
+ onChange={handleChange}
72
+ value={value?.split('/')[0]}
73
+ width="fixed-2"
74
+ />
75
+ </div>
76
+
77
+ <div>
78
+ <TextInput
79
+ className="js-datepicker-month"
80
+ error={!!error}
81
+ id={id + "-month"}
82
+ hintText={hintText}
83
+ label="Month"
84
+ name={name + "-month"}
85
+ onBlur={handleBlur}
86
+ onChange={handleChange}
87
+ value={value?.split('/')[1]}
88
+ width="fixed-2"
89
+ />
90
+ </div>
91
+
92
+ <div>
93
+ <TextInput
94
+ className="js-datepicker-year"
95
+ error={!!error}
96
+ id={id + "-year"}
97
+ hintText={hintText}
98
+ label="Year"
99
+ name={name + "-year"}
100
+ onBlur={handleBlur}
101
+ onChange={handleChange}
102
+ value={value?.split('/')[2]}
103
+ width="fixed-4"
104
+ />
105
+ </div>
106
+ </fieldset>
107
+ ) : (
108
+ <TextInput
109
+ error={!!error}
110
+ errorMessage={errorMessage}
111
+ id={id}
112
+ hasButton
113
+ hintText={hintText}
114
+ label={label}
115
+ name={name}
116
+ onBlur={handleBlur}
117
+ onChange={handleChange}
118
+ placeholder="dd/mm/yyyy"
119
+ value={value}
120
+ width={width}
121
+ />
122
+ ))}
123
+ </div>
124
+ );
125
+ };
126
+
127
+ DatePicker.displayName = 'DatePicker';
128
+
129
+ export default DatePicker;
@@ -0,0 +1,38 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import Details from './details';
4
+
5
+ const summaryText = 'I can\'t sign in';
6
+ const content = '<p>hello</p>';
7
+
8
+ test('details renders correctly', () => {
9
+
10
+ render(
11
+ <Details summary={summaryText}>
12
+ <p>hello</p>
13
+ </Details>
14
+ );
15
+
16
+ const summaryElement = screen.getByText(summaryText);
17
+ const detailsElement = summaryElement.parentNode;
18
+ const textElement = summaryElement.nextSibling;
19
+
20
+ expect(detailsElement).toHaveClass('ds_details');
21
+ expect(detailsElement.tagName).toEqual('DETAILS');
22
+ expect(summaryElement).toHaveClass('ds_details__summary');
23
+ expect(summaryElement.tagName).toEqual('SUMMARY');
24
+ expect(textElement).toHaveClass('ds_details__text');
25
+ expect(textElement.innerHTML).toEqual(content);
26
+ });
27
+
28
+ test('passing additional props', () => {
29
+ render(
30
+ <Details data-test="foo" summary={summaryText}>
31
+ <p>hello</p>
32
+ </Details>
33
+ )
34
+
35
+ const summaryElement = screen.getByText(summaryText);
36
+ const detailsElement = summaryElement.parentNode;
37
+ expect(detailsElement?.dataset.test).toEqual('foo');
38
+ });
@@ -0,0 +1,25 @@
1
+ const Details: React.FC<SGDS.Component.Details> = ({
2
+ children,
3
+ summary,
4
+ ...props
5
+ }) => {
6
+ return (
7
+ <details
8
+ className={[
9
+ "ds_details",
10
+ ].join(' ')}
11
+ {...props}
12
+ >
13
+ <summary className="ds_details__summary">
14
+ {summary}
15
+ </summary>
16
+ <div className="ds_details__text">
17
+ {children}
18
+ </div>
19
+ </details>
20
+ );
21
+ };
22
+
23
+ Details.displayName = 'Details';
24
+
25
+ export default Details;
@@ -0,0 +1,40 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import ErrorMessage from './error-message';
4
+
5
+ const errorText = 'Error message';
6
+ const errorId = 'errormessage';
7
+
8
+ test('error message renders correctly', () => {
9
+
10
+ render(
11
+ <ErrorMessage text={errorText} id={errorId}/>
12
+ );
13
+
14
+ const errorMessageElement = screen.getByRole('paragraph');
15
+
16
+ expect(errorMessageElement).toHaveAttribute('id', errorId);
17
+ expect(errorMessageElement).toHaveClass('ds_question__error-message');
18
+ expect(errorMessageElement.textContent).toEqual(errorText);
19
+ });
20
+
21
+ test('error message with HTML content', () => {
22
+ const errorId = 'errormessage';
23
+
24
+ render(
25
+ <ErrorMessage id={errorId}>hello <a href="#foo">world</a></ErrorMessage>
26
+ );
27
+
28
+ const errorMessageElement = screen.getByRole('paragraph');
29
+
30
+ expect(errorMessageElement.innerHTML).toEqual('hello <a href="#foo">world</a>');
31
+ });
32
+
33
+ test('passing additional props', () => {
34
+ render(
35
+ <ErrorMessage data-test="foo" text={errorText} id={errorId}/>
36
+ )
37
+
38
+ const errorMessageElement = screen.getByRole('paragraph');
39
+ expect(errorMessageElement?.dataset.test).toEqual('foo');
40
+ });
@@ -0,0 +1,23 @@
1
+ const ErrorMessage: React.FC<SGDS.Component.ErrorMessage> = ({
2
+ children,
3
+ id,
4
+ text,
5
+ ...props
6
+ }) => {
7
+ return (
8
+ <p
9
+ className={[
10
+ 'ds_question__error-message',
11
+ ].join(' ')}
12
+ dangerouslySetInnerHTML={text ? { __html: text } : undefined}
13
+ id={id}
14
+ {...props}
15
+ >
16
+ {!text ? children : null}
17
+ </p>
18
+ );
19
+ };
20
+
21
+ ErrorMessage.displayName = 'ErrorMessage';
22
+
23
+ export default ErrorMessage;
@@ -0,0 +1,33 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import InsetText from './inset-text';
4
+
5
+ const text = `You may be able to apply for free school meals at the same
6
+ time as you apply for the clothing grant.`;
7
+
8
+ test('inset text renders correctly', () => {
9
+
10
+ render(
11
+ <InsetText>
12
+ {text}
13
+ </InsetText>
14
+ );
15
+
16
+ const insetTextOuter = document.querySelector('.ds_inset-text');
17
+ const insetTextInner = insetTextOuter?.querySelector('.ds_inset-text__text');
18
+
19
+ expect(insetTextOuter).toBeInTheDocument();
20
+ expect(insetTextInner).toBeInTheDocument();
21
+ expect(insetTextInner?.textContent).toEqual(text);
22
+ });
23
+
24
+ test('passing additional props', () => {
25
+ render(
26
+ <InsetText data-test="foo">
27
+ {text}
28
+ </InsetText>
29
+ )
30
+
31
+ const insetTextOuter = document.querySelector('.ds_inset-text');
32
+ expect(insetTextOuter?.dataset.test).toEqual('foo');
33
+ });
@@ -0,0 +1,19 @@
1
+ const InsetText: React.FC<React.PropsWithChildren> = ({
2
+ children,
3
+ ...props
4
+ }) => {
5
+ return (
6
+ <div
7
+ className="ds_inset-text"
8
+ {...props}
9
+ >
10
+ <div className="ds_inset-text__text">
11
+ {children}
12
+ </div>
13
+ </div>
14
+ );
15
+ };
16
+
17
+ InsetText.displayName = 'InsetText';
18
+
19
+ export default InsetText;
@@ -0,0 +1,93 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import NotificationBanner from './notification-banner';
4
+
5
+ const text = 'We need to tell you about something';
6
+
7
+ test('notification banner renders correctly', () => {
8
+ render(
9
+ <NotificationBanner>
10
+ {text}
11
+ </NotificationBanner>
12
+ );
13
+
14
+ const bannerTitle = screen.getByRole('heading');
15
+ const bannerText = bannerTitle.nextSibling;
16
+ const bannerContent = bannerTitle.parentNode;
17
+ const bannerWrapper = bannerContent?.parentNode;
18
+ const bannerContainer = bannerWrapper?.parentNode;
19
+
20
+ expect(bannerTitle.textContent).toEqual('Information');
21
+ expect(bannerTitle.tagName).toEqual('H2');
22
+ expect(bannerTitle).toHaveClass('visually-hidden');
23
+
24
+ expect(bannerText).toHaveClass('ds_notification__text');
25
+ expect(bannerText.textContent).toEqual(text);
26
+
27
+ expect(bannerContent).toHaveClass('ds_notification__content');
28
+ expect(bannerWrapper).toHaveClass('ds_wrapper');
29
+ expect(bannerContainer).toHaveClass('ds_notification', 'ds_reversed');
30
+ });
31
+
32
+ test('notification banner with close button', () => {
33
+ render(
34
+ <NotificationBanner close>
35
+ {text}
36
+ </NotificationBanner>
37
+ );
38
+
39
+ const bannerTitle = screen.getByRole('heading');
40
+ const bannerContent = bannerTitle.parentNode;
41
+ const closeButton = screen.getByRole('button');
42
+ const closeButtonLabel = within(closeButton).getByText('Close this notification');
43
+ const closeButtonIcon = within(closeButton).getByRole('img', { hidden: true });
44
+
45
+ expect(bannerContent).toHaveClass('ds_notification__content--has-close');
46
+ expect(closeButton).toHaveClass('ds_notification__close', 'js-close-notification');
47
+ expect(closeButton).toHaveAttribute('type', 'button');
48
+
49
+ expect(closeButtonLabel).toBeInTheDocument();
50
+ expect(closeButtonLabel).toHaveClass('visually-hidden');
51
+
52
+ expect(closeButtonIcon).toHaveClass('ds_icon', 'ds_icon--fill');
53
+ });
54
+
55
+ test('notification banner with icon', () => {
56
+ render(
57
+ <NotificationBanner icon>
58
+ {text}
59
+ </NotificationBanner>
60
+ );
61
+
62
+ const bannerTitle = screen.getByRole('heading');
63
+ const bannerIconContainer = bannerTitle.nextSibling;
64
+ const bannerIcon = within(bannerIconContainer).getByRole('img', { hidden: true });
65
+
66
+ expect(bannerIconContainer).toHaveClass('ds_notification__icon');
67
+ expect(bannerIcon).toHaveClass('ds_icon');
68
+ expect(bannerIcon).toHaveAttribute('aria-hidden');
69
+ });
70
+
71
+ test('notification banner with icon modifier classes', () => {
72
+ render(
73
+ <NotificationBanner icon iconColour iconInverse>
74
+ {text}
75
+ </NotificationBanner>
76
+ );
77
+
78
+ const bannerTitle = screen.getByRole('heading');
79
+ const bannerIconContainer = bannerTitle.nextSibling;
80
+
81
+ expect(bannerIconContainer).toHaveClass('ds_notification__icon', 'ds_notification__icon--inverse', 'ds_notification__icon--colour');
82
+ });
83
+
84
+ test('passing additional props', () => {
85
+ render(
86
+ <NotificationBanner data-test="foo">
87
+ {text}
88
+ </NotificationBanner>
89
+ )
90
+
91
+ const bannerContainer = document.querySelector('.ds_notification');
92
+ expect(bannerContainer?.dataset.test).toEqual('foo');
93
+ });
@@ -0,0 +1,70 @@
1
+ import { useEffect, useRef } from 'react';
2
+ // @ts-ignore
3
+ import DSNotificationBanner from '@scottish-government/design-system/src/components/notification-banner/notification-banner';
4
+ import Button from '../button/button';
5
+ import Icon from '../../common/icon';
6
+ import ScreenReaderText from '../../common/screen-reader-text';
7
+
8
+ const NotificationBanner: React.FC<SGDS.Component.NotificationBanner> = ({
9
+ children,
10
+ close,
11
+ icon,
12
+ iconColour,
13
+ iconInverse,
14
+ title = 'Information',
15
+ ...props
16
+ }) => {
17
+ const ref = useRef(null);
18
+
19
+ useEffect(() => {
20
+ if (ref.current) {
21
+ new DSNotificationBanner(ref.current).init();
22
+ }
23
+ }, [ref]);
24
+
25
+ return (
26
+ <div
27
+ className="ds_notification ds_reversed"
28
+ data-module="ds-notification"
29
+ ref={ref}
30
+ {...props}
31
+ >
32
+ <div className="ds_wrapper">
33
+ <div className={
34
+ [
35
+ 'ds_notification__content',
36
+ close && 'ds_notification__content--has-close'
37
+ ].join(' ')}
38
+ >
39
+ <h2 className="visually-hidden">{title}</h2>
40
+
41
+ {icon &&
42
+ <span
43
+ className={[
44
+ 'ds_notification__icon',
45
+ iconInverse && 'ds_notification__icon--inverse',
46
+ iconColour && 'ds_notification__icon--colour'
47
+ ].join(' ')} aria-hidden="true">
48
+ <Icon icon="priority_high" />
49
+ </span>
50
+ }
51
+
52
+ <div className="ds_notification__text">
53
+ {children}
54
+ </div>
55
+
56
+ {close &&
57
+ <Button className="ds_notification__close js-close-notification">
58
+ <ScreenReaderText>Close this notification</ScreenReaderText>
59
+ <Icon fill icon="close" aria-hidden="true" />
60
+ </Button>
61
+ }
62
+ </div>
63
+ </div>
64
+ </div>
65
+ );
66
+ };
67
+
68
+ NotificationBanner.displayName = 'NotificationBanner';
69
+
70
+ export default NotificationBanner;
@@ -0,0 +1,77 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import NotificationPanel from './notification-panel';
4
+
5
+ const headingText = 'Thank you';
6
+ const text = 'Your Saltire Scholarship Application form has been successfully submitted.';
7
+
8
+ test('notification banner renders correctly', () => {
9
+ render(
10
+ <NotificationPanel title={headingText}>
11
+ {text}
12
+ </NotificationPanel>
13
+ );
14
+
15
+ const notificationPanelHeading = screen.getByRole('heading');
16
+ const notificationPanelContent = notificationPanelHeading.nextSibling;
17
+ const notificationPanel = notificationPanelHeading.parentNode;
18
+
19
+ expect(notificationPanel).toHaveClass('ds_notification-panel');
20
+ expect(notificationPanelHeading).toHaveClass('ds_notification-panel__title');
21
+ expect(notificationPanelHeading.textContent).toEqual(headingText);
22
+ expect(notificationPanelHeading.tagName).toEqual('H1');
23
+ expect(notificationPanelContent).toHaveClass('ds_notification-panel__content');
24
+ expect(notificationPanelContent.textContent).toEqual(text);
25
+ });
26
+
27
+ test('notification banner with custom heading level', () => {
28
+ const headerLevel = 'h2';
29
+
30
+ render(
31
+ <NotificationPanel headerLevel={headerLevel} title={headingText}>
32
+ {text}
33
+ </NotificationPanel>
34
+ );
35
+
36
+ const notificationPanelHeading = screen.getByRole('heading');
37
+ expect(notificationPanelHeading.tagName).toEqual(headerLevel.toUpperCase());
38
+ });
39
+
40
+ test('notification banner with aria-live', () => {
41
+ const ariaLive = 'polite';
42
+
43
+ render(
44
+ <NotificationPanel ariaLive={ariaLive} title={headingText}>
45
+ {text}
46
+ </NotificationPanel>
47
+ );
48
+
49
+ const notificationPanelHeading = screen.getByRole('heading');
50
+ const notificationPanel = notificationPanelHeading.parentNode;
51
+
52
+ expect(notificationPanel).toHaveAttribute('aria-live', ariaLive);
53
+ });
54
+
55
+ test('notification banner with nonsense heading level reverts to H1', () => {
56
+ const headerLevel = 'h2';
57
+ render(
58
+ <NotificationPanel headerLevel={headerLevel} title={headingText}>
59
+ {text}
60
+ </NotificationPanel>
61
+ );
62
+
63
+ const notificationPanelHeading = screen.getByRole('heading');
64
+ expect(notificationPanelHeading.tagName).toEqual(headerLevel.toUpperCase());
65
+ });
66
+
67
+ test('passing additional props', () => {
68
+ render(
69
+ <NotificationPanel title={headingText} data-test="foo">
70
+ {text}
71
+ </NotificationPanel>
72
+ )
73
+
74
+ const notificationPanelHeading = screen.getByRole('heading');
75
+ const notificationPanel = notificationPanelHeading.parentNode;
76
+ expect(notificationPanel?.dataset.test).toEqual('foo');
77
+ });
@@ -0,0 +1,31 @@
1
+ import WrapperTag from '../../common/wrapper-tag';
2
+
3
+ const NotificationPanel: React.FC<SGDS.Component.NotificationPanel> = function ({
4
+ ariaLive,
5
+ children,
6
+ headerLevel = 'h1',
7
+ title,
8
+ ...props
9
+ }) {
10
+ return (
11
+ <div
12
+ aria-live={ariaLive}
13
+ className="ds_notification-panel"
14
+ {...props}
15
+ >
16
+ <WrapperTag
17
+ className="ds_notification-panel__title"
18
+ tagName={headerLevel}
19
+ >
20
+ {title}
21
+ </WrapperTag>
22
+ <div className="ds_notification-panel__content">
23
+ {children}
24
+ </div>
25
+ </div>
26
+ );
27
+ };
28
+
29
+ NotificationPanel.displayName = 'NotificationPanel';
30
+
31
+ export default NotificationPanel;
@@ -0,0 +1,48 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import PageHeader from './page-header';
4
+
5
+ const labelText = 'Guide';
6
+ const titleText = 'Apply for or renew a disabled parking permit';
7
+
8
+ test('notification banner renders correctly', () => {
9
+ render(
10
+ <PageHeader label={labelText} title={titleText}/>
11
+ );
12
+
13
+ const header = screen.getByRole('banner');
14
+ const title = within(header).getByRole('heading');
15
+ const label = title.previousSibling;;
16
+
17
+ expect(header).toHaveClass('ds_page-header');
18
+ expect(header.tagName).toEqual('HEADER');
19
+
20
+ expect(label).toHaveClass('ds_page-header__label', 'ds_content-label');
21
+ expect(label?.textContent).toEqual(labelText);
22
+ expect(label?.tagName).toEqual('SPAN');
23
+
24
+ expect(title).toHaveClass('ds_page-header__title');
25
+ expect(title.textContent).toEqual(titleText);
26
+ expect(title.tagName).toEqual('H1');
27
+ });
28
+
29
+ test('header with no label', () => {
30
+ render(
31
+ <PageHeader title={titleText}/>
32
+ );
33
+
34
+ const header = screen.getByRole('banner');
35
+ const title = within(header).getByRole('heading');
36
+ const label = title.previousSibling;
37
+
38
+ expect(label).not.toBeInTheDocument();
39
+ });
40
+
41
+ test('passing additional props', () => {
42
+ render(
43
+ <PageHeader data-test="foo" label={labelText} title={titleText}/>
44
+ )
45
+
46
+ const header = screen.getByRole('banner');
47
+ expect(header?.dataset.test).toEqual('foo');
48
+ });
@@ -0,0 +1,22 @@
1
+ const PageHeader: React.FC<SGDS.Component.PageHeader> = ({
2
+ children,
3
+ label,
4
+ title,
5
+ ...props
6
+ }) => {
7
+ return (
8
+ <header
9
+ className="ds_page-header"
10
+ {...props}
11
+ >
12
+ {label && <span className="ds_page-header__label ds_content-label">{label}</span>}
13
+ <h1 className="ds_page-header__title">{title}</h1>
14
+
15
+ {children}
16
+ </header>
17
+ );
18
+ };
19
+
20
+ PageHeader.displayName = 'PageHeader';
21
+
22
+ export default PageHeader;