uikit-react-public 0.44.4 → 0.45.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.
@@ -1,2 +1,3 @@
1
1
  export { default } from './Badge';
2
2
  export type { BadgeProps } from './Badge';
3
+ export type { BadgeVariant } from './Badge';
@@ -0,0 +1,304 @@
1
+ import {
2
+ MouseEvent as ReactMouseEvent,
3
+ MouseEventHandler,
4
+ ReactNode,
5
+ useCallback,
6
+ useId,
7
+ } from 'react';
8
+ import { css, cx } from '@emotion/css';
9
+ import useTheme from '../../theme/useTheme';
10
+ import BaseDialog, { BaseDialogProps } from '../Dialog/BaseDialog';
11
+ import Heading from '../Heading';
12
+ import Paragraph from '../Paragraph';
13
+ import Button from '../Button';
14
+ import Icon from '../Icon';
15
+ import IconButton from '../IconButton';
16
+ import Link from '../Link';
17
+ import StandaloneLink from '../StandaloneLink';
18
+ import Toggle from '../Toggle';
19
+ import {
20
+ DEFAULT_COOKIE_CATEGORIES,
21
+ DEFAULT_DESCRIPTION,
22
+ DEFAULT_DESCRIPTION_SUFFIX,
23
+ DEFAULT_POLICY_HREF,
24
+ } from './defaults';
25
+ import { CookieCategory, CookiePreferences } from './types';
26
+
27
+ const NAME = 'ucl-uikit-cookie-settings';
28
+
29
+ interface CookieSettingsProps extends Omit<
30
+ BaseDialogProps,
31
+ 'children' | 'onChange' | 'onClose' | 'size' | 'modal'
32
+ > {
33
+ cookies: CookieCategory[];
34
+ preferences: CookiePreferences;
35
+ description?: ReactNode;
36
+ policyHref?: string;
37
+ policyLabel?: ReactNode;
38
+ onClose?: (ev?: ReactMouseEvent | KeyboardEvent) => void;
39
+ onPreferenceToggle: (cookie: CookieCategory) => void;
40
+ onSavePreferences?: () => void;
41
+ saveButtonLabel?: ReactNode;
42
+ }
43
+
44
+ const CookieSettings = ({
45
+ open = false,
46
+ cookies = DEFAULT_COOKIE_CATEGORIES,
47
+ preferences,
48
+ description,
49
+ policyHref = DEFAULT_POLICY_HREF,
50
+ policyLabel = 'UCL privacy policy.',
51
+ onClose,
52
+ onPreferenceToggle,
53
+ onSavePreferences,
54
+ saveButtonLabel = 'Save preferences',
55
+ testId = NAME,
56
+ className,
57
+ ...props
58
+ }: CookieSettingsProps) => {
59
+ const [theme] = useTheme();
60
+ const headingId = useId();
61
+ const descriptionId = useId();
62
+
63
+ const handleSave = useCallback(() => {
64
+ onSavePreferences?.();
65
+ }, [onSavePreferences]);
66
+
67
+ const handleCloseClick: MouseEventHandler<HTMLButtonElement> = useCallback(
68
+ (ev) => {
69
+ onClose?.(ev);
70
+ },
71
+ [onClose]
72
+ );
73
+
74
+ const rootStyle = css`
75
+ background-color: ${theme.colour.surface.default};
76
+ `;
77
+
78
+ const headerStyle = css`
79
+ display: flex;
80
+ justify-content: space-between;
81
+ align-items: flex-start;
82
+ gap: ${theme.padding.p16};
83
+ padding: ${theme.padding.p32} ${theme.padding.p24} 0 ${theme.padding.p32};
84
+
85
+ @media (min-width: ${theme.breakpoints.tablet}px) {
86
+ padding-right: ${theme.padding.p32};
87
+ }
88
+ `;
89
+
90
+ const headingStyle = css`
91
+ padding-top: ${theme.padding.p32};
92
+ `;
93
+
94
+ const closeButtonStyle = css`
95
+ flex: 0 0 auto;
96
+ color: ${theme.colour.icon.secondary};
97
+ `;
98
+
99
+ const bodyStyle = css`
100
+ padding: ${theme.padding.p24} ${theme.padding.p32};
101
+ `;
102
+
103
+ const contentStyle = css`
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: ${theme.padding.p24};
107
+ `;
108
+
109
+ const policyLinkStyle = css`
110
+ color: ${theme.colour.text.default};
111
+
112
+ &:visited {
113
+ color: ${theme.colour.text.default};
114
+ }
115
+ `;
116
+
117
+ const listStyle = css`
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: ${theme.padding.p24};
121
+ `;
122
+
123
+ const cookieStyle = css`
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: ${theme.padding.p16};
127
+ `;
128
+
129
+ const cookieHeaderStyle = css`
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: flex-start;
133
+ gap: ${theme.padding.p16};
134
+ `;
135
+
136
+ const cookieTextStyle = css`
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: ${theme.padding.p16};
140
+ min-width: 0;
141
+ `;
142
+
143
+ const toggleStyle = css`
144
+ flex: 0 0 auto;
145
+ margin-top: ${theme.margin.m2};
146
+ `;
147
+
148
+ const learnMoreStyle = css`
149
+ width: fit-content;
150
+ `;
151
+
152
+ const footerStyle = css`
153
+ padding: 0 ${theme.padding.p32} ${theme.padding.p32};
154
+ display: flex;
155
+ justify-content: stretch;
156
+
157
+ @media (min-width: ${theme.breakpoints.tablet}px) {
158
+ justify-content: flex-end;
159
+ padding-top: ${theme.padding.p16};
160
+ }
161
+ `;
162
+
163
+ const saveButtonStyle = css`
164
+ width: 100%;
165
+
166
+ @media (min-width: ${theme.breakpoints.tablet}px) {
167
+ width: auto;
168
+ }
169
+ `;
170
+
171
+ const style = cx(NAME, rootStyle, className);
172
+
173
+ return (
174
+ <BaseDialog
175
+ open={open}
176
+ modal
177
+ size='large'
178
+ onClose={onClose}
179
+ className={style}
180
+ aria-labelledby={headingId}
181
+ aria-describedby={descriptionId}
182
+ testId={testId}
183
+ {...props}
184
+ >
185
+ <header className={cx(`${NAME}__header`, headerStyle)}>
186
+ <Heading
187
+ id={headingId}
188
+ level='lg'
189
+ noMargins
190
+ className={cx(`${NAME}__heading`, headingStyle)}
191
+ >
192
+ Privacy preferences
193
+ </Heading>
194
+ <IconButton
195
+ aria-label='Close cookie settings'
196
+ className={cx(`${NAME}__close-button`, closeButtonStyle)}
197
+ onClick={handleCloseClick}
198
+ >
199
+ <Icon.X
200
+ role='presentation'
201
+ aria-hidden='true'
202
+ />
203
+ </IconButton>
204
+ </header>
205
+
206
+ <div className={cx(`${NAME}__body`, bodyStyle)}>
207
+ <div className={cx(`${NAME}__content`, contentStyle)}>
208
+ {description !== undefined && (
209
+ <Paragraph
210
+ id={descriptionId}
211
+ level='md'
212
+ noMargins
213
+ >
214
+ {description}
215
+ </Paragraph>
216
+ )}
217
+ {description === undefined && (
218
+ <Paragraph
219
+ id={descriptionId}
220
+ level='md'
221
+ noMargins
222
+ >
223
+ {DEFAULT_DESCRIPTION}
224
+ <Link
225
+ href={policyHref}
226
+ className={policyLinkStyle}
227
+ noVisited
228
+ >
229
+ {policyLabel}
230
+ </Link>
231
+ {DEFAULT_DESCRIPTION_SUFFIX}
232
+ </Paragraph>
233
+ )}
234
+
235
+ <div className={cx(`${NAME}__cookies`, listStyle)}>
236
+ {cookies.map((cookie) => (
237
+ <section
238
+ key={cookie.id}
239
+ className={cx(`${NAME}__cookie`, cookieStyle)}
240
+ >
241
+ <div
242
+ className={cx(`${NAME}__cookie-header`, cookieHeaderStyle)}
243
+ >
244
+ <div className={cx(`${NAME}__cookie-text`, cookieTextStyle)}>
245
+ <Heading
246
+ level='md'
247
+ as='h3'
248
+ noMargins
249
+ >
250
+ {cookie.title}
251
+ </Heading>
252
+ <Paragraph
253
+ level='md'
254
+ noMargins
255
+ >
256
+ {cookie.description}
257
+ </Paragraph>
258
+ </div>
259
+ <Toggle
260
+ checked={preferences[cookie.id]}
261
+ disabled={cookie.required}
262
+ aria-label={
263
+ cookie.toggleLabel ??
264
+ (typeof cookie.title === 'string'
265
+ ? `Allow ${cookie.title}`
266
+ : `Allow ${cookie.id} cookies`)
267
+ }
268
+ className={cx(`${NAME}__cookie-toggle`, toggleStyle)}
269
+ onClick={() => onPreferenceToggle(cookie)}
270
+ />
271
+ </div>
272
+ {cookie.learnMoreHref && (
273
+ <StandaloneLink
274
+ href={cookie.learnMoreHref}
275
+ variant='primary'
276
+ icon={<Icon.ChevronRight />}
277
+ iconPosition='right'
278
+ noVisited
279
+ className={cx(`${NAME}__learn-more`, learnMoreStyle)}
280
+ >
281
+ {cookie.learnMoreLabel ?? 'Learn more'}
282
+ </StandaloneLink>
283
+ )}
284
+ </section>
285
+ ))}
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <footer className={cx(`${NAME}__footer`, footerStyle)}>
291
+ <Button
292
+ variant='accent'
293
+ size='medium'
294
+ className={cx(`${NAME}__save-button`, saveButtonStyle)}
295
+ onClick={handleSave}
296
+ >
297
+ {saveButtonLabel}
298
+ </Button>
299
+ </footer>
300
+ </BaseDialog>
301
+ );
302
+ };
303
+
304
+ export default CookieSettings;
@@ -0,0 +1,60 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import CookiesNotice from '.';
3
+
4
+ const cookies = [
5
+ {
6
+ id: 'necessary',
7
+ title: 'Necessary Cookies',
8
+ description:
9
+ "These cookies are necessary for the website to function and can't be switched off in our systems.",
10
+ required: true,
11
+ defaultEnabled: true,
12
+ learnMoreHref: 'https://www.ucl.ac.uk/legal-services/privacy/cookie-policy',
13
+ },
14
+ {
15
+ id: 'analytics',
16
+ title: 'Analytics',
17
+ description:
18
+ 'These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site.',
19
+ defaultEnabled: true,
20
+ learnMoreHref: 'https://www.ucl.ac.uk/legal-services/privacy/cookie-policy',
21
+ },
22
+ {
23
+ id: 'marketing',
24
+ title: 'Marketing',
25
+ description:
26
+ 'These cookies are set through our site by our advertising partners. They may be used to build a profile of your interests and show you relevant ads.',
27
+ defaultEnabled: true,
28
+ learnMoreHref: 'https://www.ucl.ac.uk/legal-services/privacy/cookie-policy',
29
+ },
30
+ ];
31
+
32
+ const meta = {
33
+ title: 'Components/CookiesNotice',
34
+ component: CookiesNotice,
35
+ parameters: {
36
+ layout: 'centered',
37
+ },
38
+ tags: ['autodocs'],
39
+ } satisfies Meta<typeof CookiesNotice>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ export const NonModal: Story = {
45
+ name: 'Non-modal',
46
+ args: {
47
+ cookies,
48
+ },
49
+ };
50
+
51
+ export const Modal: Story = {
52
+ name: 'Modal',
53
+ args: {
54
+ cookies,
55
+ modal: true,
56
+ },
57
+ parameters: {
58
+ layout: 'fullscreen',
59
+ },
60
+ };