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