@scottish-government/designsystem-react 0.8.0 → 0.10.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 (169) hide show
  1. package/.storybook/main.ts +20 -0
  2. package/.storybook/manager.ts +13 -0
  3. package/.storybook/preview-head.html +1 -0
  4. package/.storybook/preview.tsx +56 -0
  5. package/.storybook/sgdsArgTypes.ts +123 -0
  6. package/.storybook/sgdsTheme.ts +9 -0
  7. package/.storybook/vitest.setup.ts +7 -0
  8. package/@types/common/AbstractNotificationBanner.d.ts +2 -2
  9. package/@types/common/ActionLink.d.ts +1 -1
  10. package/@types/common/Icon.d.ts +1 -1
  11. package/@types/components/Accordion.d.ts +2 -3
  12. package/@types/components/Button.d.ts +5 -5
  13. package/@types/components/CategoryItem.d.ts +10 -0
  14. package/@types/components/CategoryList.d.ts +7 -0
  15. package/@types/components/Checkbox.d.ts +2 -2
  16. package/@types/components/ContentsNav.d.ts +1 -1
  17. package/@types/components/DatePicker.d.ts +1 -1
  18. package/@types/components/ErrorMessage.d.ts +1 -2
  19. package/@types/components/ErrorSummary.d.ts +1 -1
  20. package/@types/components/FileDownload.d.ts +2 -2
  21. package/@types/components/Metadata.d.ts +1 -1
  22. package/@types/components/Pagination.d.ts +1 -1
  23. package/@types/components/RadioButton.d.ts +2 -2
  24. package/@types/components/SideNavigation.d.ts +1 -1
  25. package/@types/components/SiteNavigation.d.ts +1 -1
  26. package/@types/components/SummaryList.d.ts +1 -1
  27. package/@types/components/Tabs.d.ts +3 -3
  28. package/@types/components/TextInput.d.ts +1 -1
  29. package/@types/sgds.d.ts +2 -1
  30. package/CHANGELOG.md +29 -0
  31. package/dist/common/AbstractNotificationBanner.jsx +4 -4
  32. package/dist/common/Icon.jsx +2 -2
  33. package/dist/components/Accordion/Accordion.jsx +8 -7
  34. package/dist/components/Button/Button.jsx +6 -6
  35. package/dist/components/CategoryItem/CategoryItem.jsx +35 -0
  36. package/dist/components/CategoryList/CategoryList.jsx +55 -0
  37. package/dist/components/Checkbox/Checkbox.jsx +7 -4
  38. package/dist/components/Checkbox/CheckboxGroup.jsx +5 -11
  39. package/dist/components/ContentsNav/ContentsNav.jsx +2 -2
  40. package/dist/components/DatePicker/DatePicker.jsx +1 -1
  41. package/dist/components/ErrorMessage/ErrorMessage.jsx +3 -3
  42. package/dist/components/FileDownload/FileDownload.jsx +2 -2
  43. package/dist/components/NotificationBanner/NotificationBanner.jsx +2 -2
  44. package/dist/components/PageMetadata/PageMetadata.jsx +4 -4
  45. package/dist/components/Pagination/Pagination.jsx +4 -4
  46. package/dist/components/Question/Question.jsx +1 -1
  47. package/dist/components/RadioButton/RadioButton.jsx +6 -2
  48. package/dist/components/RadioButton/RadioGroup.jsx +7 -47
  49. package/dist/components/Select/Select.jsx +1 -1
  50. package/dist/components/SideNavigation/SideNavigation.jsx +2 -2
  51. package/dist/components/SiteHeader/SiteHeader.jsx +3 -3
  52. package/dist/components/SiteNavigation/SiteNavigation.jsx +2 -2
  53. package/dist/components/SiteSearch/SiteSearch.jsx +1 -1
  54. package/dist/components/SkipLinks/SkipLinks.jsx +1 -1
  55. package/dist/components/SummaryList/SummaryList.jsx +3 -3
  56. package/dist/components/Tabs/Tabs.jsx +6 -7
  57. package/dist/components/TextInput/TextInput.jsx +5 -5
  58. package/dist/components/Textarea/Textarea.jsx +1 -1
  59. package/dist/hooks/useTracking.js +21 -0
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/utils/context.js +5 -0
  62. package/package.json +15 -4
  63. package/src/common/AbstractNotificationBanner.test.tsx +1 -1
  64. package/src/common/AbstractNotificationBanner.tsx +4 -4
  65. package/src/common/Icon.test.tsx +1 -1
  66. package/src/common/Icon.tsx +2 -2
  67. package/src/components/Accordion/Accordion.stories.tsx +111 -0
  68. package/src/components/Accordion/Accordion.test.tsx +5 -17
  69. package/src/components/Accordion/Accordion.tsx +11 -10
  70. package/src/components/AspectBox/AspectBox.stories.tsx +64 -0
  71. package/src/components/BackToTop/BackToTop.stories.tsx +36 -0
  72. package/src/components/Breadcrumbs/Breadcrumbs.stories.tsx +49 -0
  73. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +0 -1
  74. package/src/components/Breadcrumbs/Breadcrumbs.tsx +1 -1
  75. package/src/components/Button/Button.stories.tsx +194 -0
  76. package/src/components/Button/Button.test.tsx +4 -4
  77. package/src/components/Button/Button.tsx +9 -9
  78. package/src/components/CategoryItem/CategoryItem.stories.tsx +55 -0
  79. package/src/components/CategoryItem/CategoryItem.test.tsx +93 -0
  80. package/src/components/CategoryItem/CategoryItem.tsx +56 -0
  81. package/src/components/CategoryList/CategoryList.stories.tsx +65 -0
  82. package/src/components/CategoryList/CategoryList.test.tsx +59 -0
  83. package/src/components/CategoryList/CategoryList.tsx +33 -0
  84. package/src/components/Checkbox/Checkbox.stories.tsx +85 -0
  85. package/src/components/Checkbox/Checkbox.test.tsx +2 -2
  86. package/src/components/Checkbox/Checkbox.tsx +11 -6
  87. package/src/components/Checkbox/CheckboxGroup.stories.tsx +68 -0
  88. package/src/components/Checkbox/CheckboxGroup.tsx +7 -12
  89. package/src/components/ConfirmationMessage/ConfirmationMessage.stories.tsx +38 -0
  90. package/src/components/ContentsNav/ContentsNav.stories.tsx +43 -0
  91. package/src/components/ContentsNav/ContentsNav.test.tsx +2 -2
  92. package/src/components/ContentsNav/ContentsNav.tsx +2 -2
  93. package/src/components/CookieBanner/CookieBanner.stories.tsx +33 -0
  94. package/src/components/DatePicker/DatePicker.stories.tsx +113 -0
  95. package/src/components/DatePicker/DatePicker.tsx +1 -1
  96. package/src/components/Details/Details.stories.tsx +36 -0
  97. package/src/components/ErrorMessage/ErrorMessage.stories.tsx +19 -0
  98. package/src/components/ErrorMessage/ErrorMessage.test.tsx +3 -15
  99. package/src/components/ErrorMessage/ErrorMessage.tsx +1 -3
  100. package/src/components/ErrorSummary/ErrorSummary.stories.tsx +38 -0
  101. package/src/components/FileDownload/FileDownload.stories.tsx +75 -0
  102. package/src/components/FileDownload/FileDownload.test.tsx +1 -1
  103. package/src/components/FileDownload/FileDownload.tsx +2 -2
  104. package/src/components/HideThisPage/HideThisPage.stories.tsx +20 -0
  105. package/src/components/InsetText/InsetText.stories.tsx +21 -0
  106. package/src/components/NotificationBanner/NotificationBanner.stories.tsx +57 -0
  107. package/src/components/NotificationBanner/NotificationBanner.test.tsx +1 -1
  108. package/src/components/NotificationBanner/NotificationBanner.tsx +4 -4
  109. package/src/components/NotificationPanel/NotificationPanel.stories.tsx +32 -0
  110. package/src/components/PageHeader/PageHeader.stories.tsx +60 -0
  111. package/src/components/PageMetadata/PageMetadata.stories.tsx +58 -0
  112. package/src/components/PageMetadata/PageMetadata.test.tsx +2 -2
  113. package/src/components/PageMetadata/PageMetadata.tsx +4 -4
  114. package/src/components/Pagination/Pagination.stories.tsx +69 -0
  115. package/src/components/Pagination/Pagination.test.tsx +1 -1
  116. package/src/components/Pagination/Pagination.tsx +4 -4
  117. package/src/components/PhaseBanner/PhaseBanner.stories.tsx +38 -0
  118. package/src/components/Question/Question.stories.tsx +78 -0
  119. package/src/components/Question/Question.tsx +1 -1
  120. package/src/components/RadioButton/RadioButton.stories.tsx +67 -0
  121. package/src/components/RadioButton/RadioButton.test.tsx +2 -1
  122. package/src/components/RadioButton/RadioButton.tsx +9 -3
  123. package/src/components/RadioButton/RadioGroup.stories.tsx +77 -0
  124. package/src/components/RadioButton/RadioGroup.test.tsx +2 -2
  125. package/src/components/RadioButton/RadioGroup.tsx +8 -15
  126. package/src/components/Select/Select.stories.tsx +76 -0
  127. package/src/components/Select/Select.tsx +1 -1
  128. package/src/components/SequentialNavigation/SequentialNavigation.stories.tsx +31 -0
  129. package/src/components/SideNavigation/SideNavigation.stories.tsx +92 -0
  130. package/src/components/SideNavigation/SideNavigation.test.tsx +2 -2
  131. package/src/components/SideNavigation/SideNavigation.tsx +2 -2
  132. package/src/components/SiteFooter/SiteFooter.stories.tsx +65 -0
  133. package/src/components/SiteHeader/SiteHeader.stories.tsx +92 -0
  134. package/src/components/SiteHeader/SiteHeader.tsx +2 -7
  135. package/src/components/SiteNavigation/SiteNavigation.stories.tsx +45 -0
  136. package/src/components/SiteNavigation/SiteNavigation.test.tsx +1 -1
  137. package/src/components/SiteNavigation/SiteNavigation.tsx +2 -2
  138. package/src/components/SiteSearch/SiteSearch.stories.tsx +81 -0
  139. package/src/components/SiteSearch/SiteSearch.tsx +1 -1
  140. package/src/components/SkipLinks/SkipLinks.stories.tsx +57 -0
  141. package/src/components/SkipLinks/SkipLinks.tsx +1 -1
  142. package/src/components/SummaryCard/SummaryCard.stories.tsx +46 -0
  143. package/src/components/SummaryList/SummaryList.stories.tsx +75 -0
  144. package/src/components/SummaryList/SummaryList.test.tsx +1 -1
  145. package/src/components/SummaryList/SummaryList.tsx +3 -3
  146. package/src/components/Table/Table.stories.tsx +96 -0
  147. package/src/components/Tabs/Tabs.stories.tsx +90 -0
  148. package/src/components/Tabs/Tabs.test.tsx +6 -8
  149. package/src/components/Tabs/Tabs.tsx +8 -9
  150. package/src/components/Tag/Tag.stories.tsx +25 -0
  151. package/src/components/TaskList/TaskList.stories.tsx +129 -0
  152. package/src/components/TextInput/TextInput.stories.tsx +123 -0
  153. package/src/components/TextInput/TextInput.test.tsx +2 -2
  154. package/src/components/TextInput/TextInput.tsx +5 -5
  155. package/src/components/Textarea/Textarea.stories.tsx +71 -0
  156. package/src/components/Textarea/Textarea.tsx +1 -1
  157. package/src/components/WarningText/WarningText.stories.tsx +21 -0
  158. package/src/hooks/useTracking.test.tsx +64 -0
  159. package/src/hooks/useTracking.ts +19 -0
  160. package/src/utils/context.ts +3 -0
  161. package/static/data/autocomplete-dummy-data.json +2361 -0
  162. package/static/images/highland-cow.jpg +0 -0
  163. package/static/images/scottish-government--min.svg +11 -0
  164. package/static/images/scottish-government.svg +6 -0
  165. package/tsconfig.json +3 -2
  166. package/vite.config.ts +45 -11
  167. package/vitest-setup.ts +1 -0
  168. package/vitest.shims.d.ts +1 -0
  169. package/src/utils/slugify.ts +0 -13
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ // @ts-ignore
5
+ import coo from '../../../static/images/highland-cow.jpg';
6
+ import AspectBox from './AspectBox';
7
+
8
+ const meta = {
9
+ title: 'Components/Aspect Box',
10
+ component: AspectBox,
11
+ argTypes: {
12
+ ratio: {
13
+ control: { type: 'radio' },
14
+ description: 'Aspect ratio to use',
15
+ options: ['1:1', '4:3', '16:9', '21:9'],
16
+ type: 'string'
17
+ },
18
+ children: argTypes.children()
19
+ },
20
+ decorators: [
21
+ (Story) => (
22
+ <div style={{ maxWidth: '32em' }}>
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ],
27
+ args: {children: <img
28
+ alt="A highland cow nuzzling its calf"
29
+ src={coo}
30
+ />}
31
+ } satisfies Meta<typeof AspectBox>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {
37
+ args: {
38
+ ratio: undefined
39
+ }
40
+ };
41
+
42
+ export const Square: Story = {
43
+ args: {
44
+ ratio: '1:1'
45
+ }
46
+ };
47
+
48
+ export const Aspect4To3: Story = {
49
+ args: {
50
+ ratio: '4:3'
51
+ }
52
+ };
53
+
54
+ export const Aspect16To9: Story = {
55
+ args: {
56
+ ratio: '16:9'
57
+ }
58
+ };
59
+
60
+ export const Aspect21To9: Story = {
61
+ args: {
62
+ ratio: '21:9'
63
+ }
64
+ };
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+
3
+ import BackToTop from './BackToTop';
4
+
5
+ const meta = {
6
+ title: 'Components/Back to top',
7
+ component: BackToTop,
8
+ argTypes: {
9
+ href: {
10
+ control: { type: 'text' },
11
+ description: 'URL fragment to scroll to',
12
+ table: {
13
+ type: {
14
+ summary: 'string'
15
+ }
16
+ }
17
+ }
18
+ },
19
+ decorators: [
20
+ (Story) => (
21
+ <div>
22
+ <p>Scroll down to see 👇</p>
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ]
27
+ } satisfies Meta<typeof BackToTop>;
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ export const Default: Story = {
33
+ args: {
34
+ href: undefined,
35
+ }
36
+ };
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import Breadcrumbs from './Breadcrumbs';
5
+
6
+ const meta = {
7
+ title: 'Components/Breadcrumbs',
8
+ component: Breadcrumbs,
9
+ argTypes: {
10
+ linkComponent: argTypes.linkComponent(),
11
+ children: argTypes.children()
12
+ },
13
+ args: {
14
+ children: <>
15
+ <Breadcrumbs.Item href="#home">
16
+ Home
17
+ </Breadcrumbs.Item>
18
+ <Breadcrumbs.Item href="#category">
19
+ Category
20
+ </Breadcrumbs.Item>
21
+ <Breadcrumbs.Item>
22
+ Page
23
+ </Breadcrumbs.Item>
24
+ </>
25
+ }
26
+ } satisfies Meta<typeof Breadcrumbs>;
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ export const Default: Story = {
32
+
33
+ };
34
+
35
+ export const HideLastItem: Story = {
36
+ args: {
37
+ children: <>
38
+ <Breadcrumbs.Item href="#home">
39
+ Home
40
+ </Breadcrumbs.Item>
41
+ <Breadcrumbs.Item href="#category">
42
+ Category
43
+ </Breadcrumbs.Item>
44
+ <Breadcrumbs.Item isHidden>
45
+ Page
46
+ </Breadcrumbs.Item>
47
+ </>
48
+ }
49
+ };
@@ -16,7 +16,6 @@ test('breadcrumbs render correctly', () => {
16
16
 
17
17
  const nav = screen.getByRole('navigation');
18
18
  const list = within(nav).getByRole('list');
19
- const listItems = within(list).getAllByRole('listitem');
20
19
 
21
20
  // check nav
22
21
  expect(nav).toHaveAttribute('aria-label', 'Breadcrumb');
@@ -32,7 +32,7 @@ const BreadcrumbItem = ({
32
32
  const Breadcrumbs = ({
33
33
  children,
34
34
  ...props
35
- }: SGDS.Component.Breadcrumbs & { Item: SGDS.Component.Breadcrumbs.Item }) => {
35
+ }: SGDS.Component.Breadcrumbs) => {
36
36
  return (
37
37
  <nav
38
38
  aria-label="Breadcrumb"
@@ -0,0 +1,194 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import Button from './Button';
3
+
4
+ const meta = {
5
+ title: 'Components/Button',
6
+ component: Button,
7
+ parameters: {
8
+ controls: { sort: 'alpha' }
9
+ },
10
+ argTypes: {
11
+ buttonStyle: {
12
+ options: ['primary', 'secondary'],
13
+ control: { type: 'radio' },
14
+ type: 'string'
15
+ },
16
+ hasLinkStyle: {
17
+ description: 'Make the button look like a link',
18
+ control: 'boolean',
19
+ table: {
20
+ type: {
21
+ summary: 'boolean'
22
+ }
23
+ }
24
+ },
25
+ href: {
26
+ control: { type: 'text' },
27
+ description: 'Href attribute, changes element to <code>&lt;a&gt;</code> if set',
28
+ table: {
29
+ type: {
30
+ summary: 'string'
31
+ }
32
+ }
33
+ },
34
+ icon: {
35
+ description: 'Name of the icon component to use',
36
+ options: ['ArrowUpward', 'CalendarToday', 'Cancel', 'CheckCircle', 'ChevronLeft', 'ChevronRight', 'Close', 'Description', 'DoubleChevronLeft', 'DoubleChevronRight' ,'Error', 'ExpandLess', 'ExpandMore', 'List', 'PriorityHigh', 'Search'],
37
+ control: { type: 'select' },
38
+ type: 'string'
39
+ },
40
+ isIconLeft: {
41
+ description: 'Show icon on left of button',
42
+ control: 'boolean',
43
+ table: {
44
+ type: {
45
+ summary: 'boolean'
46
+ }
47
+ }
48
+ },
49
+ isIconOnly: {
50
+ description: 'Show only the icon',
51
+ control: 'boolean',
52
+ table: {
53
+ type: {
54
+ summary: 'boolean'
55
+ }
56
+ }
57
+ },
58
+ isSmall: {
59
+ control: 'boolean',
60
+ table: {
61
+ type: {
62
+ summary: 'boolean'
63
+ }
64
+ }
65
+ },
66
+ type: {
67
+ options: ['submit', 'reset', 'button'],
68
+ control: { type: 'radio' },
69
+ type: 'string'
70
+ },
71
+ width: {
72
+ options: ['fluid', 'fixed', 'max'],
73
+ control: { type: 'radio' },
74
+ type: 'string'
75
+ }
76
+ },
77
+ args: {
78
+ children: 'Button text'
79
+ }
80
+ } satisfies Meta<typeof Button>;
81
+
82
+ export default meta;
83
+ type Story = StoryObj<typeof meta>;
84
+
85
+ export const Default: Story = {
86
+ };
87
+
88
+ export const Secondary: Story = {
89
+ args: {
90
+ buttonStyle: 'secondary'
91
+ }
92
+ };
93
+
94
+ export const SmallButton: Story = {
95
+ args: {
96
+ isSmall: true
97
+ }
98
+ }
99
+
100
+ export const FluidWidth: Story = {
101
+ args: {
102
+ width: 'fluid'
103
+ }
104
+ }
105
+
106
+ export const FixedWidth: Story = {
107
+ args: {
108
+ width: 'fixed'
109
+ }
110
+ }
111
+
112
+ export const MaxWidth: Story = {
113
+ args: {
114
+ width: 'max'
115
+ }
116
+ }
117
+
118
+ export const WithIcon: Story = {
119
+ args: {
120
+ icon: 'Search'
121
+ },
122
+ parameters: {
123
+ controls: {
124
+ exclude: [
125
+ 'isIconOnly',
126
+ 'hasLinkStyle'
127
+ ]
128
+ }
129
+ }
130
+ }
131
+
132
+ export const IconOnLeft: Story = {
133
+ args: {
134
+ icon: 'Search',
135
+ isIconLeft: true
136
+ },
137
+ parameters: {
138
+ controls: {
139
+ exclude: [
140
+ 'isIconOnly',
141
+ 'hasLinkStyle'
142
+ ]
143
+ }
144
+ }
145
+ }
146
+
147
+ export const IconOnly: Story = {
148
+ args: {
149
+ icon: 'Search',
150
+ isIconOnly: true
151
+ },
152
+ parameters: {
153
+ controls: {
154
+ exclude: [
155
+ 'isIconLeft',
156
+ 'hasLinkStyle',
157
+ 'width'
158
+ ]
159
+ }
160
+ }
161
+ }
162
+
163
+ export const LinkStyledAsButton: Story = {
164
+ args: {
165
+ href: '#'
166
+ },
167
+ parameters: {
168
+ controls: {
169
+ exclude: [
170
+ 'hasLinkStyle',
171
+ 'type'
172
+ ]
173
+ }
174
+ }
175
+ }
176
+
177
+ export const ButtonStyledAsLink: Story = {
178
+ args: {
179
+ hasLinkStyle: true
180
+ },
181
+ parameters: {
182
+ controls: {
183
+ exclude: [
184
+ 'buttonStyle',
185
+ 'icon',
186
+ 'isIconLeft',
187
+ 'isIconOnly',
188
+ 'isSmall',
189
+ 'type',
190
+ 'width'
191
+ ]
192
+ }
193
+ }
194
+ }
@@ -52,7 +52,7 @@ test('max-width button', () => {
52
52
 
53
53
  test('small button', () => {
54
54
  render(
55
- <Button small>Button text</Button>
55
+ <Button isSmall>Button text</Button>
56
56
  );
57
57
 
58
58
  const button = screen.getByRole('button');
@@ -73,7 +73,7 @@ test('button with icon', () => {
73
73
 
74
74
  test('button with icon (left)', () => {
75
75
  render(
76
- <Button icon="ChevronLeft" iconLeft>Button text</Button>
76
+ <Button icon="ChevronLeft" isIconLeft>Button text</Button>
77
77
  );
78
78
 
79
79
  const button = screen.getByRole('button');
@@ -83,7 +83,7 @@ test('button with icon (left)', () => {
83
83
 
84
84
  test('button only icon', () => {
85
85
  render(
86
- <Button icon="Search" iconOnly>Button text</Button>
86
+ <Button icon="Search" isIconOnly>Button text</Button>
87
87
  );
88
88
 
89
89
  const button = screen.getByRole('button');
@@ -106,7 +106,7 @@ test('link styled as button', () => {
106
106
 
107
107
  test('button styled as link', () => {
108
108
  render(
109
- <Button styleAsLink>Button text</Button>
109
+ <Button hasLinkStyle>Button text</Button>
110
110
  );
111
111
 
112
112
  const button = screen.getByRole('button');
@@ -7,11 +7,11 @@ const Button = ({
7
7
  children,
8
8
  className,
9
9
  icon,
10
- iconLeft,
11
- iconOnly = false,
10
+ hasLinkStyle,
12
11
  href,
13
- small,
14
- styleAsLink,
12
+ isIconLeft,
13
+ isIconOnly = false,
14
+ isSmall,
15
15
  type = 'button',
16
16
  width,
17
17
  ...props
@@ -26,19 +26,19 @@ const Button = ({
26
26
  <WrapperTag
27
27
  tagName={tagName}
28
28
  className={[
29
- !styleAsLink ? 'ds_button' : 'ds_link',
29
+ !hasLinkStyle ? 'ds_button' : 'ds_link',
30
30
  width && `ds_button--${width}`,
31
31
  buttonStyle && `ds_button--${buttonStyle}`,
32
- small && 'ds_button--small',
33
- (icon && !iconOnly) ? 'ds_button--has-icon' : undefined,
34
- iconLeft && 'ds_button--has-icon--left',
32
+ isSmall && 'ds_button--small',
33
+ (icon && !isIconOnly) ? 'ds_button--has-icon' : undefined,
34
+ isIconLeft && 'ds_button--has-icon--left',
35
35
  className
36
36
  ].join(' ')}
37
37
  href={href}
38
38
  {...(tagName === 'button' ? { type: type } : {})}
39
39
  {...props}
40
40
  >
41
- {iconOnly ? <ScreenReaderText>{children}</ScreenReaderText> : children}
41
+ {isIconOnly ? <ScreenReaderText>{children}</ScreenReaderText> : children}
42
42
 
43
43
  {icon && <Icon icon={icon}/>}
44
44
  </WrapperTag>
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import CategoryItem from './CategoryItem';
5
+
6
+ const meta = {
7
+ title: 'Components/Category item',
8
+ component: CategoryItem,
9
+ argTypes: {
10
+ children: argTypes.children(),
11
+ headingLevel: argTypes.headingLevel(),
12
+ href: {
13
+ description: 'URL used in the title\'s link',
14
+ type: 'string'
15
+ },
16
+ linkComponent: argTypes.linkComponent(),
17
+ tagName: {
18
+ description: 'HTML tag name to use for the item',
19
+ type: 'string'
20
+ },
21
+ title: {
22
+ description: 'The title of the category item',
23
+ type: 'string'
24
+ }
25
+ },
26
+ args: {
27
+ title: 'Public transport, bus passes and discounts',
28
+ children: 'Find information about local travel by road, rail and water and National Entitlement Cards.',
29
+ href: '/driving-transport/local-travel'
30
+ }
31
+ } satisfies Meta<typeof CategoryItem>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {
37
+ };
38
+
39
+ export const NoSummary: Story = {
40
+ args: {
41
+ children: undefined
42
+ }
43
+ };
44
+
45
+ export const DifferentHeadingLevel: Story = {
46
+ args: {
47
+ headingLevel: 'h3'
48
+ }
49
+ };
50
+
51
+ export const DifferentContainerElement: Story = {
52
+ args: {
53
+ tagName: 'article'
54
+ }
55
+ };
@@ -0,0 +1,93 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import CategoryItem from './CategoryItem';
4
+
5
+ const ITEM_TITLE = 'Bananas';
6
+ const ITEM_HREF = '#foo';
7
+ const ITEM_CONTENT = 'A banana is an elongated, edible fruit—botanically a berry—produced by several kinds of large treelike herbaceous flowering plants in the genus Musa.';
8
+
9
+ test('category item renders correctly', () => {
10
+ render(
11
+ <CategoryItem title={ITEM_TITLE} href={ITEM_HREF} data-testid="category-item">
12
+ {ITEM_CONTENT}
13
+ </CategoryItem>
14
+ );
15
+
16
+ const categoryItem = screen.getByTestId('category-item');
17
+ const title = screen.getByRole('heading');
18
+ const link = screen.getByRole('link');
19
+ const content = categoryItem.querySelector('p');
20
+
21
+ expect(categoryItem).toHaveClass('ds_category-item');
22
+ expect(categoryItem.tagName).toEqual('DIV');
23
+
24
+ expect(title).toHaveClass('ds_category-item__title');
25
+ expect(title.tagName).toEqual('H2');
26
+ expect(title.textContent).toEqual(ITEM_TITLE);
27
+ expect(title.parentElement).toEqual(categoryItem);
28
+
29
+ expect(link).toHaveClass('ds_category-item__link');
30
+ expect(link).toHaveAttribute('href', ITEM_HREF);
31
+ expect(link.textContent).toEqual(ITEM_TITLE);
32
+ expect(link.parentElement).toEqual(title);
33
+
34
+ expect(content).toHaveClass('ds_category-item__summary');
35
+ expect(content?.textContent).toEqual(ITEM_CONTENT);
36
+ expect(content?.parentElement).toEqual(categoryItem);
37
+ });
38
+
39
+ test('custom heading level', () => {
40
+ render(
41
+ <CategoryItem title={ITEM_TITLE} href={ITEM_HREF} headingLevel="h3" data-testid="category-item">
42
+ {ITEM_CONTENT}
43
+ </CategoryItem>
44
+ );
45
+
46
+ const title = screen.getByRole('heading');
47
+ expect(title.tagName).toEqual('H3');
48
+ });
49
+
50
+ test('custom element', () => {
51
+ render(
52
+ <CategoryItem title={ITEM_TITLE} href={ITEM_HREF} tagName="article" data-testid="category-item">
53
+ {ITEM_CONTENT}
54
+ </CategoryItem>
55
+ );
56
+
57
+ const categoryItem = screen.getByTestId('category-item');
58
+ expect(categoryItem.tagName).toEqual('ARTICLE');
59
+ });
60
+
61
+ test('custom link component', () => {
62
+ render(
63
+ <CategoryItem title={ITEM_TITLE} href={ITEM_HREF} tagName="article" data-testid="category-item" linkComponent={
64
+ ({ className, ...props }) => (
65
+ <span role="link" className={className} {...props}/>
66
+ )}>
67
+ {ITEM_CONTENT}
68
+ </CategoryItem>
69
+ );
70
+
71
+ const link = screen.getByRole('link');
72
+
73
+ expect(link?.tagName).toEqual('SPAN');
74
+ expect(link?.textContent).toEqual(ITEM_TITLE);
75
+ });
76
+
77
+ test('passing additional props', () => {
78
+ render(
79
+ <CategoryItem data-test="foo" data-testid="category-item"/>
80
+ );
81
+
82
+ const categoryItem = screen.getByTestId('category-item');
83
+ expect(categoryItem?.dataset.test).toEqual('foo');
84
+ });
85
+
86
+ test('passing additional CSS classes', () => {
87
+ render(
88
+ <CategoryItem className="foo" data-testid="category-item"/>
89
+ );
90
+
91
+ const categoryItem = screen.getByTestId('category-item');
92
+ expect(categoryItem).toHaveClass('foo');
93
+ });
@@ -0,0 +1,56 @@
1
+ import ConditionalWrapper from "../../common/ConditionalWrapper";
2
+ import WrapperTag from "../../common/WrapperTag";
3
+
4
+ const CategoryItem = ({
5
+ children,
6
+ className,
7
+ headingLevel = 'h2',
8
+ href,
9
+ linkComponent,
10
+ tagName = 'div',
11
+ title,
12
+ ...props
13
+ }: SGDS.Component.CategoryItem) => {
14
+ const LINK_CLASS = 'ds_category-item__link';
15
+
16
+ function getLinkElement(children: React.ReactNode) {
17
+ if (linkComponent) {
18
+ return linkComponent({ className: LINK_CLASS, href, children });
19
+ } else if (href) {
20
+ return <a href={href} className={LINK_CLASS}>{children}</a>;
21
+ }
22
+ }
23
+
24
+ return (
25
+ <WrapperTag
26
+ tagName={tagName}
27
+ className={[
28
+ 'ds_category-item',
29
+ className
30
+ ].join(' ')}
31
+ {...props}
32
+ >
33
+ <WrapperTag
34
+ className="ds_category-item__title"
35
+ tagName={headingLevel}
36
+ >
37
+ <ConditionalWrapper
38
+ condition={typeof href !== 'undefined'}
39
+ wrapper={(children: React.JSX.Element) => getLinkElement(children)}
40
+ >
41
+ {title}
42
+ </ConditionalWrapper>
43
+ </WrapperTag>
44
+
45
+ {children &&
46
+ <p className="ds_category-item__summary">
47
+ {children}
48
+ </p>
49
+ }
50
+ </WrapperTag>
51
+ );
52
+ };
53
+
54
+ CategoryItem.displayName = 'CategoryItem';
55
+
56
+ export default CategoryItem;