@scottish-government/designsystem-react 0.7.1 → 0.8.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 (140) 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/Breadcrumbs.d.ts +2 -5
  6. package/@types/components/Checkbox.d.ts +0 -2
  7. package/@types/components/ConfirmationMessage.d.ts +1 -1
  8. package/@types/components/ContentsNav.d.ts +4 -6
  9. package/@types/components/DatePicker.d.ts +1 -1
  10. package/@types/components/ErrorSummary.d.ts +3 -4
  11. package/@types/components/NotificationPanel.d.ts +1 -1
  12. package/@types/components/Pagination.d.ts +5 -4
  13. package/@types/components/PhaseBanner.d.ts +0 -1
  14. package/@types/components/Question.d.ts +1 -1
  15. package/@types/components/RadioButton.d.ts +0 -1
  16. package/@types/components/Select.d.ts +0 -7
  17. package/@types/components/SequentialNavigation.d.ts +4 -4
  18. package/@types/components/SideNavigation.d.ts +4 -5
  19. package/@types/components/SiteFooter.d.ts +25 -0
  20. package/@types/components/SiteHeader.d.ts +10 -3
  21. package/@types/components/SiteNavigation.d.ts +2 -3
  22. package/@types/components/SkipLinks.d.ts +3 -4
  23. package/@types/components/SummaryCard.d.ts +0 -2
  24. package/@types/components/SummaryList.d.ts +0 -13
  25. package/@types/components/Tabs.d.ts +0 -1
  26. package/@types/components/Tag.d.ts +1 -3
  27. package/@types/components/TaskList.d.ts +1 -0
  28. package/@types/sgds.d.ts +13 -2
  29. package/CHANGELOG.md +63 -1
  30. package/dist/common/AbstractNotificationBanner.jsx +8 -6
  31. package/dist/common/ActionLink.jsx +19 -0
  32. package/dist/common/FileIcon.jsx +2 -7
  33. package/dist/common/Icon.jsx +3 -9
  34. package/dist/components/Accordion/Accordion.jsx +2 -2
  35. package/dist/components/Breadcrumbs/Breadcrumbs.jsx +20 -15
  36. package/dist/components/Checkbox/Checkbox.jsx +2 -30
  37. package/dist/components/Checkbox/CheckboxGroup.jsx +69 -0
  38. package/dist/components/ContentsNav/ContentsNav.jsx +27 -16
  39. package/dist/components/CookieBanner/CookieBanner.jsx +1 -0
  40. package/dist/components/DatePicker/DatePicker.jsx +5 -5
  41. package/dist/components/ErrorSummary/ErrorSummary.jsx +28 -18
  42. package/dist/components/NotificationBanner/NotificationBanner.jsx +2 -2
  43. package/dist/components/Pagination/Pagination.jsx +42 -22
  44. package/dist/components/PhaseBanner/PhaseBanner.jsx +3 -3
  45. package/dist/components/Question/Question.jsx +3 -3
  46. package/dist/components/RadioButton/RadioButton.jsx +3 -17
  47. package/dist/components/RadioButton/RadioGroup.jsx +61 -0
  48. package/dist/components/Select/Select.jsx +4 -7
  49. package/dist/components/SequentialNavigation/SequentialNavigation.jsx +31 -18
  50. package/dist/components/SideNavigation/SideNavigation.jsx +17 -16
  51. package/dist/components/SiteFooter/SiteFooter.jsx +104 -0
  52. package/dist/components/SiteHeader/SiteHeader.jsx +113 -32
  53. package/dist/components/SiteNavigation/SiteNavigation.jsx +20 -7
  54. package/dist/components/SkipLinks/SkipLinks.jsx +10 -10
  55. package/dist/components/SummaryCard/SummaryCard.jsx +25 -14
  56. package/dist/components/SummaryList/SummaryList.jsx +65 -47
  57. package/dist/components/Tabs/Tabs.jsx +6 -6
  58. package/dist/components/Tag/Tag.jsx +2 -2
  59. package/dist/components/TaskList/TaskList.jsx +14 -3
  60. package/dist/components/TextInput/TextInput.jsx +3 -3
  61. package/dist/components/Textarea/Textarea.jsx +3 -3
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +1 -1
  64. package/src/common/AbstractNotificationBanner.test.tsx +1 -1
  65. package/src/common/AbstractNotificationBanner.tsx +14 -13
  66. package/src/common/ActionLink.test.tsx +80 -0
  67. package/src/common/ActionLink.tsx +27 -0
  68. package/src/common/ConditionalWrapper.tsx +1 -1
  69. package/src/common/FileIcon.tsx +7 -11
  70. package/src/common/HintText.tsx +2 -2
  71. package/src/common/Icon.tsx +13 -17
  72. package/src/common/ScreenReaderText.tsx +2 -2
  73. package/src/common/WrapperTag.tsx +2 -2
  74. package/src/components/Accordion/Accordion.test.tsx +1 -1
  75. package/src/components/Accordion/Accordion.tsx +6 -7
  76. package/src/components/AspectBox/AspectBox.tsx +2 -2
  77. package/src/components/BackToTop/BackToTop.tsx +2 -2
  78. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +79 -47
  79. package/src/components/Breadcrumbs/Breadcrumbs.tsx +31 -31
  80. package/src/components/Button/Button.tsx +2 -2
  81. package/src/components/Checkbox/Checkbox.test.tsx +1 -96
  82. package/src/components/Checkbox/Checkbox.tsx +3 -55
  83. package/src/components/Checkbox/CheckboxGroup.test.tsx +37 -0
  84. package/src/components/Checkbox/CheckboxGroup.tsx +46 -0
  85. package/src/components/ConfirmationMessage/ConfirmationMessage.tsx +2 -2
  86. package/src/components/ContentsNav/ContentsNav.test.tsx +40 -51
  87. package/src/components/ContentsNav/ContentsNav.tsx +32 -25
  88. package/src/components/CookieBanner/CookieBanner.tsx +3 -3
  89. package/src/components/DatePicker/DatePicker.test.tsx +1 -1
  90. package/src/components/DatePicker/DatePicker.tsx +7 -7
  91. package/src/components/Details/Details.tsx +2 -2
  92. package/src/components/ErrorMessage/ErrorMessage.tsx +2 -2
  93. package/src/components/ErrorSummary/ErrorSummary.test.tsx +40 -34
  94. package/src/components/ErrorSummary/ErrorSummary.tsx +40 -32
  95. package/src/components/FileDownload/FileDownload.tsx +2 -2
  96. package/src/components/HideThisPage/HideThisPage.tsx +2 -2
  97. package/src/components/InsetText/InsetText.tsx +2 -2
  98. package/src/components/NotificationBanner/NotificationBanner.tsx +6 -7
  99. package/src/components/NotificationPanel/NotificationPanel.tsx +2 -2
  100. package/src/components/PageHeader/PageHeader.tsx +2 -2
  101. package/src/components/PageMetadata/PageMetadata.tsx +4 -5
  102. package/src/components/Pagination/Pagination.test.tsx +26 -7
  103. package/src/components/Pagination/Pagination.tsx +70 -36
  104. package/src/components/PhaseBanner/PhaseBanner.tsx +4 -5
  105. package/src/components/Question/Question.test.tsx +1 -1
  106. package/src/components/Question/Question.tsx +5 -5
  107. package/src/components/RadioButton/RadioButton.test.tsx +7 -126
  108. package/src/components/RadioButton/RadioButton.tsx +4 -41
  109. package/src/components/RadioButton/RadioGroup.test.tsx +65 -0
  110. package/src/components/RadioButton/RadioGroup.tsx +38 -0
  111. package/src/components/Select/Select.test.tsx +39 -37
  112. package/src/components/Select/Select.tsx +7 -22
  113. package/src/components/SequentialNavigation/SequentialNavigation.test.tsx +32 -21
  114. package/src/components/SequentialNavigation/SequentialNavigation.tsx +52 -30
  115. package/src/components/SideNavigation/SideNavigation.test.tsx +39 -85
  116. package/src/components/SideNavigation/SideNavigation.tsx +27 -29
  117. package/src/components/SiteFooter/SiteFooter.test.tsx +153 -0
  118. package/src/components/SiteFooter/SiteFooter.tsx +107 -0
  119. package/src/components/SiteHeader/SiteHeader.test.tsx +87 -79
  120. package/src/components/SiteHeader/SiteHeader.tsx +103 -40
  121. package/src/components/SiteNavigation/SiteNavigation.test.tsx +42 -23
  122. package/src/components/SiteNavigation/SiteNavigation.tsx +28 -16
  123. package/src/components/SiteSearch/SiteSearch.tsx +2 -2
  124. package/src/components/SkipLinks/SkipLinks.test.tsx +22 -10
  125. package/src/components/SkipLinks/SkipLinks.tsx +16 -15
  126. package/src/components/SummaryCard/SummaryCard.test.tsx +31 -35
  127. package/src/components/SummaryCard/SummaryCard.tsx +39 -28
  128. package/src/components/SummaryList/SummaryList.test.tsx +49 -148
  129. package/src/components/SummaryList/SummaryList.tsx +54 -92
  130. package/src/components/Table/Table.tsx +2 -2
  131. package/src/components/Tabs/Tabs.tsx +14 -15
  132. package/src/components/Tag/Tag.test.tsx +4 -4
  133. package/src/components/Tag/Tag.tsx +4 -4
  134. package/src/components/TaskList/TaskList.test.tsx +26 -0
  135. package/src/components/TaskList/TaskList.tsx +21 -11
  136. package/src/components/TextInput/TextInput.test.tsx +1 -1
  137. package/src/components/TextInput/TextInput.tsx +5 -5
  138. package/src/components/Textarea/Textarea.test.tsx +1 -1
  139. package/src/components/Textarea/Textarea.tsx +5 -5
  140. package/src/components/WarningText/WarningText.tsx +2 -2
@@ -0,0 +1,107 @@
1
+ import React, { Children } from 'react';
2
+ import ConditionalWrapper from '../../common/ConditionalWrapper';
3
+
4
+ const License = ({
5
+ children,
6
+ ...props
7
+ }: SGDS.Component.SiteFooter.License) => {
8
+ return (
9
+ <div className="ds_site-footer__copyright" {...props}>
10
+ {children}
11
+ </div>
12
+ );
13
+ }
14
+
15
+ const Links = ({
16
+ children,
17
+ ...props
18
+ }: SGDS.Component.SiteFooter.Links) => {
19
+ return (
20
+ <ul className="ds_site-footer__site-items" {...props}>
21
+ {children}
22
+ </ul>
23
+ );
24
+ }
25
+
26
+ const Link = ({
27
+ children,
28
+ href,
29
+ linkComponent,
30
+ ...props
31
+ }: SGDS.Component.SiteFooter.Link) => {
32
+ function processChildren(children: React.ReactNode) {
33
+ if (linkComponent) {
34
+ return linkComponent({ href, children });
35
+ } else if (href) {
36
+ return <a href={href}>{children}</a>;
37
+ } else {
38
+ return children;
39
+ }
40
+ }
41
+
42
+ return <li className="ds_site-items__item" {...props}>
43
+ {processChildren(children)}
44
+ </li>;
45
+ }
46
+
47
+ const Org = ({
48
+ href,
49
+ title,
50
+ children,
51
+ ...props
52
+ }: SGDS.Component.SiteFooter.Org) => {
53
+ children = Children.map(children, child => {
54
+ let thisChild = child as React.ReactElement<HTMLElement>;
55
+ if (thisChild && ['img', 'svg', 'picture'].includes(thisChild.type as string)) {
56
+ return React.cloneElement(thisChild, { className: 'ds_site-footer__org-logo' });
57
+ } else {
58
+ return child;
59
+ }
60
+ });
61
+
62
+ return (
63
+ <div className="ds_site-footer__org" {...props}>
64
+ <ConditionalWrapper
65
+ condition={typeof href !== 'undefined'}
66
+ wrapper={(children: React.JSX.Element) => <a className="ds_site-footer__org-link" title={title} href={href}>{children}</a>}
67
+ >
68
+ {children}
69
+ </ConditionalWrapper>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ const SiteFooter = ({
75
+ children,
76
+ className,
77
+ ...props
78
+ }: SGDS.Component.SiteFooter) => {
79
+ return (
80
+ <footer
81
+ className={[
82
+ "ds_site-footer",
83
+ className
84
+ ].join(' ')}
85
+ {...props}
86
+ >
87
+ <div className="ds_wrapper">
88
+ <div className="ds_site-footer__content">
89
+ {children}
90
+ </div>
91
+ </div>
92
+ </footer>
93
+ );
94
+ };
95
+
96
+ SiteFooter.Links = Links;
97
+ SiteFooter.Link = Link;
98
+ SiteFooter.License = License;
99
+ SiteFooter.Org = Org;
100
+
101
+ SiteFooter.displayName = 'SiteFooter';
102
+ Links.displayName = 'SiteFooter.Links';
103
+ Link.displayName = 'SiteFooter.Link';
104
+ License.displayName = 'SiteFooter.License';
105
+ Org.displayName = 'SiteFooter.Org';
106
+
107
+ export default SiteFooter;
@@ -7,25 +7,29 @@ import PhaseBanner from '../PhaseBanner/PhaseBanner';
7
7
 
8
8
  test('site header renders correctly (maximal, testing markup structure)', () => {
9
9
  render(
10
- <SiteHeader
11
- logo={{
12
- alt: 'The Scottish Government',
13
- src: './scottish-government.svg'
14
- }}
15
- navigationItems={[
16
- { title: 'About', href: '#about' },
17
- { title: 'Get started', href: '#get-started' },
18
- { title: 'Styles', href: '#styles' },
19
- { title: 'Components', href: '#components' },
20
- { title: 'Patterns', href: '#patterns' },
21
- { title: 'Guidance', href: '#guidance' },
22
- ]}
23
- phaseBanner={{
24
- phaseName: 'Beta'
25
- }}
26
- siteSearch
27
- siteTitle="Design System React"
28
- />
10
+ <SiteHeader>
11
+ <SiteHeader.Brand homeUrl="/" siteTitle="Design System React">
12
+ <img src="./scottish-government.svg" alt="gov.scot" loading="lazy" width="300" height="58" />
13
+ </SiteHeader.Brand>
14
+ <SiteHeader.Navigation>
15
+ <SiteNavigation>
16
+ <SiteNavigation.Item href="#about">About</SiteNavigation.Item>
17
+ <SiteNavigation.Item href="#get-started">Get started</SiteNavigation.Item>
18
+ <SiteNavigation.Item href="#styles">Styles</SiteNavigation.Item>
19
+ <SiteNavigation.Item href="#components" current>Components</SiteNavigation.Item>
20
+ <SiteNavigation.Item href="#patterns">Patterns</SiteNavigation.Item>
21
+ <SiteNavigation.Item href="#guidance">Guidance</SiteNavigation.Item>
22
+ </SiteNavigation>
23
+ </SiteHeader.Navigation>
24
+ <SiteHeader.Search>
25
+ <SiteSearch id="site-header-search"/>
26
+ </SiteHeader.Search>
27
+ <SiteHeader.Phase>
28
+ <PhaseBanner phaseName="Beta">
29
+ This is a new service. Your <a href="#feedback">feedback</a> will help us to improve it.
30
+ </PhaseBanner>
31
+ </SiteHeader.Phase>
32
+ </SiteHeader>
29
33
  );
30
34
 
31
35
  const siteHeader = screen.getByRole('banner');
@@ -37,7 +41,7 @@ test('site header renders correctly (maximal, testing markup structure)', () =>
37
41
  const siteHeaderNavigationMobile = within(siteHeader).getAllByRole('navigation')[0];
38
42
  const siteHeaderNavigationDesktop = within(siteHeader).getAllByRole('navigation')[1];
39
43
  const siteHeaderPhaseBanner = siteHeader.querySelector('.ds_phase-banner');
40
- const siteHeaderSearch = within(siteHeader).getByRole('search').parentElement;
44
+ const siteHeaderSearch = within(siteHeader).getByRole('search').parentElement?.parentElement;
41
45
 
42
46
  expect(siteHeader).toHaveClass('ds_site-header');
43
47
  expect(siteHeaderContentWrapper).toHaveClass('ds_wrapper');
@@ -67,15 +71,12 @@ test('site header renders correctly (maximal, testing markup structure)', () =>
67
71
  });
68
72
 
69
73
  test('site header branding: logo only, default URL', () => {
70
- const LOGO = {
71
- alt: 'The Scottish Government',
72
- src: './scottish-government.svg'
73
- };
74
-
75
74
  render(
76
- <SiteHeader
77
- logo={LOGO}
78
- />
75
+ <SiteHeader>
76
+ <SiteHeader.Brand>
77
+ <img src="./scottish-government.svg" alt="gov.scot" loading="lazy" width="300" height="58" />
78
+ </SiteHeader.Brand>
79
+ </SiteHeader>
79
80
  );
80
81
 
81
82
  const siteHeader = screen.getByRole('banner');
@@ -87,25 +88,22 @@ test('site header branding: logo only, default URL', () => {
87
88
  expect(siteHeaderLogoLink).toHaveAttribute('href', '/');
88
89
 
89
90
  expect(siteHeaderLogoImg).toHaveClass('ds_site-branding__logo-image');
90
- expect(siteHeaderLogoImg).toHaveAttribute('src', LOGO.src);
91
- expect(siteHeaderLogoImg).toHaveAttribute('alt', LOGO.alt);
92
91
 
93
92
  expect(siteHeaderLogoImg.parentElement).toEqual(siteHeaderLogoLink);
94
93
  expect(siteHeaderLogoLink.parentElement).toEqual(siteHeaderBranding);
94
+
95
+ expect(siteHeaderLogoImg).toHaveClass('ds_site-branding__logo-image');
95
96
  });
96
97
 
97
98
  test('site header branding: logo and site title', () => {
98
- const LOGO = {
99
- alt: 'The Scottish Government',
100
- src: './scottish-government.svg'
101
- };
102
99
  const SITE_TITLE_CONTENT = 'Design System React';
103
100
 
104
101
  render(
105
- <SiteHeader
106
- logo={LOGO}
107
- siteTitle={SITE_TITLE_CONTENT}
108
- />
102
+ <SiteHeader>
103
+ <SiteHeader.Brand homeUrl="/" siteTitle={SITE_TITLE_CONTENT}>
104
+ <img src="./scottish-government.svg" alt="gov.scot" loading="lazy" width="300" height="58" />
105
+ </SiteHeader.Brand>
106
+ </SiteHeader>
109
107
  );
110
108
 
111
109
  const siteHeader = screen.getByRole('banner');
@@ -120,64 +118,66 @@ test('site header branding: logo and site title', () => {
120
118
  });
121
119
 
122
120
  test('site header branding: custom link URL', () => {
123
- const LOGO = {
124
- alt: 'The Scottish Government',
125
- href: '/home.aspx',
126
- src: './scottish-government.svg'
127
- };
121
+ const HOME_URL = '/home.aspx';
128
122
 
129
123
  render(
130
- <SiteHeader
131
- logo={LOGO}
132
- />
124
+ <SiteHeader>
125
+ <SiteHeader.Brand homeUrl={HOME_URL}>
126
+ <img src="./scottish-government.svg" alt="gov.scot" loading="lazy" width="300" height="58" />
127
+ </SiteHeader.Brand>
128
+ </SiteHeader>
133
129
  );
134
130
 
135
131
  const siteHeader = screen.getByRole('banner');
136
132
  const siteHeaderLogoLink = within(siteHeader).getByRole('link');
137
133
 
138
- expect(siteHeaderLogoLink).toHaveAttribute('href', LOGO.href);
134
+ expect(siteHeaderLogoLink).toHaveAttribute('href', HOME_URL);
139
135
  });
140
136
 
141
- test('site header with site search', () => {
142
- const SEARCH_PROPS = {
143
- action: 'apple',
144
- autocompleteEndpoint: 'banana',
145
- autocompleteSuggestionMappingFunction: 'cucumber',
146
- className: 'durian',
147
- id: 'eggplant',
148
- method: 'POST',
149
- name: 'guava',
150
- placeholder: 'haw'
151
- };
137
+ test('site header logo link link with custom element', () => {
138
+ const LINK_CONTENT = <img src="./scottish-government.svg" alt="gov.scot" loading="lazy" width="300" height="58" />
152
139
 
153
140
  render(
154
- <>
155
- <SiteHeader siteSearch={SEARCH_PROPS} />
156
- <SiteSearch data-testid="sitesearch" {...SEARCH_PROPS} />
157
- </>
141
+ <SiteHeader>
142
+ <SiteHeader.Brand linkComponent={
143
+ ({ className, ...props }) => (
144
+ <strong role="link" className={className} {...props}/>
145
+ )}>
146
+ {LINK_CONTENT}
147
+ </SiteHeader.Brand>
148
+ </SiteHeader>
158
149
  );
159
150
 
160
- const siteHeader = screen.getByRole('banner');
161
- const siteHeaderSearch = within(siteHeader).getByRole('search')
162
- const siteSearchReference = screen.getByTestId('sitesearch');
151
+ const item = screen.getByRole('banner');
152
+ const link = within(item).getByRole('link');
163
153
 
164
- expect(siteHeaderSearch.outerHTML).toEqual(siteSearchReference.innerHTML);
154
+ expect(link?.tagName).toEqual('STRONG');
165
155
  });
166
156
 
167
157
  test('site header with site navigation', () => {
168
- const NAVIGATION_ITEMS = [
169
- { title: 'About', href: '#about' },
170
- { title: 'Get started', href: '#get-started' },
171
- { title: 'Styles', href: '#styles' },
172
- { title: 'Components', href: '#components' },
173
- { title: 'Patterns', href: '#patterns' },
174
- { title: 'Guidance', href: '#guidance' },
175
- ];
158
+ const NAVIGATION_ITEMS = (
159
+ <>
160
+ <SiteNavigation.Item href="#about">About</SiteNavigation.Item>
161
+ <SiteNavigation.Item href="#get-started">Get started</SiteNavigation.Item>
162
+ <SiteNavigation.Item href="#styles">Styles</SiteNavigation.Item>
163
+ <SiteNavigation.Item href="#components" current>Components</SiteNavigation.Item>
164
+ <SiteNavigation.Item href="#patterns">Patterns</SiteNavigation.Item>
165
+ <SiteNavigation.Item href="#guidance">Guidance</SiteNavigation.Item>
166
+ </>
167
+ );
176
168
 
177
169
  render(
178
170
  <>
179
- <SiteHeader navigationItems={NAVIGATION_ITEMS} />
180
- <SiteNavigation data-testid="sitenavigation" items={NAVIGATION_ITEMS} />
171
+ <SiteHeader>
172
+ <SiteHeader.Navigation>
173
+ <SiteNavigation>
174
+ {NAVIGATION_ITEMS}
175
+ </SiteNavigation>
176
+ </SiteHeader.Navigation>
177
+ </SiteHeader>
178
+ <SiteNavigation data-testid="sitenavigation">
179
+ {NAVIGATION_ITEMS}
180
+ </SiteNavigation>
181
181
  </>
182
182
  );
183
183
 
@@ -212,15 +212,23 @@ test('site header with site navigation', () => {
212
212
  });
213
213
 
214
214
  test('site header with phase banner', () => {
215
- const PHASE_BANNER_PROPS = {
215
+ const PHASE_BANNER = {
216
216
  content: 'My content',
217
217
  phaseName: 'Beta'
218
218
  };
219
219
 
220
220
  render(
221
221
  <>
222
- <SiteHeader phaseBanner={PHASE_BANNER_PROPS} />
223
- <PhaseBanner data-testid="phasebanner" {...PHASE_BANNER_PROPS} />
222
+ <SiteHeader>
223
+ <SiteHeader.Phase>
224
+ <PhaseBanner phaseName={PHASE_BANNER.phaseName}>
225
+ {PHASE_BANNER.content}
226
+ </PhaseBanner>
227
+ </SiteHeader.Phase>
228
+ </SiteHeader>
229
+ <PhaseBanner data-testid="phasebanner" phaseName={PHASE_BANNER.phaseName}>
230
+ {PHASE_BANNER.content}
231
+ </PhaseBanner>
224
232
  </>
225
233
  );
226
234
 
@@ -1,25 +1,94 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef } from 'react';
4
-
3
+ import React, { Children, useEffect, useRef } from 'react';
5
4
  import Icon from '../../common/Icon';
6
- import PhaseBanner from '../PhaseBanner/PhaseBanner';
7
- import SiteNavigation from "../SiteNavigation/SiteNavigation";
8
- import SiteSearch from "../SiteSearch/SiteSearch";
5
+ import SiteNavigation from '../SiteNavigation/SiteNavigation';
9
6
 
10
7
  // @ts-ignore
11
8
  import DSMobileMenu from '@scottish-government/design-system/src/components/site-navigation/site-navigation';
12
9
 
13
- const SiteHeader: React.FC<SGDS.Component.SiteHeader> = ({
10
+ const Brand = ({
11
+ children,
12
+ homeUrl = '/',
13
+ linkComponent,
14
+ siteTitle
15
+ }: SGDS.Component.SiteHeader.Brand) => {
16
+ function processChildren(children: React.ReactNode) {
17
+ const image = React.cloneElement(children as React.ReactElement<HTMLImageElement>, { className: 'ds_site-branding__logo-image' });
18
+
19
+ if (linkComponent) {
20
+ return linkComponent({ className: 'ds_site-branding__logo ds_site-branding__link', href: homeUrl, children: image });
21
+ } else if (homeUrl) {
22
+ return <a href={homeUrl} className="ds_site-branding__logo ds_site-branding__link">{image}</a>;
23
+ }
24
+ }
25
+
26
+ return (
27
+ <>
28
+ {processChildren(children)}
29
+
30
+ {siteTitle &&
31
+ <div className="ds_site-branding__title">
32
+ {siteTitle}
33
+ </div>
34
+ }
35
+ </>
36
+ );
37
+ }
38
+
39
+ const Navigation = ({
40
+ children
41
+ }: any) => {
42
+ return children;
43
+ }
44
+
45
+ const Phase = ({
46
+ children
47
+ }: any) => {
48
+ return children;
49
+ }
50
+
51
+ const Search = ({
52
+ children
53
+ }: any) => {
54
+ return children;
55
+ }
56
+
57
+ const SiteHeader = ({
58
+ children,
14
59
  logo = {},
15
60
  navigationItems,
16
61
  phaseBanner,
17
62
  siteSearch,
18
63
  siteTitle,
19
64
  ...props
20
- }) => {
65
+ }: SGDS.Component.SiteHeader) => {
21
66
  const mobileMenuRef = useRef(null);
22
67
 
68
+ let branding: React.ReactNode;
69
+ let navigation: React.ReactNode;
70
+ let mobileNavigation: React.ReactNode;
71
+ let phase: React.ReactNode;
72
+ let search: React.ReactNode;
73
+
74
+ // assign to slots
75
+ Children.forEach(children, (child: React.ReactNode) => {
76
+ const thisChild = child as React.ReactElement<any>;
77
+ if (thisChild && thisChild.type === Brand) {
78
+ branding = thisChild;
79
+ } else if (thisChild && thisChild.type === Navigation) {
80
+ navigation = thisChild;
81
+
82
+ if (thisChild.props.children.type === SiteNavigation) {
83
+ mobileNavigation = React.cloneElement(thisChild.props.children as React.ReactElement<SGDS.Component.SiteNavigation>, { className: 'ds_site-navigation--mobile', id: 'mobile-navigation', ref: mobileMenuRef});
84
+ }
85
+ } else if (thisChild && thisChild.type === Phase) {
86
+ phase = thisChild;
87
+ } else if (thisChild && thisChild.type === Search) {
88
+ search = thisChild;
89
+ }
90
+ });
91
+
23
92
  useEffect(() => {
24
93
  if (mobileMenuRef.current) {
25
94
  new DSMobileMenu(mobileMenuRef.current).init();
@@ -31,50 +100,35 @@ const SiteHeader: React.FC<SGDS.Component.SiteHeader> = ({
31
100
  <div className="ds_wrapper">
32
101
  <div className="ds_site-header__content">
33
102
  <div className="ds_site-branding">
34
- {logo &&
35
- <a className="ds_site-branding__logo ds_site-branding__link" href={logo.href ? logo.href : '/'}>
36
- <img className="ds_site-branding__logo-image" src={logo.src} alt={logo.alt} />
37
- </a>
38
- }
39
-
40
- {siteTitle && <div className="ds_site-branding__title">
41
- {siteTitle}
42
- </div>}
103
+ {branding}
43
104
  </div>
44
105
 
45
- {navigationItems &&
46
- <div className="ds_site-header__controls">
47
- <label aria-controls="mobile-navigation" className="ds_site-header__control js-toggle-menu" htmlFor="menu">
48
- <span className="ds_site-header__control-text">Menu</span>
49
- <Icon fill className="ds_site-header__control-icon" icon="Menu" aria-hidden="true" />
50
- <Icon fill className="ds_site-header__control-icon ds_site-header__control-icon--active-icon" icon="Close" aria-hidden="true" />
51
- </label>
52
- </div>
53
- }
106
+ {mobileNavigation &&
107
+ <>
108
+ <div className="ds_site-header__controls">
109
+ <label aria-controls="mobile-navigation" className="ds_site-header__control js-toggle-menu" htmlFor="menu">
110
+ <span className="ds_site-header__control-text">Menu</span>
111
+ <Icon fill className="ds_site-header__control-icon" icon="Menu" aria-hidden="true" />
112
+ <Icon fill className="ds_site-header__control-icon ds_site-header__control-icon--active-icon" icon="Close" aria-hidden="true" />
113
+ </label>
114
+ </div>
54
115
 
55
- {navigationItems &&
56
- <input className="ds_site-navigation__toggle" id="menu" type="checkbox" />
57
- }
58
- {navigationItems &&
59
- <SiteNavigation id="mobile-navigation" className="ds_site-navigation--mobile" items={navigationItems} ref={mobileMenuRef} />
116
+ <input className="ds_site-navigation__toggle" id="menu" type="checkbox" />
117
+ </>
60
118
  }
61
119
 
62
- {siteSearch &&
63
- <SiteSearch className="ds_site-header__search" {...siteSearch} />
64
- }
120
+ {mobileNavigation}
121
+
122
+ <div className="ds_site-header__search">{search}</div>
65
123
  </div>
66
124
  </div>
67
125
 
68
- {phaseBanner &&
69
- <PhaseBanner phaseName={phaseBanner.phaseName}>
70
- {phaseBanner.content}
71
- </PhaseBanner>
72
- }
126
+ {phase}
73
127
 
74
- {navigationItems &&
128
+ {navigation &&
75
129
  <div className="ds_site-header__navigation">
76
130
  <div className="ds_wrapper">
77
- <SiteNavigation items={navigationItems} />
131
+ {navigation}
78
132
  </div>
79
133
  </div>
80
134
  }
@@ -82,6 +136,15 @@ const SiteHeader: React.FC<SGDS.Component.SiteHeader> = ({
82
136
  );
83
137
  };
84
138
 
139
+ SiteHeader.Brand = Brand;
140
+ SiteHeader.Navigation = Navigation;
141
+ SiteHeader.Phase = Phase;
142
+ SiteHeader.Search = Search;
143
+
85
144
  SiteHeader.displayName = 'SiteHeader';
145
+ Brand.displayName = 'SiteHeader.Brand';
146
+ Navigation.displayName = 'SiteHeader.Navigation';
147
+ Phase.displayName = 'SiteHeader.Phase';
148
+ Search.displayName = 'SiteHeader.Search';
86
149
 
87
150
  export default SiteHeader;
@@ -2,23 +2,23 @@ import { test, expect } from 'vitest';
2
2
  import { render, screen, within } from '@testing-library/react';
3
3
  import SiteNavigation from './SiteNavigation';
4
4
 
5
- const ITEMS = [
6
- {title: 'About', href: '#about'},
7
- {title: 'Get started', href: '#get-started'},
8
- {title: 'Styles', href: '#styles'},
9
- {title: 'Components', href: '#components'},
10
- {title: 'Patterns', href: '#patterns'},
11
- {title: 'Guidance', href: '#guidance'},
12
- ]
5
+ const LINK_HREF = '#about';
6
+ const LINK_TEXT = 'About';
13
7
 
14
8
  test('renders correctly', () => {
15
9
  render(
16
- <SiteNavigation items={ITEMS}/>
10
+ <SiteNavigation>
11
+ <SiteNavigation.Item href="#about">About</SiteNavigation.Item>
12
+ <SiteNavigation.Item href="#get-started">Get started</SiteNavigation.Item>
13
+ <SiteNavigation.Item href="#styles">Styles</SiteNavigation.Item>
14
+ <SiteNavigation.Item href="#components">Components</SiteNavigation.Item>
15
+ <SiteNavigation.Item href="#patterns">Patterns</SiteNavigation.Item>
16
+ <SiteNavigation.Item href="#guidance">Guidance</SiteNavigation.Item>
17
+ </SiteNavigation>
17
18
  );
18
19
 
19
20
  const nav = screen.getByRole('navigation');
20
21
  const list = within(nav).getByRole('list');
21
- const listItems = within(list).getAllByRole('listitem');
22
22
 
23
23
  // check nav
24
24
  expect(nav).toHaveClass('ds_site-navigation');
@@ -27,25 +27,44 @@ test('renders correctly', () => {
27
27
  // check list
28
28
  expect(list.tagName).toEqual('UL');
29
29
  expect(list).toHaveClass('ds_site-navigation__list');
30
+ });
31
+
32
+ test('site navigation link renders correctly', () => {
33
+ render(
34
+ <SiteNavigation.Item href={LINK_HREF}>{LINK_TEXT}</SiteNavigation.Item>
35
+ );
36
+
37
+ const listItem =screen.getByRole('listitem');
38
+ const link = within(listItem).getByRole('link');
30
39
 
31
- // check items
32
- expect(listItems.length).toEqual(ITEMS.length);
40
+ expect(listItem).toHaveClass('ds_site-navigation__item');
33
41
 
34
- listItems.forEach((item, index) => {
35
- expect(item).toHaveClass('ds_site-navigation__item');
42
+ expect(link).toHaveClass('ds_site-navigation__link');
43
+ expect(link).not.toHaveClass('ds_current');
44
+ expect(link.textContent).toEqual(LINK_TEXT);
45
+ expect(link).toHaveAttribute('href', LINK_HREF)
46
+ });
47
+
48
+ test('site navigation link with custom element', () => {
49
+ render(
50
+ <SiteNavigation.Item href={LINK_HREF} linkComponent={
51
+ ({ className, ...props }) => (
52
+ <strong role="link" className={className} {...props}/>
53
+ )}>
54
+ {LINK_TEXT}
55
+ </SiteNavigation.Item>
56
+ );
36
57
 
37
- const link = within(item).getByRole('link');
58
+ const item = screen.getByRole('listitem');
59
+ const link = within(item).queryByRole('link');
38
60
 
39
- expect(link).toHaveClass('ds_site-navigation__link');
40
- expect(link).not.toHaveClass('ds_current');
41
- expect(link.textContent).toEqual(ITEMS[index].title);
42
- expect(link).toHaveAttribute('href', ITEMS[index].href)
43
- });
61
+ expect(link?.tagName).toEqual('STRONG');
62
+ expect(link?.textContent).toEqual(LINK_TEXT);
44
63
  });
45
64
 
46
65
  test('highlights current item', () => {
47
66
  render(
48
- <SiteNavigation data-test="foo" items={[{title: 'About', href: '#about', current: true}]}/>
67
+ <SiteNavigation.Item href={LINK_HREF} current>{LINK_TEXT}</SiteNavigation.Item>
49
68
  );
50
69
 
51
70
  const link = screen.getByRole('link');
@@ -55,7 +74,7 @@ test('highlights current item', () => {
55
74
 
56
75
  test('passing additional props', () => {
57
76
  render(
58
- <SiteNavigation data-test="foo" items={ITEMS}/>
77
+ <SiteNavigation data-test="foo"/>
59
78
  );
60
79
 
61
80
  const nav = screen.getByRole('navigation');
@@ -64,7 +83,7 @@ test('passing additional props', () => {
64
83
 
65
84
  test('passing additional CSS classes', () => {
66
85
  render(
67
- <SiteNavigation className="foo" items={ITEMS}/>
86
+ <SiteNavigation className="foo"/>
68
87
  );
69
88
 
70
89
  const nav = screen.getByRole('navigation');