diff --git a/CHANGELOG.md b/CHANGELOG.md index 71884c63..e7153994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Small button option - Tabs also respond to the up/down keys on keyboard navigation as well as left/right - Allow other image sources in a card image using a `` element +- Cookie banner and cookie handling ### Changed diff --git a/src/nationalarchives/all.mjs b/src/nationalarchives/all.mjs index bcb44e99..6146154e 100644 --- a/src/nationalarchives/all.mjs +++ b/src/nationalarchives/all.mjs @@ -5,6 +5,7 @@ import { Header } from "./components/header/header.mjs"; import { Picture } from "./components/picture/picture.mjs"; import { SensitiveImage } from "./components/sensitive-image/sensitive-image.mjs"; import { Tabs } from "./components/tabs/tabs.mjs"; +import Cookies from "./lib/cookies.mjs"; const $body = document.documentElement; $body.classList.add("tna-template--js-enabled"); @@ -88,6 +89,7 @@ const initAll = (options) => { export { initAll, + Cookies, Breadcrumbs, CookieBanner, Gallery, diff --git a/src/nationalarchives/components/cookie-banner/cookie-banner.mjs b/src/nationalarchives/components/cookie-banner/cookie-banner.mjs index 05513291..c18b9864 100644 --- a/src/nationalarchives/components/cookie-banner/cookie-banner.mjs +++ b/src/nationalarchives/components/cookie-banner/cookie-banner.mjs @@ -1,22 +1,104 @@ +import Cookies from "../../lib/cookies.mjs"; + export class CookieBanner { constructor($module) { this.$module = $module; - this.$accept = $module && $module.querySelector('[value="accept"]'); - this.$reject = $module && $module.querySelector('[value="reject"]'); + this.$acceptButton = $module && $module.querySelector('[value="accept"]'); + this.$rejectButton = $module && $module.querySelector('[value="reject"]'); + this.$prompt = + $module && $module.querySelector(".tna-cookie-banner__message--prompt"); + this.$acceptedMessage = + $module && $module.querySelector(".tna-cookie-banner__message--accepted"); + this.$rejectedMessage = + $module && $module.querySelector(".tna-cookie-banner__message--rejected"); + this.$closeButtons = $module && $module.querySelectorAll('[value="close"]'); } init() { - if (!this.$module || !this.$accept || !this.$reject) { + if ( + !this.$module || + !this.$acceptButton || + !this.$rejectButton || + !this.$prompt || + !this.$acceptedMessage || + !this.$rejectedMessage || + !this.$closeButtons + ) { + return; + } + + const policies = this.$module.getAttribute("data-policies"); + if (!policies) { return; } + this.cookies = new Cookies( + policies.split(",").map((policy) => policy.trim()), + ); + + this.loadScriptsOnAccept = this.$module.getAttribute("data-acceptscripts"); + + this.hideCookieBannerKey = this.$module.getAttribute("data-hidekey"); + const cookiePreferencesSet = this.cookies.exists(this.hideCookieBannerKey); + + if (!cookiePreferencesSet) { + this.$module.removeAttribute("hidden"); + + this.$acceptButton.addEventListener("click", () => this.accept()); + this.$rejectButton.addEventListener("click", () => this.reject()); + + this.$closeButtons.forEach(($closeButton) => { + $closeButton.addEventListener("click", () => this.close()); + }); + } + + // ==================== DEV ==================== + // document.getElementById("reset").addEventListener("click", () => { + // this.cookies.delete(this.hideCookieBannerKey); + // this.cookies.delete("cookies_policy"); + // window.scrollY = 0; + // window.location = window.location; + // }); + // document + // .getElementById("accept-analytics-policy") + // .addEventListener("click", () => this.cookies.acceptPolicy("analytics")); + // document + // .getElementById("reject-analytics-policy") + // .addEventListener("click", () => this.cookies.rejectPolicy("analytics")); + // document + // .getElementById("accept-settings-policy") + // .addEventListener("click", () => this.cookies.acceptPolicy("settings")); + // document + // .getElementById("reject-settings-policy") + // .addEventListener("click", () => this.cookies.rejectPolicy("settings")); + // ==================== END ==================== + } - this.$module.removeAttribute("hidden"); + accept() { + this.$prompt.setAttribute("hidden", true); + this.complete(); + this.$acceptedMessage.removeAttribute("hidden"); + this.cookies.acceptAllPolicies(); + if (this.loadScriptsOnAccept) { + this.loadScriptsOnAccept.split(",").forEach((script) => { + const $script = document.createElement("script"); + $script.src = script; + document.head.appendChild($script); + }); + } + } - this.$accept.addEventListener("click", () => this.accept()); - this.$reject.addEventListener("click", () => this.reject()); + reject() { + this.$prompt.setAttribute("hidden", true); + this.complete(); + this.$rejectedMessage.removeAttribute("hidden"); + this.cookies.rejectAllPolicies(); } - accept() {} + complete() { + this.cookies.set(this.hideCookieBannerKey, true); + } - reject() {} + close() { + this.$module.setAttribute("hidden", true); + } } diff --git a/src/nationalarchives/components/cookie-banner/cookie-banner.scss b/src/nationalarchives/components/cookie-banner/cookie-banner.scss index 7e89c85e..7a6219e3 100644 --- a/src/nationalarchives/components/cookie-banner/cookie-banner.scss +++ b/src/nationalarchives/components/cookie-banner/cookie-banner.scss @@ -1,5 +1,7 @@ @use "../../tools/colour"; @use "../../tools/spacing"; +@use "../../tools/media"; +@use "../../tools/typography"; @use "../../utilities"; @use "../button"; @use "../grid"; @@ -9,4 +11,22 @@ padding-top: 2rem; padding-bottom: 2rem; + + @include media.on-tiny { + padding-top: 1rem; + padding-bottom: 1rem; + + @include typography.relative-font-size(16); + } + + &__message { + &--prompt { + } + + &--accepted { + } + + &--rejected { + } + } } diff --git a/src/nationalarchives/components/cookie-banner/cookie-banner.stories.js b/src/nationalarchives/components/cookie-banner/cookie-banner.stories.js index 37e916a8..d49c3ed8 100644 --- a/src/nationalarchives/components/cookie-banner/cookie-banner.stories.js +++ b/src/nationalarchives/components/cookie-banner/cookie-banner.stories.js @@ -1,8 +1,14 @@ import CookieBanner from "./template.njk"; import macroOptions from "./macro-options.json"; +import { expect } from "@storybook/jest"; +import { within, userEvent } from "@storybook/testing-library"; +import Cookies from "../../lib/cookies.mjs"; const argTypes = { url: { control: "text" }, + policies: { control: "text" }, + hideCookieBannerKey: { control: "text" }, + loadScriptsOnAccept: { control: "text" }, classes: { control: "text" }, attributes: { control: "object" }, }; @@ -18,10 +24,20 @@ export default { argTypes, }; -const Template = ({ url, classes, attributes }) => +const Template = ({ + url, + policies, + hideCookieBannerKey, + loadScriptsOnAccept, + classes, + attributes, +}) => CookieBanner({ params: { url, + policies, + hideCookieBannerKey, + loadScriptsOnAccept, classes, attributes, }, @@ -32,3 +48,117 @@ Standard.args = { cookiesUrl: "#", classes: "tna-cookie-banner--demo", }; + +const deleteAllCookies = (cookies) => { + Object.keys(cookies.all).forEach((cookie) => cookies.delete(cookie)); +}; + +export const Accept = Template.bind({}); +Accept.args = { + cookiesUrl: "#", + classes: "tna-cookie-banner--demo", +}; +Accept.play = async ({ canvasElement }) => { + const cookies = new Cookies(); + deleteAllCookies(cookies); + + await expect(cookies.isPolicyAccepted("analytics")).toEqual(false); + await expect(cookies.isPolicyAccepted("settings")).toEqual(false); + await expect(cookies.isPolicyAccepted("unknown")).toEqual(null); + + const canvas = within(canvasElement); + const acceptButton = canvas.getByText("Accept cookies"); + await expect(acceptButton).toBeVisible(); + await userEvent.click(acceptButton); + + await expect(cookies.isPolicyAccepted("analytics")).toEqual(true); + await expect(cookies.isPolicyAccepted("settings")).toEqual(true); + await expect(cookies.isPolicyAccepted("unknown")).toEqual(null); + await expect(acceptButton).not.toBeVisible(); + + // const closeButton = canvas.getByText("Close this message"); + // await expect(closeButton).toBeVisible(); + // await userEvent.click(closeButton); + + // await expect(closeButton).not.toBeVisible(); + + deleteAllCookies(cookies); +}; + +export const Reject = Template.bind({}); +Reject.args = { + cookiesUrl: "#", + classes: "tna-cookie-banner--demo", +}; +Reject.play = async ({ canvasElement }) => { + const cookies = new Cookies(); + deleteAllCookies(cookies); + + await expect(cookies.isPolicyAccepted("analytics")).toEqual(false); + await expect(cookies.isPolicyAccepted("settings")).toEqual(false); + await expect(cookies.isPolicyAccepted("unknown")).toEqual(null); + + const canvas = within(canvasElement); + const rejectButton = canvas.getByText("Reject cookies"); + await expect(rejectButton).toBeVisible(); + await userEvent.click(rejectButton); + + await expect(cookies.isPolicyAccepted("analytics")).toEqual(false); + await expect(cookies.isPolicyAccepted("settings")).toEqual(false); + await expect(cookies.isPolicyAccepted("unknown")).toEqual(null); + + deleteAllCookies(cookies); +}; + +export const CustomPolicies = Template.bind({}); +CustomPolicies.args = { + cookiesUrl: "#", + policies: "custom", + classes: "tna-cookie-banner--demo", +}; +CustomPolicies.play = async ({ args, canvasElement }) => { + const cookies = new Cookies(args.policies.split(",")); + deleteAllCookies(cookies); + + await expect(cookies.isPolicyAccepted("analytics")).toEqual(null); + await expect(cookies.isPolicyAccepted("settings")).toEqual(null); + await expect(cookies.isPolicyAccepted("custom")).toEqual(false); + + const canvas = within(canvasElement); + const acceptButton = canvas.getByText("Accept cookies"); + await userEvent.click(acceptButton); + + await expect(cookies.isPolicyAccepted("analytics")).toEqual(null); + await expect(cookies.isPolicyAccepted("settings")).toEqual(null); + await expect(cookies.isPolicyAccepted("custom")).toEqual(true); + + deleteAllCookies(cookies); +}; + +export const AddScriptsOnAccept = Template.bind({}); +AddScriptsOnAccept.args = { + cookiesUrl: "#", + loadScriptsOnAccept: "my-analytics-script.js", + classes: "tna-cookie-banner--demo", +}; +AddScriptsOnAccept.play = async ({ args, canvasElement }) => { + const cookies = new Cookies(); + deleteAllCookies(cookies); + + const noScript = document.querySelector( + `script[src="${args.loadScriptsOnAccept}"]`, + ); + await expect(noScript).toEqual(null); + + const canvas = within(canvasElement); + const acceptButton = canvas.getByText("Accept cookies"); + await userEvent.click(acceptButton); + + const script = document.querySelector( + `script[src="${args.loadScriptsOnAccept}"]`, + ); + await expect(script).toBeTruthy(); + + deleteAllCookies(cookies); + script.remove(); +}; diff --git a/src/nationalarchives/components/cookie-banner/macro-options.json b/src/nationalarchives/components/cookie-banner/macro-options.json index 4e4f75a2..bd7758a0 100644 --- a/src/nationalarchives/components/cookie-banner/macro-options.json +++ b/src/nationalarchives/components/cookie-banner/macro-options.json @@ -5,6 +5,24 @@ "required": true, "description": "" }, + { + "name": "policies", + "type": "string", + "required": false, + "description": "" + }, + { + "name": "hideCookieBannerKey", + "type": "string", + "required": false, + "description": "" + }, + { + "name": "loadScriptsOnAccept", + "type": "string", + "required": false, + "description": "" + }, { "name": "classes", "type": "string", diff --git a/src/nationalarchives/components/cookie-banner/template.njk b/src/nationalarchives/components/cookie-banner/template.njk index d7ff91d3..0b3d88a5 100644 --- a/src/nationalarchives/components/cookie-banner/template.njk +++ b/src/nationalarchives/components/cookie-banner/template.njk @@ -1,15 +1,103 @@ +{% from "nationalarchives/components/button/macro.njk" import tnaButton %} + {%- set containerClasses = [params.classes] if params.classes else [] -%} -