Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 1x 1x 1x 1x 1x 1x 4x 3x 1x 17x 17x 17x 8x 8x 17x 3x 3x 3x 3x 17x 17x 17x 17x 6x 1x 6x 6x 17x 8x | import React from 'react';
import { useIntl } from 'react-intl';
import Link from 'next/link';
import { BsCookie } from 'react-icons/bs';
import { clx } from '@/helpers';
import styles from './cookieConsent.module.css';
/**
* Where the visitor's answer is kept. `_app` reads the same key from its inline
* bootstrap so a returning visitor's grant is replayed into Consent Mode before
* gtag.js ever runs — otherwise every visit would start denied.
*/
export const CONSENT_STORAGE_KEY = 'cookie-consent';
export type ConsentChoice = 'granted' | 'denied';
/**
* @description The four Consent Mode v2 signals this site can move. Storage that
* is not tied to identifying the visitor (`functionality_storage`,
* `security_storage`) is granted by default in `_app` and never asked about,
* because it is exempt from prior consent.
*/
export const consentSignals = (choice: ConsentChoice) => ({
ad_storage: choice,
ad_user_data: choice,
ad_personalization: choice,
analytics_storage: choice,
});
/**
* @description Prior-consent gate for analytics storage. Until the visitor
* answers, `_app` has already told gtag every identifying signal is denied, so
* nothing is written; this component only ever moves that state.
*
* @example
* <CookieConsent />;
*
* @returns {JSX.Element | null}
*/
const CookieConsent = () => {
const { formatMessage } = useIntl();
const [needsAnswer, setNeedsAnswer] = React.useState(false);
React.useEffect(() => {
// Reading storage during render would make the server and client markup
// disagree, so the banner is decided after hydration.
try {
setNeedsAnswer(window.localStorage.getItem(CONSENT_STORAGE_KEY) === null);
} catch {
// Storage blocked (private mode, cookie-blocking extension). With
// nowhere to record an answer, asking on every page load is worse than
// staying on the denied defaults.
}
}, []);
const answer = React.useCallback((choice: ConsentChoice) => {
try {
window.localStorage.setItem(CONSENT_STORAGE_KEY, choice);
} catch {
// Same as above: the choice still applies to this page view.
}
window.gtag?.('consent', 'update', consentSignals(choice));
setNeedsAnswer(false);
}, []);
const handleAccept = React.useCallback(() => answer('granted'), [answer]);
const handleReject = React.useCallback(() => answer('denied'), [answer]);
// Escape answers "no". The default state is already denied, so dismissing without choosing must
// not be read as acceptance — and a notification the keyboard cannot dismiss is a trap.
React.useEffect(() => {
if (!needsAnswer) return undefined;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') handleReject();
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [needsAnswer, handleReject]);
if (!needsAnswer) return null;
return (
/**
* `role="dialog"` without `aria-modal`, and no focus trap: this is a macOS-style notification
* sitting beside the page, not a modal over it, so trapping focus would misdescribe it and
* strand a keyboard user. The previous markup paired `role="dialog"` with `aria-live`, which is
* invalid — a dialog is a window, not a live region — and made some screen readers announce the
* whole subtree as an atomic update while also reporting a dialog.
*/
<section
className={styles.notification}
role="dialog"
aria-labelledby="cookie-consent-title"
aria-describedby="cookie-consent-message"
>
<div className={styles.header}>
<BsCookie className={styles.icon} aria-hidden="true" />
<div className={styles.title} id="cookie-consent-title">
{formatMessage({ id: 'consent.title' })}
</div>
</div>
<div className={styles.message} id="cookie-consent-message">
{formatMessage({ id: 'consent.message' })}{' '}
<Link href="/legal/cookies-policy">{formatMessage({ id: 'legal.cookies-policy' })}</Link>
</div>
<div className={styles.actions}>
<button type="button" className={styles.button} onClick={handleReject} data-testid="consent-reject">
{formatMessage({ id: 'consent.reject' })}
</button>
<button
type="button"
className={clx(styles.button, styles.accept)}
onClick={handleAccept}
data-testid="consent-accept"
>
{formatMessage({ id: 'consent.accept' })}
</button>
</div>
</section>
);
};
export default CookieConsent;
|