@redacto.io/consent-sdk-react 0.0.1 → 0.0.3
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/.changeset/fifty-candies-drop.md +5 -0
- package/.turbo/turbo-build.log +21 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +706 -240
- package/dist/index.mjs +707 -241
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +400 -73
- package/src/RedactoNoticeConsent/api/index.ts +32 -42
- package/src/RedactoNoticeConsent/api/types.ts +14 -7
- package/src/RedactoNoticeConsent/injectStyles.ts +102 -0
- package/src/RedactoNoticeConsent/styles.ts +98 -49
- package/src/RedactoNoticeConsent/types.ts +2 -0
- package/tests/mocks.ts +2 -2
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState, useRef } from "react";
|
|
2
2
|
import { fetchConsentContent, submitConsentEvent } from "./api";
|
|
3
3
|
import type { ConsentContent } from "./api/types";
|
|
4
4
|
import type { Props, TranslationObject } from "./types";
|
|
5
5
|
import { styles } from "./styles";
|
|
6
6
|
import logo from "./assets/redacto-logo.png";
|
|
7
7
|
import { useMediaQuery } from "./useMediaQuery";
|
|
8
|
+
import { injectCheckboxStyles } from "./injectStyles";
|
|
8
9
|
|
|
9
10
|
export const RedactoNoticeConsent = ({
|
|
10
11
|
noticeId,
|
|
11
12
|
accessToken,
|
|
12
13
|
refreshToken,
|
|
14
|
+
baseUrl,
|
|
13
15
|
language = "en",
|
|
14
|
-
blockUI =
|
|
16
|
+
blockUI = true,
|
|
15
17
|
onAccept,
|
|
16
18
|
onDecline,
|
|
17
19
|
onError,
|
|
18
20
|
settings,
|
|
21
|
+
applicationId,
|
|
19
22
|
}: Props) => {
|
|
20
23
|
const [content, setContent] = useState<
|
|
21
24
|
ConsentContent["detail"]["active_config"] | null
|
|
22
25
|
>(null);
|
|
23
26
|
const [isLoading, setIsLoading] = useState(true);
|
|
24
27
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
28
|
+
const [isRefreshingToken, setIsRefreshingToken] = useState(false);
|
|
25
29
|
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
|
|
26
30
|
const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
|
|
27
31
|
const [collapsedPurposes, setCollapsedPurposes] = useState<
|
|
@@ -34,6 +38,13 @@ export const RedactoNoticeConsent = ({
|
|
|
34
38
|
Record<string, boolean>
|
|
35
39
|
>({});
|
|
36
40
|
const [hasAlreadyConsented, setHasAlreadyConsented] = useState(false);
|
|
41
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
42
|
+
const [fetchError, setFetchError] = useState<Error | null>(null);
|
|
43
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const firstFocusableRef = useRef<HTMLButtonElement>(null);
|
|
45
|
+
const lastFocusableRef = useRef<HTMLButtonElement>(null);
|
|
46
|
+
const MAX_REFRESH_ATTEMPTS = 1;
|
|
47
|
+
const [refreshAttempts, setRefreshAttempts] = useState(0);
|
|
37
48
|
|
|
38
49
|
// Add media query hooks
|
|
39
50
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
@@ -86,6 +97,28 @@ export const RedactoNoticeConsent = ({
|
|
|
86
97
|
middleSection: {
|
|
87
98
|
paddingRight: isMobile ? "10px" : "15px",
|
|
88
99
|
},
|
|
100
|
+
errorDialog: {
|
|
101
|
+
padding: isMobile ? "12px" : "16px",
|
|
102
|
+
maxWidth: isMobile ? "100%" : "400px",
|
|
103
|
+
marginBottom: isMobile ? "16px" : "20px",
|
|
104
|
+
},
|
|
105
|
+
errorTitle: {
|
|
106
|
+
fontSize: isMobile ? "16px" : "18px",
|
|
107
|
+
margin: isMobile ? "0 0 8px 0" : "0 0 12px 0",
|
|
108
|
+
},
|
|
109
|
+
errorMessage: {
|
|
110
|
+
fontSize: isMobile ? "12px" : "14px",
|
|
111
|
+
margin: isMobile ? "0 0 12px 0" : "0 0 16px 0",
|
|
112
|
+
},
|
|
113
|
+
errorButton: {
|
|
114
|
+
padding: isMobile ? "6px 12px" : "8px 16px",
|
|
115
|
+
fontSize: isMobile ? "12px" : "14px",
|
|
116
|
+
},
|
|
117
|
+
closeButton: {
|
|
118
|
+
top: isMobile ? "4px" : "8px",
|
|
119
|
+
right: isMobile ? "4px" : "8px",
|
|
120
|
+
padding: isMobile ? "2px" : "4px",
|
|
121
|
+
},
|
|
89
122
|
}),
|
|
90
123
|
[isMobile]
|
|
91
124
|
);
|
|
@@ -157,71 +190,91 @@ export const RedactoNoticeConsent = ({
|
|
|
157
190
|
return defaultText;
|
|
158
191
|
};
|
|
159
192
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
const fetchNotice = async () => {
|
|
194
|
+
setIsLoading(true);
|
|
195
|
+
setFetchError(null);
|
|
196
|
+
try {
|
|
197
|
+
const consentContentData = await fetchConsentContent({
|
|
198
|
+
noticeId,
|
|
199
|
+
accessToken,
|
|
200
|
+
refreshToken,
|
|
201
|
+
baseUrl,
|
|
202
|
+
language,
|
|
203
|
+
specific_uuid: applicationId,
|
|
204
|
+
});
|
|
205
|
+
setContent(consentContentData.detail.active_config);
|
|
206
|
+
if (consentContentData.detail.active_config.default_language) {
|
|
207
|
+
setSelectedLanguage(
|
|
208
|
+
consentContentData.detail.active_config.default_language
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const initialCollapsedState =
|
|
212
|
+
consentContentData.detail.active_config.purposes.reduce(
|
|
213
|
+
(acc, purpose) => ({
|
|
214
|
+
...acc,
|
|
215
|
+
[purpose.uuid]: true,
|
|
216
|
+
}),
|
|
217
|
+
{}
|
|
218
|
+
);
|
|
219
|
+
setCollapsedPurposes(initialCollapsedState);
|
|
220
|
+
|
|
221
|
+
// Initialize selected states
|
|
222
|
+
const initialPurposeState: Record<string, boolean> =
|
|
223
|
+
consentContentData.detail.active_config.purposes.reduce(
|
|
224
|
+
(acc: Record<string, boolean>, purpose) => {
|
|
225
|
+
return {
|
|
179
226
|
...acc,
|
|
180
|
-
[purpose.uuid]:
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} catch (err: unknown) {
|
|
212
|
-
console.error(err);
|
|
213
|
-
if ((err as Error & { status?: number }).status === 409) {
|
|
214
|
-
setHasAlreadyConsented(true);
|
|
215
|
-
} else {
|
|
216
|
-
onError?.(err as Error);
|
|
217
|
-
}
|
|
218
|
-
} finally {
|
|
219
|
-
setIsLoading(false);
|
|
227
|
+
[purpose.uuid]: false,
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
{}
|
|
231
|
+
);
|
|
232
|
+
setSelectedPurposes(initialPurposeState);
|
|
233
|
+
|
|
234
|
+
const initialDataElementState: Record<string, boolean> =
|
|
235
|
+
consentContentData.detail.active_config.purposes.reduce(
|
|
236
|
+
(acc: Record<string, boolean>, purpose) => {
|
|
237
|
+
purpose.data_elements.forEach((element) => {
|
|
238
|
+
const combinedId = `${purpose.uuid}-${element.uuid}`;
|
|
239
|
+
acc[combinedId] = false;
|
|
240
|
+
});
|
|
241
|
+
return acc;
|
|
242
|
+
},
|
|
243
|
+
{}
|
|
244
|
+
);
|
|
245
|
+
setSelectedDataElements(initialDataElementState);
|
|
246
|
+
} catch (err: unknown) {
|
|
247
|
+
console.error(err);
|
|
248
|
+
const error = err as Error & { status?: number };
|
|
249
|
+
setFetchError(error);
|
|
250
|
+
if (error.status === 401 && refreshAttempts < MAX_REFRESH_ATTEMPTS) {
|
|
251
|
+
setRefreshAttempts((c) => c + 1);
|
|
252
|
+
setIsRefreshingToken(true);
|
|
253
|
+
onError?.(error);
|
|
254
|
+
} else if (error.status === 409) {
|
|
255
|
+
setHasAlreadyConsented(true);
|
|
256
|
+
} else {
|
|
257
|
+
onError?.(error);
|
|
220
258
|
}
|
|
221
|
-
}
|
|
259
|
+
} finally {
|
|
260
|
+
setIsLoading(false);
|
|
261
|
+
setIsRefreshingToken(false);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
222
264
|
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
// Inject checkbox styles when component mounts
|
|
267
|
+
injectCheckboxStyles();
|
|
223
268
|
fetchNotice();
|
|
224
|
-
}, [
|
|
269
|
+
}, [
|
|
270
|
+
noticeId,
|
|
271
|
+
accessToken,
|
|
272
|
+
refreshToken,
|
|
273
|
+
language,
|
|
274
|
+
onError,
|
|
275
|
+
applicationId,
|
|
276
|
+
isRefreshingToken,
|
|
277
|
+
]);
|
|
225
278
|
|
|
226
279
|
const togglePurposeCollapse = (purposeUuid: string) => {
|
|
227
280
|
setCollapsedPurposes((prev) => ({
|
|
@@ -301,9 +354,11 @@ export const RedactoNoticeConsent = ({
|
|
|
301
354
|
const handleAccept = async () => {
|
|
302
355
|
try {
|
|
303
356
|
setIsSubmitting(true);
|
|
357
|
+
setErrorMessage(null);
|
|
304
358
|
if (content) {
|
|
305
359
|
await submitConsentEvent({
|
|
306
360
|
accessToken,
|
|
361
|
+
baseUrl,
|
|
307
362
|
noticeUuid: content.notice_uuid,
|
|
308
363
|
purposes: content.purposes.map((purpose) => ({
|
|
309
364
|
...purpose,
|
|
@@ -317,11 +372,20 @@ export const RedactoNoticeConsent = ({
|
|
|
317
372
|
})),
|
|
318
373
|
})),
|
|
319
374
|
declined: false,
|
|
375
|
+
meta_data: applicationId
|
|
376
|
+
? { specific_uuid: applicationId }
|
|
377
|
+
: undefined,
|
|
320
378
|
});
|
|
321
379
|
}
|
|
322
380
|
onAccept?.();
|
|
323
381
|
} catch (error) {
|
|
324
382
|
console.error("Error submitting consent:", error);
|
|
383
|
+
const err = error as Error & { status?: number };
|
|
384
|
+
if (err.status === 500) {
|
|
385
|
+
setErrorMessage(
|
|
386
|
+
"An error occurred while submitting your consent. Please try again later."
|
|
387
|
+
);
|
|
388
|
+
}
|
|
325
389
|
onError?.(error as Error);
|
|
326
390
|
} finally {
|
|
327
391
|
setIsSubmitting(false);
|
|
@@ -378,7 +442,10 @@ export const RedactoNoticeConsent = ({
|
|
|
378
442
|
|
|
379
443
|
const acceptButtonStyle = {
|
|
380
444
|
...buttonStyle,
|
|
381
|
-
backgroundColor:
|
|
445
|
+
backgroundColor:
|
|
446
|
+
settings?.button?.accept?.backgroundColor ||
|
|
447
|
+
content?.primary_color ||
|
|
448
|
+
"#4f87ff",
|
|
382
449
|
color: settings?.button?.accept?.textColor || "#ffffff",
|
|
383
450
|
};
|
|
384
451
|
|
|
@@ -390,7 +457,7 @@ export const RedactoNoticeConsent = ({
|
|
|
390
457
|
};
|
|
391
458
|
|
|
392
459
|
const linkStyle = {
|
|
393
|
-
color: settings?.link?.
|
|
460
|
+
color: settings?.link || content?.secondary_color || "#4f87ff",
|
|
394
461
|
};
|
|
395
462
|
|
|
396
463
|
const languageSelectorStyle = {
|
|
@@ -420,12 +487,74 @@ export const RedactoNoticeConsent = ({
|
|
|
420
487
|
backgroundColor: settings?.backgroundColor || "#ffffff",
|
|
421
488
|
};
|
|
422
489
|
|
|
490
|
+
const handleCloseError = () => {
|
|
491
|
+
setErrorMessage(null);
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Add keyboard event handler for the modal
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
497
|
+
if (!modalRef.current) return;
|
|
498
|
+
|
|
499
|
+
const focusableElements = modalRef.current.querySelectorAll(
|
|
500
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
501
|
+
);
|
|
502
|
+
if (!focusableElements.length) return;
|
|
503
|
+
|
|
504
|
+
if (event.key === "Escape") {
|
|
505
|
+
handleDecline();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (event.key === "Tab") {
|
|
509
|
+
const firstElement = focusableElements[0] as HTMLElement;
|
|
510
|
+
const lastElement = focusableElements[
|
|
511
|
+
focusableElements.length - 1
|
|
512
|
+
] as HTMLElement;
|
|
513
|
+
|
|
514
|
+
if (event.shiftKey) {
|
|
515
|
+
if (document.activeElement === firstElement) {
|
|
516
|
+
event.preventDefault();
|
|
517
|
+
lastElement.focus();
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
if (document.activeElement === lastElement) {
|
|
521
|
+
event.preventDefault();
|
|
522
|
+
firstElement.focus();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const modal = modalRef.current;
|
|
529
|
+
modal?.addEventListener("keydown", handleKeyDown);
|
|
530
|
+
|
|
531
|
+
// Focus the first focusable element when modal opens
|
|
532
|
+
if (modal && !isLoading) {
|
|
533
|
+
const focusableElements = modal.querySelectorAll(
|
|
534
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
535
|
+
);
|
|
536
|
+
const firstElement = focusableElements[0] as HTMLElement;
|
|
537
|
+
firstElement?.focus();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return () => {
|
|
541
|
+
modal?.removeEventListener("keydown", handleKeyDown);
|
|
542
|
+
};
|
|
543
|
+
}, [isLoading, handleDecline]);
|
|
544
|
+
|
|
423
545
|
return (
|
|
424
546
|
<>
|
|
425
547
|
{hasAlreadyConsented ? null : (
|
|
426
548
|
<>
|
|
427
|
-
{blockUI &&
|
|
549
|
+
{blockUI && (
|
|
550
|
+
<div
|
|
551
|
+
style={styles.overlay}
|
|
552
|
+
aria-hidden="true"
|
|
553
|
+
role="presentation"
|
|
554
|
+
/>
|
|
555
|
+
)}
|
|
428
556
|
<div
|
|
557
|
+
ref={modalRef}
|
|
429
558
|
style={{
|
|
430
559
|
...styles.modal,
|
|
431
560
|
...responsiveStyles.modal,
|
|
@@ -434,22 +563,178 @@ export const RedactoNoticeConsent = ({
|
|
|
434
563
|
role="dialog"
|
|
435
564
|
aria-modal="true"
|
|
436
565
|
aria-labelledby="privacy-notice-title"
|
|
566
|
+
aria-describedby="privacy-notice-description"
|
|
567
|
+
tabIndex={-1}
|
|
437
568
|
>
|
|
438
|
-
{isLoading ? (
|
|
569
|
+
{isLoading || isRefreshingToken ? (
|
|
439
570
|
<div style={styles.loadingContainer}>
|
|
440
571
|
<div style={styles.loadingSpinner}></div>
|
|
441
572
|
<p style={{ ...styles.privacyText, ...textStyle }}>
|
|
442
|
-
Loading...
|
|
573
|
+
{isRefreshingToken ? "Refreshing session..." : "Loading..."}
|
|
443
574
|
</p>
|
|
444
575
|
</div>
|
|
576
|
+
) : fetchError ? (
|
|
577
|
+
<div
|
|
578
|
+
style={{
|
|
579
|
+
...styles.content,
|
|
580
|
+
...responsiveStyles.content,
|
|
581
|
+
display: "flex",
|
|
582
|
+
flexDirection: "column",
|
|
583
|
+
alignItems: "center",
|
|
584
|
+
justifyContent: "center",
|
|
585
|
+
textAlign: "center",
|
|
586
|
+
padding: "40px 20px",
|
|
587
|
+
}}
|
|
588
|
+
>
|
|
589
|
+
<div
|
|
590
|
+
style={{
|
|
591
|
+
color: "#DC2626",
|
|
592
|
+
padding: responsiveStyles.errorDialog.padding,
|
|
593
|
+
borderRadius: "8px",
|
|
594
|
+
marginBottom: responsiveStyles.errorDialog.marginBottom,
|
|
595
|
+
width: "100%",
|
|
596
|
+
maxWidth: responsiveStyles.errorDialog.maxWidth,
|
|
597
|
+
position: "relative",
|
|
598
|
+
}}
|
|
599
|
+
>
|
|
600
|
+
<button
|
|
601
|
+
onClick={handleDecline}
|
|
602
|
+
style={{
|
|
603
|
+
position: "absolute",
|
|
604
|
+
top: responsiveStyles.closeButton.top,
|
|
605
|
+
right: responsiveStyles.closeButton.right,
|
|
606
|
+
background: "none",
|
|
607
|
+
border: "none",
|
|
608
|
+
color: "#DC2626",
|
|
609
|
+
cursor: "pointer",
|
|
610
|
+
padding: responsiveStyles.closeButton.padding,
|
|
611
|
+
display: "flex",
|
|
612
|
+
alignItems: "center",
|
|
613
|
+
justifyContent: "center",
|
|
614
|
+
}}
|
|
615
|
+
aria-label="Close error message"
|
|
616
|
+
>
|
|
617
|
+
<svg
|
|
618
|
+
width={isMobile ? "14" : "16"}
|
|
619
|
+
height={isMobile ? "14" : "16"}
|
|
620
|
+
viewBox="0 0 16 16"
|
|
621
|
+
fill="none"
|
|
622
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
623
|
+
aria-hidden="true"
|
|
624
|
+
>
|
|
625
|
+
<path
|
|
626
|
+
d="M12 4L4 12M4 4L12 12"
|
|
627
|
+
stroke="currentColor"
|
|
628
|
+
strokeWidth="2"
|
|
629
|
+
strokeLinecap="round"
|
|
630
|
+
strokeLinejoin="round"
|
|
631
|
+
/>
|
|
632
|
+
</svg>
|
|
633
|
+
</button>
|
|
634
|
+
<h3
|
|
635
|
+
style={{
|
|
636
|
+
margin: responsiveStyles.errorTitle.margin,
|
|
637
|
+
fontSize: responsiveStyles.errorTitle.fontSize,
|
|
638
|
+
fontWeight: "600",
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
Error Loading Consent Notice
|
|
642
|
+
</h3>
|
|
643
|
+
<p
|
|
644
|
+
style={{
|
|
645
|
+
margin: responsiveStyles.errorMessage.margin,
|
|
646
|
+
fontSize: responsiveStyles.errorMessage.fontSize,
|
|
647
|
+
lineHeight: "1.5",
|
|
648
|
+
}}
|
|
649
|
+
>
|
|
650
|
+
{fetchError.message ||
|
|
651
|
+
"Failed to load consent notice. Please try again."}
|
|
652
|
+
</p>
|
|
653
|
+
<button
|
|
654
|
+
onClick={fetchNotice}
|
|
655
|
+
style={{
|
|
656
|
+
backgroundColor: "#DC2626",
|
|
657
|
+
color: "#FFFFFF",
|
|
658
|
+
border: "none",
|
|
659
|
+
padding: responsiveStyles.errorButton.padding,
|
|
660
|
+
borderRadius: "6px",
|
|
661
|
+
cursor: "pointer",
|
|
662
|
+
fontSize: responsiveStyles.errorButton.fontSize,
|
|
663
|
+
fontWeight: "500",
|
|
664
|
+
transition: "background-color 0.2s",
|
|
665
|
+
}}
|
|
666
|
+
onMouseOver={(e) =>
|
|
667
|
+
(e.currentTarget.style.backgroundColor = "#B91C1C")
|
|
668
|
+
}
|
|
669
|
+
onMouseOut={(e) =>
|
|
670
|
+
(e.currentTarget.style.backgroundColor = "#DC2626")
|
|
671
|
+
}
|
|
672
|
+
>
|
|
673
|
+
Refresh
|
|
674
|
+
</button>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
445
677
|
) : (
|
|
446
678
|
<div style={{ ...styles.content, ...responsiveStyles.content }}>
|
|
679
|
+
{errorMessage && (
|
|
680
|
+
<div
|
|
681
|
+
role="alert"
|
|
682
|
+
style={{
|
|
683
|
+
backgroundColor: "#FEE2E2",
|
|
684
|
+
color: "#DC2626",
|
|
685
|
+
padding: "12px",
|
|
686
|
+
borderRadius: "6px",
|
|
687
|
+
marginBottom: "16px",
|
|
688
|
+
fontSize: "14px",
|
|
689
|
+
border: "1px solid #FCA5A5",
|
|
690
|
+
position: "relative",
|
|
691
|
+
display: "flex",
|
|
692
|
+
alignItems: "center",
|
|
693
|
+
justifyContent: "space-between",
|
|
694
|
+
}}
|
|
695
|
+
>
|
|
696
|
+
<span>{errorMessage}</span>
|
|
697
|
+
<button
|
|
698
|
+
type="button"
|
|
699
|
+
onClick={handleCloseError}
|
|
700
|
+
style={{
|
|
701
|
+
background: "none",
|
|
702
|
+
border: "none",
|
|
703
|
+
color: "#DC2626",
|
|
704
|
+
cursor: "pointer",
|
|
705
|
+
padding: "4px",
|
|
706
|
+
marginLeft: "8px",
|
|
707
|
+
display: "flex",
|
|
708
|
+
alignItems: "center",
|
|
709
|
+
justifyContent: "center",
|
|
710
|
+
}}
|
|
711
|
+
aria-label="Close error message"
|
|
712
|
+
>
|
|
713
|
+
<svg
|
|
714
|
+
width="16"
|
|
715
|
+
height="16"
|
|
716
|
+
viewBox="0 0 16 16"
|
|
717
|
+
fill="none"
|
|
718
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
719
|
+
aria-hidden="true"
|
|
720
|
+
>
|
|
721
|
+
<path
|
|
722
|
+
d="M12 4L4 12M4 4L12 12"
|
|
723
|
+
stroke="currentColor"
|
|
724
|
+
strokeWidth="2"
|
|
725
|
+
strokeLinecap="round"
|
|
726
|
+
strokeLinejoin="round"
|
|
727
|
+
/>
|
|
728
|
+
</svg>
|
|
729
|
+
</button>
|
|
730
|
+
</div>
|
|
731
|
+
)}
|
|
447
732
|
<div style={{ ...styles.topSection, ...sectionStyle }}>
|
|
448
733
|
<div style={styles.topLeft}>
|
|
449
734
|
<img
|
|
450
735
|
style={styles.logo}
|
|
451
736
|
src={content?.logo_url || logo}
|
|
452
|
-
alt="
|
|
737
|
+
alt="Redacto Logo"
|
|
453
738
|
/>
|
|
454
739
|
<h2
|
|
455
740
|
id="privacy-notice-title"
|
|
@@ -464,6 +749,7 @@ export const RedactoNoticeConsent = ({
|
|
|
464
749
|
</div>
|
|
465
750
|
<div style={styles.languageSelectorContainer}>
|
|
466
751
|
<button
|
|
752
|
+
ref={firstFocusableRef}
|
|
467
753
|
style={{
|
|
468
754
|
...styles.topRight,
|
|
469
755
|
...responsiveStyles.topRight,
|
|
@@ -472,6 +758,7 @@ export const RedactoNoticeConsent = ({
|
|
|
472
758
|
onClick={toggleLanguageDropdown}
|
|
473
759
|
aria-expanded={isLanguageDropdownOpen}
|
|
474
760
|
aria-haspopup="listbox"
|
|
761
|
+
aria-controls="language-listbox"
|
|
475
762
|
>
|
|
476
763
|
{selectedLanguage}
|
|
477
764
|
<svg
|
|
@@ -488,12 +775,14 @@ export const RedactoNoticeConsent = ({
|
|
|
488
775
|
strokeLinecap="round"
|
|
489
776
|
strokeLinejoin="round"
|
|
490
777
|
style={{ flexShrink: 0, marginLeft: "4px" }}
|
|
778
|
+
aria-hidden="true"
|
|
491
779
|
>
|
|
492
780
|
<path d="M4 6l4 4 4-4" />
|
|
493
781
|
</svg>
|
|
494
782
|
</button>
|
|
495
783
|
{isLanguageDropdownOpen && (
|
|
496
784
|
<ul
|
|
785
|
+
id="language-listbox"
|
|
497
786
|
style={{
|
|
498
787
|
...styles.languageDropdown,
|
|
499
788
|
...languageSelectorStyle,
|
|
@@ -525,6 +814,7 @@ export const RedactoNoticeConsent = ({
|
|
|
525
814
|
</div>
|
|
526
815
|
</div>
|
|
527
816
|
<div
|
|
817
|
+
id="privacy-notice-description"
|
|
528
818
|
style={{
|
|
529
819
|
...styles.middleSection,
|
|
530
820
|
...responsiveStyles.middleSection,
|
|
@@ -548,6 +838,7 @@ export const RedactoNoticeConsent = ({
|
|
|
548
838
|
href={content?.privacy_policy_url || "#"}
|
|
549
839
|
target="_blank"
|
|
550
840
|
rel="noopener noreferrer"
|
|
841
|
+
aria-label="Privacy Policy (opens in new tab)"
|
|
551
842
|
>
|
|
552
843
|
[
|
|
553
844
|
{getTranslatedText(
|
|
@@ -560,7 +851,9 @@ export const RedactoNoticeConsent = ({
|
|
|
560
851
|
<a
|
|
561
852
|
style={{ ...styles.link, ...linkStyle }}
|
|
562
853
|
href={content?.sub_processors_url || "#"}
|
|
563
|
-
target=""
|
|
854
|
+
target="_blank"
|
|
855
|
+
rel="noopener noreferrer"
|
|
856
|
+
aria-label="Vendors List (opens in new tab)"
|
|
564
857
|
>
|
|
565
858
|
[
|
|
566
859
|
{getTranslatedText(
|
|
@@ -590,6 +883,16 @@ export const RedactoNoticeConsent = ({
|
|
|
590
883
|
<div
|
|
591
884
|
style={styles.optionLeft}
|
|
592
885
|
onClick={() => togglePurposeCollapse(purpose.uuid)}
|
|
886
|
+
role="button"
|
|
887
|
+
tabIndex={0}
|
|
888
|
+
aria-expanded={!collapsedPurposes[purpose.uuid]}
|
|
889
|
+
aria-controls={`purpose-${purpose.uuid}`}
|
|
890
|
+
onKeyDown={(e) => {
|
|
891
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
892
|
+
e.preventDefault();
|
|
893
|
+
togglePurposeCollapse(purpose.uuid);
|
|
894
|
+
}
|
|
895
|
+
}}
|
|
593
896
|
>
|
|
594
897
|
<div
|
|
595
898
|
style={{
|
|
@@ -615,6 +918,7 @@ export const RedactoNoticeConsent = ({
|
|
|
615
918
|
: "rotate(0deg)",
|
|
616
919
|
transition: "transform 0.3s ease",
|
|
617
920
|
}}
|
|
921
|
+
aria-hidden="true"
|
|
618
922
|
>
|
|
619
923
|
<path d="M1 1L6 6L1 11" />
|
|
620
924
|
</svg>
|
|
@@ -649,18 +953,26 @@ export const RedactoNoticeConsent = ({
|
|
|
649
953
|
</div>
|
|
650
954
|
</div>
|
|
651
955
|
<input
|
|
652
|
-
|
|
956
|
+
className="redacto-checkbox-large"
|
|
653
957
|
type="checkbox"
|
|
654
958
|
checked={selectedPurposes[purpose.uuid] || false}
|
|
655
959
|
onChange={() =>
|
|
656
960
|
handlePurposeCheckboxChange(purpose.uuid)
|
|
657
961
|
}
|
|
962
|
+
aria-label={`Select all data elements for ${getTranslatedText(
|
|
963
|
+
`purposes.name`,
|
|
964
|
+
purpose.name,
|
|
965
|
+
purpose.uuid
|
|
966
|
+
)}`}
|
|
658
967
|
/>
|
|
659
968
|
</div>
|
|
660
969
|
{!collapsedPurposes[purpose.uuid] &&
|
|
661
970
|
purpose.data_elements &&
|
|
662
971
|
purpose.data_elements.length > 0 && (
|
|
663
|
-
<div
|
|
972
|
+
<div
|
|
973
|
+
id={`purpose-${purpose.uuid}`}
|
|
974
|
+
style={styles.dataElementsContainer}
|
|
975
|
+
>
|
|
664
976
|
{purpose.data_elements.map((dataElement) => (
|
|
665
977
|
<div
|
|
666
978
|
key={dataElement.uuid}
|
|
@@ -684,13 +996,14 @@ export const RedactoNoticeConsent = ({
|
|
|
684
996
|
color: "red",
|
|
685
997
|
marginLeft: "4px",
|
|
686
998
|
}}
|
|
999
|
+
aria-label="required"
|
|
687
1000
|
>
|
|
688
1001
|
*
|
|
689
1002
|
</span>
|
|
690
1003
|
)}
|
|
691
1004
|
</span>
|
|
692
1005
|
<input
|
|
693
|
-
|
|
1006
|
+
className="redacto-checkbox-small"
|
|
694
1007
|
type="checkbox"
|
|
695
1008
|
checked={
|
|
696
1009
|
selectedDataElements[
|
|
@@ -703,6 +1016,11 @@ export const RedactoNoticeConsent = ({
|
|
|
703
1016
|
purpose.uuid
|
|
704
1017
|
)
|
|
705
1018
|
}
|
|
1019
|
+
aria-label={`Select ${getTranslatedText(
|
|
1020
|
+
`data_elements.name`,
|
|
1021
|
+
dataElement.name,
|
|
1022
|
+
dataElement.uuid
|
|
1023
|
+
)}${dataElement.required ? " (required)" : ""}`}
|
|
706
1024
|
/>
|
|
707
1025
|
</div>
|
|
708
1026
|
))}
|
|
@@ -720,6 +1038,7 @@ export const RedactoNoticeConsent = ({
|
|
|
720
1038
|
}}
|
|
721
1039
|
>
|
|
722
1040
|
<button
|
|
1041
|
+
ref={lastFocusableRef}
|
|
723
1042
|
style={{
|
|
724
1043
|
...styles.button,
|
|
725
1044
|
...responsiveStyles.button,
|
|
@@ -729,6 +1048,10 @@ export const RedactoNoticeConsent = ({
|
|
|
729
1048
|
}}
|
|
730
1049
|
onClick={handleAccept}
|
|
731
1050
|
disabled={acceptDisabled}
|
|
1051
|
+
aria-label={getTranslatedText(
|
|
1052
|
+
"confirm_button_text",
|
|
1053
|
+
content?.confirm_button_text || "Accept"
|
|
1054
|
+
)}
|
|
732
1055
|
>
|
|
733
1056
|
{isSubmitting
|
|
734
1057
|
? "Processing..."
|
|
@@ -746,6 +1069,10 @@ export const RedactoNoticeConsent = ({
|
|
|
746
1069
|
}}
|
|
747
1070
|
onClick={handleDecline}
|
|
748
1071
|
disabled={isSubmitting}
|
|
1072
|
+
aria-label={getTranslatedText(
|
|
1073
|
+
"decline_button_text",
|
|
1074
|
+
content?.decline_button_text || "Decline"
|
|
1075
|
+
)}
|
|
749
1076
|
>
|
|
750
1077
|
{isSubmitting
|
|
751
1078
|
? "Processing..."
|