@tpzdsp/next-toolkit 1.2.9 → 1.3.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.
- package/package.json +7 -1
- package/src/components/Card/Card.stories.tsx +21 -0
- package/src/components/Card/Card.test.tsx +46 -7
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Paragraph/Paragraph.tsx +9 -5
- package/src/components/backToTop/BackToTop.stories.tsx +409 -0
- package/src/components/backToTop/BackToTop.test.tsx +57 -0
- package/src/components/backToTop/BackToTop.tsx +131 -0
- package/src/components/chip/Chip.stories.tsx +40 -0
- package/src/components/chip/Chip.test.tsx +31 -0
- package/src/components/chip/Chip.tsx +20 -0
- package/src/components/cookieBanner/CookieBanner.stories.tsx +258 -0
- package/src/components/cookieBanner/CookieBanner.test.tsx +68 -0
- package/src/components/cookieBanner/CookieBanner.tsx +73 -0
- package/src/components/form/Input.stories.tsx +435 -0
- package/src/components/form/Input.test.tsx +214 -0
- package/src/components/form/Input.tsx +24 -0
- package/src/components/form/TextArea.stories.tsx +465 -0
- package/src/components/form/TextArea.test.tsx +236 -0
- package/src/components/form/TextArea.tsx +24 -0
- package/src/components/googleAnalytics/GlobalVars.tsx +31 -0
- package/src/components/googleAnalytics/GoogleAnalytics.tsx +15 -0
- package/src/components/index.ts +13 -0
- package/src/components/skipLink/SkipLink.stories.tsx +346 -0
- package/src/components/skipLink/SkipLink.test.tsx +22 -0
- package/src/components/skipLink/SkipLink.tsx +19 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { LuArrowUp } from 'react-icons/lu';
|
|
6
|
+
|
|
7
|
+
export type BackToTopProps = {
|
|
8
|
+
/** Scroll threshold in pixels before button appears */
|
|
9
|
+
threshold?: number;
|
|
10
|
+
/** Position from bottom in pixels */
|
|
11
|
+
bottom?: number;
|
|
12
|
+
/** Position from left in pixels */
|
|
13
|
+
left?: number;
|
|
14
|
+
/** Custom className for styling */
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const BackToTop = ({
|
|
19
|
+
threshold = 600,
|
|
20
|
+
bottom = 16,
|
|
21
|
+
left = 8,
|
|
22
|
+
className = '',
|
|
23
|
+
}: BackToTopProps) => {
|
|
24
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
25
|
+
|
|
26
|
+
// Throttle scroll events for better performance
|
|
27
|
+
const throttle = useCallback((func: () => void, delay: number) => {
|
|
28
|
+
let timeoutId: number | null = null;
|
|
29
|
+
let lastExecTime = 0;
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
const currentTime = Date.now();
|
|
33
|
+
|
|
34
|
+
if (currentTime - lastExecTime > delay) {
|
|
35
|
+
func();
|
|
36
|
+
lastExecTime = currentTime;
|
|
37
|
+
} else {
|
|
38
|
+
if (timeoutId) {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
timeoutId = window.setTimeout(
|
|
43
|
+
() => {
|
|
44
|
+
func();
|
|
45
|
+
lastExecTime = Date.now();
|
|
46
|
+
},
|
|
47
|
+
delay - (currentTime - lastExecTime),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Show button when page is scrolled down
|
|
54
|
+
const toggleVisibility = useCallback(() => {
|
|
55
|
+
if (typeof window !== 'undefined') {
|
|
56
|
+
setIsVisible(window.pageYOffset > threshold);
|
|
57
|
+
}
|
|
58
|
+
}, [threshold]);
|
|
59
|
+
|
|
60
|
+
// Scroll to top smoothly
|
|
61
|
+
const scrollToTop = useCallback(() => {
|
|
62
|
+
if (typeof window !== 'undefined') {
|
|
63
|
+
window.scrollTo({
|
|
64
|
+
top: 0,
|
|
65
|
+
behavior: 'smooth',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (typeof window === 'undefined') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const throttledToggleVisibility = throttle(toggleVisibility, 100);
|
|
76
|
+
|
|
77
|
+
// Check initial scroll position
|
|
78
|
+
toggleVisibility();
|
|
79
|
+
|
|
80
|
+
window.addEventListener('scroll', throttledToggleVisibility, { passive: true });
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
window.removeEventListener('scroll', throttledToggleVisibility);
|
|
84
|
+
};
|
|
85
|
+
}, [toggleVisibility, throttle]);
|
|
86
|
+
|
|
87
|
+
// Handle keyboard interaction
|
|
88
|
+
const handleKeyDown = useCallback(
|
|
89
|
+
(event: KeyboardEvent<HTMLButtonElement>) => {
|
|
90
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
scrollToTop();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[scrollToTop],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!isVisible) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className={`
|
|
106
|
+
fixed z-50 inline-flex items-center gap-1
|
|
107
|
+
bg-white text-link border border-gray-300
|
|
108
|
+
rounded-md px-3 py-2 shadow-lg
|
|
109
|
+
hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
110
|
+
transition-all duration-200 ease-in-out
|
|
111
|
+
${className}
|
|
112
|
+
`.trim()}
|
|
113
|
+
// className={`fixed bottom-4 left-2 text-link gap-1 inline-flex items-center bg-white
|
|
114
|
+
// ${className}`.trim()}
|
|
115
|
+
style={{
|
|
116
|
+
bottom: `${bottom}px`,
|
|
117
|
+
left: `${left}px`,
|
|
118
|
+
}}
|
|
119
|
+
onClick={scrollToTop}
|
|
120
|
+
onKeyDown={handleKeyDown}
|
|
121
|
+
aria-label="Scroll back to top of page"
|
|
122
|
+
title="Back to top"
|
|
123
|
+
>
|
|
124
|
+
<LuArrowUp size={20} aria-hidden="true" />
|
|
125
|
+
|
|
126
|
+
<span className="hidden sm:inline">Back to top</span>
|
|
127
|
+
|
|
128
|
+
<span className="sr-only sm:hidden">Back to top</span>
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AiFillChrome } from 'react-icons/ai';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
4
|
+
import type { Meta, StoryFn } from '@storybook/react';
|
|
5
|
+
|
|
6
|
+
import { Chip, type ChipProps } from './Chip';
|
|
7
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
children: 'Chip',
|
|
11
|
+
component: Chip,
|
|
12
|
+
} as Meta;
|
|
13
|
+
|
|
14
|
+
const Template: StoryFn<ChipProps> = (args) => <Chip {...args} />;
|
|
15
|
+
|
|
16
|
+
export const Default = Template.bind({});
|
|
17
|
+
Default.args = {
|
|
18
|
+
children: 'Chip',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const JustText = Template.bind({});
|
|
22
|
+
JustText.args = {
|
|
23
|
+
children: 'Hello, this is some simple text',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ParagraphOfText = Template.bind({});
|
|
27
|
+
ParagraphOfText.args = {
|
|
28
|
+
children: <Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const TextWithIcon = Template.bind({});
|
|
32
|
+
TextWithIcon.args = {
|
|
33
|
+
children: (
|
|
34
|
+
<div className="flex items-center justify-center gap-2">
|
|
35
|
+
<AiFillChrome className="text-base" />
|
|
36
|
+
|
|
37
|
+
<Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Chip } from './Chip';
|
|
2
|
+
import { render, screen } from '../../test/renderers';
|
|
3
|
+
|
|
4
|
+
describe('Chip component', () => {
|
|
5
|
+
it('should render children correctly', () => {
|
|
6
|
+
render(
|
|
7
|
+
<Chip>
|
|
8
|
+
<p>Hello, World!</p>
|
|
9
|
+
</Chip>,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should merge custom className correctly', () => {
|
|
16
|
+
render(<Chip className="bg-red-500 rounded-lg">Custom</Chip>);
|
|
17
|
+
const element = screen.getByText('Custom');
|
|
18
|
+
|
|
19
|
+
expect(element).toHaveClass('bg-red-500');
|
|
20
|
+
expect(element).toHaveClass('rounded-lg');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should override conflicting className using twMerge', () => {
|
|
24
|
+
render(<Chip className="px-2">Custom</Chip>);
|
|
25
|
+
const element = screen.getByText('Custom');
|
|
26
|
+
|
|
27
|
+
// Should NOT have original 'pt-[12px]' due to twMerge override
|
|
28
|
+
expect(element?.className).not.toMatch(/px-3/);
|
|
29
|
+
expect(element).toHaveClass('px-2');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge';
|
|
2
|
+
|
|
3
|
+
export type ChipProps = {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Chip = ({ className, children }: ChipProps) => {
|
|
9
|
+
return (
|
|
10
|
+
<span
|
|
11
|
+
className={twMerge(
|
|
12
|
+
`inline-flex items-center rounded-lg bg-gray-200 px-3 py-1 text-sm font-medium
|
|
13
|
+
text-gray-800`,
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</span>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import { CookieBanner } from './CookieBanner';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/CookieBanner',
|
|
8
|
+
component: CookieBanner,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: 'Cookie banner component for displaying cookie consent options.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
} satisfies Meta<typeof CookieBanner>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
parameters: {
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
story: 'Default cookie banner (hidden by default with inline style).',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Visible: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<div>
|
|
37
|
+
<style>
|
|
38
|
+
{`
|
|
39
|
+
#cookie-banner {
|
|
40
|
+
display: block !important;
|
|
41
|
+
}
|
|
42
|
+
`}
|
|
43
|
+
</style>
|
|
44
|
+
|
|
45
|
+
<CookieBanner />
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
parameters: {
|
|
49
|
+
docs: {
|
|
50
|
+
description: {
|
|
51
|
+
story: 'Cookie banner with visibility forced on for demonstration purposes.',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const VisibleWithBackground: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<div className="min-h-screen bg-gray-100">
|
|
60
|
+
<CookieBanner />
|
|
61
|
+
|
|
62
|
+
<div className="p-8">
|
|
63
|
+
<h1 className="text-2xl font-bold mb-4">Sample Page Content</h1>
|
|
64
|
+
|
|
65
|
+
<p className="mb-4">
|
|
66
|
+
This demonstrates how the cookie banner would appear on a real page. The banner is
|
|
67
|
+
positioned at the bottom of the screen.
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<p className="mb-4">
|
|
71
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt
|
|
72
|
+
ut labore et dolore magna aliqua.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
parameters: {
|
|
78
|
+
docs: {
|
|
79
|
+
description: {
|
|
80
|
+
story:
|
|
81
|
+
'Cookie banner shown in context with page content, positioned at the bottom of the screen.',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const InteractiveDemo: Story = {
|
|
88
|
+
render: () => {
|
|
89
|
+
const handleAcceptAll = () => {
|
|
90
|
+
alert('All cookies accepted!');
|
|
91
|
+
// In a real implementation, this would hide the banner
|
|
92
|
+
const banner = document.getElementById('cookie-banner');
|
|
93
|
+
|
|
94
|
+
if (banner) {
|
|
95
|
+
banner.style.display = 'none';
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleRejectAdditional = () => {
|
|
100
|
+
alert('Additional cookies rejected!');
|
|
101
|
+
// In a real implementation, this would hide the banner
|
|
102
|
+
const banner = document.getElementById('cookie-banner');
|
|
103
|
+
|
|
104
|
+
if (banner) {
|
|
105
|
+
banner.style.display = 'none';
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="min-h-screen bg-gray-50">
|
|
111
|
+
<style>
|
|
112
|
+
{`
|
|
113
|
+
#cookie-banner {
|
|
114
|
+
display: block !important;
|
|
115
|
+
background-color: white;
|
|
116
|
+
border-top: 2px solid #1f2937;
|
|
117
|
+
position: fixed;
|
|
118
|
+
bottom: 0;
|
|
119
|
+
left: 0;
|
|
120
|
+
right: 0;
|
|
121
|
+
z-index: 1000;
|
|
122
|
+
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
123
|
+
}
|
|
124
|
+
`}
|
|
125
|
+
</style>
|
|
126
|
+
|
|
127
|
+
<div className="p-8">
|
|
128
|
+
<h1 className="text-3xl font-bold mb-6">Interactive Cookie Banner Demo</h1>
|
|
129
|
+
|
|
130
|
+
<p className="mb-4 text-lg">
|
|
131
|
+
Click either button on the cookie banner below to see the interaction.
|
|
132
|
+
</p>
|
|
133
|
+
|
|
134
|
+
<p className="mb-4">
|
|
135
|
+
This story demonstrates the banner in a more realistic context with interactive
|
|
136
|
+
behavior.
|
|
137
|
+
</p>
|
|
138
|
+
|
|
139
|
+
<div className="space-y-4">
|
|
140
|
+
<p>Sample page content continues here...</p>
|
|
141
|
+
|
|
142
|
+
<p>More content to show page scrolling behavior...</p>
|
|
143
|
+
|
|
144
|
+
<p>The cookie banner remains fixed at the bottom.</p>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div id="cookie-banner" role="region" aria-label="cookie banner">
|
|
149
|
+
<div className="mx-auto max-w-[960px] p-[1rem]">
|
|
150
|
+
<h3 className="mb-4 text-base font-bold text-govukBlack">
|
|
151
|
+
Tell us whether you accept cookies
|
|
152
|
+
</h3>
|
|
153
|
+
|
|
154
|
+
<p className="mb-4 text-sm text-black">
|
|
155
|
+
We use essential cookies to give you the best online experience. Without them, this
|
|
156
|
+
service will not work.
|
|
157
|
+
</p>
|
|
158
|
+
|
|
159
|
+
<p className="mb-4 text-sm text-black">
|
|
160
|
+
We also use non-essential cookies to analyze site usage to continually improve the
|
|
161
|
+
services we provide you with.
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
<p className="mb-4 text-sm text-black">
|
|
165
|
+
Full details of cookies collected, and the functionality to change your cookie
|
|
166
|
+
preference at any time can be accessed on our{' '}
|
|
167
|
+
<a
|
|
168
|
+
href="https://environment.data.gov.uk/help/cookies"
|
|
169
|
+
className="text-blue-600 underline hover:text-blue-800"
|
|
170
|
+
target="_blank"
|
|
171
|
+
rel="noopener noreferrer"
|
|
172
|
+
>
|
|
173
|
+
Cookie Policy Page
|
|
174
|
+
</a>
|
|
175
|
+
.
|
|
176
|
+
</p>
|
|
177
|
+
|
|
178
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_1fr_1fr]">
|
|
179
|
+
<div>
|
|
180
|
+
<button
|
|
181
|
+
className="focus:outline-3 relative box-border inline-block w-full cursor-pointer
|
|
182
|
+
appearance-none rounded-none border-2 border-transparent bg-green-500 px-[10px]
|
|
183
|
+
py-[7px] text-center align-top text-base font-normal leading-[19px] text-white
|
|
184
|
+
antialiased shadow-[0_2px_0_#002413] focus:bg-green-600 hover:bg-green-600
|
|
185
|
+
focus:outline focus:outline-offset-0 focus:outline-yellow-500"
|
|
186
|
+
id="accept-all-cookies"
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={handleAcceptAll}
|
|
189
|
+
>
|
|
190
|
+
Accept all cookies
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div>
|
|
195
|
+
<button
|
|
196
|
+
className="focus:outline-3 relative box-border inline-block w-full cursor-pointer
|
|
197
|
+
appearance-none rounded-none border-2 border-transparent bg-green-500 px-[10px]
|
|
198
|
+
py-[7px] text-center align-top text-base font-normal leading-[19px] text-white
|
|
199
|
+
antialiased shadow-[0_2px_0_#002413] focus:bg-green-600 hover:bg-green-600
|
|
200
|
+
focus:outline focus:outline-offset-0 focus:outline-yellow-500"
|
|
201
|
+
onClick={handleRejectAdditional}
|
|
202
|
+
>
|
|
203
|
+
Reject additional cookies
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
parameters: {
|
|
213
|
+
docs: {
|
|
214
|
+
description: {
|
|
215
|
+
story: 'Interactive version with working buttons that demonstrate the expected behavior.',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const MobileView: Story = {
|
|
222
|
+
render: () => (
|
|
223
|
+
<div className="max-w-sm mx-auto bg-gray-100 min-h-screen">
|
|
224
|
+
<style>
|
|
225
|
+
{`
|
|
226
|
+
#cookie-banner {
|
|
227
|
+
display: block !important;
|
|
228
|
+
background-color: white;
|
|
229
|
+
border-top: 1px solid #ccc;
|
|
230
|
+
position: fixed;
|
|
231
|
+
bottom: 0;
|
|
232
|
+
left: 0;
|
|
233
|
+
right: 0;
|
|
234
|
+
z-index: 1000;
|
|
235
|
+
}
|
|
236
|
+
`}
|
|
237
|
+
</style>
|
|
238
|
+
|
|
239
|
+
<div className="p-4">
|
|
240
|
+
<h1 className="text-xl font-bold mb-4">Mobile View</h1>
|
|
241
|
+
|
|
242
|
+
<p className="mb-4 text-sm">This shows how the cookie banner appears on mobile devices.</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<CookieBanner />
|
|
246
|
+
</div>
|
|
247
|
+
),
|
|
248
|
+
parameters: {
|
|
249
|
+
docs: {
|
|
250
|
+
description: {
|
|
251
|
+
story: 'Cookie banner optimized for mobile viewport to test responsive behavior.',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
viewport: {
|
|
255
|
+
defaultViewport: 'mobile1',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CookieBanner } from './CookieBanner';
|
|
2
|
+
import { render, screen, userEvent } from '../../test/renderers';
|
|
3
|
+
|
|
4
|
+
const handlePush = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock('next/navigation', () => ({
|
|
7
|
+
useRouter: () => ({
|
|
8
|
+
push: handlePush,
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('CookieBanner', () => {
|
|
13
|
+
it('should render cookie banner content', () => {
|
|
14
|
+
render(<CookieBanner />);
|
|
15
|
+
|
|
16
|
+
expect(
|
|
17
|
+
screen.getByRole('heading', {
|
|
18
|
+
name: /tell us whether you accept cookies/i,
|
|
19
|
+
hidden: true,
|
|
20
|
+
}),
|
|
21
|
+
).toBeInTheDocument();
|
|
22
|
+
|
|
23
|
+
expect(
|
|
24
|
+
screen.getByText(/we use essential cookies to give you the best online experience/i),
|
|
25
|
+
).toBeInTheDocument();
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
screen.getByText(/we also use non-essential cookies to analyze site usage/i),
|
|
29
|
+
).toBeInTheDocument();
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
screen.getByText(
|
|
33
|
+
/full details of cookies collected, and the functionality to change your cookie preference/i,
|
|
34
|
+
),
|
|
35
|
+
).toBeInTheDocument();
|
|
36
|
+
|
|
37
|
+
expect(screen.getByRole('link', { name: /cookie policy page/i, hidden: true })).toHaveAttribute(
|
|
38
|
+
'href',
|
|
39
|
+
'https://environment.data.gov.uk/help/cookies',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByRole('button', { name: /accept all cookies/i, hidden: true }),
|
|
44
|
+
).toBeInTheDocument();
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
screen.getByRole('button', {
|
|
48
|
+
name: /reject additional cookies/i,
|
|
49
|
+
hidden: true,
|
|
50
|
+
}),
|
|
51
|
+
).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should navigate to cookie preference page on "Reject additional cookies" click', async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
|
|
57
|
+
render(<CookieBanner />);
|
|
58
|
+
|
|
59
|
+
await user.click(
|
|
60
|
+
screen.getByRole('button', {
|
|
61
|
+
name: /reject additional cookies/i,
|
|
62
|
+
hidden: true,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(handlePush).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import Script from 'next/script';
|
|
5
|
+
|
|
6
|
+
import { ExternalLink } from '@tpzdsp/next-toolkit/components';
|
|
7
|
+
|
|
8
|
+
const helpCookieUrl = 'https://environment.data.gov.uk/help/cookies';
|
|
9
|
+
|
|
10
|
+
export const CookieBanner = () => {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
|
|
13
|
+
const setPreferences = () => {
|
|
14
|
+
router.push(helpCookieUrl);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div id="cookie-banner" role="region" aria-label="cookie banner" style={{ display: 'none' }}>
|
|
19
|
+
<Script src="https://environment.data.gov.uk/shared/cookie-banner.js"></Script>
|
|
20
|
+
|
|
21
|
+
<div className="mx-auto max-w-[960px] p-[1rem]">
|
|
22
|
+
<h3 className="mb-4 text-base font-bold text-govukBlack">
|
|
23
|
+
Tell us whether you accept cookies
|
|
24
|
+
</h3>
|
|
25
|
+
|
|
26
|
+
<p className="mb-4 text-sm text-black">
|
|
27
|
+
We use essential cookies to give you the best online experience. Without them, this
|
|
28
|
+
service will not work.
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<p className="mb-4 text-sm text-black">
|
|
32
|
+
We also use non-essential cookies to analyze site usage to continually improve the
|
|
33
|
+
services we provide you with.
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p className="mb-4 text-sm text-black">
|
|
37
|
+
Full details of cookies collected, and the functionality to change your cookie preference
|
|
38
|
+
at any time can be accessed on our{' '}
|
|
39
|
+
<ExternalLink href={helpCookieUrl}>Cookie Policy Page</ExternalLink>.
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_1fr_1fr]">
|
|
43
|
+
<div>
|
|
44
|
+
<button
|
|
45
|
+
className="focus:outline-3 relative box-border inline-block w-full cursor-pointer
|
|
46
|
+
appearance-none rounded-none border-2 border-transparent bg-green-500 px-[10px]
|
|
47
|
+
py-[7px] text-center align-top text-base font-normal leading-[19px] text-white
|
|
48
|
+
antialiased shadow-[0_2px_0_#002413] focus:bg-green-600 hover:bg-green-600
|
|
49
|
+
focus:outline focus:outline-offset-0 focus:outline-yellow-500"
|
|
50
|
+
id="accept-all-cookies"
|
|
51
|
+
type="submit"
|
|
52
|
+
>
|
|
53
|
+
Accept all cookies
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div>
|
|
58
|
+
<button
|
|
59
|
+
className="focus:outline-3 relative box-border inline-block w-full cursor-pointer
|
|
60
|
+
appearance-none rounded-none border-2 border-transparent bg-green-500 px-[10px]
|
|
61
|
+
py-[7px] text-center align-top text-base font-normal leading-[19px] text-white
|
|
62
|
+
antialiased shadow-[0_2px_0_#002413] focus:bg-green-600 hover:bg-green-600
|
|
63
|
+
focus:outline focus:outline-offset-0 focus:outline-yellow-500"
|
|
64
|
+
onClick={setPreferences}
|
|
65
|
+
>
|
|
66
|
+
Reject additional cookies
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|