@scottish-government/designsystem-react 0.7.0 → 0.8.0-beta.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 (211) hide show
  1. package/@types/common/AbstractNotificationBanner.d.ts +2 -2
  2. package/@types/common/ActionLink.d.ts +8 -0
  3. package/@types/common/FileIcon.d.ts +1 -1
  4. package/@types/common/Icon.d.ts +1 -1
  5. package/@types/components/Accordion.d.ts +0 -1
  6. package/@types/components/Breadcrumbs.d.ts +2 -5
  7. package/@types/components/Checkbox.d.ts +0 -2
  8. package/@types/components/ConfirmationMessage.d.ts +1 -1
  9. package/@types/components/ContentsNav.d.ts +4 -6
  10. package/@types/components/DatePicker.d.ts +1 -1
  11. package/@types/components/ErrorSummary.d.ts +3 -4
  12. package/@types/components/NotificationPanel.d.ts +1 -1
  13. package/@types/components/Pagination.d.ts +5 -4
  14. package/@types/components/PhaseBanner.d.ts +0 -1
  15. package/@types/components/Question.d.ts +1 -1
  16. package/@types/components/RadioButton.d.ts +0 -1
  17. package/@types/components/Select.d.ts +0 -7
  18. package/@types/components/SequentialNavigation.d.ts +4 -4
  19. package/@types/components/SideNavigation.d.ts +4 -5
  20. package/@types/components/SiteFooter.d.ts +25 -0
  21. package/@types/components/SiteHeader.d.ts +10 -3
  22. package/@types/components/SiteNavigation.d.ts +2 -3
  23. package/@types/components/SkipLinks.d.ts +3 -4
  24. package/@types/components/SummaryCard.d.ts +0 -2
  25. package/@types/components/SummaryList.d.ts +0 -13
  26. package/@types/components/Tabs.d.ts +0 -1
  27. package/@types/components/Tag.d.ts +1 -3
  28. package/@types/components/TaskList.d.ts +1 -0
  29. package/@types/sgds.d.ts +13 -2
  30. package/CHANGELOG.md +63 -1
  31. package/dist/common/AbstractNotificationBanner.jsx +8 -6
  32. package/dist/common/ActionLink.jsx +19 -0
  33. package/dist/common/FileIcon.jsx +2 -7
  34. package/dist/common/Icon.jsx +3 -9
  35. package/dist/components/Accordion/Accordion.jsx +12 -7
  36. package/dist/components/Breadcrumbs/Breadcrumbs.jsx +20 -15
  37. package/dist/components/Checkbox/Checkbox.jsx +4 -29
  38. package/dist/components/{aspect-box/aspect-box.jsx → Checkbox/CheckboxGroup.jsx} +14 -30
  39. package/dist/components/ContentsNav/ContentsNav.jsx +27 -16
  40. package/dist/components/CookieBanner/CookieBanner.jsx +1 -0
  41. package/dist/components/DatePicker/DatePicker.jsx +5 -5
  42. package/dist/components/ErrorSummary/ErrorSummary.jsx +28 -18
  43. package/dist/components/NotificationBanner/NotificationBanner.jsx +2 -2
  44. package/dist/components/Pagination/Pagination.jsx +42 -22
  45. package/dist/components/PhaseBanner/PhaseBanner.jsx +3 -3
  46. package/dist/components/Question/Question.jsx +3 -3
  47. package/dist/components/RadioButton/RadioButton.jsx +7 -17
  48. package/dist/components/RadioButton/RadioGroup.jsx +21 -0
  49. package/dist/components/Select/Select.jsx +4 -7
  50. package/dist/components/SequentialNavigation/SequentialNavigation.jsx +31 -18
  51. package/dist/components/SideNavigation/SideNavigation.jsx +17 -16
  52. package/dist/components/SiteFooter/SiteFooter.jsx +104 -0
  53. package/dist/components/SiteHeader/SiteHeader.jsx +113 -32
  54. package/dist/components/SiteNavigation/SiteNavigation.jsx +20 -7
  55. package/dist/components/SkipLinks/SkipLinks.jsx +10 -10
  56. package/dist/components/SummaryCard/SummaryCard.jsx +25 -14
  57. package/dist/components/SummaryList/SummaryList.jsx +65 -47
  58. package/dist/components/Tabs/Tabs.jsx +6 -6
  59. package/dist/components/Tag/Tag.jsx +2 -2
  60. package/dist/components/TaskList/TaskList.jsx +14 -3
  61. package/dist/components/TextInput/TextInput.jsx +3 -3
  62. package/dist/components/Textarea/Textarea.jsx +3 -3
  63. package/dist/hooks/useTracking.js +21 -0
  64. package/dist/tsconfig.tsbuildinfo +1 -1
  65. package/dist/utils/context.js +5 -0
  66. package/package.json +2 -2
  67. package/src/common/AbstractNotificationBanner.test.tsx +1 -1
  68. package/src/common/AbstractNotificationBanner.tsx +14 -13
  69. package/src/common/ActionLink.test.tsx +80 -0
  70. package/src/common/ActionLink.tsx +27 -0
  71. package/src/common/ConditionalWrapper.tsx +1 -1
  72. package/src/common/FileIcon.tsx +7 -11
  73. package/src/common/HintText.tsx +2 -2
  74. package/src/common/Icon.tsx +13 -17
  75. package/src/common/ScreenReaderText.tsx +2 -2
  76. package/src/common/WrapperTag.tsx +2 -2
  77. package/src/components/Accordion/Accordion.test.tsx +17 -4
  78. package/src/components/Accordion/Accordion.tsx +19 -14
  79. package/src/components/AspectBox/AspectBox.tsx +2 -2
  80. package/src/components/BackToTop/BackToTop.tsx +2 -2
  81. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +79 -48
  82. package/src/components/Breadcrumbs/Breadcrumbs.tsx +31 -31
  83. package/src/components/Button/Button.tsx +2 -2
  84. package/src/components/Checkbox/Checkbox.test.tsx +1 -96
  85. package/src/components/Checkbox/Checkbox.tsx +8 -55
  86. package/src/components/Checkbox/CheckboxGroup.test.tsx +37 -0
  87. package/src/components/Checkbox/CheckboxGroup.tsx +41 -0
  88. package/src/components/ConfirmationMessage/ConfirmationMessage.tsx +2 -2
  89. package/src/components/ContentsNav/ContentsNav.test.tsx +40 -51
  90. package/src/components/ContentsNav/ContentsNav.tsx +32 -25
  91. package/src/components/CookieBanner/CookieBanner.tsx +3 -3
  92. package/src/components/DatePicker/DatePicker.test.tsx +1 -1
  93. package/src/components/DatePicker/DatePicker.tsx +7 -7
  94. package/src/components/Details/Details.tsx +2 -2
  95. package/src/components/ErrorMessage/ErrorMessage.tsx +2 -2
  96. package/src/components/ErrorSummary/ErrorSummary.test.tsx +40 -34
  97. package/src/components/ErrorSummary/ErrorSummary.tsx +40 -32
  98. package/src/components/FileDownload/FileDownload.tsx +2 -2
  99. package/src/components/HideThisPage/HideThisPage.tsx +2 -2
  100. package/src/components/InsetText/InsetText.tsx +2 -2
  101. package/src/components/NotificationBanner/NotificationBanner.tsx +6 -7
  102. package/src/components/NotificationPanel/NotificationPanel.tsx +2 -2
  103. package/src/components/PageHeader/PageHeader.tsx +2 -2
  104. package/src/components/PageMetadata/PageMetadata.tsx +4 -5
  105. package/src/components/Pagination/Pagination.test.tsx +26 -7
  106. package/src/components/Pagination/Pagination.tsx +70 -36
  107. package/src/components/PhaseBanner/PhaseBanner.tsx +4 -5
  108. package/src/components/Question/Question.test.tsx +1 -1
  109. package/src/components/Question/Question.tsx +5 -5
  110. package/src/components/RadioButton/RadioButton.test.tsx +7 -126
  111. package/src/components/RadioButton/RadioButton.tsx +10 -41
  112. package/src/components/RadioButton/RadioGroup.test.tsx +65 -0
  113. package/src/components/RadioButton/RadioGroup.tsx +31 -0
  114. package/src/components/Select/Select.test.tsx +39 -37
  115. package/src/components/Select/Select.tsx +7 -22
  116. package/src/components/SequentialNavigation/SequentialNavigation.test.tsx +32 -21
  117. package/src/components/SequentialNavigation/SequentialNavigation.tsx +52 -30
  118. package/src/components/SideNavigation/SideNavigation.test.tsx +39 -85
  119. package/src/components/SideNavigation/SideNavigation.tsx +27 -29
  120. package/src/components/SiteFooter/SiteFooter.test.tsx +153 -0
  121. package/src/components/SiteFooter/SiteFooter.tsx +107 -0
  122. package/src/components/SiteHeader/SiteHeader.test.tsx +87 -79
  123. package/src/components/SiteHeader/SiteHeader.tsx +103 -40
  124. package/src/components/SiteNavigation/SiteNavigation.test.tsx +42 -23
  125. package/src/components/SiteNavigation/SiteNavigation.tsx +28 -16
  126. package/src/components/SiteSearch/SiteSearch.tsx +2 -2
  127. package/src/components/SkipLinks/SkipLinks.test.tsx +22 -10
  128. package/src/components/SkipLinks/SkipLinks.tsx +16 -15
  129. package/src/components/SummaryCard/SummaryCard.test.tsx +31 -35
  130. package/src/components/SummaryCard/SummaryCard.tsx +39 -28
  131. package/src/components/SummaryList/SummaryList.test.tsx +49 -148
  132. package/src/components/SummaryList/SummaryList.tsx +54 -92
  133. package/src/components/Table/Table.tsx +2 -2
  134. package/src/components/Tabs/Tabs.tsx +14 -15
  135. package/src/components/Tag/Tag.test.tsx +4 -4
  136. package/src/components/Tag/Tag.tsx +4 -4
  137. package/src/components/TaskList/TaskList.test.tsx +26 -0
  138. package/src/components/TaskList/TaskList.tsx +21 -11
  139. package/src/components/TextInput/TextInput.test.tsx +1 -1
  140. package/src/components/TextInput/TextInput.tsx +5 -5
  141. package/src/components/Textarea/Textarea.test.tsx +1 -1
  142. package/src/components/Textarea/Textarea.tsx +5 -5
  143. package/src/components/WarningText/WarningText.tsx +2 -2
  144. package/src/hooks/useTracking.test.tsx +64 -0
  145. package/src/hooks/useTracking.ts +19 -0
  146. package/src/utils/context.ts +3 -0
  147. package/tsconfig.json +1 -1
  148. package/dist/common/abstract-notification-banner.jsx +0 -63
  149. package/dist/common/conditional-wrapper.jsx +0 -8
  150. package/dist/common/file-icon.jsx +0 -51
  151. package/dist/common/hint-text.jsx +0 -9
  152. package/dist/common/icon.jsx +0 -57
  153. package/dist/common/screen-reader-text.jsx +0 -9
  154. package/dist/common/wrapper-tag.jsx +0 -11
  155. package/dist/components/accordion/accordion.jsx +0 -102
  156. package/dist/components/back-to-top/back-to-top.jsx +0 -27
  157. package/dist/components/breadcrumbs/breadcrumbs.jsx +0 -28
  158. package/dist/components/button/button.jsx +0 -30
  159. package/dist/components/checkbox/checkbox.jsx +0 -62
  160. package/dist/components/confirmation-message/confirmation-message.jsx +0 -24
  161. package/dist/components/contents-nav/contents-nav.jsx +0 -33
  162. package/dist/components/cookie-banner/cookie-banner.jsx +0 -21
  163. package/dist/components/date-picker/date-picker.jsx +0 -54
  164. package/dist/components/details/details.jsx +0 -17
  165. package/dist/components/error-message/error-message.jsx +0 -12
  166. package/dist/components/error-summary/error-summary.jsx +0 -27
  167. package/dist/components/file-download/file-download.jsx +0 -50
  168. package/dist/components/hide-this-page/hide-this-page.jsx +0 -71
  169. package/dist/components/inset-text/inset-text.jsx +0 -14
  170. package/dist/components/notification-banner/notification-banner.jsx +0 -26
  171. package/dist/components/notification-panel/notification-panel.jsx +0 -21
  172. package/dist/components/page-header/page-header.jsx +0 -15
  173. package/dist/components/page-metadata/page-metadata.jsx +0 -26
  174. package/dist/components/pagination/pagination.jsx +0 -97
  175. package/dist/components/phase-banner/phase-banner.jsx +0 -23
  176. package/dist/components/question/question.jsx +0 -22
  177. package/dist/components/radio-button/radio-button.jsx +0 -43
  178. package/dist/components/select/select.jsx +0 -52
  179. package/dist/components/sequential-navigation/sequential-navigation.jsx +0 -31
  180. package/dist/components/side-navigation/side-navigation.jsx +0 -52
  181. package/dist/components/site-header/site-header.jsx +0 -68
  182. package/dist/components/site-navigation/site-navigation.jsx +0 -22
  183. package/dist/components/site-search/site-search.jsx +0 -55
  184. package/dist/components/skip-links/skip-links.jsx +0 -21
  185. package/dist/components/summary-card/summary-card.jsx +0 -67
  186. package/dist/components/summary-list/summary-list.jsx +0 -75
  187. package/dist/components/table/table.jsx +0 -24
  188. package/dist/components/tabs/tabs.jsx +0 -99
  189. package/dist/components/tag/tag.jsx +0 -13
  190. package/dist/components/task-list/task-list.jsx +0 -95
  191. package/dist/components/text-input/text-input.jsx +0 -58
  192. package/dist/components/textarea/textarea.jsx +0 -54
  193. package/dist/components/warning-text/warning-text.jsx +0 -16
  194. package/dist/icons/ArrowUpward.jsx +0 -41
  195. package/dist/icons/CalendarToday.jsx +0 -41
  196. package/dist/icons/Cancel.jsx +0 -40
  197. package/dist/icons/CheckCircle.jsx +0 -41
  198. package/dist/icons/ChevronLeft.jsx +0 -41
  199. package/dist/icons/ChevronRight.jsx +0 -41
  200. package/dist/icons/Close.jsx +0 -41
  201. package/dist/icons/Description.jsx +0 -41
  202. package/dist/icons/DoubleChevronLeft.jsx +0 -40
  203. package/dist/icons/DoubleChevronRight.jsx +0 -40
  204. package/dist/icons/Error.jsx +0 -41
  205. package/dist/icons/ExpandLess.jsx +0 -41
  206. package/dist/icons/ExpandMore.jsx +0 -41
  207. package/dist/icons/List.jsx +0 -44
  208. package/dist/icons/Menu.jsx +0 -41
  209. package/dist/icons/PriorityHigh.jsx +0 -42
  210. package/dist/icons/Search.jsx +0 -41
  211. package/dist/icons/index.js +0 -40
@@ -1,32 +1,32 @@
1
- import { Children, isValidElement } from 'react';
1
+ import { Children } from 'react';
2
2
  import Icon from './Icon';
3
3
  import ScreenReaderText from './ScreenReaderText';
4
4
 
5
- const Buttons: React.FC<SGDS.Common.AbstractNotificationBanner.Buttons> = ({
5
+ const Buttons = ({
6
6
  children
7
- }) => {
7
+ }: SGDS.Common.AbstractNotificationBanner.Buttons) => {
8
8
  return (<>{children}</>);
9
9
  }
10
10
 
11
- const AbstractNotificationBanner: React.FC<SGDS.Common.AbstractNotificationBanner>
12
- & { Buttons: React.FC<SGDS.Common.AbstractNotificationBanner.Buttons> } = ({
11
+ const AbstractNotificationBanner = ({
13
12
  children,
14
13
  className,
15
14
  close,
15
+ hasColourIcon,
16
+ hasInverseIcon,
16
17
  icon,
17
- iconColour,
18
- iconInverse,
19
18
  title = 'Information',
20
19
  ...props
21
- }) => {
20
+ }: SGDS.Common.AbstractNotificationBanner) => {
22
21
  let content: any[] = [];
23
22
  let buttons;
24
23
 
25
24
  Children.forEach(children, (child) => {
26
- if (isValidElement(child) && child.type === Buttons) {
27
- buttons = child;
25
+ const thisChild = child as React.ReactElement<HTMLAnchorElement>;
26
+ if (thisChild && thisChild.type === Buttons) {
27
+ buttons = thisChild;
28
28
  } else {
29
- content.push(child);
29
+ content.push(thisChild);
30
30
  }
31
31
  });
32
32
 
@@ -52,8 +52,8 @@ const AbstractNotificationBanner: React.FC<SGDS.Common.AbstractNotificationBanne
52
52
  <span
53
53
  className={[
54
54
  'ds_notification__icon',
55
- iconInverse && 'ds_notification__icon--inverse',
56
- iconColour && 'ds_notification__icon--colour'
55
+ hasInverseIcon && 'ds_notification__icon--inverse',
56
+ hasColourIcon && 'ds_notification__icon--colour'
57
57
  ].join(' ')} aria-hidden="true">
58
58
  <Icon icon={icon} />
59
59
  </span>
@@ -83,5 +83,6 @@ const AbstractNotificationBanner: React.FC<SGDS.Common.AbstractNotificationBanne
83
83
 
84
84
  AbstractNotificationBanner.displayName = 'AbstractNotificationBanner';
85
85
  AbstractNotificationBanner.Buttons = Buttons;
86
+ Buttons.displayName = 'Buttons';
86
87
 
87
88
  export default AbstractNotificationBanner;
@@ -0,0 +1,80 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import ActionLink from './ActionLink';
4
+
5
+ const ONCLICK_FUNCTION = vi.fn();
6
+ const ACTION_HREF = "#foo"
7
+ const ACTION_ONCLICK = ONCLICK_FUNCTION;
8
+ const ACTION_TEXT = 'Name';
9
+ const DESCRIBEDBY_ID = 'q1-name';
10
+
11
+ test('button action', () => {
12
+ render(
13
+ <ActionLink
14
+ describedby={DESCRIBEDBY_ID}
15
+ href={undefined}
16
+ onclick={ACTION_ONCLICK}
17
+ >
18
+ {ACTION_TEXT}
19
+ </ActionLink>
20
+ );
21
+
22
+ const action = screen.getByRole('button');
23
+
24
+ expect(action).toHaveClass('ds_link');
25
+ expect(action).toHaveAttribute('aria-describedby', DESCRIBEDBY_ID);
26
+ expect(action).toHaveAttribute('type', 'button');
27
+ expect(action).not.toHaveAttribute('href');
28
+ expect(action.tagName).toEqual('BUTTON');
29
+ expect(action.textContent).toEqual(ACTION_TEXT);
30
+
31
+ fireEvent.click(action);
32
+
33
+ expect(ONCLICK_FUNCTION).toHaveBeenCalled();
34
+ });
35
+
36
+ test('link action', () => {
37
+ render(
38
+ <ActionLink
39
+ describedby={DESCRIBEDBY_ID}
40
+ href={ACTION_HREF}
41
+ onclick={ACTION_ONCLICK}
42
+ >
43
+ {ACTION_TEXT}
44
+ </ActionLink >
45
+ );
46
+
47
+ const action = screen.getByRole('link');
48
+
49
+ expect(action).toHaveClass('ds_link');
50
+ expect(action).toHaveAttribute('aria-describedby', DESCRIBEDBY_ID);
51
+ expect(action).toHaveAttribute('href', ACTION_HREF);
52
+ expect(action).not.toHaveAttribute('type');
53
+ expect(action.tagName).toEqual('A');
54
+ expect(action.textContent).toEqual(ACTION_TEXT);
55
+ });
56
+
57
+ test('action with custom element', () => {
58
+ render(
59
+ <ActionLink
60
+ describedby={DESCRIBEDBY_ID}
61
+ href={ACTION_HREF}
62
+ onclick={ACTION_ONCLICK}
63
+ linkComponent={
64
+ ({ className, ...props }) => (
65
+ <strong role="link" className={className} {...props}/>
66
+ )}>
67
+ {ACTION_TEXT}
68
+ </ActionLink>
69
+ );
70
+
71
+ const action = screen.getByRole('link');
72
+
73
+ expect(action).toHaveAttribute('aria-describedby', DESCRIBEDBY_ID);
74
+ expect(action?.tagName).toEqual('STRONG');
75
+ expect(action?.textContent).toEqual(ACTION_TEXT);
76
+
77
+ fireEvent.click(action);
78
+
79
+ expect(ONCLICK_FUNCTION).toHaveBeenCalled();
80
+ });
@@ -0,0 +1,27 @@
1
+ const ActionLink = ({
2
+ children,
3
+ describedby,
4
+ href,
5
+ linkComponent,
6
+ onclick
7
+ }: SGDS.Common.ActionLink) => {
8
+ const CLASSNAME = 'ds_link';
9
+
10
+ function processChildren(children: React.ReactNode) {
11
+ if (linkComponent) {
12
+ return linkComponent({ className: CLASSNAME, href, children, onClick: onclick, 'aria-describedby': describedby });
13
+ } else if (href) {
14
+ return <a aria-describedby={describedby} onClick={onclick} href={href} className={CLASSNAME}>{children}</a>;
15
+ } else {
16
+ return <button type="button" aria-describedby={describedby} onClick={onclick} className={CLASSNAME}>{children}</button>;
17
+ }
18
+ }
19
+
20
+ return (
21
+ processChildren(children)
22
+ );
23
+ };
24
+
25
+ ActionLink.displayName = 'ActionLink';
26
+
27
+ export default ActionLink;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Wraps all children in a specified HTML tag if a condition is met.
3
3
  */
4
- const ConditionalWrapper: React.FC<SGDS.Common.ConditionalWrapper> = ({ condition, wrapper, children }) =>
4
+ const ConditionalWrapper = ({ condition, wrapper, children }:SGDS.Common.ConditionalWrapper) =>
5
5
  condition ? wrapper(children) : children;
6
6
 
7
7
  ConditionalWrapper.displayName = 'ConditionalWrapper';
@@ -1,22 +1,18 @@
1
1
  import React from 'react';
2
2
  import * as FileIcons from '../images/documents';
3
3
 
4
- const FileIcon: React.FC<SGDS.Common.FileIcon> = ({
4
+ const FileIcon = ({
5
5
  ariaLabel = '',
6
6
  className,
7
7
  icon
8
- }) => {
9
- const Component = React.createElement(FileIcons[icon],
10
- {
11
- className: className,
12
- 'aria-label': ariaLabel
13
- }
14
- );
8
+ }: SGDS.Common.FileIcon) => {
9
+ const FileIconComponent = FileIcons[icon];
15
10
 
16
11
  return (
17
- <>
18
- {Component}
19
- </>
12
+ <FileIconComponent
13
+ aria-label={ariaLabel}
14
+ className={className}
15
+ />
20
16
  );
21
17
  };
22
18
 
@@ -1,9 +1,9 @@
1
- const HintText: React.FC<SGDS.Common.HintText> = ({
1
+ const HintText = ({
2
2
  children,
3
3
  id,
4
4
  text,
5
5
  ...props
6
- }) => {
6
+ }: SGDS.Common.HintText) => {
7
7
  return (
8
8
  <p
9
9
  className="ds_hint-text"
@@ -1,30 +1,26 @@
1
1
  import React from 'react';
2
2
  import * as Icons from '../images/icons';
3
3
 
4
- const Icon: React.FC<SGDS.Common.Icon> = ({
4
+ const Icon = ({
5
5
  ariaLabel,
6
6
  className,
7
7
  fill,
8
8
  icon,
9
9
  iconSize
10
- }) => {
11
- const Component = React.createElement(Icons[icon],
12
- {
13
- 'aria-hidden': ariaLabel ? undefined : true,
14
- 'aria-label': ariaLabel,
15
- className: [
16
- 'ds_icon',
17
- className,
18
- fill && 'ds_icon--fill',
19
- iconSize && `ds_icon--${iconSize}`
20
- ].join(' ')
21
- }
22
- );
10
+ }: SGDS.Common.Icon) => {
11
+ const IconComponent = Icons[icon];
23
12
 
24
13
  return (
25
- <>
26
- {Component}
27
- </>
14
+ <IconComponent
15
+ aria-hidden={ariaLabel ? undefined : true}
16
+ aria-label={ariaLabel}
17
+ className={[
18
+ 'ds_icon',
19
+ className,
20
+ fill && 'ds_icon--fill',
21
+ iconSize && `ds_icon--${iconSize}`
22
+ ].join(' ')}
23
+ />
28
24
  );
29
25
  };
30
26
 
@@ -1,7 +1,7 @@
1
- const ScreenReaderText: React.FC<SGDS.Common.ScreenReaderText> = ({
1
+ const ScreenReaderText = ({
2
2
  children,
3
3
  ...props
4
- }) => {
4
+ }: SGDS.Common.ScreenReaderText) => {
5
5
  return (
6
6
  <span
7
7
  className="visually-hidden"
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Wraps all children in a specified HTML tag.
3
3
  */
4
- const WrapperTag: React.FC<SGDS.Common.WrapperTag> = ({
4
+ const WrapperTag = ({
5
5
  children,
6
6
  tagName = 'div',
7
7
  ...props
8
- }) => {
8
+ }: SGDS.Common.WrapperTag) => {
9
9
  const TagName = tagName;
10
10
  return <TagName {...props}>{children}</TagName>;
11
11
  };
@@ -32,7 +32,6 @@ test('accordion renders correctly', () => {
32
32
 
33
33
  const accordion = screen.getByTestId(ACCORDION_ID);
34
34
  const openAllButton = document.querySelector('.ds_accordion__open-all');
35
- const accordionItems = document.querySelectorAll('.ds_accordion-item');
36
35
  const firstAccordionTitle = document.querySelector('.ds_accordion-item__title');
37
36
 
38
37
  expect(accordion).toHaveClass('ds_accordion');
@@ -43,8 +42,6 @@ test('accordion renders correctly', () => {
43
42
  expect(openAllButton?.textContent).toEqual('Open all sections');
44
43
  expect(openAllButton?.innerHTML).toEqual('Open all <span class="visually-hidden">sections</span>');
45
44
 
46
- expect(accordionItems.length).toEqual(3);
47
-
48
45
  expect(firstAccordionTitle?.tagName).toEqual(DEFAULT_HEADING_LEVEL.toUpperCase());
49
46
  });
50
47
 
@@ -102,6 +99,22 @@ test('accordion with custom heading level', () => {
102
99
  expect(firstAccordionTitle?.tagName).toEqual(HEADING_LEVEL.toUpperCase());
103
100
  });
104
101
 
102
+ test('accordion item with nonsense heading level falls back to h3', () => {
103
+ render(
104
+ <Accordion id={ACCORDION_ID} data-testid={ACCORDION_ID} headingLevel="bananas">
105
+ <Accordion.Item id="accordion-1" title="Healthcare for veterans">
106
+ <p>Veterans are entitled to the same healthcare as any citizen. And there
107
+ are health care options and support available specifically for veterans.</p>
108
+ <p>If you have a health condition that’s related to your service, you’re
109
+ entitled to priority treatment based on clinical need.</p>
110
+ </Accordion.Item>
111
+ </Accordion>
112
+ );
113
+
114
+ const firstAccordionTitle = document.querySelector('.ds_accordion-item__title');
115
+ expect(firstAccordionTitle?.tagName).toEqual('H3');
116
+ });
117
+
105
118
  test('passing additional props to accordion', () => {
106
119
  render(
107
120
  <Accordion id={ACCORDION_ID} data-testid={ACCORDION_ID} data-test="foo">
@@ -216,7 +229,7 @@ test('passing additional props to accordion item', () => {
216
229
  );
217
230
 
218
231
  const accordionItem = screen.getByTestId(ACCORDION_ITEM_ID);
219
- expect(accordionItem?.dataset.test).toEqual('foo');
232
+ expect(accordionItem.dataset.test).toEqual('foo');
220
233
  });
221
234
 
222
235
  test('passing additional CSS classes', () => {
@@ -1,21 +1,29 @@
1
- import React, { Children, useEffect, useRef } from 'react';
1
+ import React, { createContext, useContext, useEffect, useRef } from 'react';
2
2
  import WrapperTag from '../../common/WrapperTag';
3
3
  // @ts-ignore
4
4
  import DSAccordion from '@scottish-government/design-system/src/components/accordion/accordion';
5
5
 
6
6
  let accordionItemCounter = 0;
7
+ const AccordionHeadingLevelContext = createContext('h3');
7
8
 
8
- const AccordionItem: React.FC<SGDS.Component.Accordion.Item> = ({
9
+ const AccordionItem = ({
9
10
  children,
10
11
  className,
11
- headingLevel = 'h3',
12
12
  id: rawId,
13
13
  open = false,
14
14
  title,
15
15
  ...props
16
- }) => {
16
+ }: SGDS.Component.Accordion.Item) => {
17
17
  accordionItemCounter = accordionItemCounter + 1;
18
18
  const processedId = rawId || `accordion-item-${accordionItemCounter}`;
19
+ const DEFAULT_HEADING_LEVEL = 'h3';
20
+
21
+ let headingLevel = useContext(AccordionHeadingLevelContext);
22
+
23
+ if (!['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(headingLevel)) {
24
+ headingLevel = DEFAULT_HEADING_LEVEL;
25
+ }
26
+
19
27
  return (
20
28
  <div
21
29
  className={[
@@ -43,7 +51,7 @@ const AccordionItem: React.FC<SGDS.Component.Accordion.Item> = ({
43
51
  >
44
52
  {title}
45
53
  </WrapperTag>
46
- <span className='ds_accordion-item__indicator' />
54
+ <span className="ds_accordion-item__indicator" />
47
55
  <label
48
56
  className="ds_accordion-item__label"
49
57
  htmlFor={`${processedId}-control`}
@@ -58,14 +66,13 @@ const AccordionItem: React.FC<SGDS.Component.Accordion.Item> = ({
58
66
  );
59
67
  };
60
68
 
61
- const Accordion: React.FC<SGDS.Component.Accordion>
62
- & { Item: React.FC<SGDS.Component.Accordion.Item> } = ({
69
+ const Accordion = ({
63
70
  children,
64
71
  className,
65
72
  headingLevel = 'h3',
66
73
  hideOpenAll,
67
74
  ...props
68
- }) => {
75
+ }: SGDS.Component.Accordion) => {
69
76
  const ref = useRef(null);
70
77
 
71
78
  useEffect(() => {
@@ -78,10 +85,6 @@ const Accordion: React.FC<SGDS.Component.Accordion>
78
85
  hideOpenAll = true;
79
86
  }
80
87
 
81
- function processChild(child: any) {
82
- return React.cloneElement(child, { headingLevel: headingLevel });
83
- }
84
-
85
88
  return (
86
89
  <div
87
90
  className={[
@@ -106,13 +109,15 @@ const Accordion: React.FC<SGDS.Component.Accordion>
106
109
  </button>
107
110
  )}
108
111
 
109
- {Children.map(children, child => processChild(child))}
112
+ <AccordionHeadingLevelContext value={headingLevel}>
113
+ {children}
114
+ </AccordionHeadingLevelContext>
110
115
  </div>
111
116
  );
112
117
  };
113
118
 
114
119
  Accordion.displayName = 'Accordion';
115
- AccordionItem.displayName = 'AccordionItem';
120
+ AccordionItem.displayName = 'Accordion.Item';
116
121
  Accordion.Item = AccordionItem;
117
122
 
118
123
  export default Accordion;
@@ -2,12 +2,12 @@ import React, { Children, useEffect, useRef } from 'react';
2
2
  // @ts-ignore
3
3
  import DSAspectBox from '@scottish-government/design-system/src/components/aspect-box/aspect-box-fallback';
4
4
 
5
- const AspectBox: React.FC<SGDS.Component.AspectBox> = ({
5
+ const AspectBox = ({
6
6
  children,
7
7
  className,
8
8
  ratio,
9
9
  ...props
10
- }) => {
10
+ }: SGDS.Component.AspectBox) => {
11
11
  const ref = useRef(null);
12
12
 
13
13
  useEffect(() => {
@@ -3,11 +3,11 @@ import Icon from '../../common/Icon';
3
3
  // @ts-ignore
4
4
  import DSBackToTop from '@scottish-government/design-system/src/components/back-to-top/back-to-top';
5
5
 
6
- const BackToTop: React.FC<SGDS.Component.BackToTop> = ({
6
+ const BackToTop = ({
7
7
  className,
8
8
  href = '#page-top',
9
9
  ...props
10
- }) => {
10
+ }: SGDS.Component.BackToTop) => {
11
11
  const ref = useRef(null);
12
12
 
13
13
  useEffect(() => {
@@ -2,20 +2,20 @@ import { test, expect } from 'vitest';
2
2
  import { render, screen, within } from '@testing-library/react';
3
3
  import Breadcrumbs from './Breadcrumbs';
4
4
 
5
- const ITEMS = [
6
- { href: 'home', title: 'Home' },
7
- { href: 'category', title: 'Category' },
8
- { title: 'Page' }
9
- ];
5
+ const LINK_HREF = '#home';
6
+ const LINK_TEXT = 'Home';
10
7
 
11
- test('renders correctly', () => {
8
+ test('breadcrumbs render correctly', () => {
12
9
  render(
13
- <Breadcrumbs items={ITEMS} />
10
+ <Breadcrumbs>
11
+ <Breadcrumbs.Item href="home">Home</Breadcrumbs.Item>
12
+ <Breadcrumbs.Item href="category">Category</Breadcrumbs.Item>
13
+ <Breadcrumbs.Item>Page</Breadcrumbs.Item>
14
+ </Breadcrumbs>
14
15
  );
15
16
 
16
17
  const nav = screen.getByRole('navigation');
17
18
  const list = within(nav).getByRole('list');
18
- const listItems = within(list).getAllByRole('listitem');
19
19
 
20
20
  // check nav
21
21
  expect(nav).toHaveAttribute('aria-label', 'Breadcrumb');
@@ -23,67 +23,98 @@ test('renders correctly', () => {
23
23
  // check list
24
24
  expect(list.tagName).toEqual('OL');
25
25
  expect(list).toHaveClass('ds_breadcrumbs');
26
+ });
27
+
28
+ test('passing additional props to breadcrumbs', () => {
29
+ render(
30
+ <Breadcrumbs data-test="foo"/>
31
+ );
32
+
33
+ const nav = screen.getByRole('navigation');
34
+ expect(nav.dataset.test).toEqual('foo');
35
+ });
36
+
37
+ test('passing additional CSS classes to breadcrumbs', () => {
38
+ render(
39
+ <Breadcrumbs className="foo"/>
40
+ );
41
+
42
+ const nav = screen.getByRole('navigation');
43
+ expect(nav).toHaveClass('foo');
44
+ });
26
45
 
27
- // check items
28
- expect(listItems.length).toEqual(ITEMS.length);
46
+ test('breadcrumb item with link', () => {
47
+ render(
48
+ <Breadcrumbs.Item href={LINK_HREF}>{LINK_TEXT}</Breadcrumbs.Item>
49
+ );
50
+
51
+ const item = screen.getByRole('listitem');
52
+ const link = within(item).getByRole('link');
53
+
54
+ expect(item).toHaveClass('ds_breadcrumbs__item');
55
+ expect(item?.tagName).toEqual('LI');
29
56
 
30
- listItems.forEach((item, index) => {
31
- expect(item).toHaveClass('ds_breadcrumbs__item');
57
+ expect(link).toHaveClass('ds_breadcrumbs__link');
58
+ expect(link).toHaveAttribute('href', LINK_HREF);
59
+ expect(link?.tagName).toEqual('A');
60
+ expect(link?.textContent).toEqual(LINK_TEXT);
61
+ });
62
+
63
+ test('breadcrumb item without link', () => {
64
+ render(
65
+ <Breadcrumbs.Item>{LINK_TEXT}</Breadcrumbs.Item>
66
+ );
32
67
 
33
- const link = within(item).queryByRole('link');
68
+ const item = screen.getByRole('listitem');
69
+ const link = within(item).queryByRole('link');
34
70
 
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
- });
71
+ expect(item).toHaveClass('ds_breadcrumbs__item');
72
+ expect(item?.tagName).toEqual('LI');
73
+ expect(item?.textContent).toEqual(LINK_TEXT);
42
74
 
43
- // check href matches correct item
44
- const categoryLink = within(list).getByRole('link', { name: 'Category' });
45
- expect(categoryLink).toHaveAttribute('href', 'category');
75
+ expect(link).not.toBeInTheDocument();
46
76
  });
47
77
 
48
- test('renders with last item hidden', () => {
78
+ test('hidden breadcrumb item', () => {
49
79
  render(
50
- <Breadcrumbs
51
- hideLastItem
52
- items={ITEMS}
53
- />
80
+ <Breadcrumbs.Item data-testid="Breadcrumbs.Item" isHidden>{LINK_TEXT}</Breadcrumbs.Item>
54
81
  );
55
82
 
56
- // check still 3 items
57
- const list = screen.getByRole('list');
58
- const listItems = within(list).getAllByRole('listitem');
59
- expect(listItems.length).toEqual(3);
83
+ const item = screen.getByRole('listitem');
84
+ expect(item).toHaveClass('visually-hidden');
85
+ });
86
+
87
+ test('renders breadcrumb with custom element', () => {
88
+ render(
89
+ <Breadcrumbs.Item href="category" linkComponent={
90
+ ({ className, ...props }) => (
91
+ <span role="link" className={className} {...props}/>
92
+ )}>
93
+ {LINK_TEXT}
94
+ </Breadcrumbs.Item>
95
+ );
60
96
 
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');
97
+ const item = screen.getByRole('listitem');
98
+ const link = within(item).queryByRole('link');
99
+
100
+ expect(link?.tagName).toEqual('SPAN');
101
+ expect(link?.textContent).toEqual(LINK_TEXT);
65
102
  });
66
103
 
67
- test('passing additional props', () => {
104
+ test('passing additional props to breadcrumb item', () => {
68
105
  render(
69
- <Breadcrumbs
70
- items={ITEMS}
71
- data-test="foo"
72
- />
106
+ <Breadcrumbs.Item data-test="foo"/>
73
107
  );
74
108
 
75
- const nav = screen.getByRole('navigation');
109
+ const nav = screen.getByRole('listitem');
76
110
  expect(nav.dataset.test).toEqual('foo');
77
111
  });
78
112
 
79
- test('passing additional CSS classes', () => {
113
+ test('passing additional CSS classes to breadcrumb item', () => {
80
114
  render(
81
- <Breadcrumbs
82
- items={ITEMS}
83
- className="foo"
84
- />
115
+ <Breadcrumbs.Item className="foo"/>
85
116
  );
86
117
 
87
- const nav = screen.getByRole('navigation');
118
+ const nav = screen.getByRole('listitem');
88
119
  expect(nav).toHaveClass('foo');
89
120
  });