@scottish-government/designsystem-react 0.10.1 → 0.11.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 (109) hide show
  1. package/.storybook/manager.ts +1 -7
  2. package/.storybook/sgdsArgTypes.ts +19 -0
  3. package/@types/components/Accordion.d.ts +3 -2
  4. package/@types/components/ButtonGroup.d.ts +5 -0
  5. package/@types/components/RadioButton.d.ts +2 -2
  6. package/@types/components/SearchFacets.d.ts +18 -0
  7. package/@types/components/SearchFilters.d.ts +14 -0
  8. package/@types/components/SearchResult.d.ts +30 -0
  9. package/@types/components/SearchSort.d.ts +9 -0
  10. package/@types/components/SideNavigation.d.ts +1 -1
  11. package/CHANGELOG.md +31 -5
  12. package/README.md +3 -0
  13. package/dist/common/AbstractNotificationBanner.jsx +2 -5
  14. package/dist/components/Accordion/Accordion.jsx +8 -3
  15. package/dist/components/ButtonGroup/ButtonGroup.jsx +13 -0
  16. package/dist/components/RadioButton/RadioGroup.jsx +1 -1
  17. package/dist/components/SearchFacets/SearchFacets.jsx +101 -0
  18. package/dist/components/SearchFilters/SearchFilters.jsx +63 -0
  19. package/dist/components/SearchResult/SearchResult.jsx +93 -0
  20. package/dist/components/SearchSort/SearchSort.jsx +28 -0
  21. package/dist/components/SideNavigation/SideNavigation.jsx +2 -2
  22. package/dist/components/TaskList/TaskList.jsx +1 -0
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +3 -3
  25. package/src/common/AbstractNotificationBanner.tsx +2 -6
  26. package/src/components/Accordion/Accordion.Item.stories.tsx +63 -0
  27. package/src/components/Accordion/Accordion.stories.tsx +4 -54
  28. package/src/components/Accordion/Accordion.test.tsx +48 -14
  29. package/src/components/Accordion/Accordion.tsx +12 -1
  30. package/src/components/AspectBox/AspectBox.stories.tsx +1 -1
  31. package/src/components/BackToTop/BackToTop.stories.tsx +1 -1
  32. package/src/components/Breadcrumbs/Breadcrumbs.Item.stories.tsx +42 -0
  33. package/src/components/Breadcrumbs/Breadcrumbs.stories.tsx +0 -1
  34. package/src/components/Button/Button.stories.tsx +1 -1
  35. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +41 -0
  36. package/src/components/ButtonGroup/ButtonGroup.test.tsx +45 -0
  37. package/src/components/ButtonGroup/ButtonGroup.tsx +20 -0
  38. package/src/components/CategoryItem/CategoryItem.stories.tsx +1 -1
  39. package/src/components/CategoryList/CategoryList.stories.tsx +1 -1
  40. package/src/components/Checkbox/Checkbox.stories.tsx +1 -1
  41. package/src/components/Checkbox/CheckboxGroup.stories.tsx +1 -1
  42. package/src/components/ConfirmationMessage/ConfirmationMessage.stories.tsx +1 -1
  43. package/src/components/ContentsNav/ContentsNav.Item.stories.tsx +53 -0
  44. package/src/components/ContentsNav/ContentsNav.stories.tsx +1 -2
  45. package/src/components/CookieBanner/CookieBanner.Buttons.stories.tsx +27 -0
  46. package/src/components/CookieBanner/CookieBanner.stories.tsx +2 -2
  47. package/src/components/DatePicker/DatePicker.stories.tsx +1 -1
  48. package/src/components/ErrorMessage/ErrorMessage.stories.tsx +1 -1
  49. package/src/components/ErrorSummary/ErrorSummary.Item.stories.tsx +35 -0
  50. package/src/components/ErrorSummary/ErrorSummary.stories.tsx +1 -2
  51. package/src/components/FileDownload/FileDownload.stories.tsx +1 -1
  52. package/src/components/HideThisPage/HideThisPage.stories.tsx +1 -1
  53. package/src/components/InsetText/InsetText.stories.tsx +1 -1
  54. package/src/components/NotificationBanner/NotificationBanner.stories.tsx +1 -1
  55. package/src/components/NotificationPanel/NotificationPanel.stories.tsx +1 -1
  56. package/src/components/PageHeader/PageHeader.stories.tsx +1 -1
  57. package/src/components/PageMetadata/PageMetadata.Item.stories.tsx +40 -0
  58. package/src/components/PageMetadata/PageMetadata.stories.tsx +10 -5
  59. package/src/components/PhaseBanner/PhaseBanner.stories.tsx +1 -1
  60. package/src/components/RadioButton/RadioButton.stories.tsx +1 -1
  61. package/src/components/RadioButton/RadioGroup.stories.tsx +1 -1
  62. package/src/components/RadioButton/RadioGroup.tsx +2 -2
  63. package/src/components/SearchFacets/SearchFacets.Group.stories.tsx +56 -0
  64. package/src/components/SearchFacets/SearchFacets.Item.stories.tsx +53 -0
  65. package/src/components/SearchFacets/SearchFacets.stories.tsx +38 -0
  66. package/src/components/SearchFacets/SearchFacets.test.tsx +214 -0
  67. package/src/components/SearchFacets/SearchFacets.tsx +99 -0
  68. package/src/components/SearchFilters/SearchFilters.Panel.stories.tsx +201 -0
  69. package/src/components/SearchFilters/SearchFilters.stories.tsx +137 -0
  70. package/src/components/SearchFilters/SearchFilters.test.tsx +161 -0
  71. package/src/components/SearchFilters/SearchFilters.tsx +89 -0
  72. package/src/components/SearchResult/SearchResult.stories.tsx +111 -0
  73. package/src/components/SearchResult/SearchResult.test.tsx +215 -0
  74. package/src/components/SearchResult/SearchResult.tsx +137 -0
  75. package/src/components/SearchSort/SearchSort.stories.tsx +32 -0
  76. package/src/components/SearchSort/SearchSort.test.tsx +129 -0
  77. package/src/components/SearchSort/SearchSort.tsx +45 -0
  78. package/src/components/SequentialNavigation/SequentialNavigation.Next.stories.tsx +35 -0
  79. package/src/components/SequentialNavigation/SequentialNavigation.Previous.stories.tsx +35 -0
  80. package/src/components/SequentialNavigation/SequentialNavigation.stories.tsx +1 -2
  81. package/src/components/SequentialNavigation/SequentialNavigation.test.tsx +15 -0
  82. package/src/components/SideNavigation/SideNavigation.Item.stories.tsx +45 -0
  83. package/src/components/SideNavigation/SideNavigation.List.stories.tsx +53 -0
  84. package/src/components/SideNavigation/SideNavigation.stories.tsx +1 -2
  85. package/src/components/SideNavigation/SideNavigation.tsx +2 -1
  86. package/src/components/SiteFooter/SiteFooter.License.stories.tsx +46 -0
  87. package/src/components/SiteFooter/SiteFooter.Link.stories.tsx +34 -0
  88. package/src/components/SiteFooter/SiteFooter.Links.stories.tsx +32 -0
  89. package/src/components/SiteFooter/SiteFooter.Org.stories.tsx +45 -0
  90. package/src/components/SiteFooter/SiteFooter.stories.tsx +1 -1
  91. package/src/components/SiteHeader/SiteHeader.Brand.stories.tsx +45 -0
  92. package/src/components/SiteHeader/SiteHeader.stories.tsx +1 -1
  93. package/src/components/SiteNavigation/SiteNavigation.Item.stories.tsx +40 -0
  94. package/src/components/SiteNavigation/SiteNavigation.stories.tsx +1 -2
  95. package/src/components/SkipLinks/SkipLinks.Item.stories.tsx +37 -0
  96. package/src/components/SkipLinks/SkipLinks.stories.tsx +1 -1
  97. package/src/components/SummaryCard/SummaryCard.Action.stories.tsx +39 -0
  98. package/src/components/SummaryCard/SummaryCard.stories.tsx +8 -1
  99. package/src/components/SummaryList/SummaryList.Action.stories.tsx +32 -0
  100. package/src/components/SummaryList/SummaryList.Item.stories.tsx +50 -0
  101. package/src/components/SummaryList/SummaryList.Value.stories.tsx +30 -0
  102. package/src/components/SummaryList/SummaryList.stories.tsx +1 -1
  103. package/src/components/Tabs/Tabs.Item.stories.tsx +37 -0
  104. package/src/components/Tag/Tag.stories.tsx +2 -6
  105. package/src/components/TaskList/TaskList.Group.stories.tsx +110 -0
  106. package/src/components/TaskList/TaskList.Item.stories.tsx +77 -0
  107. package/src/components/TaskList/TaskList.stories.tsx +1 -1
  108. package/src/components/TaskList/TaskList.test.tsx +21 -1
  109. package/src/components/TaskList/TaskList.tsx +1 -0
@@ -0,0 +1,56 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import Facets from './SearchFacets';
5
+ import { join } from 'path';
6
+
7
+ const meta = {
8
+ title: 'Components/Search results/Facets/Group',
9
+ component: Facets.Group,
10
+ decorators: [
11
+ (Story) => (
12
+ <div className="ds_facets">
13
+ <Story />
14
+ </div>
15
+ )
16
+ ],
17
+ argTypes: {
18
+ children: argTypes.children(),
19
+ joinContent: {
20
+ description: 'Content to display between the facet items when there are multiple items in the group',
21
+ type: 'string'
22
+ },
23
+ title: {
24
+ description: 'Title of the facet group',
25
+ type: {
26
+ name: 'string',
27
+ required: true
28
+ }
29
+ }
30
+ },
31
+ args: {
32
+ title: 'Content type'
33
+ }
34
+ } satisfies Meta<typeof Facets.Group>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ export const Default: Story = {
40
+ render: (args: any) => (
41
+ <Facets.Group {...args}>
42
+ <Facets.Item title="Advice and guidance" />
43
+ <Facets.Item title="Factsheet"/>
44
+ <Facets.Item title="Statistics" />
45
+ </Facets.Group>
46
+ )
47
+ };
48
+
49
+ export const CustomJoinContent: Story = {
50
+ render: (args: any) => (
51
+ <Facets.Group joinContent="and" title="Updated between">
52
+ <Facets.Item accessibleName="Updated after 01/10/2025" title="01/10/2025" />
53
+ <Facets.Item accessibleName="Updated before 31/10/2025" title="31/10/2025"/>
54
+ </Facets.Group>
55
+ )
56
+ };
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+
3
+ import Facets from './SearchFacets';
4
+
5
+ const meta = {
6
+ title: 'Components/Search results/Facets/Item',
7
+ component: Facets.Item,
8
+ decorators: [
9
+ (Story) => (
10
+ <div className="ds_facets">
11
+ <Story />
12
+ </div>
13
+ )
14
+ ],
15
+ argTypes: {
16
+ accessibleName: {
17
+ description: 'Accessible name for the facet button. If not provided, the title will be used. This content becomes part of the facet button\'s aria-label attribute.',
18
+ type: 'string'
19
+ },
20
+ joinContent: {
21
+ description: 'Content to display before the facet button when there are multiple facets in a group. This should be provided to the parent Group instead.',
22
+ type: 'string'
23
+ },
24
+ onClick: {
25
+ description: 'Callback for when the facet button is clicked',
26
+ control: false
27
+ },
28
+ title: {
29
+ description: 'Text content of the facet button.',
30
+ type: {
31
+ name: 'string',
32
+ required: true
33
+ }
34
+ },
35
+ },
36
+ args: {
37
+ title: 'Advice and guidance'
38
+ }
39
+ } satisfies Meta<typeof Facets.Item>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ export const Default: Story = {
45
+
46
+ };
47
+
48
+ export const CustomAccessibleText: Story = {
49
+ args: {
50
+ accessibleName: 'Updated before 31st October 2025',
51
+ title: '31/10/2025'
52
+ }
53
+ }
@@ -0,0 +1,38 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import Facets from './SearchFacets';
5
+
6
+ const meta = {
7
+ title: 'Components/Search results/Facets',
8
+ component: Facets,
9
+ argTypes: {
10
+ children: argTypes.children()
11
+ },
12
+ args: {
13
+ title: 'Application incomplete'
14
+ }
15
+ } satisfies Meta<typeof Facets>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ render: (args: any) => (
22
+ <Facets {...args}>
23
+ <Facets.Group title="Content type">
24
+ <Facets.Item title="Advice and guidance" />
25
+ <Facets.Item title="Factsheet"/>
26
+ <Facets.Item title="Statistics" />
27
+ </Facets.Group>
28
+ <Facets.Group title="Topic">
29
+ <Facets.Item title="Children and families"/>
30
+ <Facets.Item title="Education"/>
31
+ </Facets.Group>
32
+ <Facets.Group title="Updated between" joinContent="and">
33
+ <Facets.Item accessibleName="Updated after 20/11/2023" title="20/11/2023"/>
34
+ <Facets.Item accessibleName="Updated before 02/04/2024" title="02/04/2024"/>
35
+ </Facets.Group>
36
+ </Facets>
37
+ )
38
+ };
@@ -0,0 +1,214 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import Facets from './SearchFacets';
4
+
5
+ test('search facets boilerplate renders correctly', () => {
6
+ render(
7
+ <Facets data-testid="searchfacets"/>
8
+ );
9
+
10
+ const facets = screen.getByTestId('searchfacets');
11
+
12
+ expect(facets).toBeInTheDocument();
13
+ expect(facets).toHaveClass('ds_facets');
14
+ expect(facets.tagName).toEqual('DIV');
15
+
16
+ const status = facets.querySelector('p');
17
+ expect(status).toBeInTheDocument();
18
+ expect(status).toHaveClass('visually-hidden');
19
+ expect(status?.textContent).toEqual('There are 0 search filters applied');
20
+ expect(status?.parentElement).toEqual(facets);
21
+
22
+ const list = facets.querySelector('dl');
23
+ expect(list).toBeInTheDocument();
24
+ expect(list).toHaveClass('ds_facets__list');
25
+ expect(list?.parentElement).toEqual(facets);
26
+ expect(list?.previousElementSibling).toEqual(status);
27
+
28
+ // clear button is not present if there are no facets
29
+ const clearButton = within(facets).queryByRole('button', { name: /clear all filters/i });
30
+ expect(clearButton).not.toBeInTheDocument();
31
+ });
32
+
33
+ test('search facets count with one facet', () => {
34
+ render(
35
+ <Facets data-testid="searchfacets">
36
+ <Facets.Item title="Facet 1" />
37
+ </Facets>
38
+ );
39
+
40
+ const facets = screen.getByTestId('searchfacets');
41
+ const status = within(facets).getByText('There is 1 search filter applied');
42
+ expect(status).toBeInTheDocument();
43
+
44
+ const clearButton = within(facets).getByRole('button', { name: /clear all filters/i });
45
+ expect(clearButton).toBeInTheDocument();
46
+ });
47
+
48
+ test('search facets count with multiple facets', () => {
49
+ render(
50
+ <Facets data-testid="searchfacets">
51
+ <Facets.Item title="Facet 1" />
52
+ <Facets.Item title="Facet 2" />
53
+ </Facets>
54
+ );
55
+
56
+ const facets = screen.getByTestId('searchfacets');
57
+ const facetItems = facets.querySelectorAll('.ds_facet');
58
+ const status = within(facets).getByText(`There are ${facetItems.length} search filters applied`);
59
+ expect(status).toBeInTheDocument();
60
+
61
+ const clearButton = within(facets).getByRole('button', { name: /clear all filters/i });
62
+ expect(clearButton).toBeInTheDocument();
63
+ });
64
+
65
+ test('search filters count with facet groups', () => {
66
+ render(
67
+ <Facets data-testid="searchfacets">
68
+ <Facets.Group title="Group 1">
69
+ <Facets.Item title="Facet 1" />
70
+ <Facets.Item title="Facet 2" />
71
+ </Facets.Group>
72
+ <Facets.Group title="Group 2">
73
+ <Facets.Item title="Facet 3" />
74
+ </Facets.Group>
75
+ </Facets>
76
+ );
77
+
78
+ const facets = screen.getByTestId('searchfacets');
79
+ const facetItems = facets.querySelectorAll('.ds_facet');
80
+ const status = within(facets).getByText(`There are ${facetItems.length} search filters applied`);
81
+ expect(status).toBeInTheDocument();
82
+
83
+ const clearButton = within(facets).getByRole('button', { name: /clear all filters/i });
84
+ expect(clearButton).toBeInTheDocument();
85
+ });
86
+
87
+ test('search facet renders correctly', () => {
88
+ const FACET_TITLE = 'Facet 1';
89
+
90
+ render(
91
+ <Facets.Item title={FACET_TITLE} data-testid="searchfacet" />
92
+ );
93
+
94
+ const facetWrapper = screen.getByTestId('searchfacet');
95
+ expect(facetWrapper).toBeInTheDocument();
96
+ expect(facetWrapper).toHaveClass('ds_facet-wrapper');
97
+ expect(facetWrapper.tagName).toEqual('DD');
98
+
99
+ const facetButton = within(facetWrapper).getByRole('button');
100
+ expect(facetButton).toHaveClass('ds_facet__button');
101
+ expect(facetButton).toHaveAttribute('aria-label', `Remove '${FACET_TITLE}' filter`);
102
+
103
+ const facet = facetButton.parentElement;
104
+ expect(facet).toHaveClass('ds_facet');
105
+ expect(facet?.textContent).toEqual(FACET_TITLE);
106
+
107
+ const facetIcon = within(facetButton).getByRole('img', { hidden: true });
108
+ expect(facetIcon).toHaveClass('ds_facet__button-icon');
109
+ expect(facetIcon).toHaveAttribute('aria-hidden', 'true');
110
+ });
111
+
112
+ test('search facet with accessible name', () => {
113
+ const FACET_TITLE = 'Facet 1';
114
+ const ACCESSIBLE_NAME = 'Custom facet name';
115
+
116
+ render(
117
+ <Facets.Item title={FACET_TITLE} accessibleName={ACCESSIBLE_NAME} data-testid="searchfacet" />
118
+ );
119
+
120
+ const facetWrapper = screen.getByTestId('searchfacet');
121
+ const facetButton = within(facetWrapper).getByRole('button');
122
+ expect(facetButton).toHaveAttribute('aria-label', `Remove '${ACCESSIBLE_NAME}' filter`);
123
+
124
+ const facet = facetButton.parentElement;
125
+ expect(facet).toHaveClass('ds_facet');
126
+ expect(facet?.textContent).toEqual(FACET_TITLE);
127
+ });
128
+
129
+ test('facet button onClick works', async () => {
130
+ const FACET_TITLE = 'Facet 1';
131
+ const handleClick = vi.fn();
132
+
133
+ render(
134
+ <Facets.Item title={FACET_TITLE} onClick={handleClick} data-testid="searchfacet" />
135
+ );
136
+
137
+ const facetWrapper = screen.getByTestId('searchfacet');
138
+ const facetButton = within(facetWrapper).getByRole('button');
139
+
140
+ await facetButton.click();
141
+ expect(handleClick).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ test('facet group renders correctly', () => {
145
+ const GROUP_TITLE = 'Group 1';
146
+
147
+ render(
148
+ <Facets.Group title={GROUP_TITLE} data-testid="searchfacetgroup">
149
+ <Facets.Item title="Facet 1" />
150
+ <Facets.Item title="Facet 2" />
151
+ </Facets.Group>
152
+ );
153
+
154
+ const facetGroup = screen.getByTestId('searchfacetgroup');
155
+ expect(facetGroup).toBeInTheDocument();
156
+ expect(facetGroup).toHaveClass('ds_facet-group');
157
+ expect(facetGroup.tagName).toEqual('DIV');
158
+
159
+ const groupTitle = within(facetGroup).getByText(`${GROUP_TITLE}:`);
160
+ expect(groupTitle).toBeInTheDocument();
161
+ expect(groupTitle).toHaveClass('ds_facet__group-title');
162
+
163
+ const facetWrappers = facetGroup.querySelectorAll('.ds_facet-wrapper');
164
+ expect(facetWrappers.length).toEqual(2);
165
+
166
+ const firstFacet = facetWrappers[0];
167
+ expect(firstFacet?.textContent).toContain('Facet 1');
168
+ expect(firstFacet?.textContent).not.toContain('or');
169
+
170
+ const secondFacet = facetWrappers[1];
171
+ expect(secondFacet?.textContent).toContain('Facet 2');
172
+ expect(secondFacet?.textContent).toContain('or');
173
+ });
174
+
175
+ test('facet group with custom join content', () => {
176
+ const GROUP_TITLE = 'Group 1';
177
+ const JOIN_CONTENT = 'and';
178
+
179
+ render(
180
+ <Facets.Group title={GROUP_TITLE} joinContent={JOIN_CONTENT} data-testid="searchfacetgroup">
181
+ <Facets.Item title="Facet 1" />
182
+ <Facets.Item title="Facet 2" />
183
+ </Facets.Group>
184
+ );
185
+
186
+ const facetGroup = screen.getByTestId('searchfacetgroup');
187
+ const facetWrappers = facetGroup.querySelectorAll('.ds_facet-wrapper');
188
+
189
+ const firstFacet = facetWrappers[0];
190
+ expect(firstFacet?.textContent).toContain('Facet 1');
191
+ expect(firstFacet?.textContent).not.toContain(JOIN_CONTENT);
192
+
193
+ const secondFacet = facetWrappers[1];
194
+ expect(secondFacet?.textContent).toContain('Facet 2');
195
+ expect(secondFacet?.textContent).toContain(JOIN_CONTENT);
196
+ });
197
+
198
+ test('passing additional props', () => {
199
+ render(
200
+ <Facets data-test="foo" data-testid="searchfacets"/>
201
+ );
202
+
203
+ const searchFacets = screen.getByTestId('searchfacets');
204
+ expect(searchFacets?.dataset.test).toEqual('foo');
205
+ });
206
+
207
+ test('passing additional CSS classes', () => {
208
+ render(
209
+ <Facets className="foo" data-testid="searchfacets"/>
210
+ );
211
+
212
+ const searchFacets = screen.getByTestId('searchfacets');
213
+ expect(searchFacets).toHaveClass('foo');
214
+ });
@@ -0,0 +1,99 @@
1
+ import React, { Children } from 'react';
2
+ import Icon from "../../common/Icon";
3
+ import { Cancel } from '../../../src/images/icons';
4
+
5
+ const FacetsItem = ({
6
+ accessibleName,
7
+ joinContent,
8
+ onClick,
9
+ title,
10
+ ...props
11
+ }: SGDS.Component.SearchFacets.Item) => {
12
+ accessibleName = accessibleName ? accessibleName : title;
13
+
14
+ return (
15
+ <dd className="ds_facet-wrapper" {...props}>
16
+ {joinContent &&
17
+ <span aria-hidden="true">{joinContent}</span>
18
+ }
19
+ <span className="ds_facet">
20
+ {title}
21
+ <button type="button" onClick={onClick} aria-label={`Remove '${accessibleName}' filter`} className="ds_facet__button">
22
+ <Icon className="ds_facet__button-icon" aria-hidden="true" role="img" icon="Cancel"/>
23
+ </button>
24
+ </span>
25
+ </dd>
26
+ );
27
+ }
28
+
29
+ const FacetsGroup = ({
30
+ children,
31
+ joinContent = 'or',
32
+ title,
33
+ ...props
34
+ }: SGDS.Component.SearchFacets.Group) => {
35
+ return (
36
+ <div className="ds_facet-group" {...props}>
37
+ <dt className="ds_facet__group-title">
38
+ {title.trim()}:
39
+ </dt>
40
+ {
41
+ Children.map(children, (child, index) => {
42
+ const thisChild = child as React.ReactElement<SGDS.Component.SearchFacets.Item>
43
+ return React.cloneElement(thisChild, { joinContent: index > 0 ? joinContent : undefined, key: 'facet' + index });
44
+ })
45
+ }
46
+ </div>
47
+ )
48
+ }
49
+
50
+ const Facets = ({
51
+ children,
52
+ className,
53
+ ...props
54
+ }: SGDS.Component.SearchFacets) => {
55
+ let facetCount = 0;
56
+
57
+ function processChild(item: any) {
58
+ if (item.type.displayName === 'Facets.Item') {
59
+ facetCount = facetCount + 1;
60
+
61
+ } else if (item.type.displayName === 'Facets.Group') {
62
+ Children.forEach(item.props.children, child => {
63
+ processChild(child);
64
+ });
65
+ }
66
+ }
67
+
68
+ Children.forEach(children, child => {
69
+ processChild(child);
70
+ });
71
+
72
+ return (
73
+ <div className={[
74
+ "ds_facets",
75
+ className
76
+ ].join(' ')}
77
+ {...props}
78
+ >
79
+ <p className="visually-hidden">There {facetCount === 1 ? 'is' : 'are'} {facetCount} search {facetCount === 1 ? 'filter' : 'filters'} applied</p>
80
+ <dl className="ds_facets__list">
81
+ {children}
82
+ </dl>
83
+ {facetCount > 0 &&
84
+ <button className="ds_button ds_button--secondary ds_button--has-icon ds_facets__clear-button" type="button">
85
+ Clear all filters
86
+ <Cancel className="ds_facet__button-icon"/>
87
+ </button>
88
+ }
89
+ </div>
90
+ );
91
+ }
92
+
93
+ Facets.displayName = 'Facets';
94
+ FacetsItem.displayName = 'Facets.Item';
95
+ FacetsGroup.displayName = 'Facets.Group';
96
+ Facets.Item = FacetsItem;
97
+ Facets.Group = FacetsGroup;
98
+
99
+ export default Facets;
@@ -0,0 +1,201 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import Filters from './SearchFilters';
5
+ import DatePicker from '../DatePicker/DatePicker';
6
+ import Checkbox from '../Checkbox/Checkbox';
7
+
8
+ const meta = {
9
+ title: 'Components/Search results/Filters/Filter panel',
10
+ component: Filters.Panel,
11
+ decorators: [(Story) => (
12
+ <div className="ds_accordion ds_accordion--small ds_!_margin-top--0">
13
+ <Story />
14
+ </div>
15
+ )],
16
+ argTypes: {
17
+ children: argTypes.children(),
18
+ isScrollable: {
19
+ description: 'Puts internal scrollbars around the filter fields',
20
+ control: 'boolean',
21
+ },
22
+ legend: {
23
+ description: 'Content for the (visually hidden) legend element',
24
+ type: {
25
+ name: 'string',
26
+ required: true
27
+ }
28
+ },
29
+ heading: {
30
+ description: 'Heading of the filter panel item',
31
+ type: {
32
+ name: 'string',
33
+ required: true
34
+ }
35
+ },
36
+ activeFilterCount: {
37
+ description: 'Number of active filter fields in this panel',
38
+ type: {
39
+ name: 'number'
40
+ }
41
+ }
42
+ }
43
+ } satisfies Meta<typeof Filters.Panel>;
44
+
45
+ export default meta;
46
+ type Story = StoryObj<typeof meta>;
47
+
48
+ const CONTENT_TYPES = [
49
+ {
50
+ label: 'Advice and guidance',
51
+ value: 'advice-and-guidance'
52
+ },
53
+ {
54
+ label: 'Agreement',
55
+ value: 'agreement'
56
+ },
57
+ {
58
+ label: 'Consultation analysis',
59
+ value: 'consultation-analysis'
60
+ },
61
+ {
62
+ label: 'Consultation paper',
63
+ value: 'consultation-paper'
64
+ },
65
+ {
66
+ label: 'Corporate report',
67
+ value: 'corporate-report'
68
+ },
69
+ {
70
+ label: 'Correspondence',
71
+ value: 'correspondence'
72
+ },
73
+ {
74
+ label: 'FOI/EIR release',
75
+ value: 'foi-eir-release'
76
+ },
77
+ {
78
+ label: 'Factsheet',
79
+ value: 'factsheet'
80
+ },
81
+ {
82
+ label: 'Form',
83
+ value: 'form'
84
+ },
85
+ {
86
+ label: 'Impact assessment',
87
+ value: 'impact-assessment'
88
+ },
89
+ {
90
+ label: 'Independent report',
91
+ value: 'independent-report'
92
+ },
93
+ {
94
+ label: 'Map',
95
+ value: 'map'
96
+ },
97
+ {
98
+ label: 'Minutes',
99
+ value: 'minutes'
100
+ },
101
+ {
102
+ label: 'Progress report',
103
+ value: 'progress-report'
104
+ },
105
+ {
106
+ label: 'Regulation/directive/order',
107
+ value: 'regulation-directive-order'
108
+ },
109
+ {
110
+ label: 'Research and analysis',
111
+ value: 'research-and-analysis'
112
+ },
113
+ {
114
+ label: 'Speech/statement',
115
+ value: 'speech-statement'
116
+ },
117
+ {
118
+ label: 'Statistics',
119
+ value: 'statistics'
120
+ },
121
+ {
122
+ label: 'Strategy/plan',
123
+ value: 'strategy-plan'
124
+ },
125
+ {
126
+ label: 'Transparency data',
127
+ value: 'transparency-data'
128
+ }
129
+ ];
130
+
131
+ const CONTENT_TYPES_WITH_SELECTED = JSON.parse(JSON.stringify(CONTENT_TYPES));
132
+ CONTENT_TYPES_WITH_SELECTED[1].checked = true;
133
+ CONTENT_TYPES_WITH_SELECTED[4].checked = true;
134
+ CONTENT_TYPES_WITH_SELECTED[7].checked = true;
135
+
136
+ export const Default: Story = {
137
+ render: (args: any) => (
138
+ <Filters.Panel
139
+ heading="Filter by date"
140
+ legend="Filter by date"
141
+ >
142
+ <DatePicker
143
+ hintText="For example, 21/01/2022"
144
+ id="date-from"
145
+ label="Updated after"
146
+ />
147
+
148
+ <DatePicker
149
+ hintText="For example, 21/01/2022"
150
+ id="date-to"
151
+ label="Updated before"
152
+ />
153
+ </Filters.Panel>
154
+ )
155
+ };
156
+
157
+ export const Scrollable: Story = {
158
+ render: (args: any) => (
159
+ <Filters.Panel
160
+ heading="Content type"
161
+ isScrollable
162
+ legend="Select which publication types you would like to see"
163
+ {...args}
164
+ >
165
+ <Filters.CheckboxGroup>
166
+ {CONTENT_TYPES.map((type) => (
167
+ <Checkbox
168
+ key={type.value}
169
+ label={type.label}
170
+ value={type.value}
171
+ id={type.value}
172
+ />
173
+ ))}
174
+ </Filters.CheckboxGroup>
175
+ </Filters.Panel>
176
+ )
177
+ };
178
+
179
+ export const WithActiveFilterCount: Story = {
180
+ render: (args: any) => (
181
+ <Filters.Panel
182
+ activeFilterCount={CONTENT_TYPES_WITH_SELECTED.filter((item: any) => item.checked).length}
183
+ heading="Content type"
184
+ isScrollable
185
+ legend="Select which publication types you would like to see"
186
+ {...args}
187
+ >
188
+ <Filters.CheckboxGroup>
189
+ {CONTENT_TYPES_WITH_SELECTED.map((type: any) => (
190
+ <Checkbox
191
+ checked={type.checked || false}
192
+ key={type.value}
193
+ label={type.label}
194
+ value={type.value}
195
+ id={type.value}
196
+ />
197
+ ))}
198
+ </Filters.CheckboxGroup>
199
+ </Filters.Panel>
200
+ )
201
+ };