From 104ad023ef60a4908e8d5b5c8ef6f888256e6f57 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 17 Nov 2021 15:44:53 +0200 Subject: [PATCH 001/292] Created a controller for cookie consents The controller handles cookie read/write actions and stores consents until user saves them to a cookie. Created cookie is accessible from *.hel.fi and *.hel.ninja (and any *.domain.suffix). Common conset 'names' are declared so same consents can be used between services and not asked again. Cookie's "secure" -property is set to 'false' so it is readable and settable without https and therefore in localhost. This is not considered as a security risk because of the insensitive content of the cookie and 'sameSite' is anyway set to 'strict' so the cookie is not sent to third party domains. Added a package for cookie read/write and some lodash helpers Added tests and mocks. --- packages/react/package.json | 6 +- .../__mocks__/mockUniversalCookie.ts | 68 +++ .../__mocks__/mockWindowLocation.ts | 36 ++ .../cookieConsentController.test.ts | 396 ++++++++++++++++++ .../cookieConsent/cookieConsentController.ts | 251 +++++++++++ yarn.lock | 59 ++- 6 files changed, 814 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts create mode 100644 packages/react/src/components/cookieConsent/__mocks__/mockWindowLocation.ts create mode 100644 packages/react/src/components/cookieConsent/cookieConsentController.test.ts create mode 100644 packages/react/src/components/cookieConsent/cookieConsentController.ts diff --git a/packages/react/package.json b/packages/react/package.json index e257fcfc85..822d900821 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -110,6 +110,9 @@ "lodash.isequal": "4.5.0", "lodash.isfunction": "3.0.9", "lodash.pickby": "^4.6.0", + "lodash.isobject": "3.0.2", + "lodash.isundefined": "3.0.1", + "lodash.pick": "^4.4.0", "lodash.uniqueid": "4.0.1", "lodash.xor": "^4.5.0", "react-merge-refs": "1.1.0", @@ -117,7 +120,8 @@ "react-spring": "9.3.0", "react-use-measure": "2.0.1", "react-virtual": "2.2.7", - "typescript": "4.5.5" + "typescript": "4.5.5", + "universal-cookie": "^4.0.4" }, "resolutions": { "@types/react": "17.0.2", diff --git a/packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts b/packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts new file mode 100644 index 0000000000..d5cb09f70d --- /dev/null +++ b/packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts @@ -0,0 +1,68 @@ +import { CookieSetOptions } from 'universal-cookie'; + +type CookieData = string | Record; +type UniversalCookieMocking = { + createMockedModule: () => unknown; + mockGet: () => string; + mockSet: (name: string, value: string, options: CookieSetOptions) => void; + reset: () => void; + setStoredCookie: (objectToStringify: CookieData) => void; + getSetCookieArguments: ( + index?: number, + ) => { + cookieName: string; + data: string; + options: CookieSetOptions; + }; +}; + +export function createUniversalCookieMockHelpers(): UniversalCookieMocking { + let cookieValue = ''; + + const getter = jest.fn(() => cookieValue); + + // eslint-disable-next-line no-unused-vars + const setter = jest.fn((name: string, value: string, options: CookieSetOptions) => { + cookieValue = value; + }); + + const setStoredCookie = (data: CookieData) => { + cookieValue = typeof data === 'string' ? data : JSON.stringify(data); + }; + + const reset = () => { + getter.mockClear(); + setter.mockClear(); + cookieValue = ''; + }; + + const createMockedModule = () => ({ + get: getter, + set: setter, + }); + + const getSetCookieArguments = ( + index = -1, + ): { + cookieName: string; + data: string; + options: CookieSetOptions; + } => { + const pos = index > -1 ? index : setter.mock.calls.length - 1; + const callArgs = setter.mock.calls[pos]; + return { + cookieName: callArgs[0], + data: callArgs[1], + options: callArgs[2], + }; + }; + + return { + createMockedModule, + mockGet: getter, + mockSet: setter, + reset, + setStoredCookie, + getSetCookieArguments, + }; +} diff --git a/packages/react/src/components/cookieConsent/__mocks__/mockWindowLocation.ts b/packages/react/src/components/cookieConsent/__mocks__/mockWindowLocation.ts new file mode 100644 index 0000000000..9943edca33 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__mocks__/mockWindowLocation.ts @@ -0,0 +1,36 @@ +export type MockedWindowLocationActions = { + setUrl: (url: string) => void; + restore: () => void; +}; +export default function mockWindowLocation(): MockedWindowLocationActions { + const globalWin = (global as unknown) as Window; + let oldWindowLocation: Location | undefined = globalWin.location; + let urlObject = new URL('https://default.domain.com'); + const location = Object.defineProperties( + {}, + { + ...Object.getOwnPropertyDescriptors(oldWindowLocation), + hostname: { + get: () => urlObject.hostname, + }, + }, + ); + Reflect.deleteProperty(globalWin, 'location'); + Reflect.defineProperty(globalWin, 'location', { + configurable: true, + value: location, + writable: true, + }); + + return { + setUrl: (url: string) => { + urlObject = new URL(url); + }, + restore: () => { + if (oldWindowLocation) { + globalWin.location = oldWindowLocation; + } + oldWindowLocation = undefined; + }, + }; +} diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts new file mode 100644 index 0000000000..b958313cf4 --- /dev/null +++ b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts @@ -0,0 +1,396 @@ +/* eslint-disable jest/no-mocks-import */ +import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; +import mockWindowLocation, { MockedWindowLocationActions } from './__mocks__/mockWindowLocation'; +import createConsentController, { + ConsentController, + ConsentList, + ConsentObject, + createStorage, +} from './cookieConsentController'; + +const mockCookieHelpers = createUniversalCookieMockHelpers(); + +jest.mock( + 'universal-cookie', + () => + function universalCookieMockClass() { + return mockCookieHelpers.createMockedModule(); + }, +); + +describe(`cookieConsentController.ts`, () => { + let controller: ConsentController; + let mockedWindowControls: MockedWindowLocationActions; + afterEach(() => { + mockCookieHelpers.reset(); + }); + beforeAll(() => { + mockedWindowControls = mockWindowLocation(); + }); + afterAll(() => { + mockedWindowControls.restore(); + }); + const defaultControllerTestData = { + requiredConsents: ['requiredConsent1', 'requiredConsent2'], + optionalConsents: ['optionalConsent1', 'optionalConsent2'], + cookie: {}, + }; + const createControllerAndInitCookie = ({ + requiredConsents, + optionalConsents, + cookie = {}, + }: { + requiredConsents?: ConsentList; + optionalConsents?: ConsentList; + cookie?: ConsentObject; + }) => { + mockCookieHelpers.setStoredCookie(cookie); + controller = createConsentController({ + requiredConsents, + optionalConsents, + }); + }; + describe('createConsentController', () => { + describe('initialization', () => { + it('parses requiredConsents and optionalConsents correctly', () => { + createControllerAndInitCookie(defaultControllerTestData); + expect(controller.getRequired()).toEqual({ + requiredConsent1: false, + requiredConsent2: false, + }); + expect(controller.getOptional()).toEqual({ + optionalConsent1: false, + optionalConsent2: false, + }); + }); + it('parses and stores unknown consents', () => { + const cookieDataWithUnknownConsents = { + unknownConsent1: true, + unknownConsent2: false, + unknownConsent3: true, + }; + const otherConsents = { + requiredConsent1: false, + requiredConsent2: false, + optionalConsent1: false, + optionalConsent2: false, + }; + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookie: cookieDataWithUnknownConsents, + }); + expect(controller.save()).toEqual({ + ...cookieDataWithUnknownConsents, + ...otherConsents, + }); + }); + it('parses props correctly without consents', () => { + createControllerAndInitCookie({}); + expect(controller.getRequired()).toEqual({}); + expect(controller.getOptional()).toEqual({}); + }); + it('throws when same consent is both optional and required', () => { + expect(() => + createConsentController({ + requiredConsents: ['consent1', 'consent2'], + optionalConsents: ['consent2', 'consent3'], + }), + ).toThrow(); + }); + it('restores saved consents correctly', () => { + const storedCookieData = { + requiredConsent1: true, + optionalConsent2: true, + unknownConsent1: true, + }; + const allConsents = { + ...storedCookieData, + requiredConsent2: false, + optionalConsent1: false, + }; + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookie: storedCookieData, + }); + expect(controller.getRequired()).toEqual({ + requiredConsent1: allConsents.requiredConsent1, + requiredConsent2: allConsents.requiredConsent2, + }); + expect(controller.getOptional()).toEqual({ + optionalConsent1: allConsents.optionalConsent1, + optionalConsent2: allConsents.optionalConsent2, + }); + expect(controller.save()).toEqual(allConsents); + }); + it('cookie is only read on init', () => { + createControllerAndInitCookie({}); + expect(mockCookieHelpers.mockGet).toHaveBeenCalledTimes(1); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + }); + }); + describe('unknown consents', () => { + it('are parsed, stored and never changed', () => { + const cookieDataWithUnknownConsents = { + unknownConsent1: true, + unknownConsent2: false, + unknownConsent3: true, + }; + const otherConsents = { + requiredConsent1: false, + requiredConsent2: false, + optionalConsent1: false, + optionalConsent2: false, + }; + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookie: cookieDataWithUnknownConsents, + }); + controller.approveAll(); + controller.approveRequired(); + controller.rejectAll(); + expect(controller.save()).toEqual({ + ...cookieDataWithUnknownConsents, + ...otherConsents, + }); + }); + }); + describe('approveAll', () => { + it('sets all consents to true. Setting consents does not store a new cookie', () => { + createControllerAndInitCookie(defaultControllerTestData); + controller.approveAll(); + expect(controller.getRequired()).toEqual({ + requiredConsent1: true, + requiredConsent2: true, + }); + expect(controller.getOptional()).toEqual({ + optionalConsent1: true, + optionalConsent2: true, + }); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + }); + }); + describe('approveRequired', () => { + it(`approves all required consents. + Optional are not affected. + Setting consents does not store a new cookie`, () => { + createControllerAndInitCookie(defaultControllerTestData); + controller.approveRequired(); + expect(controller.getRequired()).toEqual({ + requiredConsent1: true, + requiredConsent2: true, + }); + expect(controller.getOptional()).toEqual({ + optionalConsent1: false, + optionalConsent2: false, + }); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + }); + }); + describe('rejectAll', () => { + it('rejects all consents. Setting consents does not store a new cookie', () => { + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookie: { + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: true, + optionalConsent2: true, + }, + }); + + expect({ + ...controller.getRequired(), + ...controller.getOptional(), + }).toEqual({ + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: true, + optionalConsent2: true, + }); + + controller.rejectAll(); + + expect({ + ...controller.getRequired(), + ...controller.getOptional(), + }).toEqual({ + requiredConsent1: false, + requiredConsent2: false, + optionalConsent1: false, + optionalConsent2: false, + }); + + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + }); + }); + describe('getUnhandledConsents', () => { + it('returns consents that are not defined in previously saved cookie.', () => { + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookie: { + requiredConsent1: true, + optionalConsent2: true, + unknownCookie1: false, + }, + }); + const unhandled = controller.getUnhandledConsents(); + expect(unhandled.includes('requiredConsent2')).toBeTruthy(); + expect(unhandled.includes('optionalConsent1')).toBeTruthy(); + expect(unhandled.includes('unknownCookie1')).toBeFalsy(); + }); + }); + describe('getRequiredWithoutConsent', () => { + it('returns required consents that are not currently approved', () => { + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookie: { + requiredConsent1: true, + optionalConsent2: true, + }, + }); + const unhandled = controller.getUnhandledConsents(); + expect(unhandled.includes('requiredConsent2')).toBeTruthy(); + expect(unhandled.includes('optionalConsent1')).toBeTruthy(); + }); + }); + describe('update', () => { + const allConsents = [ + ...defaultControllerTestData.requiredConsents, + ...defaultControllerTestData.optionalConsents, + ]; + it('sets a consent true/false', () => { + createControllerAndInitCookie(defaultControllerTestData); + + const values = [true, false]; + + values.forEach((value) => { + allConsents.forEach((consent) => { + controller.update(consent, value); + }); + expect({ + ...controller.getRequired(), + ...controller.getOptional(), + }).toEqual({ + requiredConsent1: value, + requiredConsent2: value, + optionalConsent1: value, + optionalConsent2: value, + }); + }); + }); + it('Throws when setting an unknown consent', () => { + createControllerAndInitCookie(defaultControllerTestData); + expect(() => controller.update('consentX', false)).toThrow(); + }); + it('Does not auto save the cookie', () => { + allConsents.forEach((consent) => { + controller.update(consent, true); + }); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + }); + }); + describe('save', () => { + it('stores the data into a cookie', () => { + createControllerAndInitCookie(defaultControllerTestData); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + controller.approveRequired(); + controller.save(); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: false, + optionalConsent2: false, + }); + }); + it('the domain of the cookie is set to . so it is readable from *.hel.fi and *.hel.ninja', () => { + createControllerAndInitCookie(defaultControllerTestData); + mockedWindowControls.setUrl('https://subdomain.hel.fi'); + controller.save(); + expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.fi'); + mockedWindowControls.setUrl('http://profiili.hel.ninja:3000?foo=bar'); + controller.save(); + expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.ninja'); + }); + }); + }); + describe('createStorage', () => { + const createTestStorage = () => + createStorage({ + required: { + requiredConsent1: false, + requiredConsent2: true, + }, + optional: { + optionalConsent1: false, + optionalConsent2: true, + }, + unknown: { + unknownConsent1: false, + unknownConsent2: true, + }, + }); + + it('Updates given consent and returns a new copy of data without mutating old data', () => { + const storage = createTestStorage(); + const initialVersion = storage.getAll(); + let unknownConsents = initialVersion.unknown as ConsentObject; + expect(initialVersion.required.requiredConsent1).toBeFalsy(); + expect(initialVersion.required.requiredConsent2).toBeTruthy(); + expect(initialVersion.optional.optionalConsent1).toBeFalsy(); + expect(initialVersion.optional.optionalConsent2).toBeTruthy(); + expect(unknownConsents.unknownConsent1).toBeFalsy(); + expect(unknownConsents.unknownConsent2).toBeTruthy(); + + const approvedVersion = storage.approve(['requiredConsent1', 'optionalConsent1']); + unknownConsents = approvedVersion.unknown as ConsentObject; + expect(approvedVersion.required.requiredConsent1).toBeTruthy(); + expect(approvedVersion.required.requiredConsent2).toBeTruthy(); + expect(approvedVersion.optional.optionalConsent1).toBeTruthy(); + expect(approvedVersion.optional.optionalConsent2).toBeTruthy(); + // initial version is unchanged + expect(initialVersion.required.requiredConsent1).toBeFalsy(); + expect(initialVersion.optional.optionalConsent1).toBeFalsy(); + // unknown consents are unchanged + expect(unknownConsents.unknownConsent1).toBeFalsy(); + expect(unknownConsents.unknownConsent2).toBeTruthy(); + + const rejectedVersion = storage.reject([ + 'requiredConsent1', + 'requiredConsent2', + 'optionalConsent1', + 'optionalConsent2', + ]); + unknownConsents = rejectedVersion.unknown as ConsentObject; + expect(rejectedVersion.required.requiredConsent1).toBeFalsy(); + expect(rejectedVersion.required.requiredConsent2).toBeFalsy(); + expect(rejectedVersion.optional.optionalConsent1).toBeFalsy(); + expect(rejectedVersion.optional.optionalConsent2).toBeFalsy(); + // previous version is unchanged + expect(approvedVersion.required.requiredConsent1).toBeTruthy(); + expect(approvedVersion.required.requiredConsent2).toBeTruthy(); + expect(approvedVersion.optional.optionalConsent1).toBeTruthy(); + expect(approvedVersion.optional.optionalConsent2).toBeTruthy(); + // unknown consents are unchanged + expect(unknownConsents.unknownConsent1).toBeFalsy(); + expect(unknownConsents.unknownConsent2).toBeTruthy(); + }); + it('Throws when setting an unknown consent', () => { + const storage = createTestStorage(); + expect(() => storage.approve(['consentX'])).toThrow(); + expect(() => storage.reject(['consentX'])).toThrow(); + expect(() => storage.approve(['unknownConsent1'])).toThrow(); + expect(() => storage.reject(['unknownConsent2'])).toThrow(); + }); + it('getConsentByName returns true/false even for unknown consents. Unknown consents are false', () => { + const storage = createTestStorage(); + expect(storage.getConsentByName('requiredConsent1')).toBeFalsy(); + expect(storage.getConsentByName('requiredConsent2')).toBeTruthy(); + expect(storage.getConsentByName('optionalConsent1')).toBeFalsy(); + expect(storage.getConsentByName('optionalConsent2')).toBeTruthy(); + expect(storage.getConsentByName('consentZ')).toBeFalsy(); + expect(storage.getConsentByName('unknownConsent1')).toBeFalsy(); + expect(storage.getConsentByName('unknownConsent2')).toBeFalsy(); + }); + }); +}); diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.ts b/packages/react/src/components/cookieConsent/cookieConsentController.ts new file mode 100644 index 0000000000..ebaeb79ced --- /dev/null +++ b/packages/react/src/components/cookieConsent/cookieConsentController.ts @@ -0,0 +1,251 @@ +import _pick from 'lodash.pick'; +import _isObject from 'lodash.isobject'; +import _isUndefined from 'lodash.isundefined'; +import CookieController, { CookieSetOptions } from 'universal-cookie'; + +export type ConsentList = string[]; + +export type ConsentObject = { [x: string]: boolean }; + +export type ConsentStorage = { + required: ConsentObject; + optional: ConsentObject; + unknown?: ConsentObject; +}; + +export type ConsentControllerProps = { + requiredConsents?: ConsentList; + optionalConsents?: ConsentList; +}; + +export type ConsentController = { + getRequired: () => ConsentObject; + getOptional: () => ConsentObject; + update: (key: string, value: boolean) => void; + approveRequired: () => void; + approveAll: () => void; + rejectAll: () => void; + getUnhandledConsents: () => string[]; + getRequiredWithoutConsent: () => string[]; + save: () => ConsentObject; +}; + +const COOKIE_NAME = 'city-of-helsinki-cookie-consents'; + +export const commonConsents = { + matomo: 'matomo', + tunnistamo: 'tunnistamo', + language: 'language', + marketing: 'marketing', + preferences: 'preferences', +}; + +function convertStringArrayToKeyConsentObject(array: string[]): ConsentObject { + return array.reduce((current, key) => { + // eslint-disable-next-line no-param-reassign + current[key] = false; + return current; + }, {} as ConsentObject); +} + +function mergeConsents(set1: ConsentObject, set2: ConsentObject, set3?: ConsentObject): ConsentObject { + return { ...set1, ...set2, ...set3 }; +} + +function parseConsents(jsonString: string | undefined): ConsentObject { + if (!jsonString || jsonString.length < 2 || jsonString.charAt(0) !== '{') { + return {}; + } + try { + return JSON.parse(jsonString); + } catch (e) { + return {}; + } +} + +function createConsentsString(consents: ConsentObject): string { + if (!_isObject(consents)) { + return '{}'; + } + return JSON.stringify(consents); +} + +function createCookieController(): { + get: () => string; + set: (data: string) => void; +} { + const cookieController = new CookieController(); + const oneYearInSeconds = 60 * 60 * 24 * 365; + const defaultCookieSetOptions: CookieSetOptions = { + path: '/', + secure: false, + sameSite: 'strict', + maxAge: oneYearInSeconds, + }; + + const getCookieDomainFromUrl = (): string => window.location.hostname.split('.').slice(-2).join('.'); + + const createCookieOptions = (): CookieSetOptions => ({ + ...defaultCookieSetOptions, + domain: getCookieDomainFromUrl(), + }); + + const get = (): string => cookieController.get(COOKIE_NAME, { doNotParse: true }) || ''; + + const set = (data: string): void => { + cookieController.set(COOKIE_NAME, data, createCookieOptions()); + }; + return { + get, + set, + }; +} + +export function createStorage( + initialValues: ConsentStorage, +): { + getAll: () => ConsentStorage; + getConsentByName: (consentName: string) => boolean; + approve: (keys: string[]) => ConsentStorage; + reject: (keys: string[]) => ConsentStorage; +} { + let storage: ConsentStorage = { ...initialValues }; + + const getStorage = () => storage; + + const copyStorage = (): ConsentStorage => ({ + optional: { ...storage.optional }, + required: { ...storage.required }, + unknown: { ...storage.unknown }, + }); + + const updateStorage = (newStorage: ConsentStorage): void => { + storage = newStorage; + }; + + const findConsentSource = (consentName: string, targetStorage: ConsentStorage): ConsentObject | undefined => { + if (!_isUndefined(targetStorage.required[consentName])) { + return targetStorage.required; + } + if (!_isUndefined(targetStorage.optional[consentName])) { + return targetStorage.optional; + } + return undefined; + }; + + const updateConsent = (targetStorage: ConsentStorage, consentName: string, value: boolean): void => { + const target = findConsentSource(consentName, targetStorage); + if (!target) { + throw new Error(`Unknown consent ${consentName}`); + } + target[consentName] = value; + }; + + const setConsents = (keys: string[], value: boolean): ConsentStorage => { + const copiedStorage = copyStorage(); + keys.forEach((key) => { + updateConsent(copiedStorage, key, value); + }); + updateStorage(copiedStorage); + return copiedStorage; + }; + + const getConsentByName = (consentName: string): boolean => { + const target = findConsentSource(consentName, getStorage()); + if (!target) { + return false; + } + return target[consentName]; + }; + + const approve = (keys: string[]): ConsentStorage => setConsents(keys, true); + + const reject = (keys: string[]): ConsentStorage => setConsents(keys, false); + + return { + getAll: () => getStorage(), + getConsentByName, + approve, + reject, + }; +} + +function verifyConsentProps({ optionalConsents, requiredConsents }: ConsentControllerProps) { + if (!requiredConsents || !optionalConsents) { + return; + } + requiredConsents.forEach((consent) => { + if (optionalConsents.includes(consent)) { + throw new Error(`optional consent '${consent}' found in requiredConsents.`); + } + }); +} + +export default function createConsentController(props: ConsentControllerProps): ConsentController { + verifyConsentProps(props); + const { optionalConsents = [], requiredConsents = [] } = props; + const allConsents = [...optionalConsents, ...requiredConsents]; + const cookieController = createCookieController(); + const currentConsentsInCookie = parseConsents(cookieController.get()); + + const required = mergeConsents( + convertStringArrayToKeyConsentObject(requiredConsents), + _pick(currentConsentsInCookie, requiredConsents), + ); + + const optional = mergeConsents( + convertStringArrayToKeyConsentObject(optionalConsents), + _pick(currentConsentsInCookie, optionalConsents), + ); + + const unknownConsentKeys = Object.keys(currentConsentsInCookie).filter((key) => !allConsents.includes(key)); + + const unknown = unknownConsentKeys.length ? _pick(currentConsentsInCookie, unknownConsentKeys) : undefined; + + const storage = createStorage({ required, optional, unknown }); + + const getConsents = () => storage.getAll(); + + const save = () => { + const currentVersion = getConsents(); + const consents = mergeConsents(currentVersion.required, currentVersion.optional, currentVersion.unknown); + cookieController.set(createConsentsString(consents)); + return consents; + }; + + const rejectAll = () => { + storage.reject(allConsents); + }; + + const approveRequired = () => { + storage.approve(requiredConsents); + }; + + const approveAll = () => { + storage.approve(allConsents); + }; + + const update = (consentName: string, value: boolean) => { + const arr = [consentName]; + if (value) { + storage.approve(arr); + } else { + storage.reject(arr); + } + }; + + return { + getRequired: () => storage.getAll().required, + getOptional: () => storage.getAll().optional, + update, + approveAll, + approveRequired, + getRequiredWithoutConsent: () => requiredConsents.filter((key) => storage.getConsentByName(key) !== true), + rejectAll, + getUnhandledConsents: () => { + const storedCookies = parseConsents(cookieController.get()); + return allConsents.filter((key) => _isUndefined(storedCookies[key])); + }, + save, + }; +} diff --git a/yarn.lock b/yarn.lock index d59aa45b91..5025952b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7107,6 +7107,11 @@ resolved "https://registry.yarnpkg.com/@types/configstore/-/configstore-2.1.1.tgz#cd1e8553633ad3185c3f2f239ecff5d2643e92b6" integrity sha512-YY+hm3afkDHeSM2rsFXxeZtu0garnusBWNG1+7MknmDWQHqcH2w21/xOU9arJUi8ch4qyFklidANLCu3ihhVwQ== +"@types/cookie@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" + integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== + "@types/cookie@^0.4.0": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" @@ -11356,6 +11361,11 @@ cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@^0.4.0, cookie@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -19567,6 +19577,36 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isnumber@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isobject@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.isundefined@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" + integrity sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g= + +lodash.iteratee@^4.5.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz#be4177db289a8ccc3c0990f1db26b5b22fc1554c" + integrity sha1-vkF32yiajMw8CZDx2ya1si/BVUw= + lodash.map@^4.4.0, lodash.map@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -19587,7 +19627,7 @@ lodash.merge@4.6.2, lodash.merge@^4.4.0, lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.pick@^4.2.1: +lodash.pick@^4.2.1, lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= @@ -28003,6 +28043,23 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.4.1: dependencies: unist-util-visit-parents "^2.0.0" +unist-util-visit@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5" + integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + +universal-cookie@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" + integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== + dependencies: + "@types/cookie" "^0.3.3" + cookie "^0.4.0" + universal-user-agent@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" From 7b2f3bc135e3ac9178f22601d653f51c2966fe45 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 17 Nov 2021 15:46:08 +0200 Subject: [PATCH 002/292] Created React context for cookie consents The current state cannot be in one component, so context stores and shares the state. Status of the consents must be readable from multiple components like matomo, i18n and so on. Also some components may need to hide themselves from screen readers and prevent scrolling etc while dialog is open. The context also simplifies the usage of the cookie consent controller by providing helpers like "willRenderCookieConsentDialog" and "hasUserHandledAllConsents" --- .../CookieConsentContext.test.tsx | 180 ++++++++++++++++++ .../cookieConsent/CookieConsentContext.tsx | 78 ++++++++ 2 files changed, 258 insertions(+) create mode 100644 packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx create mode 100644 packages/react/src/components/cookieConsent/CookieConsentContext.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx new file mode 100644 index 0000000000..c43fa80adf --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -0,0 +1,180 @@ +/* eslint-disable jest/expect-expect */ +/* eslint-disable jest/no-mocks-import */ +import React, { useContext } from 'react'; +import { render, RenderResult } from '@testing-library/react'; + +import { ConsentList, ConsentObject } from './cookieConsentController'; +import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; +import { CookieConsentContext, Provider as CookieContextProvider } from './CookieConsentContext'; + +type ConsentData = { + requiredConsents?: ConsentList; + optionalConsents?: ConsentList; + cookie?: ConsentObject; +}; + +const mockCookieHelpers = createUniversalCookieMockHelpers(); + +jest.mock( + 'universal-cookie', + () => + function universalCookieMockClass() { + return mockCookieHelpers.createMockedModule(); + }, +); + +describe('CookieConsentContext ', () => { + const allApprovedConsentData = { + requiredConsents: ['requiredConsent1'], + optionalConsents: ['optionalConsent1'], + cookie: { + requiredConsent1: true, + optionalConsent1: true, + }, + }; + const allNotApprovedConsentData = { + ...allApprovedConsentData, + cookie: { + requiredConsent1: false, + optionalConsent1: false, + }, + }; + + const unknownConsents = { + unknownConsent1: true, + unknownConsent2: false, + }; + + const ContextConsumer = ({ consumerId }: { consumerId: string }) => { + const { willRenderCookieConsentDialog, hasUserHandledAllConsents, approveAll, save } = useContext( + CookieConsentContext, + ); + const allUserConsentsAreHandled = hasUserHandledAllConsents(); + const onButtonClick = () => { + approveAll(); + save(); + }; + return ( +
+ {allUserConsentsAreHandled && } + {!allUserConsentsAreHandled && } + {willRenderCookieConsentDialog && } + {!willRenderCookieConsentDialog && } + +
+ ); + }; + + const onAllConsentsGiven = jest.fn(); + const onConsentsParsed = jest.fn(); + + afterEach(() => { + onAllConsentsGiven.mockReset(); + onConsentsParsed.mockReset(); + mockCookieHelpers.reset(); + }); + + const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { + expect(result.getAllByTestId(testId)).toHaveLength(1); + }; + + const verifyElementDoesNotExistsByTestId = (result: RenderResult, testId: string) => { + expect(() => result.getAllByTestId(testId)).toThrow(); + }; + + const verifyConsumersShowConsentsHandled = (result: RenderResult) => { + verifyElementExistsByTestId(result, 'consumer-1-all-handled'); + verifyElementDoesNotExistsByTestId(result, 'consumer-1-should-render'); + verifyElementExistsByTestId(result, 'consumer-2-all-handled'); + verifyElementDoesNotExistsByTestId(result, 'consumer-2-should-render'); + }; + const verifyConsumersShowConsentsNotHandled = (result: RenderResult) => { + verifyElementDoesNotExistsByTestId(result, 'consumer-1-all-handled'); + verifyElementExistsByTestId(result, 'consumer-1-should-render'); + verifyElementDoesNotExistsByTestId(result, 'consumer-2-all-handled'); + verifyElementExistsByTestId(result, 'consumer-2-should-render'); + }; + + const consumer1ApproveAllButtonSelector = 'consumer-1-approve-all-button'; + + const clickElement = (result: RenderResult, testId: string) => { + result.getByTestId(testId).click(); + }; + + const renderCookieConsent = ({ + requiredConsents = [], + optionalConsents = [], + cookie = {}, + }: ConsentData): RenderResult => { + // inject unknown consents to verify those are + // stored and handled, but not required or optional + const cookieWithInjectedUnknowns = { + ...cookie, + ...unknownConsents, + }; + mockCookieHelpers.setStoredCookie(cookieWithInjectedUnknowns); + return render( + + + + , + ); + }; + + describe('willRenderCookieConsentDialog ', () => { + it('is false all consents are true/false', () => { + verifyConsumersShowConsentsNotHandled(renderCookieConsent(allNotApprovedConsentData)); + }); + it('is true if user has unhandled consents', () => { + verifyConsumersShowConsentsHandled(renderCookieConsent(allApprovedConsentData)); + }); + it('reflects the value of hasUserHandledAllConsents and both change with approval', () => { + const result = renderCookieConsent(allNotApprovedConsentData); + verifyConsumersShowConsentsNotHandled(result); + clickElement(result, consumer1ApproveAllButtonSelector); + verifyConsumersShowConsentsHandled(result); + }); + }); + describe('onConsentsParsed is called when context is created and controller has read the cookie. ', () => { + it('Arguments are ({consents}, false) when user has not handled all consents', () => { + renderCookieConsent(allNotApprovedConsentData); + expect(onConsentsParsed).toHaveBeenCalledTimes(1); + expect(onConsentsParsed).toHaveBeenLastCalledWith(allNotApprovedConsentData.cookie, false); + }); + it('Arguments are ({consents}, true) when user has given all consents', () => { + renderCookieConsent(allNotApprovedConsentData); + expect(onConsentsParsed).toHaveBeenCalledTimes(1); + expect(onConsentsParsed).toHaveBeenLastCalledWith(allNotApprovedConsentData.cookie, false); + }); + }); + describe('onAllConsentsGiven ', () => { + it('is called only when user has given all consents.', () => { + const result = renderCookieConsent(allNotApprovedConsentData); + expect(onAllConsentsGiven).toHaveBeenCalledTimes(0); + clickElement(result, consumer1ApproveAllButtonSelector); + expect(onAllConsentsGiven).toHaveBeenCalledTimes(1); + expect(onAllConsentsGiven).toHaveBeenLastCalledWith(allApprovedConsentData.cookie); + }); + it('is not called even if consents are initially given', () => { + renderCookieConsent(allApprovedConsentData); + expect(onAllConsentsGiven).toHaveBeenCalledTimes(0); + }); + }); + describe('Saving ', () => { + it('by clicking "Approve all" sends also unknown consents', () => { + const result = renderCookieConsent(allNotApprovedConsentData); + clickElement(result, consumer1ApproveAllButtonSelector); + expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ + ...allApprovedConsentData.cookie, + ...unknownConsents, + }); + }); + }); +}); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx new file mode 100644 index 0000000000..df4981976a --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -0,0 +1,78 @@ +import React, { createContext, useMemo, useState } from 'react'; + +import create, { ConsentController, ConsentList, ConsentObject } from './cookieConsentController'; + +export type CookieConsentContextType = { + getRequired: () => ConsentObject; + getOptional: () => ConsentObject; + update: ConsentController['update']; + approveRequired: ConsentController['approveRequired']; + approveAll: ConsentController['approveAll']; + save: ConsentController['save']; + willRenderCookieConsentDialog: boolean; + hasUserHandledAllConsents: () => boolean; +}; + +type CookieConsentContextProps = { + optionalConsents?: ConsentList; + requiredConsents?: ConsentList; + children: React.ReactNode | React.ReactNode[] | null; + onAllConsentsGiven?: (consents: ConsentObject) => void; + onConsentsParsed?: (consents: ConsentObject, hasUserHandledAllConsents: boolean) => void; +}; + +export const CookieConsentContext = createContext({ + getRequired: () => ({}), + getOptional: () => ({}), + update: () => undefined, + approveRequired: () => undefined, + approveAll: () => undefined, + save: () => ({}), + hasUserHandledAllConsents: () => false, + willRenderCookieConsentDialog: false, +}); + +export const Provider = ({ + optionalConsents, + requiredConsents, + onAllConsentsGiven = () => undefined, + onConsentsParsed = () => undefined, + children, +}: CookieConsentContextProps): React.ReactElement => { + const consentController = useMemo(() => create({ requiredConsents, optionalConsents }), [ + requiredConsents, + optionalConsents, + ]); + + const hasUserHandledAllConsents = () => + consentController.getRequiredWithoutConsent().length === 0 && consentController.getUnhandledConsents().length === 0; + + const [willRenderCookieConsentDialog, setWillRenderCookieConsentDialog] = useState( + !hasUserHandledAllConsents(), + ); + + const mergeConsents = () => ({ + ...consentController.getRequired(), + ...consentController.getOptional(), + }); + + const contextData: CookieConsentContextType = { + getRequired: () => consentController.getRequired(), + getOptional: () => consentController.getOptional(), + update: (key, value) => consentController.update(key, value), + approveRequired: () => consentController.approveRequired(), + approveAll: () => consentController.approveAll(), + save: () => { + const savedData = consentController.save(); + if (hasUserHandledAllConsents()) { + setWillRenderCookieConsentDialog(false); + onAllConsentsGiven(mergeConsents()); + } + return savedData; + }, + willRenderCookieConsentDialog, + hasUserHandledAllConsents, + }; + onConsentsParsed(mergeConsents(), hasUserHandledAllConsents()); + return {children}; +}; From 9e556e0bc1151477e565766959c8104a9421d1d0 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 17 Nov 2021 15:47:56 +0200 Subject: [PATCH 003/292] Content components for Storybook These components demonstrate how to show required and optional consents to the user. Components render a main view and a detailed view where user can approve consents. Also added an Application component to show how other parts of the app should also change, if dialog is open. --- .../__storybook__/Application.tsx | 34 +++++++ .../cookieConsent/__storybook__/Buttons.tsx | 46 +++++++++ .../cookieConsent/__storybook__/Content.tsx | 42 ++++++++ .../cookieConsent/__storybook__/Demo.tsx | 15 +++ .../cookieConsent/__storybook__/Details.tsx | 46 +++++++++ .../cookieConsent/__storybook__/Main.tsx | 38 +++++++ .../__storybook__/OptionalConsents.tsx | 56 +++++++++++ .../__storybook__/RequiredConsents.tsx | 46 +++++++++ .../__storybook__/styles.module.scss | 98 +++++++++++++++++++ .../cookieConsent/__storybook__/texts.ts | 33 +++++++ .../src/components/cookieConsent/types.ts | 8 ++ 11 files changed, 462 insertions(+) create mode 100644 packages/react/src/components/cookieConsent/__storybook__/Application.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/Content.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/Demo.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/Details.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/Main.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx create mode 100644 packages/react/src/components/cookieConsent/__storybook__/styles.module.scss create mode 100644 packages/react/src/components/cookieConsent/__storybook__/texts.ts create mode 100644 packages/react/src/components/cookieConsent/types.ts diff --git a/packages/react/src/components/cookieConsent/__storybook__/Application.tsx b/packages/react/src/components/cookieConsent/__storybook__/Application.tsx new file mode 100644 index 0000000000..92cded631f --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/Application.tsx @@ -0,0 +1,34 @@ +import React, { useContext } from 'react'; + +import { CookieConsentContext } from '../CookieConsentContext'; +import classNames from '../../../utils/classNames'; +import styles from './styles.module.scss'; + +function Application(): React.ReactElement { + const { willRenderCookieConsentDialog } = useContext(CookieConsentContext); + return ( +
+

This is a dummy application

+ {willRenderCookieConsentDialog && ( + <> +

+ Cookie consent dialog is visible.   + + Can't touch this! + +

+ + )} + {!willRenderCookieConsentDialog && ( + <> +

Cookie consents have been given. Remove the cookie to see the dialog again.

+ + )} +
+ ); +} + +export default Application; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx b/packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx new file mode 100644 index 0000000000..d09eefd33b --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { CookieConsentActionListener } from '../types'; +import { Button } from '../../button/Button'; +import styles from './styles.module.scss'; + +export type Props = { + onClick: CookieConsentActionListener; +}; + +function Buttons({ onClick }: Props): React.ReactElement { + return ( +
+ + + +
+ ); +} + +export default Buttons; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Content.tsx b/packages/react/src/components/cookieConsent/__storybook__/Content.tsx new file mode 100644 index 0000000000..445dc61b39 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/Content.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; + +import { CookieConsentActionListener, ViewProps } from '../types'; +import Buttons from './Buttons'; +import Details from './Details'; +import Main from './Main'; +import styles from './styles.module.scss'; + +function Content({ onClick }: ViewProps): React.ReactElement { + const [showMore, setShowMore] = useState(false); + const onAction: CookieConsentActionListener = (action, value) => { + if (action === 'showDetails') { + setShowMore(true); + } else if (action === 'hideDetails') { + setShowMore(false); + } else { + onClick(action, value); + } + }; + + return ( + + ); +} + +export default Content; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Demo.tsx b/packages/react/src/components/cookieConsent/__storybook__/Demo.tsx new file mode 100644 index 0000000000..085ba99e75 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/Demo.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import Application from './Application'; +import { CookieConsent } from '../CookieConsent'; + +function Demo(): React.ReactElement { + return ( +
+ + +
+ ); +} + +export default Demo; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Details.tsx b/packages/react/src/components/cookieConsent/__storybook__/Details.tsx new file mode 100644 index 0000000000..132b114e83 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/Details.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import styles from './styles.module.scss'; +import { ViewProps } from '../types'; +import RequiredConsents from './RequiredConsents'; +import OptionalConsents from './OptionalConsents'; +import { Button } from '../../button/Button'; + +function Details({ onClick }: ViewProps): React.ReactElement { + return ( +
+ + Tietoa sivustolla käytetyistä evästeistä + +

+ Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta + kategoriasta ja sallia tai kieltää evästeiden käytön. +

+ + + +

+ +

+
+ ); +} + +export default Details; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Main.tsx b/packages/react/src/components/cookieConsent/__storybook__/Main.tsx new file mode 100644 index 0000000000..51f4cf0c22 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/Main.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { ViewProps } from '../types'; +import styles from './styles.module.scss'; + +function Main({ onClick }: ViewProps): React.ReactElement { + return ( +
+ + Evästesuostumukset + + + +
+ ); +} + +export default Main; diff --git a/packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx b/packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx new file mode 100644 index 0000000000..5a630ec338 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx @@ -0,0 +1,56 @@ +import React, { useContext } from 'react'; + +import { getAriaLabel, getText } from './texts'; +import { ViewProps } from '../types'; +import { Checkbox } from '../../checkbox/Checkbox'; +import { CookieConsentContext } from '../CookieConsentContext'; +import styles from './styles.module.scss'; + +type ConsentData = { + id: string; + checked: boolean; + text: string; + ariaLabel: string; + onToggle: () => void; +}; +type ConsentList = ConsentData[]; + +function OptionalConsents({ onClick }: ViewProps): React.ReactElement { + const cookieConsentContext = useContext(CookieConsentContext); + const consents = cookieConsentContext.getOptional(); + const consentEntries = Object.entries(consents); + const consentList: ConsentList = consentEntries.map(([key, value]) => ({ + id: `optional-cookie-consent-${key}`, + checked: Boolean(value), + text: getText(key), + ariaLabel: getAriaLabel(key), + onToggle: () => { + onClick('changeConsent', { key, value: !value }); + }, + })); + return ( + <> + + Muut evästeet + +

Voit hyväksyä tai jättää hyväksymättä muut evästeet.

+
    + {consentList.map((data) => ( +
  • + +
  • + ))} +
+ + ); +} + +export default OptionalConsents; diff --git a/packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx b/packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx new file mode 100644 index 0000000000..e46f3f3734 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx @@ -0,0 +1,46 @@ +import React, { useContext } from 'react'; + +import { getText, getTitle } from './texts'; +import { CookieConsentContext } from '../CookieConsentContext'; +import styles from './styles.module.scss'; + +type ConsentData = { + id: string; + text: string; + title: string; +}; +type ConsentList = ConsentData[]; + +function RequiredConsents(): React.ReactElement { + const cookieConsentContext = useContext(CookieConsentContext); + const consents = cookieConsentContext.getRequired(); + const consentEntries = Object.entries(consents); + const consentList: ConsentList = consentEntries.map(([key]) => ({ + id: `required-cookie-consent-${key}`, + title: getTitle(key), + text: getText(key), + })); + return ( + <> + + Välttämättömät evästeet + +

+ Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston + käyttäjäystävällisyyteen. +

+ +
    + {consentList.map((data) => ( +
  • + + {data.title}: {data.text} + +
  • + ))} +
+ + ); +} + +export default RequiredConsents; diff --git a/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss b/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss new file mode 100644 index 0000000000..98d8625842 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss @@ -0,0 +1,98 @@ +.content-spacing-and-border { + border-top: 8px solid var(--color-bus); + margin-top: 40px; + box-sizing: border-box; +} + +.content { + position: relative; + padding: 20px 20px 0 20px; + background: #ffffff; + width: 100%; + box-sizing: border-box; +} + +.text-content { + padding: 0; +} + +.buttons { + padding: 20px 0; +} + +.buttons > * { + margin: 0 20px 20px 0; +} + +.language-switcher { + position: absolute; + z-index: 3; + right: -57px; + top: 20px; + max-width: 200px; + box-sizing: border-box; +} + +.list { + list-style: none; + margin-top: 0; + padding-left: 0; +} + +.list li { + padding-bottom: 10px; +} + +.list li label { + padding-left: 10px; +} + +.plain-text-button { + border: none; + background: transparent; + display: inline-block; + text-decoration: underline; + color: var(--color-bus); + padding: 0; + cursor: pointer; +} + +.emulated-h1 { + font-size: var(--fontsize-heading-m); + font-weight: bold; + display: block; + padding: 0; +} + +.emulated-h2 { + font-size: var(--fontsize-heading-s); + font-weight: bold; + display: block; + padding: 1.2em 0 0.5em; +} + +.emulated-h2 + p { + margin-top: 0; +} + +.screen-reader-save-notification + p { + margin-top: 0; +} + +.content .emulated-h1 { + margin-right: 50px; +} + +.no-scroll { + overflow: hidden; + max-height: 100vh; +} + +@media (min-width: 768px) { + .language-switcher { + right: 20px; + } + .buttons > * { + margin-bottom: 0; + } +} diff --git a/packages/react/src/components/cookieConsent/__storybook__/texts.ts b/packages/react/src/components/cookieConsent/__storybook__/texts.ts new file mode 100644 index 0000000000..2f5ca52b50 --- /dev/null +++ b/packages/react/src/components/cookieConsent/__storybook__/texts.ts @@ -0,0 +1,33 @@ +const texts = { + matomoTitle: 'Tilastointievästeet', + matomoText: 'Tilastointievästeiden keräämää tietoa käytetään verkkosivuston kehittämiseen', + matomoAriaInputText: 'Hyväksy tai hylkää tilastotointieväste. {{consentText}}', + tunnistamoTitle: 'Kirjautumiseväste', + tunnistamoText: 'Sivuston pakollinen eväste mahdollistaa kävijän vierailun sivustolla.', + tunnistamoAriaInputText: 'Hyväksy tai hylkää kirjautumiseväste. {{consentText}}', + languageTitle: 'Kielieväste', + languageText: 'Tallennamme valitsemasi käyttöliittymäkielen', + preferencesTitle: 'Mieltymysevästeet', + preferencesText: 'Mieltymysevästeet mukauttavat sivuston ulkoasua ja toimintaa käyttäjän aiemman käytön perusteella.', + preferencesAriaInputText: 'Hyväksy tai hylkää mieltymyseväste. {{consentText}}', + marketingTitle: 'Markkinointievästeet', + marketingText: 'Markkinointievästeiden avulla sivuston käyttäjille voidaan kohdentaa sisältöjä.', + marketingAriaInputText: 'Hyväksy tai hylkää markkinointieväste. {{consentText}}', + someOtherConsentTitle: 'Palvelun oma eväste', + someOtherConsentText: 'Palvelun omaa eväste on demoa varten', + someOtherConsentAriaInputText: 'Hyväksy tai hylkää {{consentText}}', +}; + +export const getTitle = (key: string): string => { + return texts[`${key}Title`] || key; +}; + +export const getText = (key: string): string => { + return texts[`${key}Text`] || key; +}; + +export const getAriaLabel = (key: string): string => { + const text = getText(key); + const label = (texts[`${key}AriaInputText`] as string) || key; + return label.replace('{{consentText}}', text); +}; diff --git a/packages/react/src/components/cookieConsent/types.ts b/packages/react/src/components/cookieConsent/types.ts new file mode 100644 index 0000000000..1ab3c173b1 --- /dev/null +++ b/packages/react/src/components/cookieConsent/types.ts @@ -0,0 +1,8 @@ +export type CookieConsentAction = 'showDetails' | 'approveAll' | 'approveRequired' | 'changeConsent' | 'hideDetails'; +export type CookieConsentActionListener = ( + action: CookieConsentAction, + consent?: { key: string; value: boolean }, +) => void; +export type ViewProps = { + onClick: CookieConsentActionListener; +}; From 17b4f9c76cb98d13d997eaafc758995cef5da63f Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 17 Nov 2021 15:51:12 +0200 Subject: [PATCH 004/292] Added CookieConsent component and core Storybook files This component uses the cookie consent context to control consents and shows the dialog to the user. It has animations and renders a blocking overlay so users cannot bypass the dialog easily. --- .../cookieConsent/CookieConsent.module.scss | 41 ++++ .../cookieConsent/CookieConsent.stories.tsx | 40 ++++ .../cookieConsent/CookieConsent.test.tsx | 209 ++++++++++++++++++ .../cookieConsent/CookieConsent.tsx | 86 +++++++ .../__snapshots__/CookieConsent.test.tsx.snap | 3 + .../src/components/cookieConsent/index.ts | 2 + packages/react/src/components/index.ts | 1 + 7 files changed, 382 insertions(+) create mode 100644 packages/react/src/components/cookieConsent/CookieConsent.module.scss create mode 100644 packages/react/src/components/cookieConsent/CookieConsent.stories.tsx create mode 100644 packages/react/src/components/cookieConsent/CookieConsent.test.tsx create mode 100644 packages/react/src/components/cookieConsent/CookieConsent.tsx create mode 100644 packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap create mode 100644 packages/react/src/components/cookieConsent/index.ts diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss new file mode 100644 index 0000000000..6667da98f2 --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -0,0 +1,41 @@ +.container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 999; +} + +.overlay { + position: absolute; + inset: 0; + z-index: 1; + background: rgb(0, 0, 0); + pointer-events: none; +} + +.aligner { + position: absolute; + bottom: 0; + z-index: 2; + width: 100%; + overflow-y: scroll; + max-height: 100%; +} + +.container .aligner { + transform: translateY(100%); + transition: transform 1s; +} +.container .overlay { + opacity: 0.01; + transition: opacity 1s; +} + +.container.animate-in .aligner { + transform: translateY(0%); +} +.container.animate-in .overlay { + opacity: 0.7; +} diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx new file mode 100644 index 0000000000..cd92e09f26 --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { commonConsents } from './cookieConsentController'; +import { Provider as CookieContextProvider } from './CookieConsentContext'; +import Demo from './__storybook__/Demo'; + +export default { + component: Demo, + title: 'Components/CookieConsent', + parameters: { + controls: { expanded: true }, + }, + args: {}, +}; + +export const Example = () => { + return ( + { + if (consents.matomo) { + // start tracking + } + }} + onConsentsParsed={(consents) => { + if (consents.matomo) { + // start tracking + } + }} + > + + + ); +}; diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx new file mode 100644 index 0000000000..3757f72541 --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -0,0 +1,209 @@ +/* eslint-disable jest/expect-expect */ +/* eslint-disable jest/no-mocks-import */ +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import { CookieConsent } from './CookieConsent'; +import { ConsentList, ConsentObject } from './cookieConsentController'; +import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; +import { Provider as CookieContextProvider } from './CookieConsentContext'; + +type ConsentData = { + requiredConsents?: ConsentList; + optionalConsents?: ConsentList; + cookie?: ConsentObject; +}; + +const mockCookieHelpers = createUniversalCookieMockHelpers(); + +jest.mock( + 'universal-cookie', + () => + function universalCookieMockClass() { + return mockCookieHelpers.createMockedModule(); + }, +); + +describe(' spec', () => { + it('renders the component', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + it('should not have basic accessibility issues', async () => { + const { container } = render(); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); + +describe(' ', () => { + const dataTestIds = { + container: 'cookie-consent', + languageSwitcher: 'cookie-consent-language-switcher', + approveAllButton: 'cookie-consent-approve-all-button', + approveRequiredButton: 'cookie-consent-approve-required-button', + readMoreButton: 'cookie-consent-read-more-button', + readMoreTextButton: 'cookie-consent-read-more-text-button', + detailsComponent: 'cookie-consent-details', + informationComponent: 'cookie-consent-information', + approveSelectionsButton: 'cookie-consent-approve-selections-button', + screenReaderNotification: 'cookie-consent-screen-reader-notification', + getOptionalConsentId: (key: string) => `optional-cookie-consent-${key}`, + getRequiredConsentId: (key: string) => `required-cookie-consent-${key}`, + }; + + const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { + expect(result.getAllByTestId(testId)).toHaveLength(1); + }; + + const verifyElementDoesNotExistsByTestId = (result: RenderResult, testId: string) => { + expect(() => result.getAllByTestId(testId)).toThrow(); + }; + + const clickElement = (result: RenderResult, testId: string) => { + result.getByTestId(testId).click(); + }; + + const defaultConsentData = { + requiredConsents: ['requiredConsent1', 'requiredConsent2'], + optionalConsents: ['optionalConsent1', 'optionalConsent2'], + cookie: {}, + }; + + const unknownConsents = { + unknownConsent1: true, + unknownConsent2: false, + }; + + const renderCookieConsent = ({ + requiredConsents = [], + optionalConsents = [], + cookie = {}, + }: ConsentData): RenderResult => { + // inject unknown consents to verify those are + // stored and handled, but not required or optional + const cookieWithInjectedUnknowns = { + ...cookie, + ...unknownConsents, + }; + mockCookieHelpers.setStoredCookie(cookieWithInjectedUnknowns); + return render( + + + , + ); + }; + + afterEach(() => { + mockCookieHelpers.reset(); + }); + + describe('Cookie consent ', () => { + it('and child components are rendered when consents have not been handled', () => { + const result = renderCookieConsent(defaultConsentData); + verifyElementExistsByTestId(result, dataTestIds.container); + verifyElementExistsByTestId(result, dataTestIds.languageSwitcher); + verifyElementExistsByTestId(result, dataTestIds.informationComponent); + verifyElementExistsByTestId(result, dataTestIds.approveAllButton); + verifyElementExistsByTestId(result, dataTestIds.approveRequiredButton); + verifyElementExistsByTestId(result, dataTestIds.readMoreTextButton); + }); + it('is rendered if a required consent has not been approved. It could have been optional before', () => { + const result = renderCookieConsent({ + ...defaultConsentData, + cookie: { + requiredConsent1: true, + requiredConsent2: false, + optionalConsent1: true, + optionalConsent2: true, + }, + }); + verifyElementExistsByTestId(result, dataTestIds.container); + }); + it('is not shown when all consents have been handled and are true/false', () => { + const result = renderCookieConsent({ + ...defaultConsentData, + cookie: { + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: false, + optionalConsent2: true, + }, + }); + verifyElementDoesNotExistsByTestId(result, dataTestIds.container); + verifyElementDoesNotExistsByTestId(result, dataTestIds.screenReaderNotification); + }); + }); + describe(`Approve buttons will + - hide the cookie consent + - show a prompt for screen readers + - save cookie`, () => { + it('Approve all -button approves all consents', () => { + const result = renderCookieConsent(defaultConsentData); + const consentResult = { + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: true, + optionalConsent2: true, + ...unknownConsents, + }; + clickElement(result, dataTestIds.approveAllButton); + expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual(consentResult); + verifyElementDoesNotExistsByTestId(result, dataTestIds.container); + verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + }); + it('Approve required -button, approves only required consents', () => { + const result = renderCookieConsent(defaultConsentData); + const consentResult = { + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: false, + optionalConsent2: false, + ...unknownConsents, + }; + clickElement(result, dataTestIds.approveRequiredButton); + expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual(consentResult); + verifyElementDoesNotExistsByTestId(result, dataTestIds.container); + verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + }); + }); + describe('In details view ', () => { + const initDetailsView = (data: ConsentData): RenderResult => { + const result = renderCookieConsent(data); + clickElement(result, dataTestIds.readMoreButton); + return result; + }; + it('required and optional consents are rendered', () => { + expect.assertions(4); + const result = initDetailsView(defaultConsentData); + defaultConsentData.requiredConsents.forEach((consent) => { + verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentId(consent)); + }); + defaultConsentData.optionalConsents.forEach((consent) => { + verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentId(consent)); + }); + }); + it(`clicking an optional consent sets the consent true/false. + Cookie consent is not hidden until an approve -button is clicked`, () => { + const result = initDetailsView(defaultConsentData); + defaultConsentData.optionalConsents.forEach((consent) => { + clickElement(result, dataTestIds.getOptionalConsentId(consent)); + }); + clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent2')); + clickElement(result, dataTestIds.approveSelectionsButton); + expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ + requiredConsent1: true, + requiredConsent2: true, + optionalConsent1: true, + optionalConsent2: false, + ...unknownConsents, + }); + verifyElementDoesNotExistsByTestId(result, dataTestIds.container); + verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); + expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx new file mode 100644 index 0000000000..d856351e93 --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -0,0 +1,86 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { VisuallyHidden } from '@react-aria/visually-hidden'; + +import classNames from '../../utils/classNames'; +import styles from './CookieConsent.module.scss'; +import { ConsentController } from './cookieConsentController'; +import { CookieConsentContext } from './CookieConsentContext'; +import Content from './__storybook__/Content'; +import { CookieConsentActionListener } from './types'; + +export function CookieConsent(): React.ReactElement | null { + const cookieConsentContext = useContext(CookieConsentContext); + const [, forceUpdate] = useState(0); + const [showScreenReaderSaveNotification, setShowScreenReaderSaveNotification] = useState(false); + const [popupTimerComplete, setPopupTimerComplete] = useState(false); + const popupDelayInMs = 500; + + const reRender = () => { + forceUpdate((p) => p + 1); + }; + + const save = (): void => { + cookieConsentContext.save(); + if (cookieConsentContext.hasUserHandledAllConsents()) { + setShowScreenReaderSaveNotification(true); + } + }; + + const approveRequired: ConsentController['approveRequired'] = () => { + cookieConsentContext.approveRequired(); + save(); + reRender(); + }; + + const approveAll: ConsentController['approveAll'] = () => { + cookieConsentContext.approveAll(); + save(); + reRender(); + }; + + const onChange: ConsentController['update'] = (key, value) => { + cookieConsentContext.update(key, value); + reRender(); + }; + + const onAction: CookieConsentActionListener = (action, consent) => { + if (action === 'approveAll') { + approveAll(); + } else if (action === 'approveRequired') { + approveRequired(); + } else if (action === 'changeConsent') { + const { key, value } = consent; + onChange(key, value); + } + }; + + useEffect(() => { + setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); + }, []); + + if (showScreenReaderSaveNotification) { + return ( + +
+ Asetukset tallennettu! +
+
+ ); + } + + if (!cookieConsentContext.willRenderCookieConsentDialog) { + return null; + } + + return ( +
+
+ +
+
+
+ ); +} diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap new file mode 100644 index 0000000000..1b41f2817f --- /dev/null +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ``; diff --git a/packages/react/src/components/cookieConsent/index.ts b/packages/react/src/components/cookieConsent/index.ts new file mode 100644 index 0000000000..dbcf18eb5f --- /dev/null +++ b/packages/react/src/components/cookieConsent/index.ts @@ -0,0 +1,2 @@ +export * from './CookieConsent'; +export * from './types'; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 9b78df44a4..9a63ceeb8d 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -37,3 +37,4 @@ export * from './table'; export * from './tabs'; export * from './stepper'; export * from './passwordInput'; +export * from './cookieConsent'; From c5df282ca215b2110e4e46b7a785280a2be1c7a3 Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Fri, 19 Nov 2021 15:55:46 +0200 Subject: [PATCH 005/292] Refactor: Separate storybook example app and cookie consent to own modules --- .../cookieConsent/CookieConsent.module.scss | 87 +++++++++++++++++++ .../cookieConsent/CookieConsent.tsx | 2 +- .../__storybook__/styles.module.scss | 84 ------------------ .../{__storybook__ => buttons}/Buttons.tsx | 2 +- .../{__storybook__ => content}/Content.tsx | 8 +- .../{__storybook__ => details}/Details.tsx | 8 +- .../{__storybook__ => main}/Main.tsx | 2 +- .../OptionalConsents.tsx | 6 +- .../RequiredConsents.tsx | 4 +- .../{__storybook__ => }/texts.ts | 0 10 files changed, 103 insertions(+), 100 deletions(-) rename packages/react/src/components/cookieConsent/{__storybook__ => buttons}/Buttons.tsx (95%) rename packages/react/src/components/cookieConsent/{__storybook__ => content}/Content.tsx (87%) rename packages/react/src/components/cookieConsent/{__storybook__ => details}/Details.tsx (84%) rename packages/react/src/components/cookieConsent/{__storybook__ => main}/Main.tsx (96%) rename packages/react/src/components/cookieConsent/{__storybook__ => optionalConsents}/OptionalConsents.tsx (91%) rename packages/react/src/components/cookieConsent/{__storybook__ => requiredConsents}/RequiredConsents.tsx (92%) rename packages/react/src/components/cookieConsent/{__storybook__ => }/texts.ts (100%) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 6667da98f2..804c96448a 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -39,3 +39,90 @@ .container.animate-in .overlay { opacity: 0.7; } + +.buttons { + padding: 20px 0; +} + +.buttons > * { + margin: 0 20px 20px 0; +} + +@media (min-width: 768px) { + .buttons > * { + margin-bottom: 0; + } +} + +.content { + position: relative; + padding: 20px 20px 0 20px; + background: #ffffff; + width: 100%; + box-sizing: border-box; +} + +.content .emulated-h1 { + margin-right: 50px; +} + +.language-switcher { + position: absolute; + z-index: 3; + right: -57px; + top: 20px; + max-width: 200px; + box-sizing: border-box; +} + +@media (min-width: 768px) { + .language-switcher { + right: 20px; + } +} + +.text-content { + padding: 0; +} + +.emulated-h1 { + font-size: var(--fontsize-heading-m); + font-weight: bold; + display: block; + padding: 0; +} + +.plain-text-button { + border: none; + background: transparent; + display: inline-block; + text-decoration: underline; + color: var(--color-bus); + padding: 0; + cursor: pointer; +} + +.emulated-h2 { + font-size: var(--fontsize-heading-s); + font-weight: bold; + display: block; + padding: 1.2em 0 0.5em; +} + +.emulated-h2 + p { + margin-top: 0; +} + +.list { + list-style: none; + margin-top: 0; + padding-left: 0; +} + +.list li { + padding-bottom: 10px; +} + +.list li label { + padding-left: 10px; +} diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index d856351e93..d3489ffe2d 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -5,7 +5,7 @@ import classNames from '../../utils/classNames'; import styles from './CookieConsent.module.scss'; import { ConsentController } from './cookieConsentController'; import { CookieConsentContext } from './CookieConsentContext'; -import Content from './__storybook__/Content'; +import Content from './content/Content'; import { CookieConsentActionListener } from './types'; export function CookieConsent(): React.ReactElement | null { diff --git a/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss b/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss index 98d8625842..63fe8a668f 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss +++ b/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss @@ -4,95 +4,11 @@ box-sizing: border-box; } -.content { - position: relative; - padding: 20px 20px 0 20px; - background: #ffffff; - width: 100%; - box-sizing: border-box; -} - -.text-content { - padding: 0; -} - -.buttons { - padding: 20px 0; -} - -.buttons > * { - margin: 0 20px 20px 0; -} - -.language-switcher { - position: absolute; - z-index: 3; - right: -57px; - top: 20px; - max-width: 200px; - box-sizing: border-box; -} - -.list { - list-style: none; - margin-top: 0; - padding-left: 0; -} - -.list li { - padding-bottom: 10px; -} - -.list li label { - padding-left: 10px; -} - -.plain-text-button { - border: none; - background: transparent; - display: inline-block; - text-decoration: underline; - color: var(--color-bus); - padding: 0; - cursor: pointer; -} - -.emulated-h1 { - font-size: var(--fontsize-heading-m); - font-weight: bold; - display: block; - padding: 0; -} - -.emulated-h2 { - font-size: var(--fontsize-heading-s); - font-weight: bold; - display: block; - padding: 1.2em 0 0.5em; -} - -.emulated-h2 + p { - margin-top: 0; -} - .screen-reader-save-notification + p { margin-top: 0; } -.content .emulated-h1 { - margin-right: 50px; -} - .no-scroll { overflow: hidden; max-height: 100vh; } - -@media (min-width: 768px) { - .language-switcher { - right: 20px; - } - .buttons > * { - margin-bottom: 0; - } -} diff --git a/packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx similarity index 95% rename from packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx rename to packages/react/src/components/cookieConsent/buttons/Buttons.tsx index d09eefd33b..aa996b8def 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { CookieConsentActionListener } from '../types'; import { Button } from '../../button/Button'; -import styles from './styles.module.scss'; +import styles from '../CookieConsent.module.scss'; export type Props = { onClick: CookieConsentActionListener; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx similarity index 87% rename from packages/react/src/components/cookieConsent/__storybook__/Content.tsx rename to packages/react/src/components/cookieConsent/content/Content.tsx index 445dc61b39..027d3a21e2 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { CookieConsentActionListener, ViewProps } from '../types'; -import Buttons from './Buttons'; -import Details from './Details'; -import Main from './Main'; -import styles from './styles.module.scss'; +import Buttons from '../buttons/Buttons'; +import Details from '../details/Details'; +import Main from '../main/Main'; +import styles from '../CookieConsent.module.scss'; function Content({ onClick }: ViewProps): React.ReactElement { const [showMore, setShowMore] = useState(false); diff --git a/packages/react/src/components/cookieConsent/__storybook__/Details.tsx b/packages/react/src/components/cookieConsent/details/Details.tsx similarity index 84% rename from packages/react/src/components/cookieConsent/__storybook__/Details.tsx rename to packages/react/src/components/cookieConsent/details/Details.tsx index 132b114e83..4ff31147e0 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/Details.tsx +++ b/packages/react/src/components/cookieConsent/details/Details.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import styles from './styles.module.scss'; +import styles from '../CookieConsent.module.scss'; import { ViewProps } from '../types'; -import RequiredConsents from './RequiredConsents'; -import OptionalConsents from './OptionalConsents'; -import { Button } from '../../button/Button'; +import RequiredConsents from '../requiredConsents/RequiredConsents'; +import OptionalConsents from '../optionalConsents/OptionalConsents'; +import { Button } from '../../button'; function Details({ onClick }: ViewProps): React.ReactElement { return ( diff --git a/packages/react/src/components/cookieConsent/__storybook__/Main.tsx b/packages/react/src/components/cookieConsent/main/Main.tsx similarity index 96% rename from packages/react/src/components/cookieConsent/__storybook__/Main.tsx rename to packages/react/src/components/cookieConsent/main/Main.tsx index 51f4cf0c22..428b8a4d78 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/Main.tsx +++ b/packages/react/src/components/cookieConsent/main/Main.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ViewProps } from '../types'; -import styles from './styles.module.scss'; +import styles from '../CookieConsent.module.scss'; function Main({ onClick }: ViewProps): React.ReactElement { return ( diff --git a/packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx similarity index 91% rename from packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx rename to packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx index 5a630ec338..695aef9b50 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/OptionalConsents.tsx +++ b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx @@ -1,10 +1,10 @@ import React, { useContext } from 'react'; -import { getAriaLabel, getText } from './texts'; +import { getAriaLabel, getText } from '../texts'; import { ViewProps } from '../types'; -import { Checkbox } from '../../checkbox/Checkbox'; +import { Checkbox } from '../../checkbox'; import { CookieConsentContext } from '../CookieConsentContext'; -import styles from './styles.module.scss'; +import styles from '../CookieConsent.module.scss'; type ConsentData = { id: string; diff --git a/packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx similarity index 92% rename from packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx rename to packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx index e46f3f3734..b379243374 100644 --- a/packages/react/src/components/cookieConsent/__storybook__/RequiredConsents.tsx +++ b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx @@ -1,8 +1,8 @@ import React, { useContext } from 'react'; -import { getText, getTitle } from './texts'; +import { getText, getTitle } from '../texts'; import { CookieConsentContext } from '../CookieConsentContext'; -import styles from './styles.module.scss'; +import styles from '../CookieConsent.module.scss'; type ConsentData = { id: string; diff --git a/packages/react/src/components/cookieConsent/__storybook__/texts.ts b/packages/react/src/components/cookieConsent/texts.ts similarity index 100% rename from packages/react/src/components/cookieConsent/__storybook__/texts.ts rename to packages/react/src/components/cookieConsent/texts.ts From 1cd0392b99db29243d2c7cccf5ed65217297b6fc Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Fri, 19 Nov 2021 16:12:25 +0200 Subject: [PATCH 006/292] Add cookie consent to rollup --- packages/react/rollup.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/rollup.config.js b/packages/react/rollup.config.js index 9a72d5417f..969a84da2f 100644 --- a/packages/react/rollup.config.js +++ b/packages/react/rollup.config.js @@ -81,6 +81,7 @@ export default [ 'components/Card/index': 'src/components/card/index.ts', 'components/Checkbox/index': 'src/components/checkbox/index.ts', 'components/Columns/index': 'src/components/columns/index.ts', + 'components/CookieConsent/index': 'src/components/cookieConsent/index.ts', 'components/Combobox/index': 'src/components/dropdown/combobox/index.ts', 'components/Container/index': 'src/components/container/index.ts', 'components/DateInput/index': 'src/components/dateInput/index.ts', From a22824bf937e06032e794888961e39d94fc6725e Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Mon, 29 Nov 2021 14:12:36 +0200 Subject: [PATCH 007/292] Open up Demo to show CookieConsentContext usage --- .../cookieConsent/CookieConsent.stories.tsx | 40 ++++++++++++++++--- .../cookieConsent/__storybook__/Demo.tsx | 15 ------- 2 files changed, 35 insertions(+), 20 deletions(-) delete mode 100644 packages/react/src/components/cookieConsent/__storybook__/Demo.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index cd92e09f26..602df5bd19 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { commonConsents } from './cookieConsentController'; -import { Provider as CookieContextProvider } from './CookieConsentContext'; -import Demo from './__storybook__/Demo'; +import { CookieConsentContext, Provider as CookieContextProvider } from './CookieConsentContext'; +import { CookieConsent } from './CookieConsent'; +import classNames from '../../utils/classNames'; +import styles from './__storybook__/styles.module.scss'; export default { - component: Demo, + component: CookieConsent, title: 'Components/CookieConsent', parameters: { controls: { expanded: true }, @@ -14,6 +16,33 @@ export default { }; export const Example = () => { + const Application = () => { + const { willRenderCookieConsentDialog } = useContext(CookieConsentContext); + return ( +
+

This is a dummy application

+ {willRenderCookieConsentDialog && ( + <> +

+ Cookie consent dialog is visible.   + + Can't touch this! + +

+ + )} + {!willRenderCookieConsentDialog && ( + <> +

Cookie consents have been given. Remove the cookie to see the dialog again.

+ + )} +
+ ); + }; + return ( { } }} > - + + ); }; diff --git a/packages/react/src/components/cookieConsent/__storybook__/Demo.tsx b/packages/react/src/components/cookieConsent/__storybook__/Demo.tsx deleted file mode 100644 index 085ba99e75..0000000000 --- a/packages/react/src/components/cookieConsent/__storybook__/Demo.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -import Application from './Application'; -import { CookieConsent } from '../CookieConsent'; - -function Demo(): React.ReactElement { - return ( -
- - -
- ); -} - -export default Demo; From e16332e48477bc90f3927d6038da781541a9cf08 Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Mon, 29 Nov 2021 16:45:07 +0200 Subject: [PATCH 008/292] Remove unneeded files and folder __storybook__ --- .../cookieConsent/CookieConsent.stories.tsx | 4 +-- .../__storybook__/Application.tsx | 34 ------------------- .../__storybook__/styles.module.scss | 14 -------- 3 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 packages/react/src/components/cookieConsent/__storybook__/Application.tsx delete mode 100644 packages/react/src/components/cookieConsent/__storybook__/styles.module.scss diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 602df5bd19..0f3fbdbf8d 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -3,8 +3,6 @@ import React, { useContext } from 'react'; import { commonConsents } from './cookieConsentController'; import { CookieConsentContext, Provider as CookieContextProvider } from './CookieConsentContext'; import { CookieConsent } from './CookieConsent'; -import classNames from '../../utils/classNames'; -import styles from './__storybook__/styles.module.scss'; export default { component: CookieConsent, @@ -20,7 +18,7 @@ export const Example = () => { const { willRenderCookieConsentDialog } = useContext(CookieConsentContext); return (

This is a dummy application

diff --git a/packages/react/src/components/cookieConsent/__storybook__/Application.tsx b/packages/react/src/components/cookieConsent/__storybook__/Application.tsx deleted file mode 100644 index 92cded631f..0000000000 --- a/packages/react/src/components/cookieConsent/__storybook__/Application.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useContext } from 'react'; - -import { CookieConsentContext } from '../CookieConsentContext'; -import classNames from '../../../utils/classNames'; -import styles from './styles.module.scss'; - -function Application(): React.ReactElement { - const { willRenderCookieConsentDialog } = useContext(CookieConsentContext); - return ( -
-

This is a dummy application

- {willRenderCookieConsentDialog && ( - <> -

- Cookie consent dialog is visible.   - - Can't touch this! - -

- - )} - {!willRenderCookieConsentDialog && ( - <> -

Cookie consents have been given. Remove the cookie to see the dialog again.

- - )} -
- ); -} - -export default Application; diff --git a/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss b/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss deleted file mode 100644 index 63fe8a668f..0000000000 --- a/packages/react/src/components/cookieConsent/__storybook__/styles.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -.content-spacing-and-border { - border-top: 8px solid var(--color-bus); - margin-top: 40px; - box-sizing: border-box; -} - -.screen-reader-save-notification + p { - margin-top: 0; -} - -.no-scroll { - overflow: hidden; - max-height: 100vh; -} From fe15b2ac800d767448aae244d0703ff7004ea944 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 30 Nov 2021 10:53:24 +0200 Subject: [PATCH 009/292] Added a new cookieDomain property Subdomain cookies cannot be set for public suffix domains (e.g. github.io). That prevents the usage of cookieConsentController in urls like city-of-helsinki.github.io. For those rare cases there is now a cookieDomain property. COOKIE_NAME and COOKIE_EXPIRATION_TIME are exported so they can be listed in cookie data. --- .../CookieConsentContext.test.tsx | 24 ++++++++++++++++++ .../cookieConsent/CookieConsentContext.tsx | 5 +++- .../cookieConsentController.test.ts | 25 +++++++++++++++++++ .../cookieConsent/cookieConsentController.ts | 15 ++++++----- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index c43fa80adf..42c2f3b564 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -6,11 +6,13 @@ import { render, RenderResult } from '@testing-library/react'; import { ConsentList, ConsentObject } from './cookieConsentController'; import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; import { CookieConsentContext, Provider as CookieContextProvider } from './CookieConsentContext'; +import mockWindowLocation, { MockedWindowLocationActions } from './__mocks__/mockWindowLocation'; type ConsentData = { requiredConsents?: ConsentList; optionalConsents?: ConsentList; cookie?: ConsentObject; + cookieDomain?: string; }; const mockCookieHelpers = createUniversalCookieMockHelpers(); @@ -69,6 +71,7 @@ describe('CookieConsentContext ', () => { const onAllConsentsGiven = jest.fn(); const onConsentsParsed = jest.fn(); + let mockedWindowControls: MockedWindowLocationActions; afterEach(() => { onAllConsentsGiven.mockReset(); @@ -76,6 +79,13 @@ describe('CookieConsentContext ', () => { mockCookieHelpers.reset(); }); + beforeAll(() => { + mockedWindowControls = mockWindowLocation(); + }); + afterAll(() => { + mockedWindowControls.restore(); + }); + const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { expect(result.getAllByTestId(testId)).toHaveLength(1); }; @@ -106,6 +116,7 @@ describe('CookieConsentContext ', () => { const renderCookieConsent = ({ requiredConsents = [], optionalConsents = [], + cookieDomain, cookie = {}, }: ConsentData): RenderResult => { // inject unknown consents to verify those are @@ -119,6 +130,7 @@ describe('CookieConsentContext ', () => { @@ -169,12 +181,24 @@ describe('CookieConsentContext ', () => { }); describe('Saving ', () => { it('by clicking "Approve all" sends also unknown consents', () => { + mockedWindowControls.setUrl('https://subdomain.hel.fi'); const result = renderCookieConsent(allNotApprovedConsentData); clickElement(result, consumer1ApproveAllButtonSelector); expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ ...allApprovedConsentData.cookie, ...unknownConsents, }); + expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.fi'); + }); + it('sets the domain of the cookie to given cookieDomain', () => { + mockedWindowControls.setUrl('https://notmyhost.com'); + const cookieDomain = 'myhost.com'; + const result = renderCookieConsent({ + ...allNotApprovedConsentData, + cookieDomain, + }); + clickElement(result, consumer1ApproveAllButtonSelector); + expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual(cookieDomain); }); }); }); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index df4981976a..c0cc4966df 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -16,6 +16,7 @@ export type CookieConsentContextType = { type CookieConsentContextProps = { optionalConsents?: ConsentList; requiredConsents?: ConsentList; + cookieDomain?: string; children: React.ReactNode | React.ReactNode[] | null; onAllConsentsGiven?: (consents: ConsentObject) => void; onConsentsParsed?: (consents: ConsentObject, hasUserHandledAllConsents: boolean) => void; @@ -35,13 +36,15 @@ export const CookieConsentContext = createContext({ export const Provider = ({ optionalConsents, requiredConsents, + cookieDomain, onAllConsentsGiven = () => undefined, onConsentsParsed = () => undefined, children, }: CookieConsentContextProps): React.ReactElement => { - const consentController = useMemo(() => create({ requiredConsents, optionalConsents }), [ + const consentController = useMemo(() => create({ requiredConsents, optionalConsents, cookieDomain }), [ requiredConsents, optionalConsents, + cookieDomain, ]); const hasUserHandledAllConsents = () => diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts index b958313cf4..372fca4e5f 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts @@ -6,6 +6,8 @@ import createConsentController, { ConsentList, ConsentObject, createStorage, + COOKIE_EXPIRATION_TIME, + COOKIE_NAME, } from './cookieConsentController'; const mockCookieHelpers = createUniversalCookieMockHelpers(); @@ -38,16 +40,19 @@ describe(`cookieConsentController.ts`, () => { const createControllerAndInitCookie = ({ requiredConsents, optionalConsents, + cookieDomain, cookie = {}, }: { requiredConsents?: ConsentList; optionalConsents?: ConsentList; cookie?: ConsentObject; + cookieDomain?: string; }) => { mockCookieHelpers.setStoredCookie(cookie); controller = createConsentController({ requiredConsents, optionalConsents, + cookieDomain, }); }; describe('createConsentController', () => { @@ -312,6 +317,26 @@ describe(`cookieConsentController.ts`, () => { controller.save(); expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.ninja'); }); + it('if "cookieDomain" property is passed in the props, it is set as the domain of the cookie', () => { + const cookieDomain = 'myhost.com'; + createControllerAndInitCookie({ + ...defaultControllerTestData, + cookieDomain, + }); + mockedWindowControls.setUrl('https://notmyhost.com'); + controller.save(); + expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual(cookieDomain); + }); + it('Cookie maxAge should match COOKIE_EXPIRATION_TIME', () => { + createControllerAndInitCookie(defaultControllerTestData); + controller.save(); + expect(mockCookieHelpers.getSetCookieArguments().options.maxAge).toEqual(COOKIE_EXPIRATION_TIME); + }); + it('Cookie name should match COOKIE_NAME', () => { + createControllerAndInitCookie(defaultControllerTestData); + controller.save(); + expect(mockCookieHelpers.getSetCookieArguments().cookieName).toEqual(COOKIE_NAME); + }); }); }); describe('createStorage', () => { diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.ts b/packages/react/src/components/cookieConsent/cookieConsentController.ts index ebaeb79ced..3602b00b9a 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.ts @@ -16,6 +16,7 @@ export type ConsentStorage = { export type ConsentControllerProps = { requiredConsents?: ConsentList; optionalConsents?: ConsentList; + cookieDomain?: string; }; export type ConsentController = { @@ -30,7 +31,8 @@ export type ConsentController = { save: () => ConsentObject; }; -const COOKIE_NAME = 'city-of-helsinki-cookie-consents'; +export const COOKIE_NAME = 'city-of-helsinki-cookie-consents'; +export const COOKIE_EXPIRATION_TIME = 60 * 60 * 24 * 365; export const commonConsents = { matomo: 'matomo', @@ -70,24 +72,25 @@ function createConsentsString(consents: ConsentObject): string { return JSON.stringify(consents); } -function createCookieController(): { +function createCookieController( + cookieDomain?: string, +): { get: () => string; set: (data: string) => void; } { const cookieController = new CookieController(); - const oneYearInSeconds = 60 * 60 * 24 * 365; const defaultCookieSetOptions: CookieSetOptions = { path: '/', secure: false, sameSite: 'strict', - maxAge: oneYearInSeconds, + maxAge: COOKIE_EXPIRATION_TIME, }; const getCookieDomainFromUrl = (): string => window.location.hostname.split('.').slice(-2).join('.'); const createCookieOptions = (): CookieSetOptions => ({ ...defaultCookieSetOptions, - domain: getCookieDomainFromUrl(), + domain: cookieDomain || getCookieDomainFromUrl(), }); const get = (): string => cookieController.get(COOKIE_NAME, { doNotParse: true }) || ''; @@ -185,7 +188,7 @@ export default function createConsentController(props: ConsentControllerProps): verifyConsentProps(props); const { optionalConsents = [], requiredConsents = [] } = props; const allConsents = [...optionalConsents, ...requiredConsents]; - const cookieController = createCookieController(); + const cookieController = createCookieController(props.cookieDomain); const currentConsentsInCookie = parseConsents(cookieController.get()); const required = mergeConsents( From 240834413fdfb3884a537f323fd634cc2809997d Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Tue, 30 Nov 2021 15:43:36 +0200 Subject: [PATCH 010/292] Add linebreaks between test cases to improve readability --- .../cookieConsent/CookieConsent.test.tsx | 7 ++++++ .../CookieConsentContext.test.tsx | 8 +++++++ .../cookieConsentController.test.ts | 22 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 3757f72541..ed79bc0abb 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -30,6 +30,7 @@ describe(' spec', () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); + it('should not have basic accessibility issues', async () => { const { container } = render(); const results = await axe(container); @@ -109,6 +110,7 @@ describe(' ', () => { verifyElementExistsByTestId(result, dataTestIds.approveRequiredButton); verifyElementExistsByTestId(result, dataTestIds.readMoreTextButton); }); + it('is rendered if a required consent has not been approved. It could have been optional before', () => { const result = renderCookieConsent({ ...defaultConsentData, @@ -121,6 +123,7 @@ describe(' ', () => { }); verifyElementExistsByTestId(result, dataTestIds.container); }); + it('is not shown when all consents have been handled and are true/false', () => { const result = renderCookieConsent({ ...defaultConsentData, @@ -135,6 +138,7 @@ describe(' ', () => { verifyElementDoesNotExistsByTestId(result, dataTestIds.screenReaderNotification); }); }); + describe(`Approve buttons will - hide the cookie consent - show a prompt for screen readers @@ -154,6 +158,7 @@ describe(' ', () => { verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); }); + it('Approve required -button, approves only required consents', () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -170,6 +175,7 @@ describe(' ', () => { expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); }); }); + describe('In details view ', () => { const initDetailsView = (data: ConsentData): RenderResult => { const result = renderCookieConsent(data); @@ -186,6 +192,7 @@ describe(' ', () => { verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentId(consent)); }); }); + it(`clicking an optional consent sets the consent true/false. Cookie consent is not hidden until an approve -button is clicked`, () => { const result = initDetailsView(defaultConsentData); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index 42c2f3b564..9e0527f27e 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -144,9 +144,11 @@ describe('CookieConsentContext ', () => { it('is false all consents are true/false', () => { verifyConsumersShowConsentsNotHandled(renderCookieConsent(allNotApprovedConsentData)); }); + it('is true if user has unhandled consents', () => { verifyConsumersShowConsentsHandled(renderCookieConsent(allApprovedConsentData)); }); + it('reflects the value of hasUserHandledAllConsents and both change with approval', () => { const result = renderCookieConsent(allNotApprovedConsentData); verifyConsumersShowConsentsNotHandled(result); @@ -154,18 +156,21 @@ describe('CookieConsentContext ', () => { verifyConsumersShowConsentsHandled(result); }); }); + describe('onConsentsParsed is called when context is created and controller has read the cookie. ', () => { it('Arguments are ({consents}, false) when user has not handled all consents', () => { renderCookieConsent(allNotApprovedConsentData); expect(onConsentsParsed).toHaveBeenCalledTimes(1); expect(onConsentsParsed).toHaveBeenLastCalledWith(allNotApprovedConsentData.cookie, false); }); + it('Arguments are ({consents}, true) when user has given all consents', () => { renderCookieConsent(allNotApprovedConsentData); expect(onConsentsParsed).toHaveBeenCalledTimes(1); expect(onConsentsParsed).toHaveBeenLastCalledWith(allNotApprovedConsentData.cookie, false); }); }); + describe('onAllConsentsGiven ', () => { it('is called only when user has given all consents.', () => { const result = renderCookieConsent(allNotApprovedConsentData); @@ -174,11 +179,13 @@ describe('CookieConsentContext ', () => { expect(onAllConsentsGiven).toHaveBeenCalledTimes(1); expect(onAllConsentsGiven).toHaveBeenLastCalledWith(allApprovedConsentData.cookie); }); + it('is not called even if consents are initially given', () => { renderCookieConsent(allApprovedConsentData); expect(onAllConsentsGiven).toHaveBeenCalledTimes(0); }); }); + describe('Saving ', () => { it('by clicking "Approve all" sends also unknown consents', () => { mockedWindowControls.setUrl('https://subdomain.hel.fi'); @@ -190,6 +197,7 @@ describe('CookieConsentContext ', () => { }); expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.fi'); }); + it('sets the domain of the cookie to given cookieDomain', () => { mockedWindowControls.setUrl('https://notmyhost.com'); const cookieDomain = 'myhost.com'; diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts index 372fca4e5f..78ba4f7227 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts @@ -68,6 +68,7 @@ describe(`cookieConsentController.ts`, () => { optionalConsent2: false, }); }); + it('parses and stores unknown consents', () => { const cookieDataWithUnknownConsents = { unknownConsent1: true, @@ -89,11 +90,13 @@ describe(`cookieConsentController.ts`, () => { ...otherConsents, }); }); + it('parses props correctly without consents', () => { createControllerAndInitCookie({}); expect(controller.getRequired()).toEqual({}); expect(controller.getOptional()).toEqual({}); }); + it('throws when same consent is both optional and required', () => { expect(() => createConsentController({ @@ -102,6 +105,7 @@ describe(`cookieConsentController.ts`, () => { }), ).toThrow(); }); + it('restores saved consents correctly', () => { const storedCookieData = { requiredConsent1: true, @@ -127,12 +131,14 @@ describe(`cookieConsentController.ts`, () => { }); expect(controller.save()).toEqual(allConsents); }); + it('cookie is only read on init', () => { createControllerAndInitCookie({}); expect(mockCookieHelpers.mockGet).toHaveBeenCalledTimes(1); expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); }); }); + describe('unknown consents', () => { it('are parsed, stored and never changed', () => { const cookieDataWithUnknownConsents = { @@ -159,6 +165,7 @@ describe(`cookieConsentController.ts`, () => { }); }); }); + describe('approveAll', () => { it('sets all consents to true. Setting consents does not store a new cookie', () => { createControllerAndInitCookie(defaultControllerTestData); @@ -174,6 +181,7 @@ describe(`cookieConsentController.ts`, () => { expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); }); }); + describe('approveRequired', () => { it(`approves all required consents. Optional are not affected. @@ -191,6 +199,7 @@ describe(`cookieConsentController.ts`, () => { expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); }); }); + describe('rejectAll', () => { it('rejects all consents. Setting consents does not store a new cookie', () => { createControllerAndInitCookie({ @@ -228,6 +237,7 @@ describe(`cookieConsentController.ts`, () => { expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); }); }); + describe('getUnhandledConsents', () => { it('returns consents that are not defined in previously saved cookie.', () => { createControllerAndInitCookie({ @@ -244,6 +254,7 @@ describe(`cookieConsentController.ts`, () => { expect(unhandled.includes('unknownCookie1')).toBeFalsy(); }); }); + describe('getRequiredWithoutConsent', () => { it('returns required consents that are not currently approved', () => { createControllerAndInitCookie({ @@ -258,6 +269,7 @@ describe(`cookieConsentController.ts`, () => { expect(unhandled.includes('optionalConsent1')).toBeTruthy(); }); }); + describe('update', () => { const allConsents = [ ...defaultControllerTestData.requiredConsents, @@ -283,10 +295,12 @@ describe(`cookieConsentController.ts`, () => { }); }); }); + it('Throws when setting an unknown consent', () => { createControllerAndInitCookie(defaultControllerTestData); expect(() => controller.update('consentX', false)).toThrow(); }); + it('Does not auto save the cookie', () => { allConsents.forEach((consent) => { controller.update(consent, true); @@ -294,6 +308,7 @@ describe(`cookieConsentController.ts`, () => { expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); }); }); + describe('save', () => { it('stores the data into a cookie', () => { createControllerAndInitCookie(defaultControllerTestData); @@ -308,6 +323,7 @@ describe(`cookieConsentController.ts`, () => { optionalConsent2: false, }); }); + it('the domain of the cookie is set to . so it is readable from *.hel.fi and *.hel.ninja', () => { createControllerAndInitCookie(defaultControllerTestData); mockedWindowControls.setUrl('https://subdomain.hel.fi'); @@ -317,6 +333,7 @@ describe(`cookieConsentController.ts`, () => { controller.save(); expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.ninja'); }); + it('if "cookieDomain" property is passed in the props, it is set as the domain of the cookie', () => { const cookieDomain = 'myhost.com'; createControllerAndInitCookie({ @@ -327,11 +344,13 @@ describe(`cookieConsentController.ts`, () => { controller.save(); expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual(cookieDomain); }); + it('Cookie maxAge should match COOKIE_EXPIRATION_TIME', () => { createControllerAndInitCookie(defaultControllerTestData); controller.save(); expect(mockCookieHelpers.getSetCookieArguments().options.maxAge).toEqual(COOKIE_EXPIRATION_TIME); }); + it('Cookie name should match COOKIE_NAME', () => { createControllerAndInitCookie(defaultControllerTestData); controller.save(); @@ -339,6 +358,7 @@ describe(`cookieConsentController.ts`, () => { }); }); }); + describe('createStorage', () => { const createTestStorage = () => createStorage({ @@ -400,6 +420,7 @@ describe(`cookieConsentController.ts`, () => { expect(unknownConsents.unknownConsent1).toBeFalsy(); expect(unknownConsents.unknownConsent2).toBeTruthy(); }); + it('Throws when setting an unknown consent', () => { const storage = createTestStorage(); expect(() => storage.approve(['consentX'])).toThrow(); @@ -407,6 +428,7 @@ describe(`cookieConsentController.ts`, () => { expect(() => storage.approve(['unknownConsent1'])).toThrow(); expect(() => storage.reject(['unknownConsent2'])).toThrow(); }); + it('getConsentByName returns true/false even for unknown consents. Unknown consents are false', () => { const storage = createTestStorage(); expect(storage.getConsentByName('requiredConsent1')).toBeFalsy(); From f6601210b3795e9251c55ac647a9bb16b926a1a6 Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Tue, 30 Nov 2021 16:10:28 +0200 Subject: [PATCH 011/292] Apply ternary to make code more clear --- .../src/components/cookieConsent/CookieConsent.stories.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 0f3fbdbf8d..7eff6dc94a 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -22,7 +22,7 @@ export const Example = () => { aria-hidden={willRenderCookieConsentDialog ? 'true' : 'false'} >

This is a dummy application

- {willRenderCookieConsentDialog && ( + {willRenderCookieConsentDialog ? ( <>

Cookie consent dialog is visible.   @@ -31,8 +31,7 @@ export const Example = () => {

- )} - {!willRenderCookieConsentDialog && ( + ) : ( <>

Cookie consents have been given. Remove the cookie to see the dialog again.

From a7580895cc2febaf321e5ce32591874c93680c8e Mon Sep 17 00:00:00 2001 From: Ville Miekk-oja Date: Tue, 30 Nov 2021 16:20:04 +0200 Subject: [PATCH 012/292] Use ternary --- .../react/src/components/cookieConsent/content/Content.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index 027d3a21e2..3dbd543d45 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -27,8 +27,7 @@ function Content({ onClick }: ViewProps): React.ReactElement { id="cookie-consent-content" aria-live="assertive" > - {!showMore &&
} - {showMore &&
} + {showMore ?
:
}
e.preventDefault()}> From 68e002e0d0e6c0121a12405d4c6ec8f5310d912c Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Thu, 2 Dec 2021 14:21:42 +0200 Subject: [PATCH 013/292] Add cookie as a dependency --- packages/react/package.json | 2 + yarn.lock | 475 +++++------------------------------- 2 files changed, 61 insertions(+), 416 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 822d900821..868c01c760 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -104,6 +104,8 @@ "@juggle/resize-observer": "3.2.0", "@popperjs/core": "2.5.3", "@react-aria/visually-hidden": "3.2.0", + "@types/cookie": "^0.4.1", + "cookie": "^0.4.1", "date-fns": "2.16.1", "downshift": "6.0.6", "hds-core": "2.1.1", diff --git a/yarn.lock b/yarn.lock index 5025952b63..4e2375b76f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2831,7 +2831,7 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^24.7.1", "@jest/console@^24.9.0": +"@jest/console@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== @@ -2852,40 +2852,6 @@ jest-util "^26.6.2" slash "^3.0.0" -"@jest/core@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" - integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== - dependencies: - "@jest/console" "^24.7.1" - "@jest/reporters" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - ansi-escapes "^3.0.0" - chalk "^2.0.1" - exit "^0.1.2" - graceful-fs "^4.1.15" - jest-changed-files "^24.9.0" - jest-config "^24.9.0" - jest-haste-map "^24.9.0" - jest-message-util "^24.9.0" - jest-regex-util "^24.3.0" - jest-resolve "^24.9.0" - jest-resolve-dependencies "^24.9.0" - jest-runner "^24.9.0" - jest-runtime "^24.9.0" - jest-snapshot "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - jest-watcher "^24.9.0" - micromatch "^3.1.10" - p-each-series "^1.0.0" - realpath-native "^1.1.0" - rimraf "^2.5.4" - slash "^2.0.0" - strip-ansi "^5.0.0" - "@jest/core@^26.6.3": version "26.6.3" resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" @@ -2920,7 +2886,7 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^24.3.0", "@jest/environment@^24.9.0": +"@jest/environment@^24.3.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== @@ -2981,33 +2947,6 @@ "@jest/types" "^26.6.2" expect "^26.6.2" -"@jest/reporters@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" - integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== - dependencies: - "@jest/environment" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - exit "^0.1.2" - glob "^7.1.2" - istanbul-lib-coverage "^2.0.2" - istanbul-lib-instrument "^3.0.1" - istanbul-lib-report "^2.0.4" - istanbul-lib-source-maps "^3.0.1" - istanbul-reports "^2.2.6" - jest-haste-map "^24.9.0" - jest-resolve "^24.9.0" - jest-runtime "^24.9.0" - jest-util "^24.9.0" - jest-worker "^24.6.0" - node-notifier "^5.4.2" - slash "^2.0.0" - source-map "^0.6.0" - string-length "^2.0.0" - "@jest/reporters@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" @@ -3040,7 +2979,7 @@ optionalDependencies: node-notifier "^8.0.0" -"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": +"@jest/source-map@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== @@ -3077,16 +3016,6 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" - integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== - dependencies: - "@jest/test-result" "^24.9.0" - jest-haste-map "^24.9.0" - jest-runner "^24.9.0" - jest-runtime "^24.9.0" - "@jest/test-sequencer@^26.6.3": version "26.6.3" resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" @@ -7112,16 +7041,11 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== -"@types/cookie@^0.4.0": +"@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/cors@^2.8.8": - version "2.8.12" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" - integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== - "@types/css-modules-loader-core@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz#67af15aa16603ac2ffc1d3a7f08547ac809c3005" @@ -8398,7 +8322,12 @@ accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-globals@^4.1.0, acorn-globals@^4.3.0: +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + +acorn-globals@^4.3.0: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== @@ -9297,13 +9226,6 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" - integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== - dependencies: - "@types/babel__traverse" "^7.0.6" - babel-plugin-jest-hoist@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" @@ -9502,14 +9424,6 @@ babel-preset-gatsby@^2.17.0: gatsby-core-utils "^3.17.0" gatsby-legacy-polyfills "^2.17.0" -babel-preset-jest@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" - integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== - dependencies: - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^24.9.0" - babel-preset-jest@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" @@ -11361,7 +11275,7 @@ cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -cookie@^0.4.0, cookie@~0.4.1: +cookie@^0.4.0, cookie@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== @@ -12068,7 +11982,7 @@ csso@^4.0.2, csso@^4.2.0: dependencies: css-tree "^1.1.2" -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@^0.3.4, cssom@~0.3.6: +cssom@0.3.x, cssom@^0.3.4, cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== @@ -12078,7 +11992,7 @@ cssom@^0.4.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== -cssstyle@^1.0.0, cssstyle@^1.1.1: +cssstyle@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== @@ -12141,7 +12055,7 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-urls@^1.0.0, data-urls@^1.1.0: +data-urls@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== @@ -12517,6 +12431,16 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-match-patch@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -14017,18 +13941,6 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expect@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" - integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== - dependencies: - "@jest/types" "^24.9.0" - ansi-styles "^3.2.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-regex-util "^24.9.0" - expect@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" @@ -17784,7 +17696,7 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: +istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== @@ -17794,7 +17706,7 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: +istanbul-lib-instrument@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== @@ -17846,17 +17758,6 @@ istanbul-lib-report@^3.0.0: make-dir "^3.0.0" supports-color "^7.1.0" -istanbul-lib-source-maps@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" - source-map "^0.6.1" - istanbul-lib-source-maps@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" @@ -17909,15 +17810,6 @@ jest-axe@^5.0.1: jest-matcher-utils "27.0.2" lodash.merge "4.6.2" -jest-changed-files@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" - integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== - dependencies: - "@jest/types" "^24.9.0" - execa "^1.0.0" - throat "^4.0.0" - jest-changed-files@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" @@ -17927,25 +17819,6 @@ jest-changed-files@^26.6.2: execa "^4.0.0" throat "^5.0.0" -jest-cli@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" - integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== - dependencies: - "@jest/core" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - exit "^0.1.2" - import-local "^2.0.0" - is-ci "^2.0.0" - jest-config "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - prompts "^2.0.1" - realpath-native "^1.1.0" - yargs "^13.3.0" - jest-cli@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" @@ -17965,29 +17838,6 @@ jest-cli@^26.6.3: prompts "^2.0.1" yargs "^15.4.1" -jest-config@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" - integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^24.9.0" - "@jest/types" "^24.9.0" - babel-jest "^24.9.0" - chalk "^2.0.1" - glob "^7.1.1" - jest-environment-jsdom "^24.9.0" - jest-environment-node "^24.9.0" - jest-get-type "^24.9.0" - jest-jasmine2 "^24.9.0" - jest-regex-util "^24.3.0" - jest-resolve "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - micromatch "^3.1.10" - pretty-format "^24.9.0" - realpath-native "^1.1.0" - jest-config@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" @@ -18022,6 +17872,16 @@ jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-diff@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-diff@^26.0.0, jest-diff@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" @@ -18042,13 +17902,6 @@ jest-diff@^27.0.2, jest-diff@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-docblock@^24.3.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" - integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== - dependencies: - detect-newline "^2.1.0" - jest-docblock@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" @@ -18056,17 +17909,6 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" - integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== - dependencies: - "@jest/types" "^24.9.0" - chalk "^2.0.1" - jest-get-type "^24.9.0" - jest-util "^24.9.0" - pretty-format "^24.9.0" - jest-each@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" @@ -18100,18 +17942,6 @@ jest-environment-jsdom-sixteen@^2.0.0: jest-util "^25.1.0" jsdom "^16.2.1" -jest-environment-jsdom@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" - integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== - dependencies: - "@jest/environment" "^24.9.0" - "@jest/fake-timers" "^24.9.0" - "@jest/types" "^24.9.0" - jest-mock "^24.9.0" - jest-util "^24.9.0" - jsdom "^11.5.1" - jest-environment-jsdom@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" @@ -18125,17 +17955,6 @@ jest-environment-jsdom@^26.6.2: jest-util "^26.6.2" jsdom "^16.4.0" -jest-environment-node@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" - integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== - dependencies: - "@jest/environment" "^24.9.0" - "@jest/fake-timers" "^24.9.0" - "@jest/types" "^24.9.0" - jest-mock "^24.9.0" - jest-util "^24.9.0" - jest-environment-node@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" @@ -18153,6 +17972,11 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -18203,28 +18027,6 @@ jest-haste-map@^26.6.2: optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" - integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - co "^4.6.0" - expect "^24.9.0" - is-generator-fn "^2.0.0" - jest-each "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-runtime "^24.9.0" - jest-snapshot "^24.9.0" - jest-util "^24.9.0" - pretty-format "^24.9.0" - throat "^4.0.0" - jest-jasmine2@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" @@ -18249,14 +18051,6 @@ jest-jasmine2@^26.6.3: pretty-format "^26.6.2" throat "^5.0.0" -jest-leak-detector@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" - integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== - dependencies: - jest-get-type "^24.9.0" - pretty-format "^24.9.0" - jest-leak-detector@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" @@ -18275,16 +18069,6 @@ jest-matcher-utils@27.0.2: jest-get-type "^27.0.1" pretty-format "^27.0.2" -jest-matcher-utils@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" - integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== - dependencies: - chalk "^2.0.1" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - pretty-format "^24.9.0" - jest-matcher-utils@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" @@ -18375,7 +18159,7 @@ jest-pnp-resolver@^1.2.1, jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: +jest-regex-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== @@ -18385,15 +18169,6 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" - integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== - dependencies: - "@jest/types" "^24.9.0" - jest-regex-util "^24.3.0" - jest-snapshot "^24.9.0" - jest-resolve-dependencies@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" @@ -18403,7 +18178,7 @@ jest-resolve-dependencies@^26.6.3: jest-regex-util "^26.0.0" jest-snapshot "^26.6.2" -jest-resolve@24.9.0, jest-resolve@^24.9.0: +jest-resolve@24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== @@ -18428,31 +18203,6 @@ jest-resolve@^26.6.2: resolve "^1.18.1" slash "^3.0.0" -jest-runner@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" - integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== - dependencies: - "@jest/console" "^24.7.1" - "@jest/environment" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.4.2" - exit "^0.1.2" - graceful-fs "^4.1.15" - jest-config "^24.9.0" - jest-docblock "^24.3.0" - jest-haste-map "^24.9.0" - jest-jasmine2 "^24.9.0" - jest-leak-detector "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - jest-runtime "^24.9.0" - jest-util "^24.9.0" - jest-worker "^24.6.0" - source-map-support "^0.5.6" - throat "^4.0.0" - jest-runner@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" @@ -18479,35 +18229,6 @@ jest-runner@^26.6.3: source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" - integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== - dependencies: - "@jest/console" "^24.7.1" - "@jest/environment" "^24.9.0" - "@jest/source-map" "^24.3.0" - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - "@types/yargs" "^13.0.0" - chalk "^2.0.1" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.1.15" - jest-config "^24.9.0" - jest-haste-map "^24.9.0" - jest-message-util "^24.9.0" - jest-mock "^24.9.0" - jest-regex-util "^24.3.0" - jest-resolve "^24.9.0" - jest-snapshot "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - realpath-native "^1.1.0" - slash "^2.0.0" - strip-bom "^3.0.0" - yargs "^13.3.0" - jest-runtime@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" @@ -18554,25 +18275,6 @@ jest-serializer@^26.6.2: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" - integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - expect "^24.9.0" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^24.9.0" - semver "^6.2.0" - jest-snapshot@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" @@ -18636,18 +18338,6 @@ jest-util@^26.1.0, jest-util@^26.6.2: is-ci "^2.0.0" micromatch "^4.0.2" -jest-validate@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" - integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== - dependencies: - "@jest/types" "^24.9.0" - camelcase "^5.3.1" - chalk "^2.0.1" - jest-get-type "^24.9.0" - leven "^3.1.0" - pretty-format "^24.9.0" - jest-validate@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" @@ -18673,7 +18363,7 @@ jest-watch-typeahead@0.4.2: string-length "^3.1.0" strip-ansi "^5.0.0" -jest-watcher@^24.3.0, jest-watcher@^24.9.0: +jest-watcher@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== @@ -18699,7 +18389,7 @@ jest-watcher@^26.6.2: jest-util "^26.6.2" string-length "^4.0.1" -jest-worker@^24.6.0, jest-worker@^24.9.0: +jest-worker@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== @@ -18810,38 +18500,6 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom@^11.5.1: - version "11.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - jsdom@^14.1.0: version "14.1.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-14.1.0.tgz#916463b6094956b0a6c1782c94e380cd30e1981b" @@ -19195,11 +18853,6 @@ lcid@^2.0.0: dependencies: invert-kv "^2.0.0" -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - lerna@^3.16.4: version "3.22.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" @@ -19587,16 +19240,6 @@ lodash.isobject@3.0.2: resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.isundefined@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" @@ -21211,7 +20854,7 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -nwsapi@^2.0.7, nwsapi@^2.1.3, nwsapi@^2.2.0: +nwsapi@^2.1.3, nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== @@ -23460,6 +23103,15 @@ pretty-format@^24.9.0: ansi-regex "^4.0.0" ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" + integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== + dependencies: + "@jest/types" "^25.5.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" pretty-format@^26.0.0, pretty-format@^26.6.2: version "26.6.2" @@ -25375,7 +25027,7 @@ sax@1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -27427,7 +27079,7 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= -tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0, tough-cookie@~2.5.0: +tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -28043,15 +27695,6 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.4.1: dependencies: unist-util-visit-parents "^2.0.0" -unist-util-visit@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5" - integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.0.0" - universal-cookie@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" @@ -28859,7 +28502,7 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== @@ -28871,7 +28514,7 @@ whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: +whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== From d1d1ba5303616778cb89010bd56ed26588621458 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Thu, 2 Dec 2021 14:44:15 +0200 Subject: [PATCH 014/292] Added cookieController to replace universal-cookie MockDocumentCookie mocks document.cookie and appends new cookies like browsers --- .../__mocks__/mockDocumentCookie.ts | 87 +++++++++++++ .../cookieConsent/cookieController.test.ts | 120 ++++++++++++++++++ .../cookieConsent/cookieController.ts | 20 +++ 3 files changed, 227 insertions(+) create mode 100644 packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts create mode 100644 packages/react/src/components/cookieConsent/cookieController.test.ts create mode 100644 packages/react/src/components/cookieConsent/cookieController.ts diff --git a/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts b/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts new file mode 100644 index 0000000000..c359a3db0c --- /dev/null +++ b/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts @@ -0,0 +1,87 @@ +import cookie, { CookieSerializeOptions } from 'cookie'; + +type Options = Record; + +export type MockedDocumentCookieActions = { + add: (keyValuePairs: Record) => void; + getCookie: () => string; + getCookieOptions: (key: string) => Options; + restore: () => void; + clear: () => void; +}; + +export default function mockDocumentCookie(): MockedDocumentCookieActions { + const COOKIE_OPTIONS_DELIMETER = ';'; + const globalDocument = global.document; + let oldDocumentCookie = globalDocument.cookie; + const current = new Map(); + const cookieWithOptions = new Map(); + + const getter = (): string => { + return Array.from(current.entries()) + .map(([k, v]) => `${k} = ${v}${COOKIE_OPTIONS_DELIMETER}`) + .join(' '); + }; + + const setter = (cookieData: string): void => { + const [key, value] = cookieData.split('='); + const trimmedKey = key.trim(); + if (!trimmedKey) { + return; + } + const newValue = String(value.split(COOKIE_OPTIONS_DELIMETER)[0]).trim(); + current.set(trimmedKey, newValue); + cookieWithOptions.set(trimmedKey, cookieData); + }; + + Reflect.deleteProperty(globalDocument, 'cookie'); + Reflect.defineProperty(globalDocument, 'cookie', { + configurable: true, + get: () => getter(), + set: (newValue) => setter(newValue), + }); + + return { + add: (keyValuePairs) => { + Object.entries(keyValuePairs).forEach(([k, v]) => { + setter(`${k}=${v}`); + }); + }, + getCookie: () => { + return getter(); + }, + getCookieOptions: (key) => { + const cookieStr = cookieWithOptions.get(key); + const fullCookie = cookie.parse(cookieStr); + const options: Partial = {}; + if (cookieStr.includes('HttpOnly')) { + options.httpOnly = true; + } + if (cookieStr.includes('Secure')) { + options.secure = true; + } + return Object.entries(fullCookie).reduce((current, [objectKey, objectValue]) => { + /* remove the cookie name and value from options*/ + if (objectKey === key) { + return current; + } else if (objectKey === 'Max-Age') { + current.maxAge = Number(objectValue); + } else if (objectKey === 'Expires') { + current.expires = new Date(objectValue); + } else if (objectKey === 'SameSite') { + current.sameSite = objectValue.toLowerCase(); + } else { + current[objectKey.toLowerCase()] = objectValue; + } + return current; + }, options) as Options; + }, + restore: () => { + globalDocument.cookie = oldDocumentCookie; + }, + clear: () => { + current.clear(); + cookieWithOptions.clear(); + }, + }; +} diff --git a/packages/react/src/components/cookieConsent/cookieController.test.ts b/packages/react/src/components/cookieConsent/cookieController.test.ts new file mode 100644 index 0000000000..6787700fc6 --- /dev/null +++ b/packages/react/src/components/cookieConsent/cookieController.test.ts @@ -0,0 +1,120 @@ +import { CookieSerializeOptions } from 'cookie'; + +import mockDocumentCookie from './__mocks__/mockDocumentCookie'; +import cookie from './cookieController'; + +describe(`cookieController.ts`, () => { + let mockedCookieControls = mockDocumentCookie(); + + afterEach(() => { + mockedCookieControls.clear(); + }); + afterAll(() => { + mockedCookieControls.restore(); + }); + + const dummyCookieList = { + cookieKey: 'cookie', + jsonCookie: JSON.stringify({ json: true }), + objectStringCookie: '{ obj: true }', + emptyCookie: '', + }; + + const dummyValue = 'cookieValue'; + const dummyKey = 'cookieName'; + + describe('cookie.getAll', () => { + it('Gets all cookies as an object from document.cookie', () => { + mockedCookieControls.add(dummyCookieList); + expect(cookie.getAll()).toEqual(dummyCookieList); + }); + it('Returns an empty object if document.cookie is an empty string', () => { + expect(cookie.getAll()).toEqual({}); + }); + }); + + describe('cookie.get', () => { + it('Gets cookie value by name from document.cookie', () => { + mockedCookieControls.add(dummyCookieList); + expect(cookie.get('cookieKey')).toEqual(dummyCookieList.cookieKey); + expect(cookie.get('jsonCookie')).toEqual(dummyCookieList.jsonCookie); + expect(cookie.get('objectStringCookie')).toEqual(dummyCookieList.objectStringCookie); + expect(cookie.get('emptyCookie')).toEqual(dummyCookieList.emptyCookie); + }); + it('returns undefined if cookie is not found', () => { + mockedCookieControls.add(dummyCookieList); + expect(cookie.get('doesnotExist')).toBeUndefined(); + }); + }); + + describe('cookie.set', () => { + it('stores a new cookie to document.cookie. Options are empty if not set', () => { + const allCookies = { ...dummyCookieList, [dummyKey]: dummyValue }; + mockedCookieControls.add(dummyCookieList); + cookie.set(dummyKey, dummyValue); + expect(cookie.get(dummyKey)).toEqual(dummyValue); + const cookies = cookie.getAll(); + expect(cookies).toEqual(allCookies); + const options = mockedCookieControls.getCookieOptions(dummyKey); + expect(options).toEqual({}); + + const cookiesAsString = mockedCookieControls.getCookie(); + Object.entries(allCookies).forEach(([key, value]) => { + expect(cookiesAsString.includes(`${key} = ${value};`)); + }); + }); + + it('updates previous cookie', () => { + const target = 'emptyCookie'; + const newValue = 'notEmpty'; + mockedCookieControls.add(dummyCookieList); + expect(cookie.get(target)).toEqual(dummyCookieList.emptyCookie); + cookie.set(target, newValue); + expect(cookie.get(target)).toEqual(newValue); + }); + + it('passed value is encoded', () => { + const target = 'specialChars'; + const newValue = '"+{}&!=%"[]:'; + cookie.set(target, newValue); + expect(cookie.get(target)).toEqual(newValue); + const cookiesAsString = mockedCookieControls.getCookie(); + expect(cookiesAsString).toEqual(`${target} = ${encodeURIComponent(newValue)};`); + }); + + it('passes also options to document.cookie', () => { + const options: CookieSerializeOptions = { + domain: 'domain.com', + expires: new Date('Sun, 24 Dec 2050 12:12:12 GMT'), + path: '/path', + httpOnly: true, + sameSite: 'lax', + secure: true, + maxAge: 100, + }; + cookie.set(dummyKey, dummyValue, options); + const optionsFromCookie = mockedCookieControls.getCookieOptions(dummyKey); + expect(optionsFromCookie).toEqual(options); + }); + + it('passes also partial options to document.cookie', () => { + const options: CookieSerializeOptions = { + domain: 'domain.com', + sameSite: 'none', + }; + cookie.set(dummyKey, dummyValue, options); + const optionsFromCookie = mockedCookieControls.getCookieOptions(dummyKey); + expect(optionsFromCookie).toEqual(options); + }); + it('throws when setting invalid options', () => { + const options: CookieSerializeOptions = { + expires: (1111 as unknown) as Date, + }; + expect(() => cookie.set(dummyKey, dummyValue, options)).toThrow(); + }); + + it('throws when setting an invalid cookie name', () => { + expect(() => cookie.set(`${dummyKey}\n`, dummyValue)).toThrow(); + }); + }); +}); diff --git a/packages/react/src/components/cookieConsent/cookieController.ts b/packages/react/src/components/cookieConsent/cookieController.ts new file mode 100644 index 0000000000..637dc8d348 --- /dev/null +++ b/packages/react/src/components/cookieConsent/cookieController.ts @@ -0,0 +1,20 @@ +import cookie, { CookieSerializeOptions } from 'cookie'; + +function getAll() { + return cookie.parse(document.cookie); +} + +function get(name: string): string | undefined { + const cookies = getAll(); + return cookies[name]; +} + +function set(name: string, value: string, options?: CookieSerializeOptions) { + document.cookie = cookie.serialize(name, value, options); +} + +export default { + getAll, + get, + set, +}; From 8d249a8b3fb2e3433724706bc7a6b092cbfe827f Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 3 Dec 2021 10:09:58 +0200 Subject: [PATCH 015/292] Use cookieController in cookieConsentController Also export set options from cookieController --- .../cookieConsent/cookieConsentController.ts | 8 ++++---- .../cookieConsent/cookieController.test.ts | 15 +++++++-------- .../components/cookieConsent/cookieController.ts | 2 ++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.ts b/packages/react/src/components/cookieConsent/cookieConsentController.ts index 3602b00b9a..c51175064b 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.ts @@ -1,7 +1,8 @@ import _pick from 'lodash.pick'; import _isObject from 'lodash.isobject'; import _isUndefined from 'lodash.isundefined'; -import CookieController, { CookieSetOptions } from 'universal-cookie'; + +import cookieControllerModule, { CookieSetOptions } from './cookieController'; export type ConsentList = string[]; @@ -78,7 +79,6 @@ function createCookieController( get: () => string; set: (data: string) => void; } { - const cookieController = new CookieController(); const defaultCookieSetOptions: CookieSetOptions = { path: '/', secure: false, @@ -93,10 +93,10 @@ function createCookieController( domain: cookieDomain || getCookieDomainFromUrl(), }); - const get = (): string => cookieController.get(COOKIE_NAME, { doNotParse: true }) || ''; + const get = (): string => cookieControllerModule.get(COOKIE_NAME) || ''; const set = (data: string): void => { - cookieController.set(COOKIE_NAME, data, createCookieOptions()); + cookieControllerModule.set(COOKIE_NAME, data, createCookieOptions()); }; return { get, diff --git a/packages/react/src/components/cookieConsent/cookieController.test.ts b/packages/react/src/components/cookieConsent/cookieController.test.ts index 6787700fc6..e84d8db035 100644 --- a/packages/react/src/components/cookieConsent/cookieController.test.ts +++ b/packages/react/src/components/cookieConsent/cookieController.test.ts @@ -1,10 +1,9 @@ -import { CookieSerializeOptions } from 'cookie'; - +/* eslint-disable jest/no-mocks-import */ import mockDocumentCookie from './__mocks__/mockDocumentCookie'; -import cookie from './cookieController'; +import cookie, { CookieSetOptions } from './cookieController'; describe(`cookieController.ts`, () => { - let mockedCookieControls = mockDocumentCookie(); + const mockedCookieControls = mockDocumentCookie(); afterEach(() => { mockedCookieControls.clear(); @@ -60,7 +59,7 @@ describe(`cookieController.ts`, () => { const cookiesAsString = mockedCookieControls.getCookie(); Object.entries(allCookies).forEach(([key, value]) => { - expect(cookiesAsString.includes(`${key} = ${value};`)); + expect(cookiesAsString.includes(`${key} = ${value};`)).toBeTruthy(); }); }); @@ -83,7 +82,7 @@ describe(`cookieController.ts`, () => { }); it('passes also options to document.cookie', () => { - const options: CookieSerializeOptions = { + const options: CookieSetOptions = { domain: 'domain.com', expires: new Date('Sun, 24 Dec 2050 12:12:12 GMT'), path: '/path', @@ -98,7 +97,7 @@ describe(`cookieController.ts`, () => { }); it('passes also partial options to document.cookie', () => { - const options: CookieSerializeOptions = { + const options: CookieSetOptions = { domain: 'domain.com', sameSite: 'none', }; @@ -107,7 +106,7 @@ describe(`cookieController.ts`, () => { expect(optionsFromCookie).toEqual(options); }); it('throws when setting invalid options', () => { - const options: CookieSerializeOptions = { + const options: CookieSetOptions = { expires: (1111 as unknown) as Date, }; expect(() => cookie.set(dummyKey, dummyValue, options)).toThrow(); diff --git a/packages/react/src/components/cookieConsent/cookieController.ts b/packages/react/src/components/cookieConsent/cookieController.ts index 637dc8d348..686c2a5e53 100644 --- a/packages/react/src/components/cookieConsent/cookieController.ts +++ b/packages/react/src/components/cookieConsent/cookieController.ts @@ -1,5 +1,7 @@ import cookie, { CookieSerializeOptions } from 'cookie'; +export type CookieSetOptions = CookieSerializeOptions; + function getAll() { return cookie.parse(document.cookie); } From f4337cbbaed830c02fc438cfa7682cddd7b621d9 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 3 Dec 2021 10:17:05 +0200 Subject: [PATCH 016/292] Update mockDocumentCookie Added mock wrappers to "getter" and "setter" to count function calls and extract passed arguments. Added "init" function which differs from "add" by clearing the mock wrapper of the "setter". This way call counts etc start from 0 after initialization --- .../__mocks__/mockDocumentCookie.ts | 93 ++++++++++++------- 1 file changed, 58 insertions(+), 35 deletions(-) diff --git a/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts b/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts index c359a3db0c..dd92d7aebf 100644 --- a/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts +++ b/packages/react/src/components/cookieConsent/__mocks__/mockDocumentCookie.ts @@ -1,29 +1,33 @@ -import cookie, { CookieSerializeOptions } from 'cookie'; +import cookie from 'cookie'; type Options = Record; export type MockedDocumentCookieActions = { + init: (keyValuePairs: Record) => void; add: (keyValuePairs: Record) => void; getCookie: () => string; getCookieOptions: (key: string) => Options; + extractCookieOptions: (cookieStr: string, keyToRemove: string) => Options; restore: () => void; clear: () => void; + mockGet: jest.Mock; + mockSet: jest.Mock; }; export default function mockDocumentCookie(): MockedDocumentCookieActions { const COOKIE_OPTIONS_DELIMETER = ';'; const globalDocument = global.document; - let oldDocumentCookie = globalDocument.cookie; + const oldDocumentCookie = globalDocument.cookie; const current = new Map(); const cookieWithOptions = new Map(); - const getter = (): string => { + const getter = jest.fn((): string => { return Array.from(current.entries()) .map(([k, v]) => `${k} = ${v}${COOKIE_OPTIONS_DELIMETER}`) .join(' '); - }; + }); - const setter = (cookieData: string): void => { + const setter = jest.fn((cookieData: string): void => { const [key, value] = cookieData.split('='); const trimmedKey = key.trim(); if (!trimmedKey) { @@ -32,7 +36,7 @@ export default function mockDocumentCookie(): MockedDocumentCookieActions { const newValue = String(value.split(COOKIE_OPTIONS_DELIMETER)[0]).trim(); current.set(trimmedKey, newValue); cookieWithOptions.set(trimmedKey, cookieData); - }; + }); Reflect.deleteProperty(globalDocument, 'cookie'); Reflect.defineProperty(globalDocument, 'cookie', { @@ -41,47 +45,66 @@ export default function mockDocumentCookie(): MockedDocumentCookieActions { set: (newValue) => setter(newValue), }); + const setWithObject = (keyValuePairs: Record) => + Object.entries(keyValuePairs).forEach(([k, v]) => { + setter(`${k}=${v}`); + }); + + const extractCookieOptions = (cookieStr: string, keyToRemove: string): Options => { + const fullCookie = cookie.parse(cookieStr); + const options: Partial = {}; + if (cookieStr.includes('HttpOnly')) { + options.httpOnly = true; + } + if (cookieStr.includes('Secure')) { + options.secure = true; + } + return Object.entries(fullCookie).reduce((returnObj, [objectKey, objectValue]) => { + /* eslint-disable no-param-reassign */ + /* + The object from cookie.parse() includes also cookieName:cookieValue + remove those from options + */ + if (objectKey === keyToRemove) { + return returnObj; + } + if (objectKey === 'Max-Age') { + returnObj.maxAge = Number(objectValue); + } else if (objectKey === 'Expires') { + returnObj.expires = new Date(objectValue); + } else if (objectKey === 'SameSite') { + returnObj.sameSite = objectValue.toLowerCase(); + } else { + returnObj[objectKey.toLowerCase()] = objectValue; + } + /* eslint-enable no-param-reassign */ + return returnObj; + }, options) as Options; + }; + return { - add: (keyValuePairs) => { - Object.entries(keyValuePairs).forEach(([k, v]) => { - setter(`${k}=${v}`); - }); - }, + add: (keyValuePairs) => setWithObject(keyValuePairs), getCookie: () => { return getter(); }, getCookieOptions: (key) => { - const cookieStr = cookieWithOptions.get(key); - const fullCookie = cookie.parse(cookieStr); - const options: Partial = {}; - if (cookieStr.includes('HttpOnly')) { - options.httpOnly = true; - } - if (cookieStr.includes('Secure')) { - options.secure = true; - } - return Object.entries(fullCookie).reduce((current, [objectKey, objectValue]) => { - /* remove the cookie name and value from options*/ - if (objectKey === key) { - return current; - } else if (objectKey === 'Max-Age') { - current.maxAge = Number(objectValue); - } else if (objectKey === 'Expires') { - current.expires = new Date(objectValue); - } else if (objectKey === 'SameSite') { - current.sameSite = objectValue.toLowerCase(); - } else { - current[objectKey.toLowerCase()] = objectValue; - } - return current; - }, options) as Options; + return extractCookieOptions(cookieWithOptions.get(key), key); }, + extractCookieOptions, restore: () => { globalDocument.cookie = oldDocumentCookie; }, clear: () => { current.clear(); cookieWithOptions.clear(); + getter.mockClear(); + setter.mockClear(); + }, + init: (keyValuePairs) => { + setWithObject(keyValuePairs); + setter.mockClear(); }, + mockGet: getter, + mockSet: setter, }; } From 7f9aa24f74a8dd06d2ff53b7c09f719cf831f238 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 3 Dec 2021 10:17:58 +0200 Subject: [PATCH 017/292] Updated tests to handle new cookieController --- .../cookieConsent/CookieConsent.test.tsx | 44 +++++++------- .../CookieConsentContext.test.tsx | 35 +++++------ .../cookieConsentController.test.ts | 58 +++++++++---------- .../src/components/cookieConsent/test.util.ts | 28 +++++++++ 4 files changed, 89 insertions(+), 76 deletions(-) create mode 100644 packages/react/src/components/cookieConsent/test.util.ts diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index ed79bc0abb..5559cdda5e 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -5,9 +5,10 @@ import { render, RenderResult } from '@testing-library/react'; import { axe } from 'jest-axe'; import { CookieConsent } from './CookieConsent'; -import { ConsentList, ConsentObject } from './cookieConsentController'; -import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; +import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentController'; import { Provider as CookieContextProvider } from './CookieConsentContext'; +import mockDocumentCookie from './__mocks__/mockDocumentCookie'; +import extractSetCookieArguments from './test.util'; type ConsentData = { requiredConsents?: ConsentList; @@ -15,16 +16,6 @@ type ConsentData = { cookie?: ConsentObject; }; -const mockCookieHelpers = createUniversalCookieMockHelpers(); - -jest.mock( - 'universal-cookie', - () => - function universalCookieMockClass() { - return mockCookieHelpers.createMockedModule(); - }, -); - describe(' spec', () => { it('renders the component', () => { const { asFragment } = render(); @@ -39,6 +30,17 @@ describe(' spec', () => { }); describe(' ', () => { + const mockedCookieControls = mockDocumentCookie(); + afterEach(() => { + mockedCookieControls.clear(); + }); + + afterAll(() => { + mockedCookieControls.restore(); + }); + + const getSetCookieArguments = (index = -1) => extractSetCookieArguments(mockedCookieControls, index); + const dataTestIds = { container: 'cookie-consent', languageSwitcher: 'cookie-consent-language-switcher', @@ -88,7 +90,7 @@ describe(' ', () => { ...cookie, ...unknownConsents, }; - mockCookieHelpers.setStoredCookie(cookieWithInjectedUnknowns); + mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); return render( @@ -96,10 +98,6 @@ describe(' ', () => { ); }; - afterEach(() => { - mockCookieHelpers.reset(); - }); - describe('Cookie consent ', () => { it('and child components are rendered when consents have not been handled', () => { const result = renderCookieConsent(defaultConsentData); @@ -153,10 +151,10 @@ describe(' ', () => { ...unknownConsents, }; clickElement(result, dataTestIds.approveAllButton); - expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual(consentResult); + expect(JSON.parse(getSetCookieArguments().data)).toEqual(consentResult); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(1); }); it('Approve required -button, approves only required consents', () => { @@ -169,10 +167,10 @@ describe(' ', () => { ...unknownConsents, }; clickElement(result, dataTestIds.approveRequiredButton); - expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual(consentResult); + expect(JSON.parse(getSetCookieArguments().data)).toEqual(consentResult); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(1); }); }); @@ -201,7 +199,7 @@ describe(' ', () => { }); clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent2')); clickElement(result, dataTestIds.approveSelectionsButton); - expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ + expect(JSON.parse(getSetCookieArguments().data)).toEqual({ requiredConsent1: true, requiredConsent2: true, optionalConsent1: true, @@ -210,7 +208,7 @@ describe(' ', () => { }); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index 9e0527f27e..12d4602819 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -3,10 +3,11 @@ import React, { useContext } from 'react'; import { render, RenderResult } from '@testing-library/react'; -import { ConsentList, ConsentObject } from './cookieConsentController'; -import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; +import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentController'; import { CookieConsentContext, Provider as CookieContextProvider } from './CookieConsentContext'; -import mockWindowLocation, { MockedWindowLocationActions } from './__mocks__/mockWindowLocation'; +import mockWindowLocation from './__mocks__/mockWindowLocation'; +import mockDocumentCookie from './__mocks__/mockDocumentCookie'; +import extractSetCookieArguments from './test.util'; type ConsentData = { requiredConsents?: ConsentList; @@ -15,17 +16,12 @@ type ConsentData = { cookieDomain?: string; }; -const mockCookieHelpers = createUniversalCookieMockHelpers(); +describe('CookieConsentContext ', () => { + const mockedCookieControls = mockDocumentCookie(); + const mockedWindowControls = mockWindowLocation(); -jest.mock( - 'universal-cookie', - () => - function universalCookieMockClass() { - return mockCookieHelpers.createMockedModule(); - }, -); + const getSetCookieArguments = (index = -1) => extractSetCookieArguments(mockedCookieControls, index); -describe('CookieConsentContext ', () => { const allApprovedConsentData = { requiredConsents: ['requiredConsent1'], optionalConsents: ['optionalConsent1'], @@ -71,19 +67,16 @@ describe('CookieConsentContext ', () => { const onAllConsentsGiven = jest.fn(); const onConsentsParsed = jest.fn(); - let mockedWindowControls: MockedWindowLocationActions; afterEach(() => { onAllConsentsGiven.mockReset(); onConsentsParsed.mockReset(); - mockCookieHelpers.reset(); + mockedCookieControls.clear(); }); - beforeAll(() => { - mockedWindowControls = mockWindowLocation(); - }); afterAll(() => { mockedWindowControls.restore(); + mockedCookieControls.restore(); }); const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { @@ -125,7 +118,7 @@ describe('CookieConsentContext ', () => { ...cookie, ...unknownConsents, }; - mockCookieHelpers.setStoredCookie(cookieWithInjectedUnknowns); + mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); return render( { mockedWindowControls.setUrl('https://subdomain.hel.fi'); const result = renderCookieConsent(allNotApprovedConsentData); clickElement(result, consumer1ApproveAllButtonSelector); - expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ + expect(JSON.parse(getSetCookieArguments().data)).toEqual({ ...allApprovedConsentData.cookie, ...unknownConsents, }); - expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.fi'); + expect(getSetCookieArguments().options.domain).toEqual('hel.fi'); }); it('sets the domain of the cookie to given cookieDomain', () => { @@ -206,7 +199,7 @@ describe('CookieConsentContext ', () => { cookieDomain, }); clickElement(result, consumer1ApproveAllButtonSelector); - expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual(cookieDomain); + expect(getSetCookieArguments().options.domain).toEqual(cookieDomain); }); }); }); diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts index 78ba4f7227..3cea2d61ab 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts @@ -1,6 +1,5 @@ /* eslint-disable jest/no-mocks-import */ -import { createUniversalCookieMockHelpers } from './__mocks__/mockUniversalCookie'; -import mockWindowLocation, { MockedWindowLocationActions } from './__mocks__/mockWindowLocation'; +import mockWindowLocation from './__mocks__/mockWindowLocation'; import createConsentController, { ConsentController, ConsentList, @@ -9,29 +8,24 @@ import createConsentController, { COOKIE_EXPIRATION_TIME, COOKIE_NAME, } from './cookieConsentController'; - -const mockCookieHelpers = createUniversalCookieMockHelpers(); - -jest.mock( - 'universal-cookie', - () => - function universalCookieMockClass() { - return mockCookieHelpers.createMockedModule(); - }, -); +import mockDocumentCookie from './__mocks__/mockDocumentCookie'; +import extractSetCookieArguments from './test.util'; describe(`cookieConsentController.ts`, () => { let controller: ConsentController; - let mockedWindowControls: MockedWindowLocationActions; + const mockedWindowControls = mockWindowLocation(); + const mockedCookieControls = mockDocumentCookie(); afterEach(() => { - mockCookieHelpers.reset(); - }); - beforeAll(() => { - mockedWindowControls = mockWindowLocation(); + mockedCookieControls.clear(); }); + afterAll(() => { + mockedCookieControls.restore(); mockedWindowControls.restore(); }); + + const getSetCookieArguments = (index = -1) => extractSetCookieArguments(mockedCookieControls, index); + const defaultControllerTestData = { requiredConsents: ['requiredConsent1', 'requiredConsent2'], optionalConsents: ['optionalConsent1', 'optionalConsent2'], @@ -48,7 +42,7 @@ describe(`cookieConsentController.ts`, () => { cookie?: ConsentObject; cookieDomain?: string; }) => { - mockCookieHelpers.setStoredCookie(cookie); + mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookie) }); controller = createConsentController({ requiredConsents, optionalConsents, @@ -134,8 +128,8 @@ describe(`cookieConsentController.ts`, () => { it('cookie is only read on init', () => { createControllerAndInitCookie({}); - expect(mockCookieHelpers.mockGet).toHaveBeenCalledTimes(1); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + expect(mockedCookieControls.mockGet).toHaveBeenCalledTimes(1); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(0); }); }); @@ -178,7 +172,7 @@ describe(`cookieConsentController.ts`, () => { optionalConsent1: true, optionalConsent2: true, }); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(0); }); }); @@ -196,7 +190,7 @@ describe(`cookieConsentController.ts`, () => { optionalConsent1: false, optionalConsent2: false, }); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(0); }); }); @@ -234,7 +228,7 @@ describe(`cookieConsentController.ts`, () => { optionalConsent2: false, }); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(0); }); }); @@ -305,18 +299,18 @@ describe(`cookieConsentController.ts`, () => { allConsents.forEach((consent) => { controller.update(consent, true); }); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(0); }); }); describe('save', () => { it('stores the data into a cookie', () => { createControllerAndInitCookie(defaultControllerTestData); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(0); + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(0); controller.approveRequired(); controller.save(); - expect(mockCookieHelpers.mockSet).toHaveBeenCalledTimes(1); - expect(JSON.parse(mockCookieHelpers.getSetCookieArguments().data)).toEqual({ + expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(1); + expect(JSON.parse(getSetCookieArguments().data)).toEqual({ requiredConsent1: true, requiredConsent2: true, optionalConsent1: false, @@ -328,10 +322,10 @@ describe(`cookieConsentController.ts`, () => { createControllerAndInitCookie(defaultControllerTestData); mockedWindowControls.setUrl('https://subdomain.hel.fi'); controller.save(); - expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.fi'); + expect(getSetCookieArguments().options.domain).toEqual('hel.fi'); mockedWindowControls.setUrl('http://profiili.hel.ninja:3000?foo=bar'); controller.save(); - expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual('hel.ninja'); + expect(getSetCookieArguments().options.domain).toEqual('hel.ninja'); }); it('if "cookieDomain" property is passed in the props, it is set as the domain of the cookie', () => { @@ -342,19 +336,19 @@ describe(`cookieConsentController.ts`, () => { }); mockedWindowControls.setUrl('https://notmyhost.com'); controller.save(); - expect(mockCookieHelpers.getSetCookieArguments().options.domain).toEqual(cookieDomain); + expect(getSetCookieArguments().options.domain).toEqual(cookieDomain); }); it('Cookie maxAge should match COOKIE_EXPIRATION_TIME', () => { createControllerAndInitCookie(defaultControllerTestData); controller.save(); - expect(mockCookieHelpers.getSetCookieArguments().options.maxAge).toEqual(COOKIE_EXPIRATION_TIME); + expect(getSetCookieArguments().options.maxAge).toEqual(COOKIE_EXPIRATION_TIME); }); it('Cookie name should match COOKIE_NAME', () => { createControllerAndInitCookie(defaultControllerTestData); controller.save(); - expect(mockCookieHelpers.getSetCookieArguments().cookieName).toEqual(COOKIE_NAME); + expect(getSetCookieArguments().cookieName).toEqual(COOKIE_NAME); }); }); }); diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts new file mode 100644 index 0000000000..39cdfcc5ed --- /dev/null +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -0,0 +1,28 @@ +/* eslint-disable jest/no-mocks-import */ +import cookie from 'cookie'; + +import { COOKIE_NAME } from './cookieConsentController'; +import { CookieSetOptions } from './cookieController'; +import { MockedDocumentCookieActions } from './__mocks__/mockDocumentCookie'; + +export default function extractSetCookieArguments( + mockedCookieControls: MockedDocumentCookieActions, + index = -1, +): { + cookieName: string; + data: string; + options: CookieSetOptions; +} { + const mockCalls = mockedCookieControls.mockSet.mock.calls; + const pos = index > -1 ? index : mockCalls.length - 1; + const callArgs = mockCalls[pos]; + const dataStr = callArgs[0] || ''; + const parsed = callArgs ? cookie.parse(dataStr) : {}; + const keyFound = Object.keys(parsed).includes(COOKIE_NAME); + const data = parsed[COOKIE_NAME]; + return { + cookieName: keyFound ? COOKIE_NAME : '', + data, + options: mockedCookieControls.extractCookieOptions(dataStr, COOKIE_NAME), + }; +} From 3799e64ed4262c372a316547281f43581ca3eb4a Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 3 Dec 2021 10:18:22 +0200 Subject: [PATCH 018/292] Remove universal-cookie and its mock --- .../__mocks__/mockUniversalCookie.ts | 68 ------------------- yarn.lock | 25 +++---- 2 files changed, 8 insertions(+), 85 deletions(-) delete mode 100644 packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts diff --git a/packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts b/packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts deleted file mode 100644 index d5cb09f70d..0000000000 --- a/packages/react/src/components/cookieConsent/__mocks__/mockUniversalCookie.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { CookieSetOptions } from 'universal-cookie'; - -type CookieData = string | Record; -type UniversalCookieMocking = { - createMockedModule: () => unknown; - mockGet: () => string; - mockSet: (name: string, value: string, options: CookieSetOptions) => void; - reset: () => void; - setStoredCookie: (objectToStringify: CookieData) => void; - getSetCookieArguments: ( - index?: number, - ) => { - cookieName: string; - data: string; - options: CookieSetOptions; - }; -}; - -export function createUniversalCookieMockHelpers(): UniversalCookieMocking { - let cookieValue = ''; - - const getter = jest.fn(() => cookieValue); - - // eslint-disable-next-line no-unused-vars - const setter = jest.fn((name: string, value: string, options: CookieSetOptions) => { - cookieValue = value; - }); - - const setStoredCookie = (data: CookieData) => { - cookieValue = typeof data === 'string' ? data : JSON.stringify(data); - }; - - const reset = () => { - getter.mockClear(); - setter.mockClear(); - cookieValue = ''; - }; - - const createMockedModule = () => ({ - get: getter, - set: setter, - }); - - const getSetCookieArguments = ( - index = -1, - ): { - cookieName: string; - data: string; - options: CookieSetOptions; - } => { - const pos = index > -1 ? index : setter.mock.calls.length - 1; - const callArgs = setter.mock.calls[pos]; - return { - cookieName: callArgs[0], - data: callArgs[1], - options: callArgs[2], - }; - }; - - return { - createMockedModule, - mockGet: getter, - mockSet: setter, - reset, - setStoredCookie, - getSetCookieArguments, - }; -} diff --git a/yarn.lock b/yarn.lock index 4e2375b76f..4393fe3980 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7036,12 +7036,7 @@ resolved "https://registry.yarnpkg.com/@types/configstore/-/configstore-2.1.1.tgz#cd1e8553633ad3185c3f2f239ecff5d2643e92b6" integrity sha512-YY+hm3afkDHeSM2rsFXxeZtu0garnusBWNG1+7MknmDWQHqcH2w21/xOU9arJUi8ch4qyFklidANLCu3ihhVwQ== -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - -"@types/cookie@^0.4.1": +"@types/cookie@^0.4.0", "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== @@ -11275,11 +11270,6 @@ cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -cookie@^0.4.0, cookie@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -27695,13 +27685,14 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.4.1: dependencies: unist-util-visit-parents "^2.0.0" -universal-cookie@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" - integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== +unist-util-visit@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5" + integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ== dependencies: - "@types/cookie" "^0.3.3" - cookie "^0.4.0" + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" universal-user-agent@^4.0.0: version "4.0.1" From 36bf72da48cfcfdaa953fff0d7306b8ae7a469e5 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 5 Jan 2022 16:12:19 +0200 Subject: [PATCH 019/292] Moved details into an Accordion New layout was introduced. Removed redundant hide/show buttons and code. Added css for the new button. --- .../cookieConsent/CookieConsent.module.scss | 19 +++++++++ .../cookieConsent/buttons/Buttons.tsx | 9 ----- .../cookieConsent/content/Content.tsx | 39 ++++++++++++------- .../cookieConsent/details/Details.tsx | 5 --- .../components/cookieConsent/main/Main.tsx | 13 +------ .../src/components/cookieConsent/types.ts | 2 +- 6 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 804c96448a..9b6bc0c5ad 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -126,3 +126,22 @@ .list li label { padding-left: 10px; } + +.accordion-button { + border: none; + background: transparent; + text-decoration: underline; + color: var(--color-bus); + padding: 0; + cursor: pointer; + display: flex; + align-items: center; +} + +.accordion-button svg { + margin-left: -5px; +} + +.accordion-button span { + padding-left: var(--spacing-3-xs); +} diff --git a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx index aa996b8def..eb1b2079b7 100644 --- a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -30,15 +30,6 @@ function Buttons({ onClick }: Props): React.ReactElement { > Pakolliset evästeet -
); } diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index 3dbd543d45..8ebeddd006 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -1,22 +1,20 @@ -import React, { useState } from 'react'; +import React from 'react'; -import { CookieConsentActionListener, ViewProps } from '../types'; +import { ViewProps } from '../types'; import Buttons from '../buttons/Buttons'; +import { IconAngleDown, IconAngleUp } from '../../../icons'; +import { useAccordion } from '../../accordion'; import Details from '../details/Details'; import Main from '../main/Main'; import styles from '../CookieConsent.module.scss'; +import { Card } from '../../card/Card'; function Content({ onClick }: ViewProps): React.ReactElement { - const [showMore, setShowMore] = useState(false); - const onAction: CookieConsentActionListener = (action, value) => { - if (action === 'showDetails') { - setShowMore(true); - } else if (action === 'hideDetails') { - setShowMore(false); - } else { - onClick(action, value); - } - }; + const { isOpen, buttonProps, contentProps } = useAccordion({ + initiallyOpen: false, + }); + const Icon = isOpen ? IconAngleUp : IconAngleDown; + const settingsButtonText = isOpen ? 'Piilota asetukset' : 'Näytä asetukset'; return ( ); } From 145ff8ade441a0954580253f1bc9a68cce9935a0 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 7 Jan 2022 10:26:51 +0200 Subject: [PATCH 022/292] Added checkboxes to required consents Also modified styles according to latest layout --- .../cookieConsent/CookieConsent.module.scss | 7 ++++--- .../optionalConsents/OptionalConsents.tsx | 7 +++++-- .../requiredConsents/RequiredConsents.tsx | 14 +++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index ac4413e740..58c04db67b 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -107,11 +107,12 @@ } .list li { - padding-bottom: 10px; + padding-bottom: var(--spacing-s); } -.list li label { - padding-left: 10px; +.list li span { + display: inline-block; + padding: var(--spacing-xs) 0 0 32px; } .accordion-button, diff --git a/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx index 695aef9b50..3ff2412995 100644 --- a/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx +++ b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; -import { getAriaLabel, getText } from '../texts'; +import { getAriaLabel, getText, getTitle } from '../texts'; import { ViewProps } from '../types'; import { Checkbox } from '../../checkbox'; import { CookieConsentContext } from '../CookieConsentContext'; @@ -10,6 +10,7 @@ type ConsentData = { id: string; checked: boolean; text: string; + title: string; ariaLabel: string; onToggle: () => void; }; @@ -23,6 +24,7 @@ function OptionalConsents({ onClick }: ViewProps): React.ReactElement { id: `optional-cookie-consent-${key}`, checked: Boolean(value), text: getText(key), + title: getTitle(key), ariaLabel: getAriaLabel(key), onToggle: () => { onClick('changeConsent', { key, value: !value }); @@ -44,8 +46,9 @@ function OptionalConsents({ onClick }: ViewProps): React.ReactElement { checked={data.checked} data-testid={data.id} aria-label={data.ariaLabel} - label={data.text} + label={data.title} /> + - {data.text} ))} diff --git a/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx index b379243374..6735466735 100644 --- a/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx +++ b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx @@ -3,6 +3,7 @@ import React, { useContext } from 'react'; import { getText, getTitle } from '../texts'; import { CookieConsentContext } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; +import { Checkbox } from '../../checkbox/Checkbox'; type ConsentData = { id: string; @@ -33,9 +34,16 @@ function RequiredConsents(): React.ReactElement {
    {consentList.map((data) => (
  • - - {data.title}: {data.text} - + undefined} + id={data.id} + name={data.id} + checked + disabled + data-testid={data.id} + label={data.title} + /> + - {data.text}
  • ))}
From 40edcf1f585dd16de1b0a48e41bbb819a4ba9697 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 7 Jan 2022 13:15:43 +0200 Subject: [PATCH 023/292] Removed blocking overlay Users should not be prevented from using the site without accepting some cookies. Modified story text, because user can now click through to the content. Added also top border to the content. --- .../cookieConsent/CookieConsent.module.scss | 21 +++---------------- .../cookieConsent/CookieConsent.stories.tsx | 7 +------ .../cookieConsent/CookieConsent.tsx | 1 - 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 58c04db67b..7eedc8cbdd 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -1,18 +1,9 @@ .container { position: fixed; - top: 0; left: 0; width: 100vw; - height: 100vh; z-index: 999; -} - -.overlay { - position: absolute; - inset: 0; - z-index: 1; - background: rgb(0, 0, 0); - pointer-events: none; + bottom: 0; } .aligner { @@ -21,24 +12,17 @@ z-index: 2; width: 100%; overflow-y: scroll; - max-height: 100%; + max-height: 100vh; } .container .aligner { transform: translateY(100%); transition: transform 1s; } -.container .overlay { - opacity: 0.01; - transition: opacity 1s; -} .container.animate-in .aligner { transform: translateY(0%); } -.container.animate-in .overlay { - opacity: 0.7; -} .buttons { padding: 20px 0; @@ -54,6 +38,7 @@ background: #ffffff; width: 100%; box-sizing: border-box; + border-top: 8px solid var(--color-bus); } .content .emulated-h1 { diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 7eff6dc94a..450ba84f76 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -24,12 +24,7 @@ export const Example = () => {

This is a dummy application

{willRenderCookieConsentDialog ? ( <> -

- Cookie consent dialog is visible.   - - Can't touch this! - -

+

Cookie consent dialog will be shown.

) : ( <> diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index f30fa17116..2bd77f78b9 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -91,7 +91,6 @@ export function CookieConsent(): React.ReactElement | null {
-
); } From 29a98ed10b4504096d341dda33cb25b1d89ca112 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 7 Jan 2022 17:25:05 +0200 Subject: [PATCH 024/292] Accessibility fix: order of elements Tab order should be: heading, lang selection, content, action buttons, close. Changed elements' positions and moved contents of
to Content. Language should be after H1 in tab ordering, so injected it to the right place in dom. Visually it is never there, but avoided manual tabIndexes.
became obsolete, so removed. --- .../cookieConsent/CookieConsent.module.scss | 58 ++++++++++++------- .../cookieConsent/content/Content.tsx | 40 ++++++++----- .../cookieConsent/details/Details.tsx | 9 +-- .../components/cookieConsent/main/Main.tsx | 29 ---------- 4 files changed, 64 insertions(+), 72 deletions(-) delete mode 100644 packages/react/src/components/cookieConsent/main/Main.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 7eedc8cbdd..2c33d94328 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -4,6 +4,7 @@ width: 100vw; z-index: 999; bottom: 0; + --common-spacing: var(--spacing-s); } .aligner { @@ -25,38 +26,46 @@ } .buttons { - padding: 20px 0; + padding: var(--common-spacing) 0; } .buttons > * { - margin: 0 20px 20px 0; + margin: 0 var(--common-spacing) var(--common-spacing) 0; } .content { position: relative; - padding: 20px 20px 0 20px; + padding: var(--common-spacing) var(--common-spacing) 0 var(--common-spacing); background: #ffffff; width: 100%; box-sizing: border-box; border-top: 8px solid var(--color-bus); + --close-button-size: 24px; + --close-button-padding: calc(var(--spacing-s) / 2); + --close-button-left-side: calc(var(--close-button-size) + var(--common-spacing)); + --lang-pos-x: calc(var(--close-button-left-side) + var(--close-button-padding) + var(--common-spacing)); } .content .emulated-h1 { - margin-right: 50px; + margin-right: 70px; } -.language-switcher-and-close { - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--spacing-s); +.language-switcher { + position: absolute; + z-index: 3; + top: var(--common-spacing); + left: var(--common-spacing); } +.main-content, .text-content { padding: 0; } +.main-content { + padding-top: calc(var(--common-spacing) * 2); +} + .emulated-h1 { font-size: var(--fontsize-heading-m); font-weight: bold; @@ -92,7 +101,7 @@ } .list li { - padding-bottom: var(--spacing-s); + padding-bottom: var(--common-spacing); } .list li span { @@ -124,24 +133,29 @@ } .close-button { - padding-left: var(--spacing-s); - height: 24px; - margin-right: -6px; + position: absolute; + z-index: 3; + top: var(--close-button-padding); + right: var(--close-button-padding); + padding: var(--close-button-padding); + height: var(--close-button-size); + box-sizing: content-box; } @media (min-width: 768px) { - .language-switcher-and-close { - position: absolute; - z-index: 3; - top: var(--spacing-m); - right: var(--spacing-s); - justify-content: end; - margin-bottom: 0; + .language-switcher { + top: var(--close-button-padding); + right: var(--lang-pos-x); + padding-top: var(--close-button-padding); + left: unset; } .buttons > * { margin-bottom: 0; } .close-button { - margin-right: 0; + right: var(--common-spacing); + } + .main-content { + padding-top: 0; } } diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index 502efe6767..93cf409974 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -5,7 +5,6 @@ import Buttons from '../buttons/Buttons'; import { IconAngleDown, IconAngleUp, IconCross } from '../../../icons'; import { useAccordion } from '../../accordion'; import Details from '../details/Details'; -import Main from '../main/Main'; import styles from '../CookieConsent.module.scss'; import { Card } from '../../card/Card'; @@ -25,23 +24,29 @@ function Content({ onClick }: ViewProps): React.ReactElement { id="cookie-consent-content" aria-live="assertive" > -
+
+ + Evästesuostumukset + - +
-
); } diff --git a/packages/react/src/components/cookieConsent/details/Details.tsx b/packages/react/src/components/cookieConsent/details/Details.tsx index fea8731e50..fca58b9456 100644 --- a/packages/react/src/components/cookieConsent/details/Details.tsx +++ b/packages/react/src/components/cookieConsent/details/Details.tsx @@ -8,14 +8,7 @@ import OptionalConsents from '../optionalConsents/OptionalConsents'; function Details({ onClick }: ViewProps): React.ReactElement { return (
- + Tietoa sivustolla käytetyistä evästeistä

diff --git a/packages/react/src/components/cookieConsent/main/Main.tsx b/packages/react/src/components/cookieConsent/main/Main.tsx deleted file mode 100644 index 8e4bf33983..0000000000 --- a/packages/react/src/components/cookieConsent/main/Main.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -import styles from '../CookieConsent.module.scss'; - -function Main(): React.ReactElement { - return ( -

- - Evästesuostumukset - - - -
- ); -} - -export default Main; From c4154aba009aa32463d3f0226a525e7fea54c04a Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 10 Jan 2022 10:22:39 +0200 Subject: [PATCH 025/292] Autofocus to H1 and fix heading styles Removed dialog role and aria-live. Blocks accessibility instead of helping. Removed unnecessary ids, aria-labelledby and aria-describedby as element is not a dialog anymore. --- .../cookieConsent/CookieConsent.module.scss | 21 ++++++++++++------- .../cookieConsent/content/Content.tsx | 21 +++++++++---------- .../cookieConsent/details/Details.tsx | 2 +- .../optionalConsents/OptionalConsents.tsx | 2 +- .../requiredConsents/RequiredConsents.tsx | 2 +- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 2c33d94328..23cbebaca1 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -46,10 +46,6 @@ --lang-pos-x: calc(var(--close-button-left-side) + var(--close-button-padding) + var(--common-spacing)); } -.content .emulated-h1 { - margin-right: 70px; -} - .language-switcher { position: absolute; z-index: 3; @@ -67,7 +63,7 @@ } .emulated-h1 { - font-size: var(--fontsize-heading-m); + font-size: var(--fontsize-heading-l); font-weight: bold; display: block; padding: 0; @@ -84,16 +80,24 @@ } .emulated-h2 { - font-size: var(--fontsize-heading-s); + font-size: var(--fontsize-heading-m); font-weight: bold; display: block; padding: 1.2em 0 0.5em; } -.emulated-h2 + p { +.emulated-h2 + p, +.emulated-h3 + p { margin-top: 0; } +.emulated-h3 { + font-size: var(--fontsize-heading-s); + font-weight: bold; + display: block; + padding: 1.2em 0 0.5em; +} + .list { list-style: none; margin-top: 0; @@ -158,4 +162,7 @@ .main-content { padding-top: 0; } + .content .emulated-h1 { + margin-right: 70px; + } } diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index 93cf409974..3a322d755c 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { ViewProps } from '../types'; import Buttons from '../buttons/Buttons'; @@ -12,26 +12,25 @@ function Content({ onClick }: ViewProps): React.ReactElement { const { isOpen, buttonProps, contentProps } = useAccordion({ initiallyOpen: false, }); + const titleRef = useRef(); const Icon = isOpen ? IconAngleUp : IconAngleDown; const settingsButtonText = isOpen ? 'Piilota asetukset' : 'Näytä asetukset'; + useEffect(() => { + if (titleRef.current) { + titleRef.current.focus(); + } + }, [titleRef]); return ( - diff --git a/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx index 9748eeda56..3147c1d786 100644 --- a/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx +++ b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx @@ -1,9 +1,8 @@ import React, { useContext } from 'react'; -import { getText, getTitle } from '../texts'; import { ViewProps } from '../types'; import { Checkbox } from '../../checkbox'; -import { CookieConsentContext } from '../CookieConsentContext'; +import { CookieConsentContext, useCookieConsentData, getCookieConsentContent } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; type ConsentData = { @@ -17,13 +16,15 @@ type ConsentList = ConsentData[]; function OptionalConsents({ onClick }: ViewProps): React.ReactElement { const cookieConsentContext = useContext(CookieConsentContext); + const { optionalConsentsTitle, optionalConsentsText } = getCookieConsentContent(cookieConsentContext); const consents = cookieConsentContext.getOptional(); + const getConsetTexts = useCookieConsentData(); const consentEntries = Object.entries(consents); const consentList: ConsentList = consentEntries.map(([key, value]) => ({ id: `optional-cookie-consent-${key}`, checked: Boolean(value), - text: getText(key), - title: getTitle(key), + title: getConsetTexts(key, 'title'), + text: getConsetTexts(key, 'text'), onToggle: () => { onClick('changeConsent', { key, value: !value }); }, @@ -31,9 +32,9 @@ function OptionalConsents({ onClick }: ViewProps): React.ReactElement { return ( <> - Muut evästeet + {optionalConsentsTitle} -

Voit hyväksyä tai jättää hyväksymättä muut evästeet.

+

{optionalConsentsText}

    {consentList.map((data) => (
  • diff --git a/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx index 5334813886..50c27caa0e 100644 --- a/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx +++ b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx @@ -1,7 +1,6 @@ import React, { useContext } from 'react'; -import { getText, getTitle } from '../texts'; -import { CookieConsentContext } from '../CookieConsentContext'; +import { CookieConsentContext, useCookieConsentData, getCookieConsentContent } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; import { Checkbox } from '../../checkbox/Checkbox'; @@ -14,22 +13,21 @@ type ConsentList = ConsentData[]; function RequiredConsents(): React.ReactElement { const cookieConsentContext = useContext(CookieConsentContext); + const { requiredConsentsTitle, requiredConsentsText } = getCookieConsentContent(cookieConsentContext); const consents = cookieConsentContext.getRequired(); + const getConsentTexts = useCookieConsentData(); const consentEntries = Object.entries(consents); const consentList: ConsentList = consentEntries.map(([key]) => ({ id: `required-cookie-consent-${key}`, - title: getTitle(key), - text: getText(key), + title: getConsentTexts(key, 'title'), + text: getConsentTexts(key, 'text'), })); return ( <> - Välttämättömät evästeet + {requiredConsentsTitle} -

    - Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston - käyttäjäystävällisyyteen. -

    +

    {requiredConsentsText}

      {consentList.map((data) => ( diff --git a/packages/react/src/components/cookieConsent/texts.ts b/packages/react/src/components/cookieConsent/texts.ts deleted file mode 100644 index f4683f4a6f..0000000000 --- a/packages/react/src/components/cookieConsent/texts.ts +++ /dev/null @@ -1,22 +0,0 @@ -const texts = { - matomoTitle: 'Tilastointievästeet', - matomoText: 'Tilastointievästeiden keräämää tietoa käytetään verkkosivuston kehittämiseen', - tunnistamoTitle: 'Kirjautumiseväste', - tunnistamoText: 'Sivuston pakollinen eväste mahdollistaa kävijän vierailun sivustolla.', - languageTitle: 'Kielieväste', - languageText: 'Tallennamme valitsemasi käyttöliittymäkielen', - preferencesTitle: 'Mieltymysevästeet', - preferencesText: 'Mieltymysevästeet mukauttavat sivuston ulkoasua ja toimintaa käyttäjän aiemman käytön perusteella.', - marketingTitle: 'Markkinointievästeet', - marketingText: 'Markkinointievästeiden avulla sivuston käyttäjille voidaan kohdentaa sisältöjä.', - someOtherConsentTitle: 'Palvelun oma eväste', - someOtherConsentText: 'Palvelun omaa eväste on demoa varten', -}; - -export const getTitle = (key: string): string => { - return texts[`${key}Title`] || key; -}; - -export const getText = (key: string): string => { - return texts[`${key}Text`] || key; -}; From 07edc1eb73171efea5c22b3cf0070a230f4d33ef Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 21 Jan 2022 12:21:30 +0200 Subject: [PATCH 034/292] Added a working language selector Created a component which uses HDS Navigator.LanguageSelector. Some styles needed overriding, because assumed placement is in navigation --- .../cookieConsent/CookieConsent.module.scss | 19 ++++++----- .../cookieConsent/content/Content.tsx | 5 ++- .../languageSwitcher/LanguageSwitcher.tsx | 34 +++++++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 6d871d870d..79cfea83c6 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -56,15 +56,18 @@ left: var(--common-spacing); } -.language-switcher a { - display: flex; - align-items: center; - text-decoration: none; - color: var(--color-black); +.language-selector-override { + position: relative; + right: unset; } -.language-switcher span { - padding-right: var(--spacing-3-xs); +.language-selector-override > button { + padding-top: 0; +} + +.language-selector-override > button + div { + right: unset; + left: 0; } .main-content, @@ -174,9 +177,9 @@ } .language-switcher { - line-height: calc(var(--fontsize-heading-l) * var(--lineheight-l)); right: var(--lang-pos-x); left: unset; + padding-top: 15px; } .close-button { diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index a46d510766..53e0634cc3 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -8,6 +8,7 @@ import Details from '../details/Details'; import styles from '../CookieConsent.module.scss'; import { Card } from '../../card/Card'; import { useCookieConsentContent } from '../CookieConsentContext'; +import LanguageSwitcher from '../languageSwitcher/LanguageSwitcher'; function Content({ onClick }: ViewProps): React.ReactElement { const { isOpen, buttonProps, contentProps } = useAccordion({ @@ -44,9 +45,7 @@ function Content({ onClick }: ViewProps): React.ReactElement { {mainTitle}

      {mainText}

diff --git a/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx b/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx new file mode 100644 index 0000000000..93d23236c6 --- /dev/null +++ b/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { useCookieConsentContent } from '../CookieConsentContext'; +import { Navigation } from '../../navigation/Navigation'; +import styles from '../CookieConsent.module.scss'; + +function LanguageSwitcher(): React.ReactElement { + const { onLanguageChange, language, languageOptions, languageSelectorAriaLabel } = useCookieConsentContent(); + const setLanguage = (code: string, e: React.MouseEvent) => { + e.preventDefault(); + onLanguageChange(code); + }; + const currentOption = languageOptions.find((option) => option.code === language); + return ( + + {languageOptions.map((option) => ( + setLanguage(option.code, e)} + label={option.label} + active={language === option.code} + key={option.code} + lang={option.code} + /> + ))} + + ); +} +export default LanguageSwitcher; From 1d69ef86d82b8252838d0b14169b38359845f7e6 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 21 Jan 2022 13:58:50 +0200 Subject: [PATCH 035/292] Add content to tests --- .../cookieConsent/CookieConsent.test.tsx | 8 ++++++-- .../cookieConsent/CookieConsentContext.test.tsx | 3 ++- .../cookieConsentController.test.ts | 2 +- .../src/components/cookieConsent/test.util.ts | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 7034f44ad9..312020f884 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -9,7 +9,7 @@ import { CookieConsent } from './CookieConsent'; import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentController'; import { Provider as CookieContextProvider } from './CookieConsentContext'; import mockDocumentCookie from './__mocks__/mockDocumentCookie'; -import extractSetCookieArguments from './test.util'; +import { extractSetCookieArguments, getContent } from './test.util'; type ConsentData = { requiredConsents?: ConsentList; @@ -115,7 +115,11 @@ describe(' ', () => { jest.useFakeTimers(); mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( - + , ); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index 12d4602819..7a3111b089 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -7,7 +7,7 @@ import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentControll import { CookieConsentContext, Provider as CookieContextProvider } from './CookieConsentContext'; import mockWindowLocation from './__mocks__/mockWindowLocation'; import mockDocumentCookie from './__mocks__/mockDocumentCookie'; -import extractSetCookieArguments from './test.util'; +import { extractSetCookieArguments, getContent } from './test.util'; type ConsentData = { requiredConsents?: ConsentList; @@ -126,6 +126,7 @@ describe('CookieConsentContext ', () => { cookieDomain={cookieDomain} onAllConsentsGiven={onAllConsentsGiven} onConsentsParsed={onConsentsParsed} + content={getContent()} > diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts index 3cea2d61ab..39f2cc9729 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.test.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.test.ts @@ -9,7 +9,7 @@ import createConsentController, { COOKIE_NAME, } from './cookieConsentController'; import mockDocumentCookie from './__mocks__/mockDocumentCookie'; -import extractSetCookieArguments from './test.util'; +import { extractSetCookieArguments } from './test.util'; describe(`cookieConsentController.ts`, () => { let controller: ConsentController; diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index 39cdfcc5ed..3c9c126bdf 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -1,11 +1,12 @@ /* eslint-disable jest/no-mocks-import */ import cookie from 'cookie'; +import { Content } from './CookieConsentContext'; import { COOKIE_NAME } from './cookieConsentController'; import { CookieSetOptions } from './cookieController'; import { MockedDocumentCookieActions } from './__mocks__/mockDocumentCookie'; -export default function extractSetCookieArguments( +export function extractSetCookieArguments( mockedCookieControls: MockedDocumentCookieActions, index = -1, ): { @@ -26,3 +27,16 @@ export default function extractSetCookieArguments( options: mockedCookieControls.extractCookieOptions(dataStr, COOKIE_NAME), }; } + +export const getContent = (): Content => { + return { + consents: {}, + approveAllConsents: 'approveAllConsents', + approveRequiredAndSelectedConsents: 'approveRequiredAndSelectedConsents', + approveOnlyRequiredConsents: 'approveOnlyRequiredConsents', + showSettings: 'showSettings', + hideSettings: 'hideSettings', + language: 'af', + languageOptions: [{ code: 'af', label: 'AF' }], + } as Content; +}; From cb93031173f7152fe58f5b6dfc15ef1ca96ab3a6 Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Fri, 21 Jan 2022 17:00:33 +0200 Subject: [PATCH 036/292] Fix snapshot and axe test --- .../cookieConsent/CookieConsent.test.tsx | 103 +++--- .../__snapshots__/CookieConsent.test.tsx.snap | 332 +++++++++++++++++- .../src/components/cookieConsent/test.util.ts | 28 +- 3 files changed, 403 insertions(+), 60 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 312020f884..f6700abd52 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -17,32 +17,70 @@ type ConsentData = { cookie?: ConsentObject; }; -describe(' spec', () => { - const renderComponentWithTimers = (): RenderResult => { - jest.useFakeTimers(); - const result = render(); - act(() => { - jest.runAllTimers(); - }); - // axe uses timers so must use real ones - jest.useRealTimers(); - return result; +const defaultConsentData = { + requiredConsents: ['requiredConsent1', 'requiredConsent2'], + optionalConsents: ['optionalConsent1', 'optionalConsent2'], + cookie: {}, +}; + +const unknownConsents = { + unknownConsent1: true, + unknownConsent2: false, +}; + +const mockedCookieControls = mockDocumentCookie(); + +const renderCookieConsent = ( + { requiredConsents = [], optionalConsents = [], cookie = {} }: ConsentData, + withRealTimers = false, +): RenderResult => { + // inject unknown consents to verify those are + // stored and handled, but not required or optional + const cookieWithInjectedUnknowns = { + ...cookie, + ...unknownConsents, }; + jest.useFakeTimers(); + mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); + const result = render( + + + , + ); + act(() => { + jest.runAllTimers(); + }); + + // For example, axe uses timers so sometimes the test must use real ones + if (withRealTimers) { + jest.useRealTimers(); + } + + return result; +}; + +describe(' spec', () => { + afterEach(() => { + mockedCookieControls.clear(); + }); + + afterAll(() => { + mockedCookieControls.restore(); + }); it('renders the component', () => { - const { asFragment } = renderComponentWithTimers(); + const { asFragment } = renderCookieConsent(defaultConsentData); expect(asFragment()).toMatchSnapshot(); }); it('should not have basic accessibility issues', async () => { - const { container } = renderComponentWithTimers(); + const { container } = renderCookieConsent(defaultConsentData, true); const results = await axe(container); expect(results).toHaveNoViolations(); }); }); describe(' ', () => { - const mockedCookieControls = mockDocumentCookie(); afterEach(() => { mockedCookieControls.clear(); }); @@ -90,45 +128,6 @@ describe(' ', () => { }); }; - const defaultConsentData = { - requiredConsents: ['requiredConsent1', 'requiredConsent2'], - optionalConsents: ['optionalConsent1', 'optionalConsent2'], - cookie: {}, - }; - - const unknownConsents = { - unknownConsent1: true, - unknownConsent2: false, - }; - - const renderCookieConsent = ({ - requiredConsents = [], - optionalConsents = [], - cookie = {}, - }: ConsentData): RenderResult => { - // inject unknown consents to verify those are - // stored and handled, but not required or optional - const cookieWithInjectedUnknowns = { - ...cookie, - ...unknownConsents, - }; - jest.useFakeTimers(); - mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); - const result = render( - - - , - ); - act(() => { - jest.runAllTimers(); - }); - return result; - }; - describe('Cookie consent ', () => { it('and child components are rendered when consents have not been handled', () => { const result = renderCookieConsent(defaultConsentData); diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index 1b41f2817f..add601453c 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -1,3 +1,333 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` spec renders the component 1`] = ``; +exports[` spec renders the component 1`] = ` + +
+
+ +
+
+
+`; diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index 3c9c126bdf..b4c9a289af 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -30,13 +30,27 @@ export function extractSetCookieArguments( export const getContent = (): Content => { return { + mainTitle: 'Evästesuostumukset', + mainText: `Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. + Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön + näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden + käytön.`, + detailsTitle: 'Tietoa sivustolla käytetyistä evästeistä', + detailsText: `Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta + kategoriasta ja sallia tai kieltää evästeiden käytön.`, + requiredConsentsTitle: 'Välttämättömät evästeet', + requiredConsentsText: + 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', + optionalConsentsTitle: 'Muut evästeet', + optionalConsentsText: 'Voit hyväksyä tai jättää hyväksymättä muut evästeet.', consents: {}, - approveAllConsents: 'approveAllConsents', - approveRequiredAndSelectedConsents: 'approveRequiredAndSelectedConsents', - approveOnlyRequiredConsents: 'approveOnlyRequiredConsents', - showSettings: 'showSettings', - hideSettings: 'hideSettings', - language: 'af', - languageOptions: [{ code: 'af', label: 'AF' }], + approveAllConsents: 'Hyväksy kaikki evästeet', + approveRequiredAndSelectedConsents: 'Hyväksy valitut ja pakolliset evästeet', + approveOnlyRequiredConsents: 'Hyväksy vain pakolliset evästeet', + showSettings: 'Näytä asetukset', + hideSettings: 'Piilota asetukset', + language: 'fi', + languageOptions: [{ code: 'fi', label: 'Suomeksi (FI)' }], + languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', } as Content; }; From 296e3b1f9ffd33a086d2ac065c697427d0c236f2 Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Fri, 21 Jan 2022 17:00:43 +0200 Subject: [PATCH 037/292] Reformat code --- .../src/components/cookieConsent/CookieConsent.test.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index f6700abd52..97e93c44d1 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -176,6 +176,7 @@ describe(' ', () => { verifyElementExistsByTestId(result, dataTestIds.screenReaderNotification); expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(1); }; + it('Approve -button approves all consents when details are not shown', () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -188,6 +189,7 @@ describe(' ', () => { clickElement(result, dataTestIds.approveButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); + it('Approve required -button approves only required consents and clears selected consents', async () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -203,6 +205,7 @@ describe(' ', () => { clickElement(result, dataTestIds.approveRequiredButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); + it('Close -button will approve only required consents when details are not shown', () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -215,6 +218,7 @@ describe(' ', () => { clickElement(result, dataTestIds.closeButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); + it('Close -button will approve required and selected consents when details are shown', async () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -229,6 +233,7 @@ describe(' ', () => { clickElement(result, dataTestIds.closeButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); + it('Approve -button will approve required and selected consents when details are shown', async () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -251,6 +256,7 @@ describe(' ', () => { await openAccordion(result); return result; }; + it('required and optional consents are rendered', async () => { const result = await initDetailsView(defaultConsentData); defaultConsentData.requiredConsents.forEach((consent) => { @@ -260,6 +266,7 @@ describe(' ', () => { verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentId(consent)); }); }); + it('Approve and close button texts change when accordion is open vs closed', async () => { const result = await initDetailsView(defaultConsentData); const approveButtonTextWhileOpen = (result.getByTestId(dataTestIds.approveButton) as HTMLElement).innerHTML; From e94ec3329f3f57277df472ab7e26afe3ee1dc714 Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Fri, 21 Jan 2022 17:00:51 +0200 Subject: [PATCH 038/292] Add initial loki-test reference images --- .../chrome_iphone7_Components_CookieConsent_Example.png | 3 +++ .../chrome_laptop_Components_CookieConsent_Example.png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 packages/react/.loki/reference/chrome_iphone7_Components_CookieConsent_Example.png create mode 100644 packages/react/.loki/reference/chrome_laptop_Components_CookieConsent_Example.png diff --git a/packages/react/.loki/reference/chrome_iphone7_Components_CookieConsent_Example.png b/packages/react/.loki/reference/chrome_iphone7_Components_CookieConsent_Example.png new file mode 100644 index 0000000000..eef91aae9f --- /dev/null +++ b/packages/react/.loki/reference/chrome_iphone7_Components_CookieConsent_Example.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fcfa21635b58f6ff9780bceeedeaaefb9aeafcf84b20df768759bd4bbf3f06e +size 95047 diff --git a/packages/react/.loki/reference/chrome_laptop_Components_CookieConsent_Example.png b/packages/react/.loki/reference/chrome_laptop_Components_CookieConsent_Example.png new file mode 100644 index 0000000000..d3c871fd91 --- /dev/null +++ b/packages/react/.loki/reference/chrome_laptop_Components_CookieConsent_Example.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43ba8d76623752b35383a8d23e54695a28c2e2c438035a5c3dad4257080f6f24 +size 37860 From e872d7251a51444706fc53feb6f519a6b9a36020 Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Mon, 24 Jan 2022 08:43:49 +0200 Subject: [PATCH 039/292] Reformat code --- .../src/components/cookieConsent/CookieConsent.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 97e93c44d1..07f1e12938 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -43,7 +43,11 @@ const renderCookieConsent = ( jest.useFakeTimers(); mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( - + , ); From 0bc0168a68e6b73973259c705c422b3b303c1623 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 24 Jan 2022 09:31:18 +0200 Subject: [PATCH 040/292] Test onLanguageChange is called Added second language to test content. Updated snapshot --- .../cookieConsent/CookieConsent.test.tsx | 22 +++++++++++++------ .../__snapshots__/CookieConsent.test.tsx.snap | 11 ++++++++++ .../src/components/cookieConsent/test.util.ts | 5 ++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 07f1e12938..0ff15357e3 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { CookieConsent } from './CookieConsent'; import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentController'; -import { Provider as CookieContextProvider } from './CookieConsentContext'; +import { Content, Provider as CookieContextProvider } from './CookieConsentContext'; import mockDocumentCookie from './__mocks__/mockDocumentCookie'; import { extractSetCookieArguments, getContent } from './test.util'; @@ -15,6 +15,7 @@ type ConsentData = { requiredConsents?: ConsentList; optionalConsents?: ConsentList; cookie?: ConsentObject; + contentOverrides?: Partial; }; const defaultConsentData = { @@ -31,7 +32,7 @@ const unknownConsents = { const mockedCookieControls = mockDocumentCookie(); const renderCookieConsent = ( - { requiredConsents = [], optionalConsents = [], cookie = {} }: ConsentData, + { requiredConsents = [], optionalConsents = [], cookie = {}, contentOverrides = {} }: ConsentData, withRealTimers = false, ): RenderResult => { // inject unknown consents to verify those are @@ -40,14 +41,14 @@ const renderCookieConsent = ( ...cookie, ...unknownConsents, }; + const content = { + ...getContent(), + ...contentOverrides, + }; jest.useFakeTimers(); mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( - + , ); @@ -168,6 +169,13 @@ describe(' ', () => { verifyElementDoesNotExistsByTestId(result, dataTestIds.container); verifyElementDoesNotExistsByTestId(result, dataTestIds.screenReaderNotification); }); + it('changing language calls content.onLanguageChange', () => { + const onLanguageChange = jest.fn(); + const result = renderCookieConsent({ ...defaultConsentData, contentOverrides: { onLanguageChange } }); + result.container.querySelector('#cookie-consent-language-selector-button').click(); + result.container.querySelector('a[lang="sv"]').click(); + expect(onLanguageChange).toHaveBeenLastCalledWith('sv'); + }); }); describe(`Approve and close buttons will diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index add601453c..7f56ab9fce 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -86,6 +86,17 @@ exports[` spec renders the component 1`] = ` Suomeksi (FI) + + + På svenska (SV) + +
diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index b4c9a289af..2b4b5454ff 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -50,7 +50,10 @@ export const getContent = (): Content => { showSettings: 'Näytä asetukset', hideSettings: 'Piilota asetukset', language: 'fi', - languageOptions: [{ code: 'fi', label: 'Suomeksi (FI)' }], + languageOptions: [ + { code: 'fi', label: 'Suomeksi (FI)' }, + { code: 'sv', label: 'På svenska (SV)' }, + ], languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', } as Content; }; From 4f5eef609e5bb9efc5a2194cacd12e3687d5bc54 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 24 Jan 2022 15:20:35 +0200 Subject: [PATCH 041/292] Focus an element when CookieConsent is closed Also improved matomo related commenting in onConsentsParsed and onAllConsentsGiven --- .../cookieConsent/CookieConsent.stories.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index c43eb8fb3e..0524dc195a 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -21,7 +21,10 @@ export const Example = () => { style={willRenderCookieConsentDialog ? { overflow: 'hidden', maxHeight: '100vh' } : {}} aria-hidden={willRenderCookieConsentDialog ? 'true' : 'false'} > -

This is a dummy application

+ {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */} +

+ This is a dummy application +

{willRenderCookieConsentDialog ? ( <>

Cookie consent dialog will be shown.

@@ -95,11 +98,22 @@ export const Example = () => { onAllConsentsGiven={(consents) => { if (consents.matomo) { // start tracking + // window._paq.push(['setConsentGiven']); + // window._paq.push(['setCookieConsentGiven']); } + document.getElementById('focused-element-after-cookie-consent-closed').focus(); }} - onConsentsParsed={(consents) => { - if (consents.matomo) { - // start tracking + onConsentsParsed={(consents, hasUserHandledAllConsents) => { + if (consents.matomo === undefined) { + // tell matomo to wait for consent: + // window._paq.push(['requireConsent']); + // window._paq.push(['requireCookieConsent']); + } else if (consents.matomo === false) { + // tell matomo to forget conset + // window._paq.push(['forgetConsentGiven']); + } + if (hasUserHandledAllConsents) { + // cookie consent dialog will not be shown } }} content={content} From a3941c129e193edfb12961478f99d9aff8a94227 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 24 Jan 2022 15:21:39 +0200 Subject: [PATCH 042/292] Added aria-describedby to checkboxes Also added aria-hidden to the description element so it is not spoken twice Fixed test snapshot --- .../__snapshots__/CookieConsent.test.tsx.snap | 24 +++++++++++++++---- .../optionalConsents/OptionalConsents.tsx | 7 +++++- .../requiredConsents/RequiredConsents.tsx | 7 +++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index 7f56ab9fce..7382986e5b 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -175,6 +175,7 @@ exports[` spec renders the component 1`] = ` class="checkbox" > spec renders the component 1`] = ` requiredConsent1Title - + @@ -200,6 +204,7 @@ exports[` spec renders the component 1`] = ` class="checkbox" > spec renders the component 1`] = ` requiredConsent2Title - + @@ -239,6 +247,7 @@ exports[` spec renders the component 1`] = ` class="checkbox" > spec renders the component 1`] = ` optionalConsent1Title - + @@ -262,6 +274,7 @@ exports[` spec renders the component 1`] = ` class="checkbox" > spec renders the component 1`] = ` optionalConsent2Title - + diff --git a/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx index 3147c1d786..12ec7c26bd 100644 --- a/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx +++ b/packages/react/src/components/cookieConsent/optionalConsents/OptionalConsents.tsx @@ -11,6 +11,7 @@ type ConsentData = { text: string; title: string; onToggle: () => void; + descriptionId: string; }; type ConsentList = ConsentData[]; @@ -22,6 +23,7 @@ function OptionalConsents({ onClick }: ViewProps): React.ReactElement { const consentEntries = Object.entries(consents); const consentList: ConsentList = consentEntries.map(([key, value]) => ({ id: `optional-cookie-consent-${key}`, + descriptionId: `optional-cookie-consent-${key}-description`, checked: Boolean(value), title: getConsetTexts(key, 'title'), text: getConsetTexts(key, 'text'), @@ -45,8 +47,11 @@ function OptionalConsents({ onClick }: ViewProps): React.ReactElement { checked={data.checked} data-testid={data.id} label={data.title} + aria-describedby={data.descriptionId} /> - - {data.text} + + - {data.text} + ))} diff --git a/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx index 50c27caa0e..97efa0287d 100644 --- a/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx +++ b/packages/react/src/components/cookieConsent/requiredConsents/RequiredConsents.tsx @@ -6,6 +6,7 @@ import { Checkbox } from '../../checkbox/Checkbox'; type ConsentData = { id: string; + descriptionId: string; text: string; title: string; }; @@ -19,6 +20,7 @@ function RequiredConsents(): React.ReactElement { const consentEntries = Object.entries(consents); const consentList: ConsentList = consentEntries.map(([key]) => ({ id: `required-cookie-consent-${key}`, + descriptionId: `required-cookie-consent-${key}-description`, title: getConsentTexts(key, 'title'), text: getConsentTexts(key, 'text'), })); @@ -40,8 +42,11 @@ function RequiredConsents(): React.ReactElement { disabled data-testid={data.id} label={data.title} + aria-describedby={data.descriptionId} /> - - {data.text} + + - {data.text} + ))} From a9f34f83c3a6e33c3aa133f1ac26b22a46ddf807 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 5 Apr 2022 09:16:38 +0300 Subject: [PATCH 043/292] Remove universal-cookie --- packages/react/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 868c01c760..882fc9a002 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -122,8 +122,7 @@ "react-spring": "9.3.0", "react-use-measure": "2.0.1", "react-virtual": "2.2.7", - "typescript": "4.5.5", - "universal-cookie": "^4.0.4" + "typescript": "4.5.5" }, "resolutions": { "@types/react": "17.0.2", From 8e5f892d20f65558f980cb1c7094424ef398fdf2 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 5 Apr 2022 14:58:33 +0300 Subject: [PATCH 044/292] New Content type for Context --- .../cookieConsent/CookieConsent.test.tsx | 19 +-- .../CookieConsentContext.test.tsx | 3 +- .../cookieConsent/CookieConsentContext.tsx | 106 ++++++++++------ .../src/components/cookieConsent/test.util.ts | 120 +++++++++++++----- 4 files changed, 173 insertions(+), 75 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 0ff15357e3..a66e649754 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -15,7 +15,7 @@ type ConsentData = { requiredConsents?: ConsentList; optionalConsents?: ConsentList; cookie?: ConsentObject; - contentOverrides?: Partial; + contentModifier?: (content: Content) => Content; }; const defaultConsentData = { @@ -32,7 +32,7 @@ const unknownConsents = { const mockedCookieControls = mockDocumentCookie(); const renderCookieConsent = ( - { requiredConsents = [], optionalConsents = [], cookie = {}, contentOverrides = {} }: ConsentData, + { requiredConsents = [], optionalConsents = [], cookie = {}, contentModifier }: ConsentData, withRealTimers = false, ): RenderResult => { // inject unknown consents to verify those are @@ -41,14 +41,11 @@ const renderCookieConsent = ( ...cookie, ...unknownConsents, }; - const content = { - ...getContent(), - ...contentOverrides, - }; + const content = getContent([requiredConsents], [optionalConsents], contentModifier); jest.useFakeTimers(); mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( - + undefined} content={content}> , ); @@ -171,7 +168,13 @@ describe(' ', () => { }); it('changing language calls content.onLanguageChange', () => { const onLanguageChange = jest.fn(); - const result = renderCookieConsent({ ...defaultConsentData, contentOverrides: { onLanguageChange } }); + const result = renderCookieConsent({ + ...defaultConsentData, + contentModifier: (content) => { + content.language.onLanguageChange = onLanguageChange; + return content; + }, + }); result.container.querySelector('#cookie-consent-language-selector-button').click(); result.container.querySelector('a[lang="sv"]').click(); expect(onLanguageChange).toHaveBeenLastCalledWith('sv'); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index 7a3111b089..f34e7393f4 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -121,12 +121,11 @@ describe('CookieConsentContext ', () => { mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); return render( undefined} > diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index 7c79a37453..7e5531af45 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -2,6 +2,64 @@ import React, { createContext, useContext, useMemo, useState } from 'react'; import create, { ConsentController, ConsentList, ConsentObject } from './cookieConsentController'; +export type Description = { + title: string; + text: string; +}; + +export type TableData = { + name: string; + hostName: string; + path: string; + description: string; + expiration: string; +}; + +export type ConsentData = TableData & { + id: string; +}; + +export type ConsentGroup = Description & { + expandAriaLabel: string; + checkboxAriaLabel: string; + consents: ConsentData[]; +}; + +export type UiTexts = { + showSettings: string; + hideSettings: string; + approveAllConsents: string; + approveRequiredAndSelectedConsents: string; + approveOnlyRequiredConsents: string; + settingsSaved: string; +}; + +export type SectionTexts = { + main: Description; + details: Description; +}; + +export type RequiredOrOptionalConsents = Description & { + checkboxAriaLabel: string; + groups: ConsentGroup[]; +}; + +export type Content = { + texts: { + sections: SectionTexts; + ui: UiTexts; + tableHeadings: TableData; + }; + requiredConsents?: RequiredOrOptionalConsents; + optionalConsents?: RequiredOrOptionalConsents; + language: { + languageOptions: { code: string; label: string }[]; + current: string; + languageSelectorAriaLabel: string; + onLanguageChange: (newLanguage: string) => void; + }; +}; + export type CookieConsentContextType = { getRequired: () => ConsentObject; getOptional: () => ConsentObject; @@ -14,38 +72,13 @@ export type CookieConsentContextType = { content: Content; }; -export type Content = { - mainTitle: string; - mainText: string; - detailsTitle: string; - detailsText: string; - requiredConsentsTitle: string; - requiredConsentsText: string; - optionalConsentsTitle: string; - optionalConsentsText: string; - showSettings: string; - hideSettings: string; - approveAllConsents: string; - approveRequiredAndSelectedConsents: string; - approveOnlyRequiredConsents: string; - settingsSaved: string; - languageOptions: { code: string; label: string }[]; - language: string; - languageSelectorAriaLabel: string; - onLanguageChange: (newLanguage: string) => void; - consents: { - [x: string]: string; - }; -}; - type CookieConsentContextProps = { - optionalConsents?: ConsentList; - requiredConsents?: ConsentList; cookieDomain?: string; children: React.ReactNode | React.ReactNode[] | null; content: Content; onAllConsentsGiven?: (consents: ConsentObject) => void; onConsentsParsed?: (consents: ConsentObject, hasUserHandledAllConsents: boolean) => void; + onLanguageChange: (newLanguage: string) => void; }; export const CookieConsentContext = createContext({ @@ -60,15 +93,24 @@ export const CookieConsentContext = createContext({ content: {} as Content, }); +const getConsentsFromConsentGroup = (groups: ConsentGroup[]): ConsentList => { + return groups.reduce((ids, currentGroup) => { + currentGroup.consents.forEach((consentData) => { + ids.push(consentData.id); + }); + return ids; + }, []); +}; + export const Provider = ({ - optionalConsents, - requiredConsents, cookieDomain, onAllConsentsGiven = () => undefined, onConsentsParsed = () => undefined, children, content, }: CookieConsentContextProps): React.ReactElement => { + const requiredConsents = getConsentsFromConsentGroup(content.requiredConsents.groups); + const optionalConsents = getConsentsFromConsentGroup(content.optionalConsents.groups); const consentController = useMemo(() => create({ requiredConsents, optionalConsents, cookieDomain }), [ requiredConsents, optionalConsents, @@ -117,11 +159,3 @@ export const useCookieConsentContent = (): Content => { const cookieConsentContext = useContext(CookieConsentContext); return getCookieConsentContent(cookieConsentContext); }; - -export const useCookieConsentData = (): ((key: string, prop: 'title' | 'text') => string) => { - const content = useCookieConsentContent(); - return (key, prop) => { - const textKey = prop === 'title' ? `${key}Title` : `${key}Text`; - return content.consents[textKey] || textKey; - }; -}; diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index 2b4b5454ff..4d01b4249d 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -1,8 +1,8 @@ /* eslint-disable jest/no-mocks-import */ import cookie from 'cookie'; -import { Content } from './CookieConsentContext'; -import { COOKIE_NAME } from './cookieConsentController'; +import { ConsentGroup, Content } from './CookieConsentContext'; +import { ConsentList, COOKIE_NAME } from './cookieConsentController'; import { CookieSetOptions } from './cookieController'; import { MockedDocumentCookieActions } from './__mocks__/mockDocumentCookie'; @@ -28,32 +28,94 @@ export function extractSetCookieArguments( }; } -export const getContent = (): Content => { +const createConsentGroup = (id: string, consents: ConsentList): ConsentGroup => { return { - mainTitle: 'Evästesuostumukset', - mainText: `Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. - Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön - näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden - käytön.`, - detailsTitle: 'Tietoa sivustolla käytetyistä evästeistä', - detailsText: `Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta - kategoriasta ja sallia tai kieltää evästeiden käytön.`, - requiredConsentsTitle: 'Välttämättömät evästeet', - requiredConsentsText: - 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', - optionalConsentsTitle: 'Muut evästeet', - optionalConsentsText: 'Voit hyväksyä tai jättää hyväksymättä muut evästeet.', - consents: {}, - approveAllConsents: 'Hyväksy kaikki evästeet', - approveRequiredAndSelectedConsents: 'Hyväksy valitut ja pakolliset evästeet', - approveOnlyRequiredConsents: 'Hyväksy vain pakolliset evästeet', - showSettings: 'Näytä asetukset', - hideSettings: 'Piilota asetukset', - language: 'fi', - languageOptions: [ - { code: 'fi', label: 'Suomeksi (FI)' }, - { code: 'sv', label: 'På svenska (SV)' }, - ], - languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', - } as Content; + title: `Consent group title for ${id}`, + text: `Consent group description for ${id}`, + expandAriaLabel: `expandAriaLabel for ${id}`, + checkboxAriaLabel: `checkboxAriaLabel for ${id}`, + consents: consents.map((consent) => { + return { + id: consent, + name: `Name of ${consent}`, + hostName: `HostName of ${consent}`, + path: `Path of ${consent}`, + description: `Description of ${consent}`, + expiration: `Expiration of ${consent}`, + }; + }), + }; +}; + +export const getContent = ( + requiredConsentGroups?: ConsentList[], + optionalConsentsGroups?: ConsentList[], + contentModifier?: (content: Content) => Content, +): Content => { + const content: Content = { + texts: { + sections: { + main: { + title: 'Evästesuostumukset', + text: `Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. + Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön + näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden + käytön.`, + }, + details: { + title: 'Tietoa sivustolla käytetyistä evästeistä', + text: `Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta + kategoriasta ja sallia tai kieltää evästeiden käytön.`, + }, + }, + ui: { + showSettings: 'Näytä asetukset', + hideSettings: 'Piilota asetukset', + approveAllConsents: 'Hyväksy kaikki evästeet', + approveRequiredAndSelectedConsents: 'Hyväksy valitut ja pakolliset evästeet', + approveOnlyRequiredConsents: 'Hyväksy vain pakolliset evästeet', + settingsSaved: 'Asetukset tallennettu!', + }, + tableHeadings: { + name: 'Name', + hostName: 'Host name', + path: 'Path', + description: 'Description', + expiration: 'Expiration', + }, + }, + language: { + languageOptions: [ + { code: 'fi', label: 'Suomeksi (FI)' }, + { code: 'en', label: 'English (EN)' }, + ], + current: 'fi', + languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', + onLanguageChange: () => undefined, + }, + }; + if (requiredConsentGroups) { + content.requiredConsents = { + title: 'Title for required consents', + text: 'Text for required consents', + checkboxAriaLabel: 'checkboxAriaLabel', + groups: requiredConsentGroups.map((consents, index) => + createConsentGroup(`requiredConsentGroup${index}`, consents), + ), + }; + } + if (optionalConsentsGroups) { + content.requiredConsents = { + title: 'Title for optional consents', + text: 'Text for optional consents', + checkboxAriaLabel: 'checkboxAriaLabel', + groups: optionalConsentsGroups.map((consents, index) => + createConsentGroup(`optionalConsentGroups${index}`, consents), + ), + }; + } + if (contentModifier) { + return contentModifier(content); + } + return content; }; From af2573a344e55174eb57aca51aaa0d3f3031517b Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 5 Apr 2022 14:58:53 +0300 Subject: [PATCH 045/292] Use new content in LanguageSwitcher --- .../cookieConsent/languageSwitcher/LanguageSwitcher.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx b/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx index 93d23236c6..44ff5a3bfa 100644 --- a/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx +++ b/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx @@ -5,12 +5,13 @@ import { Navigation } from '../../navigation/Navigation'; import styles from '../CookieConsent.module.scss'; function LanguageSwitcher(): React.ReactElement { - const { onLanguageChange, language, languageOptions, languageSelectorAriaLabel } = useCookieConsentContent(); + const content = useCookieConsentContent(); + const { current, languageOptions, languageSelectorAriaLabel, onLanguageChange } = content.language; const setLanguage = (code: string, e: React.MouseEvent) => { e.preventDefault(); onLanguageChange(code); }; - const currentOption = languageOptions.find((option) => option.code === language); + const currentOption = languageOptions.find((option) => option.code === current); return ( setLanguage(option.code, e)} label={option.label} - active={language === option.code} + active={current === option.code} key={option.code} lang={option.code} /> From b1abc69305cabc998b6e85ebe86cdbc87464a78e Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 5 Apr 2022 14:59:27 +0300 Subject: [PATCH 046/292] Use new content in Stories --- .../cookieConsent/CookieConsent.stories.tsx | 215 ++++++++++++++---- 1 file changed, 167 insertions(+), 48 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 0524dc195a..1c91587d7a 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -40,61 +40,179 @@ export const Example = () => { const [language, setLanguage] = useState('fi'); const onLanguageChange = (newLang) => setLanguage(newLang); - const content: Content = useMemo(() => { + + const content: Content = useMemo((): Content => { return { - mainTitle: 'Evästesuostumukset', - mainText: `Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. - Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön - näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden - käytön. (${language})`, - detailsTitle: 'Tietoa sivustolla käytetyistä evästeistä', - detailsText: `Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta - kategoriasta ja sallia tai kieltää evästeiden käytön.`, - requiredConsentsTitle: 'Välttämättömät evästeet', - requiredConsentsText: - 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', - optionalConsentsTitle: 'Muut evästeet', - optionalConsentsText: 'Voit hyväksyä tai jättää hyväksymättä muut evästeet.', - showSettings: 'Näytä asetukset', - hideSettings: 'Piilota asetukset', - approveAllConsents: 'Hyväksy kaikki evästeet', - approveRequiredAndSelectedConsents: 'Hyväksy valitut ja pakolliset evästeet', - approveOnlyRequiredConsents: 'Hyväksy vain pakolliset evästeet', - settingsSaved: 'Asetukset tallennettu!', - consents: { - matomoTitle: 'Tilastointievästeet', - matomoText: 'Tilastointievästeiden keräämää tietoa käytetään verkkosivuston kehittämiseen', - tunnistamoTitle: 'Kirjautumiseväste', - tunnistamoText: 'Sivuston pakollinen eväste mahdollistaa kävijän vierailun sivustolla.', - languageTitle: 'Kielieväste', - languageText: 'Tallennamme valitsemasi käyttöliittymäkielen', - preferencesTitle: 'Mieltymysevästeet', - preferencesText: - 'Mieltymysevästeet mukauttavat sivuston ulkoasua ja toimintaa käyttäjän aiemman käytön perusteella.', - marketingTitle: 'Markkinointievästeet', - marketingText: 'Markkinointievästeiden avulla sivuston käyttäjille voidaan kohdentaa sisältöjä.', - someOtherConsentTitle: 'Palvelun oma eväste', - someOtherConsentText: 'Palvelun omaa eväste on demoa varten', + texts: { + sections: { + main: { + title: 'Evästesuostumukset', + text: `Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. + Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön + näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden + käytön.`, + }, + details: { + title: 'Tietoa sivustolla käytetyistä evästeistä', + text: `Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta + kategoriasta ja sallia tai kieltää evästeiden käytön.`, + }, + }, + ui: { + showSettings: 'Näytä asetukset', + hideSettings: 'Piilota asetukset', + approveAllConsents: 'Hyväksy kaikki evästeet', + approveRequiredAndSelectedConsents: 'Hyväksy valitut ja pakolliset evästeet', + approveOnlyRequiredConsents: 'Hyväksy vain pakolliset evästeet', + settingsSaved: 'Asetukset tallennettu!', + }, + tableHeadings: { + name: 'Name', + hostName: 'Osoite', + path: 'Polku', + description: 'Kuvaus', + expiration: 'Voimassaoloaika', + }, + }, + requiredConsents: { + title: 'Välttämättömät evästeet', + text: + 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', + checkboxAriaLabel: 'checkboxAriaLabel', + groups: [ + { + title: 'Evästeet sivuston perustoimintoja varten', + text: 'Näitä eväisteitä käytetään sivuston perustoiminnoissa', + expandAriaLabel: 'toggleAriaLabel', + checkboxAriaLabel: 'checkboxAriaLabel', + consents: [ + { + id: commonConsents.tunnistamo, + name: 'Kuormanjako', + hostName: 'Osoite', + path: 'Polku', + description: + 'Kuvaus lectus lacinia sed. Phasellus purus nisi, imperdiet id volutpat vel, pellentesque in ex. In pretium maximus finibus', + expiration: 'Voimassaoloaika', + }, + { + id: commonConsents.language, + name: 'Kielivalinta', + hostName: 'Osoite', + path: 'Polku', + description: 'Quisque vest molestie convallis. Don el dui vel.', + expiration: 'Voimassaoloaika', + }, + ], + }, + { + title: 'Evästeet kirjautumista varten', + text: 'Näitä eväisteitä käytetään kirjautumisessa', + expandAriaLabel: 'toggleAriaLabel', + checkboxAriaLabel: 'checkboxAriaLabel', + consents: [ + { + id: commonConsents.tunnistamo, + name: 'Tunnistamo', + hostName: 'Osoite', + path: 'Polku', + description: + 'Kuvaus lectus lacinia sed. Phasellus purus nisi, imperdiet id volutpat vel, pellentesque in ex. In pretium maximus finibus', + expiration: 'Voimassaoloaika', + }, + { + id: 'keycloak', + name: 'Tunnistus', + hostName: 'Osoite', + path: 'Polku', + description: 'Quisque vest molestie convallis. Don el dui vel.', + expiration: 'Voimassaoloaika', + }, + ], + }, + ], + }, + optionalConsents: { + title: 'Muut evästeet', + text: + 'Voit hyväksyä tai jättää hyväksymättä muut evästeet. Praesent vel vestibulum nunc, at eleifend sapien. Integer cursus ut orci eu pretium. Ut a orci felis. In eu eros turpis. Sed ullamcorper lacinia lorem, id ullamcorper dui accumsan in. Integer dictum fermentum mi, sit amet accumsan lacus facilisis id. Quisque blandit lacus ac sem porta.', + checkboxAriaLabel: 'checkboxAriaLabel', + groups: [ + { + title: 'Markkinointievästeet', + text: + 'Markkinointievästeillä kohdennetaan markkinointia. Nulla facilisi. Nullam mattis sapien sem, nec venenatis lectus lacinia sed. Phasellus purus nisi, imperdiet id volutpat vel, pellentesque in ex. In pretium maximus finibus.', + expandAriaLabel: 'toggleAriaLabel', + checkboxAriaLabel: 'checkboxAriaLabel', + consents: [ + { + id: commonConsents.marketing, + name: 'Marketing', + hostName: 'Osoite', + path: 'Polku', + description: 'Quisque vel dui vel est molestie convallis.', + expiration: 'Voimassaoloaika', + }, + ], + }, + { + title: 'Asetuksiin liittyvät evästeet', + text: + 'Evästeisiin tallennetaan käyttäjän tekemiä Donec lacus ligula, consequat id ligula sed, dapibus blandit nunc. Phasellus efficitur nec tellus et tempus. Sed tempor tristique purus, at auctor lectus. Ut pretium rutrum viverra. Sed felis arcu, sodales fermentum finibus in, pretium id tellus. Morbi eget eros congue, pulvinar leo ut, aliquam lectus. Cras consectetur sit amet tortor nec vulputate. Integer scelerisque dignissim auctor. Fusce pharetra dui nulla, vel elementum leo mattis vitae.', + expandAriaLabel: 'toggleAriaLabel', + checkboxAriaLabel: 'checkboxAriaLabel', + consents: [ + { + id: commonConsents.preferences, + name: 'Asetus 1', + hostName: 'Osoite', + path: 'Polku', + description: + 'Proin sodales maximus est, pulvinar tempus felis tempus quis. Aenean at vestibulum lectus. Aliquam erat volutpat. Nullam venenatis feugiat sem vitae cursus. ', + expiration: 'Voimassaoloaika', + }, + ], + }, + { + title: 'Tilastointiin liittyvät evästeet', + text: 'Tilastoinnilla parannetaan...', + expandAriaLabel: 'toggleAriaLabel', + checkboxAriaLabel: 'checkboxAriaLabel', + consents: [ + { + id: commonConsents.matomo, + name: 'Matomo', + hostName: 'Osoite', + path: 'Polku', + description: 'Quisque vel dui vel est molestie con.', + expiration: 'Voimassaoloaika', + }, + { + id: 'someOtherConsent', + name: 'Joku toinen', + hostName: 'Osoite', + path: 'Polku', + description: 'Vel est molestie Quisque vel dui vel est molestie con con', + expiration: 'Voimassaoloaika', + }, + ], + }, + ], + }, + language: { + languageOptions: [ + { code: 'fi', label: 'Suomeksi (FI)' }, + { code: 'en', label: 'English (EN)' }, + ], + current: language, + languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', + onLanguageChange, }, - languageOptions: [ - { code: 'fi', label: 'Suomeksi (FI)' }, - { code: 'en', label: 'English (EN)' }, - ], - language, - languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', - onLanguageChange, }; }, [language]); return ( { if (consents.matomo) { // start tracking @@ -117,6 +235,7 @@ export const Example = () => { } }} content={content} + onLanguageChange={onLanguageChange} > From 5872c4ec24ecc9c20a9cc875f94c5b7f0d94c907 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 5 Apr 2022 15:00:20 +0300 Subject: [PATCH 047/292] Update component to handle new content and styles --- .../cookieConsent/CookieConsent.module.scss | 97 +++++++++++++++---- .../cookieConsent/CookieConsent.tsx | 4 +- .../cookieConsent/buttons/Buttons.tsx | 7 +- .../consentGroup/ConsentGroup.tsx | 71 ++++++++++++++ .../ConsentGroupDataTable.tsx | 36 +++++++ .../cookieConsent/content/Content.tsx | 18 ++-- .../cookieConsent/details/Details.tsx | 16 +-- .../requiredConsents/RequiredConsents.tsx | 80 +++++++-------- 8 files changed, 241 insertions(+), 88 deletions(-) create mode 100644 packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx create mode 100644 packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 79cfea83c6..7798d84daf 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -75,6 +75,10 @@ padding: 0; } +.text-content > p { + padding-bottom: var(--spacing-2-xl); +} + .main-content { padding-top: calc(var(--common-spacing) * 2); } @@ -86,16 +90,6 @@ padding: 0; } -.plain-text-button { - border: none; - background: transparent; - display: inline-block; - text-decoration: underline; - color: var(--color-bus); - padding: 0; - cursor: pointer; -} - .emulated-h2 { font-size: var(--fontsize-heading-m); font-weight: bold; @@ -107,6 +101,7 @@ .emulated-h3 + p { margin: 0; } + .emulated-h3 + p { padding-bottom: var(--spacing-l); } @@ -124,15 +119,6 @@ padding-left: 0; } -.list li:not(:last-child) { - padding-bottom: var(--common-spacing); -} - -.list li span { - display: inline-block; - padding: var(--spacing-xs) 0 0 32px; -} - .accordion-button, .close-button { border: none; @@ -166,6 +152,79 @@ box-sizing: content-box; } +.consent-group-parent { + display: flex; + padding: 0 0 var(--spacing-2-xl); + flex-direction: column; + > p { + margin-bottom: 0; + } +} + +.consent-group { + display: flex; + padding: var(--spacing-xl) 0; + flex-direction: column; +} + +.consent-group-closed { + border-bottom: 1px solid var(--color-black); +} + +.consent-group-content { + display: flex; + padding: 0 var(--spacing-m); + flex-direction: column; + p { + padding: var(--spacing-l) 0 0; + margin: 0; + } +} + +.title-with-checkbox { + display: flex; + margin-right: calc(var(--spacing-s) * 3); +} + +.container .content .consent-group-parent .title-with-checkbox label { + font-weight: bold; + color: var(--color-black); +} + +.group-title-row { + display: flex; + position: relative; + button { + position: absolute; + right: 0; + top: calc(-0.5 * var(--spacing-s)); + padding: var(--spacing-s); + } +} + +.data-table-container { + border-left: 1px solid var(--color-black); + border-right: 1px solid var(--color-black); + margin: var(--spacing-l) 0 var(--spacing-xs); +} + +.data-table-container table tbody tr > *:nth-child(1) { + width: 15%; + word-break: break-word; +} +.data-table-container table tbody tr > *:nth-child(2) { + width: 15%; +} +.data-table-container table tbody tr > *:nth-child(3) { + width: 15%; +} +.data-table-container table tbody tr > *:nth-child(4) { + width: 40%; +} +.data-table-container table tbody tr > *:nth-child(5) { + width: 15%; +} + @media (min-width: 768px) { .container { --common-spacing: var(--spacing-l); diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index 7c901f9398..42fff76ad5 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -10,7 +10,7 @@ import { CookieConsentActionListener } from './types'; export function CookieConsent(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); - const { settingsSaved } = useCookieConsentContent(); + const content = useCookieConsentContent(); const [, forceUpdate] = useState(0); const [showScreenReaderSaveNotification, setShowScreenReaderSaveNotification] = useState(false); const [popupTimerComplete, setPopupTimerComplete] = useState(false); @@ -74,7 +74,7 @@ export function CookieConsent(): React.ReactElement | null { return (
- {settingsSaved} + {content.texts.ui.settingsSaved}
); diff --git a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx index 81e3d069e5..55a4295e86 100644 --- a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -11,11 +11,8 @@ export type Props = { }; function Buttons({ onClick, hasOptionalConsents }: Props): React.ReactElement { - const { - approveRequiredAndSelectedConsents, - approveOnlyRequiredConsents, - approveAllConsents, - } = useCookieConsentContent(); + const content = useCookieConsentContent(); + const { approveRequiredAndSelectedConsents, approveOnlyRequiredConsents, approveAllConsents } = content.texts.ui; const primaryButtonText = hasOptionalConsents ? approveRequiredAndSelectedConsents : approveAllConsents; const primaryButtonAction = hasOptionalConsents ? 'approveSelectedAndRequired' : 'approveAll'; return ( diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx new file mode 100644 index 0000000000..82a2e4bd67 --- /dev/null +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +import { ConsentGroup as ConsentGroupType } from '../CookieConsentContext'; +import styles from '../CookieConsent.module.scss'; +import { Checkbox } from '../../checkbox/Checkbox'; +import { useAccordion } from '../../accordion'; +import { IconAngleDown, IconAngleUp } from '../../../icons'; +import { Card } from '../../card/Card'; +import ConsentGroupDataTable from '../consentGroupDataTable/ConsentGroupDataTable'; +import classNames from '../../../utils/classNames'; + +function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean }): React.ReactElement { + const { isOpen, buttonProps, contentProps } = useAccordion({ + initiallyOpen: false, + }); + const { group, isRequired } = props; + const { title, text } = group; + const Icon = isOpen ? IconAngleUp : IconAngleDown; + const checkboxProps = { + onChange: isRequired ? () => undefined : () => undefined, + disabled: isRequired, + checked: isRequired, + }; + const checkboxStyle = { + '--label-font-size': 'var(--fontsize-heading-s)', + } as React.CSSProperties; + + const currentStyles = isOpen + ? styles['consent-group'] + : classNames(styles['consent-group'], styles['consent-group-closed']); + + return ( +
+
+
+ +
+ +
+
+

{text}

+ + + +
+
+ ); +} + +export default ConsentGroup; diff --git a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx new file mode 100644 index 0000000000..628ebc910d --- /dev/null +++ b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx @@ -0,0 +1,36 @@ +import React, { useMemo } from 'react'; +import { Table } from '../../table/Table'; +import { ConsentGroup, useCookieConsentContent } from '../CookieConsentContext'; +import styles from '../CookieConsent.module.scss'; + +function ConsentGroupDataTable(props: { consents: ConsentGroup['consents'] }): React.ReactElement { + const content = useCookieConsentContent(); + + const cols = useMemo(() => { + return Object.entries(content.texts.tableHeadings).map((entry) => { + const [key, value] = entry; + return { + key, + headerName: value, + }; + }); + }, [content.texts.tableHeadings]); + + const rows = useMemo(() => { + return props.consents.map((consent) => { + return consent; + }); + }, [props.consents]); + + const theme = { + '--header-background-color': 'var(--color-black)', + }; + + return ( +
+ + + ); +} + +export default ConsentGroupDataTable; diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index 53e0634cc3..ea15663819 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -14,14 +14,10 @@ function Content({ onClick }: ViewProps): React.ReactElement { const { isOpen, buttonProps, contentProps } = useAccordion({ initiallyOpen: false, }); - const { - mainTitle, - mainText, - hideSettings, - showSettings, - approveRequiredAndSelectedConsents, - approveOnlyRequiredConsents, - } = useCookieConsentContent(); + const content = useCookieConsentContent(); + const { sections, ui } = content.texts; + const { hideSettings, showSettings, approveRequiredAndSelectedConsents, approveOnlyRequiredConsents } = ui; + const { title, text } = sections.main; const titleRef = useRef(); const Icon = isOpen ? IconAngleUp : IconAngleDown; const settingsButtonText = isOpen ? hideSettings : showSettings; @@ -42,12 +38,12 @@ function Content({ onClick }: ViewProps): React.ReactElement { tabIndex={0} ref={titleRef} > - {mainTitle} + {title}
-

{mainText}

+

{text}

-

{text}

+

{text}

undefined : () => undefined, disabled: isRequired, @@ -23,24 +23,25 @@ function RequiredConsents(props: { '--label-font-size': 'var(--fontsize-heading-m)', } as React.CSSProperties; + const getConsentGroupIdenfier = (suffix: string) => `consent-group-${groupId}-${suffix}`; + return (
-

{text}

+

{text}

    - {groups.map((group) => ( + {groupList.map((group, index) => (
  • - +
  • ))}
@@ -48,4 +49,4 @@ function RequiredConsents(props: { ); } -export default RequiredConsents; +export default ConsentGroups; diff --git a/packages/react/src/components/cookieConsent/details/Details.tsx b/packages/react/src/components/cookieConsent/details/Details.tsx index f6e150a08b..ee9a0840f5 100644 --- a/packages/react/src/components/cookieConsent/details/Details.tsx +++ b/packages/react/src/components/cookieConsent/details/Details.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styles from '../CookieConsent.module.scss'; -import RequiredConsents from '../requiredConsents/RequiredConsents'; +import ConsentGroups from '../consentGroups/ConsentGroups'; import { useCookieConsentContent } from '../CookieConsentContext'; function Details(): React.ReactElement { @@ -14,8 +14,8 @@ function Details(): React.ReactElement { {title}

{text}

- - + +
); } diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index 4d01b4249d..e182bbea8f 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -96,20 +96,22 @@ export const getContent = ( }; if (requiredConsentGroups) { content.requiredConsents = { + groupId: 'required', title: 'Title for required consents', text: 'Text for required consents', checkboxAriaLabel: 'checkboxAriaLabel', - groups: requiredConsentGroups.map((consents, index) => + groupList: requiredConsentGroups.map((consents, index) => createConsentGroup(`requiredConsentGroup${index}`, consents), ), }; } if (optionalConsentsGroups) { - content.requiredConsents = { + content.optionalConsents = { + groupId: 'optional', title: 'Title for optional consents', text: 'Text for optional consents', checkboxAriaLabel: 'checkboxAriaLabel', - groups: optionalConsentsGroups.map((consents, index) => + groupList: optionalConsentsGroups.map((consents, index) => createConsentGroup(`optionalConsentGroups${index}`, consents), ), }; From e6737e8e7fe02448a9348155ce54a9b856f06fc5 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Thu, 7 Apr 2022 13:09:40 +0300 Subject: [PATCH 050/292] cookie-consent Removed close button --- .../cookieConsent/CookieConsent.module.scss | 32 ++------------ .../cookieConsent/CookieConsent.test.tsx | 42 ++----------------- .../__snapshots__/CookieConsent.test.tsx.snap | 28 ------------- .../cookieConsent/content/Content.tsx | 14 +------ 4 files changed, 8 insertions(+), 108 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 7798d84daf..938104d9ae 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -44,10 +44,6 @@ width: 100%; box-sizing: border-box; border-top: 8px solid var(--color-bus); - --close-button-size: 24px; - --close-button-padding: calc(var(--spacing-s) / 2); - --close-button-left-side: calc(var(--close-button-size) + var(--common-spacing)); - --lang-pos-x: calc(var(--close-button-left-side) + var(--common-spacing)); } .language-switcher { @@ -119,15 +115,11 @@ padding-left: 0; } -.accordion-button, -.close-button { +.accordion-button { border: none; background: transparent; padding: 0; cursor: pointer; -} - -.accordion-button { display: flex; align-items: center; color: var(--color-bus); @@ -143,15 +135,6 @@ padding-left: var(--spacing-3-xs); } -.close-button { - position: absolute; - top: var(--close-button-padding); - right: var(--close-button-padding); - padding: var(--close-button-padding); - height: var(--close-button-size); - box-sizing: content-box; -} - .consent-group-parent { display: flex; padding: 0 0 var(--spacing-2-xl); @@ -230,22 +213,13 @@ --common-spacing: var(--spacing-l); } - .language-switcher, - .close-button { - top: calc(var(--common-spacing) + 4px); - } - .language-switcher { - right: var(--lang-pos-x); + top: calc(var(--common-spacing) + 4px); + right: var(--common-spacing); left: unset; padding-top: 15px; } - .close-button { - right: var(--common-spacing); - padding: calc(var(--common-spacing) / 2) 0; - } - .main-content { padding-top: 0; } diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 3208dadc67..d7ee6e50db 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -101,7 +101,6 @@ describe(' ', () => { settingsToggler: 'cookie-consent-settings-toggler', detailsComponent: 'cookie-consent-details', screenReaderNotification: 'cookie-consent-screen-reader-notification', - closeButton: 'cookie-consent-close-button', getOptionalConsentId: (key: string) => `optional-cookie-consent-${key}`, getRequiredConsentId: (key: string) => `required-cookie-consent-${key}`, }; @@ -137,7 +136,6 @@ describe(' ', () => { verifyElementExistsByTestId(result, dataTestIds.languageSwitcher); verifyElementExistsByTestId(result, dataTestIds.approveButton); verifyElementExistsByTestId(result, dataTestIds.approveRequiredButton); - verifyElementExistsByTestId(result, dataTestIds.closeButton); }); it('is rendered if a required consent has not been approved. It could have been optional before', () => { @@ -182,7 +180,7 @@ describe(' ', () => { }); }); - describe(`Approve and close buttons will + describe(`Approve button will - hide the cookie consent - show a prompt for screen readers - save cookie`, () => { @@ -222,34 +220,6 @@ describe(' ', () => { checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); - it('Close -button will approve only required consents when details are not shown', () => { - const result = renderCookieConsent(defaultConsentData); - const consentResult = { - requiredConsent1: true, - requiredConsent2: true, - optionalConsent1: false, - optionalConsent2: false, - ...unknownConsents, - }; - clickElement(result, dataTestIds.closeButton); - checkCookiesAreSetAndConsentModalHidden(result, consentResult); - }); - - it('Close -button will approve required and selected consents when details are shown', async () => { - const result = renderCookieConsent(defaultConsentData); - const consentResult = { - requiredConsent1: true, - requiredConsent2: true, - optionalConsent1: false, - optionalConsent2: true, - ...unknownConsents, - }; - await openAccordion(result); - clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent2')); - clickElement(result, dataTestIds.closeButton); - checkCookiesAreSetAndConsentModalHidden(result, consentResult); - }); - it('Approve -button will approve required and selected consents when details are shown', async () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { @@ -283,22 +253,16 @@ describe(' ', () => { }); }); - it('Approve and close button texts change when accordion is open vs closed', async () => { + it('Approve button text changes when accordion is open vs closed', async () => { const result = await initDetailsView(defaultConsentData); const approveButtonTextWhileOpen = (result.getByTestId(dataTestIds.approveButton) as HTMLElement).innerHTML; - const closeButtonTitleWhileOpen = (result.getByTestId(dataTestIds.closeButton) as HTMLElement).getAttribute( - 'title', - ); + clickElement(result, dataTestIds.settingsToggler); await waitFor(() => { expect(isAccordionOpen(result)).toBeFalsy(); }); const approveButtonTextWhileClosed = (result.getByTestId(dataTestIds.approveButton) as HTMLElement).innerHTML; - const closeButtonTitleWhileClosed = (result.getByTestId(dataTestIds.closeButton) as HTMLElement).getAttribute( - 'title', - ); expect(approveButtonTextWhileOpen).not.toBe(approveButtonTextWhileClosed); - expect(closeButtonTitleWhileOpen).not.toBe(closeButtonTitleWhileClosed); }); it(`clicking an optional consent sets the consent true/false. diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index 7382986e5b..8ae2a41944 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -325,34 +325,6 @@ exports[` spec renders the component 1`] = `
- diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index ea15663819..dad4e851dc 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'; import { ViewProps } from '../types'; import Buttons from '../buttons/Buttons'; -import { IconAngleDown, IconAngleUp, IconCross } from '../../../icons'; +import { IconAngleDown, IconAngleUp } from '../../../icons'; import { useAccordion } from '../../accordion'; import Details from '../details/Details'; import styles from '../CookieConsent.module.scss'; @@ -16,12 +16,11 @@ function Content({ onClick }: ViewProps): React.ReactElement { }); const content = useCookieConsentContent(); const { sections, ui } = content.texts; - const { hideSettings, showSettings, approveRequiredAndSelectedConsents, approveOnlyRequiredConsents } = ui; + const { hideSettings, showSettings } = ui; const { title, text } = sections.main; const titleRef = useRef(); const Icon = isOpen ? IconAngleUp : IconAngleDown; const settingsButtonText = isOpen ? hideSettings : showSettings; - const closeButtonTitle = isOpen ? approveRequiredAndSelectedConsents : approveOnlyRequiredConsents; useEffect(() => { if (titleRef.current) { titleRef.current.focus(); @@ -64,15 +63,6 @@ function Content({ onClick }: ViewProps): React.ReactElement {
- ); } From 23bfbf3c3c98056570a122854ee96aabe2893d2d Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 8 Apr 2022 12:19:28 +0300 Subject: [PATCH 051/292] cookie-consent Add new actions and move actions to the context The modal should not block UI below, so context is not needed outside. Context can re-render itself and no need to have actions in CookieConsent. No need to pass "onClick". "onAction" can be picked from context. Re-rendering caused cookieController to be re-created each time, so changed useMemo props. --- .../cookieConsent/CookieConsent.tsx | 58 +-------- .../cookieConsent/CookieConsentContext.tsx | 121 +++++++++++++++--- .../cookieConsent/buttons/Buttons.tsx | 7 +- .../consentGroup/ConsentGroup.tsx | 27 +++- .../consentGroups/ConsentGroups.tsx | 17 ++- .../cookieConsent/content/Content.tsx | 5 +- .../src/components/cookieConsent/types.ts | 16 +-- 7 files changed, 153 insertions(+), 98 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index 42fff76ad5..f1ececcdad 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -3,69 +3,17 @@ import { VisuallyHidden } from '@react-aria/visually-hidden'; import classNames from '../../utils/classNames'; import styles from './CookieConsent.module.scss'; -import { ConsentController } from './cookieConsentController'; import { CookieConsentContext, useCookieConsentContent } from './CookieConsentContext'; import Content from './content/Content'; -import { CookieConsentActionListener } from './types'; export function CookieConsent(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); const content = useCookieConsentContent(); - const [, forceUpdate] = useState(0); - const [showScreenReaderSaveNotification, setShowScreenReaderSaveNotification] = useState(false); + // use this in context + const [showScreenReaderSaveNotification] = useState(false); const [popupTimerComplete, setPopupTimerComplete] = useState(false); const popupDelayInMs = 500; - const reRender = () => { - forceUpdate((p) => p + 1); - }; - - const save = (): void => { - cookieConsentContext.save(); - if (cookieConsentContext.hasUserHandledAllConsents()) { - setShowScreenReaderSaveNotification(true); - } - }; - - const approveRequired = () => { - Object.keys(cookieConsentContext.getOptional()).forEach((optionalConsent) => { - cookieConsentContext.update(optionalConsent, false); - }); - cookieConsentContext.approveRequired(); - save(); - reRender(); - }; - - const approveAll = () => { - cookieConsentContext.approveAll(); - save(); - reRender(); - }; - - const approveSelectedAndRequired = () => { - cookieConsentContext.approveRequired(); - save(); - reRender(); - }; - - const onChange: ConsentController['update'] = (key, value) => { - cookieConsentContext.update(key, value); - reRender(); - }; - - const onAction: CookieConsentActionListener = (action, consent) => { - if (action === 'approveAll') { - approveAll(); - } else if (action === 'approveRequired') { - approveRequired(); - } else if (action === 'approveSelectedAndRequired') { - approveSelectedAndRequired(); - } else if (action === 'changeConsent') { - const { key, value } = consent; - onChange(key, value); - } - }; - useEffect(() => { setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); }, []); @@ -90,7 +38,7 @@ export function CookieConsent(): React.ReactElement | null { data-testid="cookie-consent" >
- +
); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index 0c7158dd37..f66e4dec10 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useMemo, useState } from 'react'; import create, { ConsentController, ConsentList, ConsentObject } from './cookieConsentController'; +import { CookieConsentActionListener } from './types'; export type Description = { title: string; @@ -71,6 +72,9 @@ export type CookieConsentContextType = { willRenderCookieConsentDialog: boolean; hasUserHandledAllConsents: () => boolean; content: Content; + onAction: CookieConsentActionListener; + countApprovedOptional: () => number; + areGroupConsentsApproved: (consents: ConsentData[]) => boolean; }; type CookieConsentContextProps = { @@ -92,6 +96,9 @@ export const CookieConsentContext = createContext({ hasUserHandledAllConsents: () => false, willRenderCookieConsentDialog: false, content: {} as Content, + onAction: () => undefined, + countApprovedOptional: () => 0, + areGroupConsentsApproved: () => false, }); const getConsentsFromConsentGroup = (groups: ConsentGroup[]): ConsentList => { @@ -112,11 +119,7 @@ export const Provider = ({ }: CookieConsentContextProps): React.ReactElement => { const requiredConsents = getConsentsFromConsentGroup(content.requiredConsents.groupList); const optionalConsents = getConsentsFromConsentGroup(content.optionalConsents.groupList); - const consentController = useMemo(() => create({ requiredConsents, optionalConsents, cookieDomain }), [ - requiredConsents, - optionalConsents, - cookieDomain, - ]); + const consentController = useMemo(() => create({ requiredConsents, optionalConsents, cookieDomain }), []); const hasUserHandledAllConsents = () => consentController.getRequiredWithoutConsent().length === 0 && consentController.getUnhandledConsents().length === 0; @@ -130,23 +133,100 @@ export const Provider = ({ ...consentController.getOptional(), }); - const contextData: CookieConsentContextType = { - getRequired: () => consentController.getRequired(), - getOptional: () => consentController.getOptional(), - update: (key, value) => consentController.update(key, value), - approveRequired: () => consentController.approveRequired(), - approveAll: () => consentController.approveAll(), - save: () => { - const savedData = consentController.save(); - if (hasUserHandledAllConsents()) { - setWillRenderCookieConsentDialog(false); - onAllConsentsGiven(mergeConsents()); + const [, forceUpdate] = useState(0); + const reRender = () => { + forceUpdate((p) => p + 1); + }; + + const save = () => { + const savedData = consentController.save(); + if (hasUserHandledAllConsents()) { + setWillRenderCookieConsentDialog(false); + onAllConsentsGiven(mergeConsents()); + // setShowScreenReaderSaveNotification(true); + } + return savedData; + }; + + const getRequired: CookieConsentContextType['getRequired'] = () => consentController.getRequired(); + const getOptional: CookieConsentContextType['getOptional'] = () => consentController.getOptional(); + const update: CookieConsentContextType['update'] = (key, value) => { + consentController.update(key, value); + }; + const approveSelectedAndRequired = () => { + consentController.approveRequired(); + save(); + }; + const approveAll: CookieConsentContextType['approveAll'] = () => { + consentController.approveAll(); + save(); + }; + const approveRequired: CookieConsentContextType['approveRequired'] = () => { + Object.keys(getOptional()).forEach((optionalConsent) => { + update(optionalConsent, false); + }); + approveRequired(); + save(); + }; + const setOptional = (approved: boolean) => { + Object.keys(getOptional()).forEach((optionalConsent) => { + update(optionalConsent, approved); + }); + }; + + const onAction: CookieConsentContextType['onAction'] = (action, consents, value) => { + console.log('onAction:', action, consents, value); + if (action === 'approveAll') { + approveAll(); + } else if (action === 'approveRequired') { + approveRequired(); + } else if (action === 'approveSelectedAndRequired') { + approveSelectedAndRequired(); + } else if (action === 'changeConsentGroup') { + consents.forEach((consent) => { + update(consent, value); + }); + } else if (action === 'approveOptional') { + setOptional(true); + } else if (action === 'unapproveOptional') { + setOptional(false); + } + reRender(); + }; + + const countApprovedOptional: CookieConsentContextType['countApprovedOptional'] = () => { + let counter = 0; + let approved = 0; + Object.values(getOptional()).forEach((isApproved) => { + counter += 1; + if (isApproved) { + approved += 1; } - return savedData; - }, + }); + return approved / counter; + }; + + const areGroupConsentsApproved: CookieConsentContextType['areGroupConsentsApproved'] = (consentData) => { + // consentData + const optionalConsentList = consentController.getOptional(); + return !consentData.reduce((hasUnApprovedConsent, consent) => { + return hasUnApprovedConsent || optionalConsentList[consent.id] !== true; + }, false); + }; + + const contextData: CookieConsentContextType = { + getOptional, + getRequired, + approveAll, + approveRequired, + update, + save, willRenderCookieConsentDialog, hasUserHandledAllConsents, content, + onAction, + countApprovedOptional, + areGroupConsentsApproved, }; onConsentsParsed(mergeConsents(), hasUserHandledAllConsents()); return {children}; @@ -160,3 +240,8 @@ export const useCookieConsentContent = (): Content => { const cookieConsentContext = useContext(CookieConsentContext); return getCookieConsentContent(cookieConsentContext); }; + +export const useCookieConsentActions = (): CookieConsentContextType['onAction'] => { + const cookieConsentContext = useContext(CookieConsentContext); + return cookieConsentContext.onAction; +}; diff --git a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx index 55a4295e86..a28b2e141d 100644 --- a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -1,17 +1,16 @@ import React from 'react'; -import { CookieConsentActionListener } from '../types'; import { Button } from '../../button/Button'; import styles from '../CookieConsent.module.scss'; -import { useCookieConsentContent } from '../CookieConsentContext'; +import { useCookieConsentActions, useCookieConsentContent } from '../CookieConsentContext'; export type Props = { - onClick: CookieConsentActionListener; hasOptionalConsents: boolean; }; -function Buttons({ onClick, hasOptionalConsents }: Props): React.ReactElement { +function Buttons({ hasOptionalConsents }: Props): React.ReactElement { const content = useCookieConsentContent(); + const onClick = useCookieConsentActions(); const { approveRequiredAndSelectedConsents, approveOnlyRequiredConsents, approveAllConsents } = content.texts.ui; const primaryButtonText = hasOptionalConsents ? approveRequiredAndSelectedConsents : approveAllConsents; const primaryButtonAction = hasOptionalConsents ? 'approveSelectedAndRequired' : 'approveAll'; diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx index caa2634768..3d554e9ab2 100644 --- a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -1,6 +1,10 @@ -import React from 'react'; +import React, { useContext } from 'react'; -import { ConsentGroup as ConsentGroupType } from '../CookieConsentContext'; +import { + ConsentGroup as ConsentGroupType, + CookieConsentContext, + useCookieConsentActions, +} from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; import { Checkbox } from '../../checkbox/Checkbox'; import { useAccordion } from '../../accordion'; @@ -10,16 +14,27 @@ import ConsentGroupDataTable from '../consentGroupDataTable/ConsentGroupDataTabl import classNames from '../../../utils/classNames'; function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: string }): React.ReactElement { + const { group, isRequired, id } = props; const { isOpen, buttonProps, contentProps } = useAccordion({ initiallyOpen: false, }); - const { group, isRequired, id } = props; + const groupConsents = group.consents; + const cookieConsentContext = useContext(CookieConsentContext); + const onClick = useCookieConsentActions(); + const areAllApproved = isRequired || cookieConsentContext.areGroupConsentsApproved(groupConsents); const { title, text, checkboxAriaLabel, expandAriaLabel } = group; const Icon = isOpen ? IconAngleUp : IconAngleDown; const checkboxProps = { - onChange: isRequired ? () => undefined : () => undefined, + onChange: isRequired + ? () => undefined + : () => + onClick( + 'changeConsentGroup', + groupConsents.map((consent) => consent.id), + !areAllApproved, + ), disabled: isRequired, - checked: isRequired, + checked: areAllApproved, }; const checkboxStyle = { '--label-font-size': 'var(--fontsize-heading-s)', @@ -64,7 +79,7 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: '--padding-vertical': '0', }} > - + diff --git a/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx b/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx index 5050ba4274..cd9275944d 100644 --- a/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx +++ b/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx @@ -1,6 +1,10 @@ -import React from 'react'; +import React, { useContext } from 'react'; -import { RequiredOrOptionalConsentGroups } from '../CookieConsentContext'; +import { + CookieConsentContext, + RequiredOrOptionalConsentGroups, + useCookieConsentActions, +} from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; import ConsentGroup from '../consentGroup/ConsentGroup'; import { Checkbox } from '../../checkbox/Checkbox'; @@ -10,14 +14,19 @@ function ConsentGroups(props: { isRequired?: boolean; }): React.ReactElement { const { consentGroups, isRequired } = props; + const cookieConsentContext = useContext(CookieConsentContext); + const onClick = useCookieConsentActions(); if (!consentGroups) { return null; } + const selectPercentage = cookieConsentContext.countApprovedOptional(); + const allApproved = isRequired || selectPercentage === 1; const { title, text, groupList, groupId } = consentGroups; const checkboxProps = { - onChange: isRequired ? () => undefined : () => undefined, + onChange: isRequired ? () => undefined : () => onClick('approveOptional'), disabled: isRequired, - checked: isRequired, + checked: isRequired || allApproved, + indeterminate: isRequired ? false : !Number.isInteger(selectPercentage), }; const checkboxStyle = { '--label-font-size': 'var(--fontsize-heading-m)', diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index dad4e851dc..3b7635a858 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { ViewProps } from '../types'; import Buttons from '../buttons/Buttons'; import { IconAngleDown, IconAngleUp } from '../../../icons'; import { useAccordion } from '../../accordion'; @@ -10,7 +9,7 @@ import { Card } from '../../card/Card'; import { useCookieConsentContent } from '../CookieConsentContext'; import LanguageSwitcher from '../languageSwitcher/LanguageSwitcher'; -function Content({ onClick }: ViewProps): React.ReactElement { +function Content(): React.ReactElement { const { isOpen, buttonProps, contentProps } = useAccordion({ initiallyOpen: false, }); @@ -62,7 +61,7 @@ function Content({ onClick }: ViewProps): React.ReactElement { >
- + ); } diff --git a/packages/react/src/components/cookieConsent/types.ts b/packages/react/src/components/cookieConsent/types.ts index 03ddfe4ab5..0da5b30a95 100644 --- a/packages/react/src/components/cookieConsent/types.ts +++ b/packages/react/src/components/cookieConsent/types.ts @@ -1,8 +1,8 @@ -export type CookieConsentAction = 'approveAll' | 'approveRequired' | 'changeConsent' | 'approveSelectedAndRequired'; -export type CookieConsentActionListener = ( - action: CookieConsentAction, - consent?: { key: string; value: boolean }, -) => void; -export type ViewProps = { - onClick: CookieConsentActionListener; -}; +export type CookieConsentAction = + | 'approveAll' + | 'approveRequired' + | 'changeConsentGroup' + | 'approveOptional' + | 'unapproveOptional' + | 'approveSelectedAndRequired'; +export type CookieConsentActionListener = (action: CookieConsentAction, consents?: string[], value?: boolean) => void; From ce991de845706dfe5f16081dff3992f2fabb9de4 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 8 Apr 2022 13:29:10 +0300 Subject: [PATCH 052/292] cookie-consent Moved context into the component --- .../cookieConsent/CookieConsent.stories.tsx | 35 +++++++++---------- .../cookieConsent/CookieConsentContext.tsx | 19 +++++----- .../cookieConsent/CookieConsentModal.tsx | 13 +++++++ .../consentGroups/ConsentGroups.tsx | 5 +-- 4 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 packages/react/src/components/cookieConsent/CookieConsentModal.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 3397bf7d03..619c637e93 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -1,11 +1,11 @@ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { commonConsents } from './cookieConsentController'; -import { CookieConsentContext, Provider as CookieContextProvider, Content } from './CookieConsentContext'; -import { CookieConsent } from './CookieConsent'; +import { Content } from './CookieConsentContext'; +import { CookieConsentModal } from './CookieConsentModal'; export default { - component: CookieConsent, + component: CookieConsentModal, title: 'Components/CookieConsent', parameters: { controls: { expanded: true }, @@ -15,7 +15,7 @@ export default { export const Example = () => { const Application = () => { - const { willRenderCookieConsentDialog } = useContext(CookieConsentContext); + const willRenderCookieConsentDialog = false; return (
{ languageSelectorAriaLabel: 'Kieli: Suomi. Vaihda kieli. Change language. Ändra språk.', onLanguageChange, }, - }; - }, [language]); - - return ( - { + onAllConsentsGiven: (consents) => { if (consents.matomo) { // start tracking // window._paq.push(['setConsentGiven']); // window._paq.push(['setCookieConsentGiven']); } document.getElementById('focused-element-after-cookie-consent-closed').focus(); - }} - onConsentsParsed={(consents, hasUserHandledAllConsents) => { + }, + onConsentsParsed: (consents, hasUserHandledAllConsents) => { if (consents.matomo === undefined) { // tell matomo to wait for consent: // window._paq.push(['requireConsent']); @@ -236,12 +231,14 @@ export const Example = () => { if (hasUserHandledAllConsents) { // cookie consent dialog will not be shown } - }} - content={content} - onLanguageChange={onLanguageChange} - > - + }, + }; + }, [language]); + + return ( + <> + - + ); }; diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index f66e4dec10..fc2ba0b590 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -60,6 +60,8 @@ export type Content = { languageSelectorAriaLabel: string; onLanguageChange: (newLanguage: string) => void; }; + onAllConsentsGiven?: (consents: ConsentObject) => void; + onConsentsParsed?: (consents: ConsentObject, hasUserHandledAllConsents: boolean) => void; }; export type CookieConsentContextType = { @@ -83,7 +85,6 @@ type CookieConsentContextProps = { content: Content; onAllConsentsGiven?: (consents: ConsentObject) => void; onConsentsParsed?: (consents: ConsentObject, hasUserHandledAllConsents: boolean) => void; - onLanguageChange: (newLanguage: string) => void; }; export const CookieConsentContext = createContext({ @@ -110,13 +111,7 @@ const getConsentsFromConsentGroup = (groups: ConsentGroup[]): ConsentList => { }, []); }; -export const Provider = ({ - cookieDomain, - onAllConsentsGiven = () => undefined, - onConsentsParsed = () => undefined, - children, - content, -}: CookieConsentContextProps): React.ReactElement => { +export const Provider = ({ cookieDomain, children, content }: CookieConsentContextProps): React.ReactElement => { const requiredConsents = getConsentsFromConsentGroup(content.requiredConsents.groupList); const optionalConsents = getConsentsFromConsentGroup(content.optionalConsents.groupList); const consentController = useMemo(() => create({ requiredConsents, optionalConsents, cookieDomain }), []); @@ -142,7 +137,9 @@ export const Provider = ({ const savedData = consentController.save(); if (hasUserHandledAllConsents()) { setWillRenderCookieConsentDialog(false); - onAllConsentsGiven(mergeConsents()); + if (content.onAllConsentsGiven) { + content.onAllConsentsGiven(mergeConsents()); + } // setShowScreenReaderSaveNotification(true); } return savedData; @@ -228,7 +225,9 @@ export const Provider = ({ countApprovedOptional, areGroupConsentsApproved, }; - onConsentsParsed(mergeConsents(), hasUserHandledAllConsents()); + if (content.onConsentsParsed) { + content.onConsentsParsed(mergeConsents(), hasUserHandledAllConsents()); + } return {children}; }; diff --git a/packages/react/src/components/cookieConsent/CookieConsentModal.tsx b/packages/react/src/components/cookieConsent/CookieConsentModal.tsx new file mode 100644 index 0000000000..8d81c89297 --- /dev/null +++ b/packages/react/src/components/cookieConsent/CookieConsentModal.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { CookieConsent } from './CookieConsent'; +import { Content, Provider as CookieContextProvider } from './CookieConsentContext'; + +export function CookieConsentModal(props: { content: Content; cookieDomain?: string }): React.ReactElement | null { + const { cookieDomain, content } = props; + return ( + + + + ); +} diff --git a/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx b/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx index cd9275944d..9bf86300c1 100644 --- a/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx +++ b/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx @@ -22,10 +22,11 @@ function ConsentGroups(props: { const selectPercentage = cookieConsentContext.countApprovedOptional(); const allApproved = isRequired || selectPercentage === 1; const { title, text, groupList, groupId } = consentGroups; + const checked = isRequired || allApproved; const checkboxProps = { - onChange: isRequired ? () => undefined : () => onClick('approveOptional'), + onChange: isRequired ? () => undefined : () => onClick(checked ? 'unapproveOptional' : 'approveOptional'), disabled: isRequired, - checked: isRequired || allApproved, + checked, indeterminate: isRequired ? false : !Number.isInteger(selectPercentage), }; const checkboxStyle = { From 1df273a6486797f69db7b8db310fc06f959bea3e Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 8 Apr 2022 13:57:24 +0300 Subject: [PATCH 053/292] cookie-consent Added util.ts. Context is inside the component so added an util for getting consent status from outside. Reads only stored cookies. Won't change when context changes. Use onAllConsentsGiven and onConsentsParsed to detect changes in real time. --- .../cookieConsent/CookieConsent.stories.tsx | 68 ++++++++++++------- .../cookieConsent/CookieConsentContext.tsx | 2 +- .../cookieConsent/cookieConsentController.ts | 5 ++ .../src/components/cookieConsent/util.ts | 17 +++++ 4 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 packages/react/src/components/cookieConsent/util.ts diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 619c637e93..0816070778 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -1,8 +1,9 @@ import React, { useMemo, useState } from 'react'; import { commonConsents } from './cookieConsentController'; -import { Content } from './CookieConsentContext'; +import { Content, getConsentsFromConsentGroup } from './CookieConsentContext'; import { CookieConsentModal } from './CookieConsentModal'; +import { getConsentStatus, hasHandledAllConsents } from './util'; export default { component: CookieConsentModal, @@ -14,30 +15,6 @@ export default { }; export const Example = () => { - const Application = () => { - const willRenderCookieConsentDialog = false; - return ( -
- {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */} -

- This is a dummy application -

- {willRenderCookieConsentDialog ? ( - <> -

Cookie consent dialog will be shown.

- - ) : ( - <> -

Cookie consents have been given. Remove the cookie to see the dialog again.

- - )} -
- ); - }; - const [language, setLanguage] = useState('fi'); const onLanguageChange = (newLang) => setLanguage(newLang); @@ -235,6 +212,47 @@ export const Example = () => { }; }, [language]); + const MatomoCookieTracker = () => { + const isMatomoCookieApproved = getConsentStatus(commonConsents.matomo); + return ( +
+

Matomo cookie is {!isMatomoCookieApproved && NOT} set.*

+ * This won't change in real time +
+ ); + }; + + const Application = () => { + const requiredConsents = content.requiredConsents + ? getConsentsFromConsentGroup(content.requiredConsents.groupList) + : []; + const optionalConsents = content.optionalConsents + ? getConsentsFromConsentGroup(content.optionalConsents.groupList) + : []; + const willRenderCookieConsentDialog = !hasHandledAllConsents(requiredConsents, optionalConsents); + return ( +
+ {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */} +

+ This is a dummy application +

+ {willRenderCookieConsentDialog ? ( + <> +

Cookie consent dialog will be shown.

+ + ) : ( + <> +

Cookie consents have been given. Remove the cookie to see the dialog again.

+ + )} + +
+ ); + }; + return ( <> diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index fc2ba0b590..d72e437522 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -102,7 +102,7 @@ export const CookieConsentContext = createContext({ areGroupConsentsApproved: () => false, }); -const getConsentsFromConsentGroup = (groups: ConsentGroup[]): ConsentList => { +export const getConsentsFromConsentGroup = (groups: ConsentGroup[]): ConsentList => { return groups.reduce((ids, currentGroup) => { currentGroup.consents.forEach((consentData) => { ids.push(consentData.id); diff --git a/packages/react/src/components/cookieConsent/cookieConsentController.ts b/packages/react/src/components/cookieConsent/cookieConsentController.ts index c51175064b..ef550bce34 100644 --- a/packages/react/src/components/cookieConsent/cookieConsentController.ts +++ b/packages/react/src/components/cookieConsent/cookieConsentController.ts @@ -252,3 +252,8 @@ export default function createConsentController(props: ConsentControllerProps): save, }; } + +export function getConsentsFromCookie(cookieDomain?: string): ConsentObject { + const cookieController = createCookieController(cookieDomain); + return parseConsents(cookieController.get()); +} diff --git a/packages/react/src/components/cookieConsent/util.ts b/packages/react/src/components/cookieConsent/util.ts new file mode 100644 index 0000000000..8ec979e88e --- /dev/null +++ b/packages/react/src/components/cookieConsent/util.ts @@ -0,0 +1,17 @@ +import { getConsentsFromCookie } from './cookieConsentController'; + +export function getConsentStatus(consent: string, cookieDomain?: string): boolean | undefined { + const cookies = getConsentsFromCookie(cookieDomain); + return cookies[consent]; +} + +export function hasHandledAllConsents( + requiredConsents: string[], + optionalConsents: string[], + cookieDomain?: string, +): boolean | undefined { + const cookies = getConsentsFromCookie(cookieDomain); + const requiredWithoutConsent = requiredConsents.filter((consent) => cookies[consent] !== true); + const unhandledOptionalConsents = optionalConsents.filter((consent) => cookies[consent] !== true); + return requiredWithoutConsent.length === 0 && unhandledOptionalConsents.length === 0; +} From a9359cdeeda378dc17289810e66f5e5b414eb35f Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 8 Apr 2022 17:12:11 +0300 Subject: [PATCH 054/292] cookie-consent Improved accessibility --- .../cookieConsent/CookieConsent.module.scss | 18 ++++------ .../cookieConsent/CookieConsent.stories.tsx | 18 +++++----- .../cookieConsent/CookieConsentContext.tsx | 5 ++- .../consentGroup/ConsentGroup.tsx | 33 ++++++++++--------- .../consentGroups/ConsentGroups.tsx | 8 +++-- 5 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 938104d9ae..1a00019c31 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -146,6 +146,7 @@ .consent-group { display: flex; + position: relative; padding: var(--spacing-xl) 0; flex-direction: column; } @@ -162,6 +163,12 @@ padding: var(--spacing-l) 0 0; margin: 0; } + button { + position: absolute; + right: 0; + top: calc(var(--spacing-xl) - 0.5 * var(--spacing-s)); + padding: var(--spacing-s); + } } .title-with-checkbox { @@ -174,17 +181,6 @@ color: var(--color-black); } -.group-title-row { - display: flex; - position: relative; - button { - position: absolute; - right: 0; - top: calc(-0.5 * var(--spacing-s)); - padding: var(--spacing-s); - } -} - .data-table-container { border-left: 1px solid var(--color-black); border-right: 1px solid var(--color-black); diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 0816070778..68369f23da 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -56,14 +56,13 @@ export const Example = () => { title: 'Välttämättömät evästeet', text: 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', - checkboxAriaLabel: - 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', groupList: [ { title: 'Evästeet sivuston perustoimintoja varten', text: 'Näitä eväisteitä käytetään sivuston perustoiminnoissa', expandAriaLabel: 'Näytä perustoimintoihin littyvien evästeiden tiedot', - checkboxAriaLabel: 'Perustoimintoihin littyvien evästeiden käyttöä ei voi kieltää.', + checkboxAriaDescription: + 'Näitä eväisteitä käytetään sivuston perustoiminnoissa. Perustoimintoihin littyvien evästeiden käyttöä ei voi kieltää.', consents: [ { id: commonConsents.tunnistamo, @@ -88,7 +87,8 @@ export const Example = () => { title: 'Evästeet kirjautumista varten', text: 'Näitä eväisteitä käytetään kirjautumisessa', expandAriaLabel: 'Näytä kirjautumiseen littyvien evästeiden tiedot', - checkboxAriaLabel: 'Kirjautumiseen littyvien evästeiden käyttöä ei voi kieltää.', + checkboxAriaDescription: + 'Näitä eväisteitä käytetään kirjautumisessa. Kirjautumiseen littyvien evästeiden käyttöä ei voi kieltää.', consents: [ { id: commonConsents.tunnistamo, @@ -116,14 +116,14 @@ export const Example = () => { title: 'Muut evästeet', text: 'Voit hyväksyä tai jättää hyväksymättä muut evästeet. Praesent vel vestibulum nunc, at eleifend sapien. Integer cursus ut orci eu pretium. Ut a orci felis. In eu eros turpis. Sed ullamcorper lacinia lorem, id ullamcorper dui accumsan in. Integer dictum fermentum mi, sit amet accumsan lacus facilisis id. Quisque blandit lacus ac sem porta.', - checkboxAriaLabel: 'Hyväksy kaikki allaolevat evästeet', groupList: [ { title: 'Markkinointievästeet', text: 'Markkinointievästeillä kohdennetaan markkinointia. Nulla facilisi. Nullam mattis sapien sem, nec venenatis lectus lacinia sed. Phasellus purus nisi, imperdiet id volutpat vel, pellentesque in ex. In pretium maximus finibus.', expandAriaLabel: 'Näytä markkinointievästeiden tiedot', - checkboxAriaLabel: 'Hyväksy kaikki markkinointievästeet', + checkboxAriaDescription: + 'Markkinointievästeillä kohdennetaan markkinointia. Hyväksy tai jätä hyväksymättä kaikki markkinointiin liittyvät evästeet', consents: [ { id: commonConsents.marketing, @@ -140,7 +140,8 @@ export const Example = () => { text: 'Evästeisiin tallennetaan käyttäjän tekemiä Donec lacus ligula, consequat id ligula sed, dapibus blandit nunc. Phasellus efficitur nec tellus et tempus. Sed tempor tristique purus, at auctor lectus. Ut pretium rutrum viverra. Sed felis arcu, sodales fermentum finibus in, pretium id tellus. Morbi eget eros congue, pulvinar leo ut, aliquam lectus. Cras consectetur sit amet tortor nec vulputate. Integer scelerisque dignissim auctor. Fusce pharetra dui nulla, vel elementum leo mattis vitae.', expandAriaLabel: 'Näytä asetuksiin liittyvien evästeiden tiedot', - checkboxAriaLabel: 'Hyväksy kaikki asetuksiin liittyvät evästeet', + checkboxAriaDescription: + 'Evästeisiin tallennetaan käyttäjän tekemiä valintoja. Hyväksy tai jätä hyväksymättä kaikki asetuksiin liittyvät evästeet', consents: [ { id: commonConsents.preferences, @@ -157,7 +158,8 @@ export const Example = () => { title: 'Tilastointiin liittyvät evästeet', text: 'Tilastoinnilla parannetaan...', expandAriaLabel: 'Näytä tilastointiin liittyvien evästeiden tiedot', - checkboxAriaLabel: 'Hyväksy kaikki tilastointiin liittyvät evästeet', + checkboxAriaDescription: + 'Tilastoinnilla parannetaan sivustoa. Hyväksy tai jätä hyväksymättä kaikki tilastointiin liittyvät evästeet', consents: [ { id: commonConsents.matomo, diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index d72e437522..bf0020fa76 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -22,7 +22,7 @@ export type ConsentData = TableData & { export type ConsentGroup = Description & { expandAriaLabel: string; - checkboxAriaLabel: string; + checkboxAriaDescription?: string; consents: ConsentData[]; }; @@ -42,7 +42,7 @@ export type SectionTexts = { export type RequiredOrOptionalConsentGroups = Description & { groupId: 'required' | 'optional'; - checkboxAriaLabel: string; + checkboxAriaDescription?: string; groupList: ConsentGroup[]; }; @@ -172,7 +172,6 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte }; const onAction: CookieConsentContextType['onAction'] = (action, consents, value) => { - console.log('onAction:', action, consents, value); if (action === 'approveAll') { approveAll(); } else if (action === 'approveRequired') { diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx index 3d554e9ab2..7359d426e0 100644 --- a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -1,4 +1,5 @@ import React, { useContext } from 'react'; +import { VisuallyHidden } from '@react-aria/visually-hidden'; import { ConsentGroup as ConsentGroupType, @@ -22,7 +23,7 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: const cookieConsentContext = useContext(CookieConsentContext); const onClick = useCookieConsentActions(); const areAllApproved = isRequired || cookieConsentContext.areGroupConsentsApproved(groupConsents); - const { title, text, checkboxAriaLabel, expandAriaLabel } = group; + const { title, text, checkboxAriaDescription, expandAriaLabel } = group; const Icon = isOpen ? IconAngleUp : IconAngleDown; const checkboxProps = { onChange: isRequired @@ -48,18 +49,21 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: return (
-
-
- -
+
+ +
+
+

{text}

+ + {checkboxAriaDescription || text} + -
-
-

{text}

undefined : () => onClick(checked ? 'unapproveOptional' : 'approveOptional'), @@ -47,7 +48,10 @@ function ConsentGroups(props: { {...checkboxProps} />
-

{text}

+

{text}

+ + {checkboxAriaDescription || text} +
    {groupList.map((group, index) => (
  • From 95502e6e7eb9e70ae889e380949a755b72fd1b55 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 11 Apr 2022 08:46:33 +0300 Subject: [PATCH 055/292] cookie-consent Make utils easier to use with consent data Fix maximum call stack error --- .../cookieConsent/CookieConsent.stories.tsx | 13 +++++-------- .../cookieConsent/CookieConsentContext.tsx | 2 +- .../react/src/components/cookieConsent/util.ts | 15 +++++++++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 68369f23da..45b30c84a1 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { commonConsents } from './cookieConsentController'; -import { Content, getConsentsFromConsentGroup } from './CookieConsentContext'; +import { Content } from './CookieConsentContext'; import { CookieConsentModal } from './CookieConsentModal'; import { getConsentStatus, hasHandledAllConsents } from './util'; @@ -225,13 +225,10 @@ export const Example = () => { }; const Application = () => { - const requiredConsents = content.requiredConsents - ? getConsentsFromConsentGroup(content.requiredConsents.groupList) - : []; - const optionalConsents = content.optionalConsents - ? getConsentsFromConsentGroup(content.optionalConsents.groupList) - : []; - const willRenderCookieConsentDialog = !hasHandledAllConsents(requiredConsents, optionalConsents); + const willRenderCookieConsentDialog = !hasHandledAllConsents( + content.requiredConsents || [], + content.optionalConsents || [], + ); return (
    { update(optionalConsent, false); }); - approveRequired(); + consentController.approveRequired(); save(); }; const setOptional = (approved: boolean) => { diff --git a/packages/react/src/components/cookieConsent/util.ts b/packages/react/src/components/cookieConsent/util.ts index 8ec979e88e..a29df5977e 100644 --- a/packages/react/src/components/cookieConsent/util.ts +++ b/packages/react/src/components/cookieConsent/util.ts @@ -1,3 +1,4 @@ +import { getConsentsFromConsentGroup, RequiredOrOptionalConsentGroups } from './CookieConsentContext'; import { getConsentsFromCookie } from './cookieConsentController'; export function getConsentStatus(consent: string, cookieDomain?: string): boolean | undefined { @@ -6,12 +7,18 @@ export function getConsentStatus(consent: string, cookieDomain?: string): boolea } export function hasHandledAllConsents( - requiredConsents: string[], - optionalConsents: string[], + requiredConsents: string[] | RequiredOrOptionalConsentGroups, + optionalConsents: string[] | RequiredOrOptionalConsentGroups, cookieDomain?: string, ): boolean | undefined { + const requiredConsentsAsArray: string[] = Array.isArray(requiredConsents) + ? requiredConsents + : getConsentsFromConsentGroup(requiredConsents.groupList); + const optionalConsentsAsArray: string[] = Array.isArray(optionalConsents) + ? optionalConsents + : getConsentsFromConsentGroup(optionalConsents.groupList); const cookies = getConsentsFromCookie(cookieDomain); - const requiredWithoutConsent = requiredConsents.filter((consent) => cookies[consent] !== true); - const unhandledOptionalConsents = optionalConsents.filter((consent) => cookies[consent] !== true); + const requiredWithoutConsent = requiredConsentsAsArray.filter((consent) => cookies[consent] !== true); + const unhandledOptionalConsents = optionalConsentsAsArray.filter((consent) => cookies[consent] === undefined); return requiredWithoutConsent.length === 0 && unhandledOptionalConsents.length === 0; } From f23efbe7a529855db91ddb0545bc1b8b8fd3b42d Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 11 Apr 2022 10:31:29 +0300 Subject: [PATCH 056/292] cookie-consent Added mobile view Mobile view is hidden from screen readers so there are no aria-* labels etc. optimizations. --- .../cookieConsent/CookieConsent.module.scss | 46 ++++++++++++++++++ .../consentGroup/ConsentGroup.tsx | 2 + .../ConsentGroupDataMobile.tsx | 48 +++++++++++++++++++ .../ConsentGroupDataTable.tsx | 3 +- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 1a00019c31..5251d918c3 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -204,6 +204,27 @@ width: 15%; } +.data-mobile-container { + ul { + list-style: none; + padding: 0; + li { + background: var(--color-black-5); + margin: var(--spacing-m) 0 0; + padding: var(--spacing-s) var(--spacing-s) 0; + div { + display: flex; + flex-direction: column; + padding-bottom: var(--spacing-m); + span[role='heading'] { + font-weight: bold; + padding-bottom: var(--spacing-2-xs); + } + } + } + } +} + @media (min-width: 768px) { .container { --common-spacing: var(--spacing-l); @@ -231,3 +252,28 @@ width: auto; } } + +/* @extend does not work inside media queries */ +@mixin visuallyHidden { + clip-path: polygon(0 0, 0 0, 0 0, 0 0); + border: 0; + clip: 'rect(0 0 0 0)'; + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + display: block; +} + +@media (min-width: 768px) { + .visually-hidden-in-desktop { + @include visuallyHidden; + } +} +@media (max-width: 767px) { + .visually-hidden-in-mobile { + @include visuallyHidden; + } +} diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx index 7359d426e0..c51e23baeb 100644 --- a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -12,6 +12,7 @@ import { useAccordion } from '../../accordion'; import { IconAngleDown, IconAngleUp } from '../../../icons'; import { Card } from '../../card/Card'; import ConsentGroupDataTable from '../consentGroupDataTable/ConsentGroupDataTable'; +import ConsentGroupDataMobile from '../consentGroupDataMobile/ConsentGroupDataMobile'; import classNames from '../../../utils/classNames'; function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: string }): React.ReactElement { @@ -81,6 +82,7 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: }} > +
diff --git a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx new file mode 100644 index 0000000000..0c554a06f8 --- /dev/null +++ b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx @@ -0,0 +1,48 @@ +import React, { useMemo } from 'react'; + +import { ConsentGroup, TableData, useCookieConsentContent } from '../CookieConsentContext'; +import styles from '../CookieConsent.module.scss'; +import classNames from '../../../utils/classNames'; + +function ConsentGroupDataMobile(props: { consents: ConsentGroup['consents'] }): React.ReactElement { + const content = useCookieConsentContent(); + const { consents } = props; + const dataKeys = useMemo(() => { + return Object.keys(content.texts.tableHeadings); + }, []); + + const data: TableData[] = useMemo(() => { + return consents.map((consent) => { + return dataKeys.reduce((currentData, key) => { + // eslint-disable-next-line no-param-reassign + currentData[key] = consent[key]; + return currentData; + }, {} as TableData); + }); + }, [content.language.current]); + + const rowOrder: (keyof TableData)[] = ['name', 'hostName', 'path', 'description', 'expiration']; + + return ( +
+
    + {data.map((item) => { + return ( +
  • + {rowOrder.map((key) => { + return ( +
    + {content.texts.tableHeadings[key]} + {item[key]} +
    + ); + })} +
  • + ); + })} +
+
+ ); +} + +export default ConsentGroupDataMobile; diff --git a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx index a3c6839e24..1d36f2b37a 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from 'react'; import { Table } from '../../table/Table'; import { ConsentGroup, useCookieConsentContent } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; +import classNames from '../../../utils/classNames'; function ConsentGroupDataTable(props: { consents: ConsentGroup['consents'] }): React.ReactElement { const content = useCookieConsentContent(); @@ -28,7 +29,7 @@ function ConsentGroupDataTable(props: { consents: ConsentGroup['consents'] }): R }; return ( -
+
); From 2e4faf8b8a22dd8d3cb8003e9a0fb6477a94ad0f Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 11 Apr 2022 11:19:40 +0300 Subject: [PATCH 057/292] cookie-consent Fine-tune table sizes --- .../cookieConsent/CookieConsent.module.scss | 19 +++++++++---------- .../ConsentGroupDataTable.tsx | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 5251d918c3..c8ad8b9df1 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -187,22 +187,13 @@ margin: var(--spacing-l) 0 var(--spacing-xs); } -.data-table-container table tbody tr > *:nth-child(1) { +.data-table-container table tbody tr > * { width: 15%; word-break: break-word; } -.data-table-container table tbody tr > *:nth-child(2) { - width: 15%; -} -.data-table-container table tbody tr > *:nth-child(3) { - width: 15%; -} .data-table-container table tbody tr > *:nth-child(4) { width: 40%; } -.data-table-container table tbody tr > *:nth-child(5) { - width: 15%; -} .data-mobile-container { ul { @@ -225,6 +216,14 @@ } } +@media (max-width: 1000px) { + .data-table-container table tbody tr > * { + width: 20%; + } + .data-table-container table tbody tr > *:nth-child(4) { + width: 20%; + } +} @media (min-width: 768px) { .container { --common-spacing: var(--spacing-l); diff --git a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx index 1d36f2b37a..be80353354 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx @@ -30,7 +30,7 @@ function ConsentGroupDataTable(props: { consents: ConsentGroup['consents'] }): R return (
-
+
); } From b125e1c35e0fa070b665cf37d6c269ab4af3ab74 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 12 Apr 2022 15:12:27 +0300 Subject: [PATCH 058/292] cookie-consent Fixed tests --- .../cookieConsent/CookieConsent.test.tsx | 64 +- .../cookieConsent/CookieConsent.tsx | 7 +- .../CookieConsentContext.test.tsx | 28 +- .../cookieConsent/CookieConsentContext.tsx | 14 +- .../__snapshots__/CookieConsent.test.tsx.snap | 1213 +++++++++++++++-- .../consentGroup/ConsentGroup.tsx | 13 +- .../ConsentGroupDataMobile.tsx | 4 +- .../ConsentGroupDataTable.tsx | 15 +- .../consentGroups/ConsentGroups.tsx | 11 +- .../src/components/cookieConsent/test.util.ts | 7 +- 10 files changed, 1198 insertions(+), 178 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index d7ee6e50db..6aa295d0b3 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -12,15 +12,15 @@ import mockDocumentCookie from './__mocks__/mockDocumentCookie'; import { extractSetCookieArguments, getContent } from './test.util'; type ConsentData = { - requiredConsents?: ConsentList; - optionalConsents?: ConsentList; + requiredConsents?: ConsentList[]; + optionalConsents?: ConsentList[]; cookie?: ConsentObject; contentModifier?: (content: Content) => Content; }; const defaultConsentData = { - requiredConsents: ['requiredConsent1', 'requiredConsent2'], - optionalConsents: ['optionalConsent1', 'optionalConsent2'], + requiredConsents: [['requiredConsent1', 'requiredConsent2'], ['requiredConsent3']], + optionalConsents: [['optionalConsent1'], ['optionalConsent2', 'optionalConsent3']], cookie: {}, }; @@ -41,11 +41,11 @@ const renderCookieConsent = ( ...cookie, ...unknownConsents, }; - const content = getContent([requiredConsents], [optionalConsents], contentModifier); + const content = getContent(requiredConsents, optionalConsents, contentModifier); jest.useFakeTimers(); mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( - undefined} content={content}> + , ); @@ -101,8 +101,12 @@ describe(' ', () => { settingsToggler: 'cookie-consent-settings-toggler', detailsComponent: 'cookie-consent-details', screenReaderNotification: 'cookie-consent-screen-reader-notification', - getOptionalConsentId: (key: string) => `optional-cookie-consent-${key}`, - getRequiredConsentId: (key: string) => `required-cookie-consent-${key}`, + getRequiredConsentGroupCheckboxId: (index: number) => `required-consents-group-${index}-checkbox`, + getOptionalConsentGroupCheckboxId: (index: number) => `optional-consents-group-${index}-checkbox`, + getRequiredConsentGroupDetailsTogglerId: (index: number) => `required-consents-group-${index}-details-toggler`, + getOptionalConsentGroupDetailsTogglerId: (index: number) => `optional-consents-group-${index}-details-toggler`, + getRequiredConsentsCheckboxId: () => `required-consents-checkbox`, + getOptionalConsentsCheckboxId: () => `optional-consents-checkbox`, }; const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { @@ -144,8 +148,10 @@ describe(' ', () => { cookie: { requiredConsent1: true, requiredConsent2: false, + requiredConsent3: true, optionalConsent1: true, optionalConsent2: true, + optionalConsent3: true, }, }); verifyElementExistsByTestId(result, dataTestIds.container); @@ -157,8 +163,10 @@ describe(' ', () => { cookie: { requiredConsent1: true, requiredConsent2: true, + requiredConsent3: true, optionalConsent1: false, optionalConsent2: true, + optionalConsent3: false, }, }); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); @@ -196,8 +204,10 @@ describe(' ', () => { const consentResult = { requiredConsent1: true, requiredConsent2: true, + requiredConsent3: true, optionalConsent1: true, optionalConsent2: true, + optionalConsent3: true, ...unknownConsents, }; clickElement(result, dataTestIds.approveButton); @@ -209,13 +219,15 @@ describe(' ', () => { const consentResult = { requiredConsent1: true, requiredConsent2: true, + requiredConsent3: true, optionalConsent1: false, optionalConsent2: false, + optionalConsent3: false, ...unknownConsents, }; await openAccordion(result); - clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent2')); - clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent1')); + clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(0)); + clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(1)); clickElement(result, dataTestIds.approveRequiredButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); @@ -225,12 +237,14 @@ describe(' ', () => { const consentResult = { requiredConsent1: true, requiredConsent2: true, + requiredConsent3: true, optionalConsent1: true, optionalConsent2: false, + optionalConsent3: false, ...unknownConsents, }; await openAccordion(result); - clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent1')); + clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(0)); clickElement(result, dataTestIds.approveButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); @@ -243,13 +257,17 @@ describe(' ', () => { return result; }; - it('required and optional consents are rendered', async () => { + it('required and optional consent groups are rendered', async () => { const result = await initDetailsView(defaultConsentData); - defaultConsentData.requiredConsents.forEach((consent) => { - verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentId(consent)); + verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentsCheckboxId()); + verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentsCheckboxId()); + defaultConsentData.requiredConsents.forEach((consent, index) => { + verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentGroupCheckboxId(index)); + verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentGroupDetailsTogglerId(index)); }); - defaultConsentData.optionalConsents.forEach((consent) => { - verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentId(consent)); + defaultConsentData.optionalConsents.forEach((consent, index) => { + verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentGroupCheckboxId(index)); + verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentGroupDetailsTogglerId(index)); }); }); @@ -265,19 +283,21 @@ describe(' ', () => { expect(approveButtonTextWhileOpen).not.toBe(approveButtonTextWhileClosed); }); - it(`clicking an optional consent sets the consent true/false. + it(`clicking an optional consent group sets the all consents in that group true/false. Cookie consent is not hidden until an approve -button is clicked`, async () => { const result = await initDetailsView(defaultConsentData); - defaultConsentData.optionalConsents.forEach((consent) => { - clickElement(result, dataTestIds.getOptionalConsentId(consent)); + defaultConsentData.optionalConsents.forEach((consent, index) => { + clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(index)); }); - clickElement(result, dataTestIds.getOptionalConsentId('optionalConsent2')); + clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(0)); clickElement(result, dataTestIds.approveButton); expect(JSON.parse(getSetCookieArguments().data)).toEqual({ requiredConsent1: true, requiredConsent2: true, - optionalConsent1: true, - optionalConsent2: false, + requiredConsent3: true, + optionalConsent1: false, + optionalConsent2: true, + optionalConsent3: true, ...unknownConsents, }); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index f1ececcdad..c61e7d79ff 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -8,12 +8,13 @@ import Content from './content/Content'; export function CookieConsent(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); + const { willRenderCookieConsentDialog } = cookieConsentContext; const content = useCookieConsentContent(); // use this in context - const [showScreenReaderSaveNotification] = useState(false); + const [cookieConsentDialogIsShown] = useState(willRenderCookieConsentDialog); const [popupTimerComplete, setPopupTimerComplete] = useState(false); const popupDelayInMs = 500; - + const showScreenReaderSaveNotification = cookieConsentDialogIsShown && !willRenderCookieConsentDialog; useEffect(() => { setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); }, []); @@ -28,7 +29,7 @@ export function CookieConsent(): React.ReactElement | null { ); } - if (!cookieConsentContext.willRenderCookieConsentDialog) { + if (!willRenderCookieConsentDialog) { return null; } diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index bc2d10b948..21e7d0d9ac 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -44,13 +44,10 @@ describe('CookieConsentContext ', () => { }; const ContextConsumer = ({ consumerId }: { consumerId: string }) => { - const { willRenderCookieConsentDialog, hasUserHandledAllConsents, approveAll, save } = useContext( - CookieConsentContext, - ); + const { willRenderCookieConsentDialog, hasUserHandledAllConsents, approveAll } = useContext(CookieConsentContext); const allUserConsentsAreHandled = hasUserHandledAllConsents(); const onButtonClick = () => { approveAll(); - save(); }; return (
@@ -106,22 +103,27 @@ describe('CookieConsentContext ', () => { result.getByTestId(testId).click(); }; - const renderCookieConsent = ({ cookieDomain, cookie = {} }: ConsentData): RenderResult => { + const renderCookieConsent = ({ + requiredConsents, + optionalConsents, + cookieDomain, + cookie = {}, + }: ConsentData): RenderResult => { // inject unknown consents to verify those are // stored and handled, but not required or optional const cookieWithInjectedUnknowns = { ...cookie, ...unknownConsents, }; + const content = getContent( + requiredConsents ? [requiredConsents] : undefined, + optionalConsents ? [optionalConsents] : undefined, + ); + content.onAllConsentsGiven = onAllConsentsGiven; + content.onConsentsParsed = onConsentsParsed; mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); return render( - undefined} - > + , @@ -129,7 +131,7 @@ describe('CookieConsentContext ', () => { }; describe('willRenderCookieConsentDialog ', () => { - it('is false all consents are true/false', () => { + it('is false if all required consents are not true or any optional consent is undefined.', () => { verifyConsumersShowConsentsNotHandled(renderCookieConsent(allNotApprovedConsentData)); }); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index d2e35455a7..3f90ab69e7 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -70,7 +70,6 @@ export type CookieConsentContextType = { update: ConsentController['update']; approveRequired: ConsentController['approveRequired']; approveAll: ConsentController['approveAll']; - save: ConsentController['save']; willRenderCookieConsentDialog: boolean; hasUserHandledAllConsents: () => boolean; content: Content; @@ -83,8 +82,6 @@ type CookieConsentContextProps = { cookieDomain?: string; children: React.ReactNode | React.ReactNode[] | null; content: Content; - onAllConsentsGiven?: (consents: ConsentObject) => void; - onConsentsParsed?: (consents: ConsentObject, hasUserHandledAllConsents: boolean) => void; }; export const CookieConsentContext = createContext({ @@ -93,7 +90,6 @@ export const CookieConsentContext = createContext({ update: () => undefined, approveRequired: () => undefined, approveAll: () => undefined, - save: () => ({}), hasUserHandledAllConsents: () => false, willRenderCookieConsentDialog: false, content: {} as Content, @@ -112,10 +108,13 @@ export const getConsentsFromConsentGroup = (groups: ConsentGroup[]): ConsentList }; export const Provider = ({ cookieDomain, children, content }: CookieConsentContextProps): React.ReactElement => { - const requiredConsents = getConsentsFromConsentGroup(content.requiredConsents.groupList); - const optionalConsents = getConsentsFromConsentGroup(content.optionalConsents.groupList); + const requiredConsents = content.requiredConsents + ? getConsentsFromConsentGroup(content.requiredConsents.groupList) + : undefined; + const optionalConsents = content.optionalConsents + ? getConsentsFromConsentGroup(content.optionalConsents.groupList) + : undefined; const consentController = useMemo(() => create({ requiredConsents, optionalConsents, cookieDomain }), []); - const hasUserHandledAllConsents = () => consentController.getRequiredWithoutConsent().length === 0 && consentController.getUnhandledConsents().length === 0; @@ -216,7 +215,6 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte approveAll, approveRequired, update, - save, willRenderCookieConsentDialog, hasUserHandledAllConsents, content, diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index 8ae2a41944..121bf557dd 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -94,7 +94,18 @@ exports[` spec renders the component 1`] = ` - På svenska (SV) + Svenska (SV) + + + + + English (EN)
@@ -102,9 +113,9 @@ exports[` spec renders the component 1`] = `

Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. - Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön - näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden - käytön. + Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön + näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden + käytön.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Host name + + Path + + Description + + Expiration +
+ Name of requiredConsent1 + + HostName of requiredConsent1 + + Path of requiredConsent1 + + Description of requiredConsent1 + + Expiration of requiredConsent1 +
+ Name of requiredConsent2 + + HostName of requiredConsent2 + + Path of requiredConsent2 + + Description of requiredConsent2 + + Expiration of requiredConsent2 +
+
+ + + + + + +
  • + +
  • + + +
    `group-item-${id}-${suffix}`; - + const getGroupIdenfier = (suffix: string) => `${id}-${suffix}`; + const checkboxId = getGroupIdenfier('checkbox'); return (
    @@ -81,7 +82,7 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: '--padding-vertical': '0', }} > - +
    diff --git a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx index 0c554a06f8..1cd4682908 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx @@ -28,10 +28,10 @@ function ConsentGroupDataMobile(props: { consents: ConsentGroup['consents'] }):
      {data.map((item) => { return ( -
    • +
    • {rowOrder.map((key) => { return ( -
      +
      {content.texts.tableHeadings[key]} {item[key]}
      diff --git a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx index be80353354..c3f6d36640 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx @@ -5,9 +5,9 @@ import { ConsentGroup, useCookieConsentContent } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; import classNames from '../../../utils/classNames'; -function ConsentGroupDataTable(props: { consents: ConsentGroup['consents'] }): React.ReactElement { +function ConsentGroupDataTable(props: { consents: ConsentGroup['consents']; id: string }): React.ReactElement { const content = useCookieConsentContent(); - const { consents } = props; + const { consents, id } = props; const cols = useMemo(() => { return Object.entries(content.texts.tableHeadings).map((entry) => { const [key, value] = entry; @@ -30,7 +30,16 @@ function ConsentGroupDataTable(props: { consents: ConsentGroup['consents'] }): R return (
      - +
      ); } diff --git a/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx b/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx index 27e5a55fd2..47d9ea69a0 100644 --- a/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx +++ b/packages/react/src/components/cookieConsent/consentGroups/ConsentGroups.tsx @@ -34,14 +34,15 @@ function ConsentGroups(props: { '--label-font-size': 'var(--fontsize-heading-m)', } as React.CSSProperties; - const getConsentGroupIdenfier = (suffix: string) => `consent-group-${groupId}-${suffix}`; - + const getConsentGroupIdenfier = (suffix: string) => `${groupId}-consents-${suffix}`; + const checkboxId = getConsentGroupIdenfier('checkbox'); return (
      {groupList.map((group, index) => (
    • - +
    • ))} diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index e182bbea8f..7160d9f7e9 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -33,7 +33,7 @@ const createConsentGroup = (id: string, consents: ConsentList): ConsentGroup => title: `Consent group title for ${id}`, text: `Consent group description for ${id}`, expandAriaLabel: `expandAriaLabel for ${id}`, - checkboxAriaLabel: `checkboxAriaLabel for ${id}`, + checkboxAriaDescription: `checkboxAriaLabel for ${id}`, consents: consents.map((consent) => { return { id: consent, @@ -87,6 +87,7 @@ export const getContent = ( language: { languageOptions: [ { code: 'fi', label: 'Suomeksi (FI)' }, + { code: 'sv', label: 'Svenska (SV)' }, { code: 'en', label: 'English (EN)' }, ], current: 'fi', @@ -99,7 +100,7 @@ export const getContent = ( groupId: 'required', title: 'Title for required consents', text: 'Text for required consents', - checkboxAriaLabel: 'checkboxAriaLabel', + checkboxAriaDescription: 'checkboxAriaLabel', groupList: requiredConsentGroups.map((consents, index) => createConsentGroup(`requiredConsentGroup${index}`, consents), ), @@ -110,7 +111,7 @@ export const getContent = ( groupId: 'optional', title: 'Title for optional consents', text: 'Text for optional consents', - checkboxAriaLabel: 'checkboxAriaLabel', + checkboxAriaDescription: 'checkboxAriaLabel', groupList: optionalConsentsGroups.map((consents, index) => createConsentGroup(`optionalConsentGroups${index}`, consents), ), From a9edd014876fcc45b2ec6de3dd7dd2a544bf9fe4 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 13 Apr 2022 12:04:15 +0300 Subject: [PATCH 059/292] cookie-consent Added data table testing --- .../cookieConsent/CookieConsent.test.tsx | 102 +++++++++++------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index 6aa295d0b3..e345746c74 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -18,6 +18,11 @@ type ConsentData = { contentModifier?: (content: Content) => Content; }; +type GroupParent = typeof requiredGroupParent | typeof optionalGroupParent; + +const requiredGroupParent = 'required'; +const optionalGroupParent = 'optional'; + const defaultConsentData = { requiredConsents: [['requiredConsent1', 'requiredConsent2'], ['requiredConsent3']], optionalConsents: [['optionalConsent1'], ['optionalConsent2', 'optionalConsent3']], @@ -31,6 +36,8 @@ const unknownConsents = { const mockedCookieControls = mockDocumentCookie(); +let content: Content; + const renderCookieConsent = ( { requiredConsents = [], optionalConsents = [], cookie = {}, contentModifier }: ConsentData, withRealTimers = false, @@ -41,7 +48,7 @@ const renderCookieConsent = ( ...cookie, ...unknownConsents, }; - const content = getContent(requiredConsents, optionalConsents, contentModifier); + content = getContent(requiredConsents, optionalConsents, contentModifier); jest.useFakeTimers(); mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( @@ -101,12 +108,11 @@ describe(' ', () => { settingsToggler: 'cookie-consent-settings-toggler', detailsComponent: 'cookie-consent-details', screenReaderNotification: 'cookie-consent-screen-reader-notification', - getRequiredConsentGroupCheckboxId: (index: number) => `required-consents-group-${index}-checkbox`, - getOptionalConsentGroupCheckboxId: (index: number) => `optional-consents-group-${index}-checkbox`, - getRequiredConsentGroupDetailsTogglerId: (index: number) => `required-consents-group-${index}-details-toggler`, - getOptionalConsentGroupDetailsTogglerId: (index: number) => `optional-consents-group-${index}-details-toggler`, - getRequiredConsentsCheckboxId: () => `required-consents-checkbox`, - getOptionalConsentsCheckboxId: () => `optional-consents-checkbox`, + getConsentGroupCheckboxId: (parent: GroupParent, index: number) => `${parent}-consents-group-${index}-checkbox`, + getConsentGroupDetailsTogglerId: (parent: GroupParent, index: number) => + `${parent}-consents-group-${index}-details-toggler`, + getConsentGroupTableId: (parent: GroupParent, index: number) => `${parent}-consents-group-${index}-table`, + getConsentsCheckboxId: (parent: GroupParent) => `${parent}-consents-checkbox`, }; const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { @@ -121,18 +127,24 @@ describe(' ', () => { result.getByTestId(testId).click(); }; - const isAccordionOpen = (result: RenderResult): boolean => { - const toggler = result.getByTestId(dataTestIds.settingsToggler) as HTMLElement; + const isAccordionOpen = (result: RenderResult, testId: string): boolean => { + const toggler = result.getByTestId(testId) as HTMLElement; return toggler.getAttribute('aria-expanded') === 'true'; }; - const openAccordion = async (result: RenderResult): Promise => { - clickElement(result, dataTestIds.settingsToggler); + const openAccordion = async (result: RenderResult, testId: string): Promise => { + clickElement(result, testId); await waitFor(() => { - expect(isAccordionOpen(result)).toBeTruthy(); + expect(isAccordionOpen(result, testId)).toBeTruthy(); }); }; + const initDetailsView = async (data: ConsentData): Promise => { + const result = renderCookieConsent(data); + await openAccordion(result, dataTestIds.settingsToggler); + return result; + }; + describe('Cookie consent ', () => { it('and child components are rendered when consents have not been handled', () => { const result = renderCookieConsent(defaultConsentData); @@ -176,10 +188,10 @@ describe(' ', () => { const onLanguageChange = jest.fn(); const result = renderCookieConsent({ ...defaultConsentData, - contentModifier: (content) => { + contentModifier: (currentContent) => { // eslint-disable-next-line no-param-reassign - content.language.onLanguageChange = onLanguageChange; - return content; + currentContent.language.onLanguageChange = onLanguageChange; + return currentContent; }, }); result.container.querySelector('#cookie-consent-language-selector-button').click(); @@ -225,9 +237,9 @@ describe(' ', () => { optionalConsent3: false, ...unknownConsents, }; - await openAccordion(result); - clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(0)); - clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(1)); + await openAccordion(result, dataTestIds.settingsToggler); + clickElement(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, 0)); + clickElement(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, 1)); clickElement(result, dataTestIds.approveRequiredButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); @@ -243,31 +255,25 @@ describe(' ', () => { optionalConsent3: false, ...unknownConsents, }; - await openAccordion(result); - clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(0)); + await openAccordion(result, dataTestIds.settingsToggler); + clickElement(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, 0)); clickElement(result, dataTestIds.approveButton); checkCookiesAreSetAndConsentModalHidden(result, consentResult); }); }); - describe('Accordion can be opened and in details view ', () => { - const initDetailsView = async (data: ConsentData): Promise => { - const result = renderCookieConsent(data); - await openAccordion(result); - return result; - }; - + describe('Settings accordion can be opened and in details view ', () => { it('required and optional consent groups are rendered', async () => { const result = await initDetailsView(defaultConsentData); - verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentsCheckboxId()); - verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentsCheckboxId()); + verifyElementExistsByTestId(result, dataTestIds.getConsentsCheckboxId(requiredGroupParent)); + verifyElementExistsByTestId(result, dataTestIds.getConsentsCheckboxId(optionalGroupParent)); defaultConsentData.requiredConsents.forEach((consent, index) => { - verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentGroupCheckboxId(index)); - verifyElementExistsByTestId(result, dataTestIds.getRequiredConsentGroupDetailsTogglerId(index)); + verifyElementExistsByTestId(result, dataTestIds.getConsentGroupCheckboxId(requiredGroupParent, index)); + verifyElementExistsByTestId(result, dataTestIds.getConsentGroupDetailsTogglerId(requiredGroupParent, index)); }); defaultConsentData.optionalConsents.forEach((consent, index) => { - verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentGroupCheckboxId(index)); - verifyElementExistsByTestId(result, dataTestIds.getOptionalConsentGroupDetailsTogglerId(index)); + verifyElementExistsByTestId(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, index)); + verifyElementExistsByTestId(result, dataTestIds.getConsentGroupDetailsTogglerId(optionalGroupParent, index)); }); }); @@ -277,7 +283,7 @@ describe(' ', () => { clickElement(result, dataTestIds.settingsToggler); await waitFor(() => { - expect(isAccordionOpen(result)).toBeFalsy(); + expect(isAccordionOpen(result, dataTestIds.settingsToggler)).toBeFalsy(); }); const approveButtonTextWhileClosed = (result.getByTestId(dataTestIds.approveButton) as HTMLElement).innerHTML; expect(approveButtonTextWhileOpen).not.toBe(approveButtonTextWhileClosed); @@ -287,9 +293,9 @@ describe(' ', () => { Cookie consent is not hidden until an approve -button is clicked`, async () => { const result = await initDetailsView(defaultConsentData); defaultConsentData.optionalConsents.forEach((consent, index) => { - clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(index)); + clickElement(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, index)); }); - clickElement(result, dataTestIds.getOptionalConsentGroupCheckboxId(0)); + clickElement(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, 0)); clickElement(result, dataTestIds.approveButton); expect(JSON.parse(getSetCookieArguments().data)).toEqual({ requiredConsent1: true, @@ -305,4 +311,28 @@ describe(' ', () => { expect(mockedCookieControls.mockSet).toHaveBeenCalledTimes(1); }); }); + describe('Accordions of each consent group can be opened and ', () => { + it('all consents in the group are rendered twice: in data table and in mobile view', async () => { + const result = await initDetailsView(defaultConsentData); + const checkConsentsExist = async (groupParent: GroupParent) => { + const list = + groupParent === 'required' ? content.requiredConsents.groupList : content.optionalConsents.groupList; + let index = 0; + // cannot use async/await with array.forEach + // eslint-disable-next-line no-restricted-syntax + for (const groups of list) { + expect(result.getByTestId(dataTestIds.getConsentGroupTableId(groupParent, index))).not.toBeVisible(); + // eslint-disable-next-line no-await-in-loop + await openAccordion(result, dataTestIds.getConsentGroupDetailsTogglerId(groupParent, index)); + expect(result.getByTestId(dataTestIds.getConsentGroupTableId(groupParent, index))).toBeVisible(); + index += 1; + groups.consents.forEach((consent) => { + expect(result.getAllByText(consent.name)).toHaveLength(2); + }); + } + }; + await checkConsentsExist('required'); + await checkConsentsExist('optional'); + }); + }); }); From 7f59567c6170c7295c8fb0d2c5947d4597ab5c41 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Wed, 13 Apr 2022 12:10:21 +0300 Subject: [PATCH 060/292] cookie-consent Fix consent group accordion button position --- .../src/components/cookieConsent/CookieConsent.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index c8ad8b9df1..4a92cc44e6 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -166,7 +166,7 @@ button { position: absolute; right: 0; - top: calc(var(--spacing-xl) - 0.5 * var(--spacing-s)); + top: calc(var(--spacing-xl) - 1 * var(--spacing-s)); padding: var(--spacing-s); } } From 58054408e207818f54346e58059de2d8edb39608 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 22 Apr 2022 13:20:10 +0300 Subject: [PATCH 061/292] cookie-consent Auto-generate groupId instead of requiring it Its usage is minimal and mainly used for elements ids for testing purposes. --- .../cookieConsent/CookieConsent.stories.tsx | 5 ++--- .../components/cookieConsent/CookieConsentContext.tsx | 1 - .../__snapshots__/CookieConsent.test.tsx.snap | 2 +- .../src/components/cookieConsent/buttons/Buttons.tsx | 8 ++++---- .../cookieConsent/consentGroups/ConsentGroups.tsx | 3 ++- .../src/components/cookieConsent/content/Content.tsx | 11 ++--------- .../react/src/components/cookieConsent/test.util.ts | 2 -- 7 files changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 45b30c84a1..7de3d59e16 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -44,7 +44,7 @@ export const Example = () => { settingsSaved: 'Asetukset tallennettu!', }, tableHeadings: { - name: 'Name', + name: 'Nimi', hostName: 'Osoite', path: 'Polku', description: 'Kuvaus', @@ -52,7 +52,6 @@ export const Example = () => { }, }, requiredConsents: { - groupId: 'required', title: 'Välttämättömät evästeet', text: 'Välttämättömien evästeiden käyttöä ei voi kieltää. Ne mahdollistavat sivuston toiminnan ja vaikuttavat sivuston käyttäjäystävällisyyteen.', @@ -112,7 +111,6 @@ export const Example = () => { ], }, optionalConsents: { - groupId: 'optional', title: 'Muut evästeet', text: 'Voit hyväksyä tai jättää hyväksymättä muut evästeet. Praesent vel vestibulum nunc, at eleifend sapien. Integer cursus ut orci eu pretium. Ut a orci felis. In eu eros turpis. Sed ullamcorper lacinia lorem, id ullamcorper dui accumsan in. Integer dictum fermentum mi, sit amet accumsan lacus facilisis id. Quisque blandit lacus ac sem porta.', @@ -184,6 +182,7 @@ export const Example = () => { language: { languageOptions: [ { code: 'fi', label: 'Suomeksi (FI)' }, + { code: 'sv', label: 'På svenska (SV)' }, { code: 'en', label: 'English (EN)' }, ], current: language, diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index 3f90ab69e7..b754208d48 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -41,7 +41,6 @@ export type SectionTexts = { }; export type RequiredOrOptionalConsentGroups = Description & { - groupId: 'required' | 'optional'; checkboxAriaDescription?: string; groupList: ConsentGroup[]; }; diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index 121bf557dd..1d74edd2f0 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -21,7 +21,7 @@ exports[` spec renders the component 1`] = ` aria-level="1" class="emulated-h1" role="heading" - tabindex="0" + tabindex="-1" > Evästesuostumukset diff --git a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx index a28b2e141d..98db47cd78 100644 --- a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -5,15 +5,15 @@ import styles from '../CookieConsent.module.scss'; import { useCookieConsentActions, useCookieConsentContent } from '../CookieConsentContext'; export type Props = { - hasOptionalConsents: boolean; + detailsAreShown: boolean; }; -function Buttons({ hasOptionalConsents }: Props): React.ReactElement { +function Buttons({ detailsAreShown }: Props): React.ReactElement { const content = useCookieConsentContent(); const onClick = useCookieConsentActions(); const { approveRequiredAndSelectedConsents, approveOnlyRequiredConsents, approveAllConsents } = content.texts.ui; - const primaryButtonText = hasOptionalConsents ? approveRequiredAndSelectedConsents : approveAllConsents; - const primaryButtonAction = hasOptionalConsents ? 'approveSelectedAndRequired' : 'approveAll'; + const primaryButtonText = detailsAreShown ? approveRequiredAndSelectedConsents : approveAllConsents; + const primaryButtonAction = detailsAreShown ? 'approveSelectedAndRequired' : 'approveAll'; return (
      @@ -130,16 +131,16 @@ describe('CookieConsentContext ', () => { ); }; - describe('willRenderCookieConsentDialog ', () => { - it('is false if all required consents are not true or any optional consent is undefined.', () => { + describe('hasUserHandledAllConsents ', () => { + it('returns false if all required consents are not true or any optional consent is undefined.', () => { verifyConsumersShowConsentsNotHandled(renderCookieConsent(allNotApprovedConsentData)); }); - it('is true if user has unhandled consents', () => { + it('returns true if user has unhandled consents', () => { verifyConsumersShowConsentsHandled(renderCookieConsent(allApprovedConsentData)); }); - it('reflects the value of hasUserHandledAllConsents and both change with approval', () => { + it('changes with approval', () => { const result = renderCookieConsent(allNotApprovedConsentData); verifyConsumersShowConsentsNotHandled(result); clickElement(result, consumer1ApproveAllButtonSelector); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index 6fb4030072..e33a4138c2 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -67,7 +67,6 @@ export type CookieConsentContextType = { update: ConsentController['update']; approveRequired: ConsentController['approveRequired']; approveAll: ConsentController['approveAll']; - willRenderCookieConsentDialog: boolean; hasUserHandledAllConsents: () => boolean; content: Content; onAction: CookieConsentActionListener; @@ -86,7 +85,6 @@ export const CookieConsentContext = createContext({ approveRequired: () => undefined, approveAll: () => undefined, hasUserHandledAllConsents: () => false, - willRenderCookieConsentDialog: false, content: {} as Content, onAction: () => undefined, countApprovedOptional: () => 0, @@ -113,10 +111,6 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte const hasUserHandledAllConsents = () => consentController.getRequiredWithoutConsent().length === 0 && consentController.getUnhandledConsents().length === 0; - const [willRenderCookieConsentDialog, setWillRenderCookieConsentDialog] = useState( - !hasUserHandledAllConsents(), - ); - const mergeConsents = () => ({ ...consentController.getRequired(), ...consentController.getOptional(), @@ -130,7 +124,7 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte const save = () => { const savedData = consentController.save(); if (hasUserHandledAllConsents()) { - setWillRenderCookieConsentDialog(false); + reRender(); if (content.onAllConsentsGiven) { content.onAllConsentsGiven(mergeConsents()); } @@ -206,7 +200,6 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte approveAll, approveRequired, update, - willRenderCookieConsentDialog, hasUserHandledAllConsents, content, onAction, From 3a73bb321cae041ad6922c65ce2a3e0a1eff5820 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 22 Apr 2022 13:54:41 +0300 Subject: [PATCH 064/292] cookie-consent Simplified content usage with helper functions Using better to use for example const item = useCookieContentHelper() than const content = useCookieContent const item = content.xyz.abc.q --- .../cookieConsent/CookieConsent.tsx | 6 +++--- .../cookieConsent/CookieConsentContext.tsx | 20 +++++++++++++++++++ .../cookieConsent/buttons/Buttons.tsx | 9 ++++++--- .../ConsentGroupDataMobile.tsx | 11 +++++----- .../ConsentGroupDataTable.tsx | 8 ++++---- .../cookieConsent/content/Content.tsx | 8 +++----- .../cookieConsent/details/Details.tsx | 4 ++-- .../languageSwitcher/LanguageSwitcher.tsx | 5 ++--- 8 files changed, 46 insertions(+), 25 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index fa0ce1276f..4c27449360 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -3,13 +3,13 @@ import { VisuallyHidden } from '@react-aria/visually-hidden'; import classNames from '../../utils/classNames'; import styles from './CookieConsent.module.scss'; -import { CookieConsentContext, useCookieConsentContent } from './CookieConsentContext'; +import { CookieConsentContext, useCookieConsentUiTexts } from './CookieConsentContext'; import Content from './content/Content'; export function CookieConsent(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); const showShowCookieConsents = !cookieConsentContext.hasUserHandledAllConsents(); - const content = useCookieConsentContent(); + const { settingsSaved } = useCookieConsentUiTexts(); // use this in context const [cookieConsentDialogIsShown] = useState(showShowCookieConsents); const [popupTimerComplete, setPopupTimerComplete] = useState(false); @@ -23,7 +23,7 @@ export function CookieConsent(): React.ReactElement | null { return (
      - {content.texts.ui.settingsSaved} + {settingsSaved}
      ); diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index e33a4138c2..132df2fdc8 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -225,3 +225,23 @@ export const useCookieConsentActions = (): CookieConsentContextType['onAction'] const cookieConsentContext = useContext(CookieConsentContext); return cookieConsentContext.onAction; }; + +export const useCookieConsentUiTexts = (): UiTexts => { + const content = useCookieConsentContent(); + return content.texts.ui; +}; + +export const useCookieConsentSectionTexts = (section: keyof SectionTexts): Description => { + const content = useCookieConsentContent(); + return content.texts.sections[section]; +}; + +export const useCookieConsentTableData = (): TableData => { + const content = useCookieConsentContent(); + return content.texts.tableHeadings; +}; + +export const useCookieConsentLanguage = (): Content['language'] => { + const content = useCookieConsentContent(); + return content.language; +}; diff --git a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx index 98db47cd78..73230a4c2e 100644 --- a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -2,16 +2,19 @@ import React from 'react'; import { Button } from '../../button/Button'; import styles from '../CookieConsent.module.scss'; -import { useCookieConsentActions, useCookieConsentContent } from '../CookieConsentContext'; +import { useCookieConsentActions, useCookieConsentUiTexts } from '../CookieConsentContext'; export type Props = { detailsAreShown: boolean; }; function Buttons({ detailsAreShown }: Props): React.ReactElement { - const content = useCookieConsentContent(); const onClick = useCookieConsentActions(); - const { approveRequiredAndSelectedConsents, approveOnlyRequiredConsents, approveAllConsents } = content.texts.ui; + const { + approveRequiredAndSelectedConsents, + approveOnlyRequiredConsents, + approveAllConsents, + } = useCookieConsentUiTexts(); const primaryButtonText = detailsAreShown ? approveRequiredAndSelectedConsents : approveAllConsents; const primaryButtonAction = detailsAreShown ? 'approveSelectedAndRequired' : 'approveAll'; return ( diff --git a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx index 1cd4682908..7dc9c22ce0 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx @@ -1,14 +1,15 @@ import React, { useMemo } from 'react'; -import { ConsentGroup, TableData, useCookieConsentContent } from '../CookieConsentContext'; +import { ConsentGroup, TableData, useCookieConsentLanguage, useCookieConsentTableData } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; import classNames from '../../../utils/classNames'; function ConsentGroupDataMobile(props: { consents: ConsentGroup['consents'] }): React.ReactElement { - const content = useCookieConsentContent(); + const { current } = useCookieConsentLanguage(); + const tableHeadings = useCookieConsentTableData(); const { consents } = props; const dataKeys = useMemo(() => { - return Object.keys(content.texts.tableHeadings); + return Object.keys(tableHeadings); }, []); const data: TableData[] = useMemo(() => { @@ -19,7 +20,7 @@ function ConsentGroupDataMobile(props: { consents: ConsentGroup['consents'] }): return currentData; }, {} as TableData); }); - }, [content.language.current]); + }, [current]); const rowOrder: (keyof TableData)[] = ['name', 'hostName', 'path', 'description', 'expiration']; @@ -32,7 +33,7 @@ function ConsentGroupDataMobile(props: { consents: ConsentGroup['consents'] }): {rowOrder.map((key) => { return (
      - {content.texts.tableHeadings[key]} + {tableHeadings[key]} {item[key]}
      ); diff --git a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx index c3f6d36640..753d41ca74 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx @@ -1,22 +1,22 @@ import React, { useMemo } from 'react'; import { Table } from '../../table/Table'; -import { ConsentGroup, useCookieConsentContent } from '../CookieConsentContext'; +import { ConsentGroup, useCookieConsentTableData } from '../CookieConsentContext'; import styles from '../CookieConsent.module.scss'; import classNames from '../../../utils/classNames'; function ConsentGroupDataTable(props: { consents: ConsentGroup['consents']; id: string }): React.ReactElement { - const content = useCookieConsentContent(); + const tableHeadings = useCookieConsentTableData(); const { consents, id } = props; const cols = useMemo(() => { - return Object.entries(content.texts.tableHeadings).map((entry) => { + return Object.entries(tableHeadings).map((entry) => { const [key, value] = entry; return { key, headerName: value, }; }); - }, [content.texts.tableHeadings]); + }, [tableHeadings]); const rows = useMemo(() => { return consents.map((consent) => { diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index 37d34968c5..b2b76bfac4 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -6,17 +6,15 @@ import { useAccordion } from '../../accordion'; import Details from '../details/Details'; import styles from '../CookieConsent.module.scss'; import { Card } from '../../card/Card'; -import { useCookieConsentContent } from '../CookieConsentContext'; +import { useCookieConsentSectionTexts, useCookieConsentUiTexts } from '../CookieConsentContext'; import LanguageSwitcher from '../languageSwitcher/LanguageSwitcher'; function Content(): React.ReactElement { const { isOpen, buttonProps, contentProps } = useAccordion({ initiallyOpen: false, }); - const content = useCookieConsentContent(); - const { sections, ui } = content.texts; - const { hideSettings, showSettings } = ui; - const { title, text } = sections.main; + const { hideSettings, showSettings } = useCookieConsentUiTexts(); + const { title, text } = useCookieConsentSectionTexts('main'); const titleRef = useRef(); const Icon = isOpen ? IconAngleUp : IconAngleDown; const settingsButtonText = isOpen ? hideSettings : showSettings; diff --git a/packages/react/src/components/cookieConsent/details/Details.tsx b/packages/react/src/components/cookieConsent/details/Details.tsx index ee9a0840f5..ad3c513afd 100644 --- a/packages/react/src/components/cookieConsent/details/Details.tsx +++ b/packages/react/src/components/cookieConsent/details/Details.tsx @@ -2,11 +2,11 @@ import React from 'react'; import styles from '../CookieConsent.module.scss'; import ConsentGroups from '../consentGroups/ConsentGroups'; -import { useCookieConsentContent } from '../CookieConsentContext'; +import { useCookieConsentContent, useCookieConsentSectionTexts } from '../CookieConsentContext'; function Details(): React.ReactElement { const content = useCookieConsentContent(); - const { title, text } = content.texts.sections.details; + const { title, text } = useCookieConsentSectionTexts('details'); const { requiredConsents, optionalConsents } = content; return (
      diff --git a/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx b/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx index 44ff5a3bfa..63bf366748 100644 --- a/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx +++ b/packages/react/src/components/cookieConsent/languageSwitcher/LanguageSwitcher.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { useCookieConsentContent } from '../CookieConsentContext'; +import { useCookieConsentLanguage } from '../CookieConsentContext'; import { Navigation } from '../../navigation/Navigation'; import styles from '../CookieConsent.module.scss'; function LanguageSwitcher(): React.ReactElement { - const content = useCookieConsentContent(); - const { current, languageOptions, languageSelectorAriaLabel, onLanguageChange } = content.language; + const { current, languageOptions, languageSelectorAriaLabel, onLanguageChange } = useCookieConsentLanguage(); const setLanguage = (code: string, e: React.MouseEvent) => { e.preventDefault(); onLanguageChange(code); From d310ee368fd5dfc153c333dd90d104ca3684ab7c Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 22 Apr 2022 15:34:45 +0300 Subject: [PATCH 065/292] cookie-consent Removed mobile cards Table view is also used in mobile and it is now scrollable horizontally --- .../cookieConsent/CookieConsent.module.scss | 26 +- .../cookieConsent/CookieConsent.test.tsx | 4 +- .../__snapshots__/CookieConsent.test.tsx.snap | 348 +----------------- .../consentGroup/ConsentGroup.tsx | 2 - .../ConsentGroupDataMobile.tsx | 49 --- .../ConsentGroupDataTable.tsx | 2 +- 6 files changed, 8 insertions(+), 423 deletions(-) delete mode 100644 packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 4a92cc44e6..ec42d961d9 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -190,6 +190,7 @@ .data-table-container table tbody tr > * { width: 15%; word-break: break-word; + min-width: 120px; } .data-table-container table tbody tr > *:nth-child(4) { width: 40%; @@ -251,28 +252,3 @@ width: auto; } } - -/* @extend does not work inside media queries */ -@mixin visuallyHidden { - clip-path: polygon(0 0, 0 0, 0 0, 0 0); - border: 0; - clip: 'rect(0 0 0 0)'; - height: 1px; - width: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - position: absolute; - display: block; -} - -@media (min-width: 768px) { - .visually-hidden-in-desktop { - @include visuallyHidden; - } -} -@media (max-width: 767px) { - .visually-hidden-in-mobile { - @include visuallyHidden; - } -} diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx index e345746c74..1c1909ba40 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.test.tsx @@ -312,7 +312,7 @@ describe(' ', () => { }); }); describe('Accordions of each consent group can be opened and ', () => { - it('all consents in the group are rendered twice: in data table and in mobile view', async () => { + it('all consents in the group are rendered', async () => { const result = await initDetailsView(defaultConsentData); const checkConsentsExist = async (groupParent: GroupParent) => { const list = @@ -327,7 +327,7 @@ describe(' ', () => { expect(result.getByTestId(dataTestIds.getConsentGroupTableId(groupParent, index))).toBeVisible(); index += 1; groups.consents.forEach((consent) => { - expect(result.getAllByText(consent.name)).toHaveLength(2); + expect(result.getAllByText(consent.name)).toHaveLength(1); }); } }; diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap index 1d74edd2f0..3da4537c19 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap @@ -291,7 +291,7 @@ exports[` spec renders the component 1`] = ` style="display: none;" >
      spec renders the component 1`] = `
      -
    @@ -588,7 +477,7 @@ exports[` spec renders the component 1`] = ` style="display: none;" >
    spec renders the component 1`] = `
    - @@ -848,7 +678,7 @@ exports[` spec renders the component 1`] = ` style="display: none;" >
    spec renders the component 1`] = `
    - @@ -1064,7 +835,7 @@ exports[` spec renders the component 1`] = ` style="display: none;" >
    spec renders the component 1`] = `
    - diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx index b37e72163e..da9528f3f6 100644 --- a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -12,7 +12,6 @@ import { useAccordion } from '../../accordion'; import { IconAngleDown, IconAngleUp } from '../../../icons'; import { Card } from '../../card/Card'; import ConsentGroupDataTable from '../consentGroupDataTable/ConsentGroupDataTable'; -import ConsentGroupDataMobile from '../consentGroupDataMobile/ConsentGroupDataMobile'; import classNames from '../../../utils/classNames'; function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: string }): React.ReactElement { @@ -83,7 +82,6 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: }} > - diff --git a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx b/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx deleted file mode 100644 index 7dc9c22ce0..0000000000 --- a/packages/react/src/components/cookieConsent/consentGroupDataMobile/ConsentGroupDataMobile.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useMemo } from 'react'; - -import { ConsentGroup, TableData, useCookieConsentLanguage, useCookieConsentTableData } from '../CookieConsentContext'; -import styles from '../CookieConsent.module.scss'; -import classNames from '../../../utils/classNames'; - -function ConsentGroupDataMobile(props: { consents: ConsentGroup['consents'] }): React.ReactElement { - const { current } = useCookieConsentLanguage(); - const tableHeadings = useCookieConsentTableData(); - const { consents } = props; - const dataKeys = useMemo(() => { - return Object.keys(tableHeadings); - }, []); - - const data: TableData[] = useMemo(() => { - return consents.map((consent) => { - return dataKeys.reduce((currentData, key) => { - // eslint-disable-next-line no-param-reassign - currentData[key] = consent[key]; - return currentData; - }, {} as TableData); - }); - }, [current]); - - const rowOrder: (keyof TableData)[] = ['name', 'hostName', 'path', 'description', 'expiration']; - - return ( -
    -
      - {data.map((item) => { - return ( -
    • - {rowOrder.map((key) => { - return ( -
      - {tableHeadings[key]} - {item[key]} -
      - ); - })} -
    • - ); - })} -
    -
    - ); -} - -export default ConsentGroupDataMobile; diff --git a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx index 753d41ca74..28dbe36e23 100644 --- a/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx +++ b/packages/react/src/components/cookieConsent/consentGroupDataTable/ConsentGroupDataTable.tsx @@ -29,7 +29,7 @@ function ConsentGroupDataTable(props: { consents: ConsentGroup['consents']; id: }; return ( -
    +
    Date: Mon, 25 Apr 2022 08:38:59 +0300 Subject: [PATCH 066/292] cookie-consent Fix typo and added comment --- .../src/components/cookieConsent/CookieConsent.tsx | 10 +++++----- .../cookieConsent/CookieConsentContext.test.tsx | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/CookieConsent.tsx index 4c27449360..df817569b4 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.tsx @@ -8,13 +8,13 @@ import Content from './content/Content'; export function CookieConsent(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); - const showShowCookieConsents = !cookieConsentContext.hasUserHandledAllConsents(); + const shouldShowCookieConsents = !cookieConsentContext.hasUserHandledAllConsents(); const { settingsSaved } = useCookieConsentUiTexts(); - // use this in context - const [cookieConsentDialogIsShown] = useState(showShowCookieConsents); + const [cookieConsentDialogIsShown] = useState(shouldShowCookieConsents); const [popupTimerComplete, setPopupTimerComplete] = useState(false); const popupDelayInMs = 500; - const showScreenReaderSaveNotification = cookieConsentDialogIsShown && !showShowCookieConsents; + // if hasUserHandledAllConsents() was false at first and then later true, user must have saved them. + const showScreenReaderSaveNotification = cookieConsentDialogIsShown && !shouldShowCookieConsents; useEffect(() => { setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); }, []); @@ -29,7 +29,7 @@ export function CookieConsent(): React.ReactElement | null { ); } - if (!showShowCookieConsents) { + if (!shouldShowCookieConsents) { return null; } diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx index c87ee1990b..a6295c4275 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.test.tsx @@ -46,7 +46,7 @@ describe('CookieConsentContext ', () => { const ContextConsumer = ({ consumerId }: { consumerId: string }) => { const { hasUserHandledAllConsents, approveAll } = useContext(CookieConsentContext); const allUserConsentsAreHandled = hasUserHandledAllConsents(); - const showShowCookieConsents = !allUserConsentsAreHandled; + const shouldShowCookieConsents = !allUserConsentsAreHandled; const onButtonClick = () => { approveAll(); }; @@ -54,8 +54,8 @@ describe('CookieConsentContext ', () => {
    {allUserConsentsAreHandled && } {!allUserConsentsAreHandled && } - {showShowCookieConsents && } - {!showShowCookieConsents && } + {shouldShowCookieConsents && } + {!shouldShowCookieConsents && } From ff6c53382c7323d1caec0f141806cd92b5c9f11c Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 25 Apr 2022 09:19:14 +0300 Subject: [PATCH 067/292] cookie-consent Added consent for cookieConsents to required consents User must give consent for saving consent data. --- .../cookieConsent/CookieConsent.stories.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 7de3d59e16..7103b4c1c4 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; -import { commonConsents } from './cookieConsentController'; -import { Content } from './CookieConsentContext'; +import { commonConsents, COOKIE_NAME } from './cookieConsentController'; +import { ConsentData, Content } from './CookieConsentContext'; import { CookieConsentModal } from './CookieConsentModal'; import { getConsentStatus, hasHandledAllConsents } from './util'; @@ -19,6 +19,15 @@ export const Example = () => { const onLanguageChange = (newLang) => setLanguage(newLang); const content: Content = useMemo((): Content => { + const consentForStoringCookieConsents: ConsentData = { + id: COOKIE_NAME, + name: 'Suostumusvalinnat', + hostName: 'Osoite', + path: 'Polku', + description: 'Tallennetaan Helsingin kaupungin yhteiset evästesuostumukset.', + expiration: '1 vuosi', + }; + return { texts: { sections: { @@ -80,6 +89,7 @@ export const Example = () => { description: 'Quisque vest molestie convallis. Don el dui vel.', expiration: 'Voimassaoloaika', }, + consentForStoringCookieConsents, ], }, { From e6f93cbfc81bfb46790368e30b827fe21a06caf1 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 25 Apr 2022 09:36:49 +0300 Subject: [PATCH 068/292] cookie-consent Logical file naming Renamed CookieConsent to ConsentsInModal and CookieConsentModal to just Modal Page showing consents will be added, so modal should be named accordingly. --- .../cookieConsent/CookieConsent.stories.tsx | 6 +++--- .../cookieConsent/CookieConsentModal.tsx | 13 ------------- .../consentsInModal/ConsentsInModal.tsx | 13 +++++++++++++ .../react/src/components/cookieConsent/index.ts | 2 +- .../Modal.test.tsx} | 14 +++++++------- .../{CookieConsent.tsx => modal/Modal.tsx} | 10 +++++----- 6 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 packages/react/src/components/cookieConsent/CookieConsentModal.tsx create mode 100644 packages/react/src/components/cookieConsent/consentsInModal/ConsentsInModal.tsx rename packages/react/src/components/cookieConsent/{CookieConsent.test.tsx => modal/Modal.test.tsx} (96%) rename packages/react/src/components/cookieConsent/{CookieConsent.tsx => modal/Modal.tsx} (86%) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 7103b4c1c4..0ae70bd9ef 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -2,11 +2,11 @@ import React, { useMemo, useState } from 'react'; import { commonConsents, COOKIE_NAME } from './cookieConsentController'; import { ConsentData, Content } from './CookieConsentContext'; -import { CookieConsentModal } from './CookieConsentModal'; +import { ConsentsInModal } from './consentsInModal/ConsentsInModal'; import { getConsentStatus, hasHandledAllConsents } from './util'; export default { - component: CookieConsentModal, + component: ConsentsInModal, title: 'Components/CookieConsent', parameters: { controls: { expanded: true }, @@ -263,7 +263,7 @@ export const Example = () => { return ( <> - + ); diff --git a/packages/react/src/components/cookieConsent/CookieConsentModal.tsx b/packages/react/src/components/cookieConsent/CookieConsentModal.tsx deleted file mode 100644 index 8d81c89297..0000000000 --- a/packages/react/src/components/cookieConsent/CookieConsentModal.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -import { CookieConsent } from './CookieConsent'; -import { Content, Provider as CookieContextProvider } from './CookieConsentContext'; - -export function CookieConsentModal(props: { content: Content; cookieDomain?: string }): React.ReactElement | null { - const { cookieDomain, content } = props; - return ( - - - - ); -} diff --git a/packages/react/src/components/cookieConsent/consentsInModal/ConsentsInModal.tsx b/packages/react/src/components/cookieConsent/consentsInModal/ConsentsInModal.tsx new file mode 100644 index 0000000000..12609e480f --- /dev/null +++ b/packages/react/src/components/cookieConsent/consentsInModal/ConsentsInModal.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { Modal } from '../modal/Modal'; +import { Content, Provider as CookieContextProvider } from '../CookieConsentContext'; + +export function ConsentsInModal(props: { content: Content; cookieDomain?: string }): React.ReactElement | null { + const { cookieDomain, content } = props; + return ( + + + + ); +} diff --git a/packages/react/src/components/cookieConsent/index.ts b/packages/react/src/components/cookieConsent/index.ts index dbcf18eb5f..d77510bf3e 100644 --- a/packages/react/src/components/cookieConsent/index.ts +++ b/packages/react/src/components/cookieConsent/index.ts @@ -1,2 +1,2 @@ -export * from './CookieConsent'; +export * from './modal/Modal'; export * from './types'; diff --git a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx b/packages/react/src/components/cookieConsent/modal/Modal.test.tsx similarity index 96% rename from packages/react/src/components/cookieConsent/CookieConsent.test.tsx rename to packages/react/src/components/cookieConsent/modal/Modal.test.tsx index 1c1909ba40..cfc5a6f3a8 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.test.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.test.tsx @@ -5,11 +5,11 @@ import { render, RenderResult, waitFor } from '@testing-library/react'; import { axe } from 'jest-axe'; import { act } from 'react-dom/test-utils'; -import { CookieConsent } from './CookieConsent'; -import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentController'; -import { Content, Provider as CookieContextProvider } from './CookieConsentContext'; -import mockDocumentCookie from './__mocks__/mockDocumentCookie'; -import { extractSetCookieArguments, getContent } from './test.util'; +import { Modal } from './Modal'; +import { ConsentList, ConsentObject, COOKIE_NAME } from '../cookieConsentController'; +import { Content, Provider as CookieContextProvider } from '../CookieConsentContext'; +import mockDocumentCookie from '../__mocks__/mockDocumentCookie'; +import { extractSetCookieArguments, getContent } from '../test.util'; type ConsentData = { requiredConsents?: ConsentList[]; @@ -53,7 +53,7 @@ const renderCookieConsent = ( mockedCookieControls.init({ [COOKIE_NAME]: JSON.stringify(cookieWithInjectedUnknowns) }); const result = render( - + , ); act(() => { @@ -68,7 +68,7 @@ const renderCookieConsent = ( return result; }; -describe(' spec', () => { +describe(' spec', () => { afterEach(() => { mockedCookieControls.clear(); }); diff --git a/packages/react/src/components/cookieConsent/CookieConsent.tsx b/packages/react/src/components/cookieConsent/modal/Modal.tsx similarity index 86% rename from packages/react/src/components/cookieConsent/CookieConsent.tsx rename to packages/react/src/components/cookieConsent/modal/Modal.tsx index df817569b4..534111a080 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.tsx @@ -1,12 +1,12 @@ import React, { useContext, useEffect, useState } from 'react'; import { VisuallyHidden } from '@react-aria/visually-hidden'; -import classNames from '../../utils/classNames'; -import styles from './CookieConsent.module.scss'; -import { CookieConsentContext, useCookieConsentUiTexts } from './CookieConsentContext'; -import Content from './content/Content'; +import classNames from '../../../utils/classNames'; +import styles from '../CookieConsent.module.scss'; +import { CookieConsentContext, useCookieConsentUiTexts } from '../CookieConsentContext'; +import Content from '../content/Content'; -export function CookieConsent(): React.ReactElement | null { +export function Modal(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); const shouldShowCookieConsents = !cookieConsentContext.hasUserHandledAllConsents(); const { settingsSaved } = useCookieConsentUiTexts(); From bcc9a3eb11304a1624c52b5aa5ee55fd0f1c59c2 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 25 Apr 2022 09:41:32 +0300 Subject: [PATCH 069/292] cookie-consent Updated snapshots affected by renaming --- .../__snapshots__/Modal.test.tsx.snap} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/react/src/components/cookieConsent/{__snapshots__/CookieConsent.test.tsx.snap => modal/__snapshots__/Modal.test.tsx.snap} (99%) diff --git a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap b/packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap similarity index 99% rename from packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap rename to packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap index 3da4537c19..1d9ce13a2d 100644 --- a/packages/react/src/components/cookieConsent/__snapshots__/CookieConsent.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` spec renders the component 1`] = ` +exports[` spec renders the component 1`] = `
    Date: Mon, 25 Apr 2022 10:03:01 +0300 Subject: [PATCH 070/292] cookie-consent Removed comments --- .../react/src/components/cookieConsent/CookieConsentContext.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx index 132df2fdc8..015d12b4d0 100644 --- a/packages/react/src/components/cookieConsent/CookieConsentContext.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsentContext.tsx @@ -128,7 +128,6 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte if (content.onAllConsentsGiven) { content.onAllConsentsGiven(mergeConsents()); } - // setShowScreenReaderSaveNotification(true); } return savedData; }; @@ -189,7 +188,6 @@ export const Provider = ({ cookieDomain, children, content }: CookieConsentConte }; const areGroupConsentsApproved: CookieConsentContextType['areGroupConsentsApproved'] = (consentData) => { - // consentData const optionalConsentList = consentController.getOptional(); return !consentData.reduce((hasUnApprovedConsent, consent) => { return hasUnApprovedConsent || optionalConsentList[consent.id] !== true; From c5519e5d82092b356e89430a6e9057b8793a2fcd Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 25 Apr 2022 13:36:24 +0300 Subject: [PATCH 071/292] cookie-consent Move Modal.tsx test objects and functions to test.util.ts Page component testing can then use same utils and functions --- .../cookieConsent/modal/Modal.test.tsx | 85 ++++--------------- .../src/components/cookieConsent/test.util.ts | 67 ++++++++++++++- 2 files changed, 84 insertions(+), 68 deletions(-) diff --git a/packages/react/src/components/cookieConsent/modal/Modal.test.tsx b/packages/react/src/components/cookieConsent/modal/Modal.test.tsx index cfc5a6f3a8..929fc06f64 100644 --- a/packages/react/src/components/cookieConsent/modal/Modal.test.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.test.tsx @@ -6,40 +6,30 @@ import { axe } from 'jest-axe'; import { act } from 'react-dom/test-utils'; import { Modal } from './Modal'; -import { ConsentList, ConsentObject, COOKIE_NAME } from '../cookieConsentController'; +import { COOKIE_NAME } from '../cookieConsentController'; import { Content, Provider as CookieContextProvider } from '../CookieConsentContext'; import mockDocumentCookie from '../__mocks__/mockDocumentCookie'; -import { extractSetCookieArguments, getContent } from '../test.util'; - -type ConsentData = { - requiredConsents?: ConsentList[]; - optionalConsents?: ConsentList[]; - cookie?: ConsentObject; - contentModifier?: (content: Content) => Content; -}; - -type GroupParent = typeof requiredGroupParent | typeof optionalGroupParent; - -const requiredGroupParent = 'required'; -const optionalGroupParent = 'optional'; - -const defaultConsentData = { - requiredConsents: [['requiredConsent1', 'requiredConsent2'], ['requiredConsent3']], - optionalConsents: [['optionalConsent1'], ['optionalConsent2', 'optionalConsent3']], - cookie: {}, -}; - -const unknownConsents = { - unknownConsent1: true, - unknownConsent2: false, -}; +import { + clickElement, + extractSetCookieArguments, + getContent, + isAccordionOpen, + openAccordion, + verifyElementDoesNotExistsByTestId, + verifyElementExistsByTestId, + commonTestProps, + TestConsentData, + TestGroupParent, +} from '../test.util'; + +const { requiredGroupParent, optionalGroupParent, defaultConsentData, unknownConsents, dataTestIds } = commonTestProps; const mockedCookieControls = mockDocumentCookie(); let content: Content; const renderCookieConsent = ( - { requiredConsents = [], optionalConsents = [], cookie = {}, contentModifier }: ConsentData, + { requiredConsents = [], optionalConsents = [], cookie = {}, contentModifier }: TestConsentData, withRealTimers = false, ): RenderResult => { // inject unknown consents to verify those are @@ -100,46 +90,7 @@ describe(' ', () => { const getSetCookieArguments = (index = -1) => extractSetCookieArguments(mockedCookieControls, index); - const dataTestIds = { - container: 'cookie-consent', - languageSwitcher: 'cookie-consent-language-switcher', - approveButton: 'cookie-consent-approve-button', - approveRequiredButton: 'cookie-consent-approve-required-button', - settingsToggler: 'cookie-consent-settings-toggler', - detailsComponent: 'cookie-consent-details', - screenReaderNotification: 'cookie-consent-screen-reader-notification', - getConsentGroupCheckboxId: (parent: GroupParent, index: number) => `${parent}-consents-group-${index}-checkbox`, - getConsentGroupDetailsTogglerId: (parent: GroupParent, index: number) => - `${parent}-consents-group-${index}-details-toggler`, - getConsentGroupTableId: (parent: GroupParent, index: number) => `${parent}-consents-group-${index}-table`, - getConsentsCheckboxId: (parent: GroupParent) => `${parent}-consents-checkbox`, - }; - - const verifyElementExistsByTestId = (result: RenderResult, testId: string) => { - expect(result.getAllByTestId(testId)).toHaveLength(1); - }; - - const verifyElementDoesNotExistsByTestId = (result: RenderResult, testId: string) => { - expect(() => result.getAllByTestId(testId)).toThrow(); - }; - - const clickElement = (result: RenderResult, testId: string) => { - result.getByTestId(testId).click(); - }; - - const isAccordionOpen = (result: RenderResult, testId: string): boolean => { - const toggler = result.getByTestId(testId) as HTMLElement; - return toggler.getAttribute('aria-expanded') === 'true'; - }; - - const openAccordion = async (result: RenderResult, testId: string): Promise => { - clickElement(result, testId); - await waitFor(() => { - expect(isAccordionOpen(result, testId)).toBeTruthy(); - }); - }; - - const initDetailsView = async (data: ConsentData): Promise => { + const initDetailsView = async (data: TestConsentData): Promise => { const result = renderCookieConsent(data); await openAccordion(result, dataTestIds.settingsToggler); return result; @@ -314,7 +265,7 @@ describe(' ', () => { describe('Accordions of each consent group can be opened and ', () => { it('all consents in the group are rendered', async () => { const result = await initDetailsView(defaultConsentData); - const checkConsentsExist = async (groupParent: GroupParent) => { + const checkConsentsExist = async (groupParent: TestGroupParent) => { const list = groupParent === 'required' ? content.requiredConsents.groupList : content.optionalConsents.groupList; let index = 0; diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index 031c29d665..f6c744ed5c 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -1,11 +1,24 @@ /* eslint-disable jest/no-mocks-import */ import cookie from 'cookie'; +import { RenderResult, waitFor } from '@testing-library/react'; import { ConsentGroup, Content } from './CookieConsentContext'; -import { ConsentList, COOKIE_NAME } from './cookieConsentController'; +import { ConsentList, ConsentObject, COOKIE_NAME } from './cookieConsentController'; import { CookieSetOptions } from './cookieController'; import { MockedDocumentCookieActions } from './__mocks__/mockDocumentCookie'; +export type TestConsentData = { + requiredConsents?: ConsentList[]; + optionalConsents?: ConsentList[]; + cookie?: ConsentObject; + contentModifier?: (content: Content) => Content; +}; + +export type TestGroupParent = typeof requiredGroupParent | typeof optionalGroupParent; + +const requiredGroupParent = 'required'; +const optionalGroupParent = 'optional'; + export function extractSetCookieArguments( mockedCookieControls: MockedDocumentCookieActions, index = -1, @@ -120,3 +133,55 @@ export const getContent = ( } return content; }; + +export function verifyElementExistsByTestId(result: RenderResult, testId: string) { + expect(result.getAllByTestId(testId)).toHaveLength(1); +} + +export function verifyElementDoesNotExistsByTestId(result: RenderResult, testId: string) { + expect(() => result.getAllByTestId(testId)).toThrow(); +} + +export function clickElement(result: RenderResult, testId: string) { + result.getByTestId(testId).click(); +} + +export function isAccordionOpen(result: RenderResult, testId: string): boolean { + const toggler = result.getByTestId(testId) as HTMLElement; + return toggler.getAttribute('aria-expanded') === 'true'; +} + +export async function openAccordion(result: RenderResult, testId: string): Promise { + clickElement(result, testId); + await waitFor(() => { + expect(isAccordionOpen(result, testId)).toBeTruthy(); + }); +} + +export const commonTestProps = { + dataTestIds: { + container: 'cookie-consent', + languageSwitcher: 'cookie-consent-language-switcher', + approveButton: 'cookie-consent-approve-button', + approveRequiredButton: 'cookie-consent-approve-required-button', + settingsToggler: 'cookie-consent-settings-toggler', + detailsComponent: 'cookie-consent-details', + screenReaderNotification: 'cookie-consent-screen-reader-notification', + getConsentGroupCheckboxId: (parent: TestGroupParent, index: number) => `${parent}-consents-group-${index}-checkbox`, + getConsentGroupDetailsTogglerId: (parent: TestGroupParent, index: number) => + `${parent}-consents-group-${index}-details-toggler`, + getConsentGroupTableId: (parent: TestGroupParent, index: number) => `${parent}-consents-group-${index}-table`, + getConsentsCheckboxId: (parent: TestGroupParent) => `${parent}-consents-checkbox`, + }, + requiredGroupParent: 'required' as TestGroupParent, + optionalGroupParent: 'optional' as TestGroupParent, + defaultConsentData: { + requiredConsents: [['requiredConsent1', 'requiredConsent2'], ['requiredConsent3']], + optionalConsents: [['optionalConsent1'], ['optionalConsent2', 'optionalConsent3']], + cookie: {}, + }, + unknownConsents: { + unknownConsent1: true, + unknownConsent2: false, + }, +}; From 3bf9d958f4d370b64ed0ca7f0e70bb979d30f09c Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 25 Apr 2022 14:13:16 +0300 Subject: [PATCH 072/292] cookie-consent New test util for creating cookie consents data. Reduces repetition when defining cookie data --- .../cookieConsent/modal/Modal.test.tsx | 47 +++---------------- .../src/components/cookieConsent/test.util.ts | 31 ++++++++++++ 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/react/src/components/cookieConsent/modal/Modal.test.tsx b/packages/react/src/components/cookieConsent/modal/Modal.test.tsx index 929fc06f64..0673b12c60 100644 --- a/packages/react/src/components/cookieConsent/modal/Modal.test.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.test.tsx @@ -20,6 +20,7 @@ import { commonTestProps, TestConsentData, TestGroupParent, + createCookieDataWithSelectedRejections, } from '../test.util'; const { requiredGroupParent, optionalGroupParent, defaultConsentData, unknownConsents, dataTestIds } = commonTestProps; @@ -108,14 +109,7 @@ describe(' ', () => { it('is rendered if a required consent has not been approved. It could have been optional before', () => { const result = renderCookieConsent({ ...defaultConsentData, - cookie: { - requiredConsent1: true, - requiredConsent2: false, - requiredConsent3: true, - optionalConsent1: true, - optionalConsent2: true, - optionalConsent3: true, - }, + cookie: createCookieDataWithSelectedRejections(['requiredConsent2']), }); verifyElementExistsByTestId(result, dataTestIds.container); }); @@ -123,14 +117,7 @@ describe(' ', () => { it('is not shown when all consents have been handled and are true/false', () => { const result = renderCookieConsent({ ...defaultConsentData, - cookie: { - requiredConsent1: true, - requiredConsent2: true, - requiredConsent3: true, - optionalConsent1: false, - optionalConsent2: true, - optionalConsent3: false, - }, + cookie: createCookieDataWithSelectedRejections(['optionalConsent1', 'optionalConsent3']), }); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); verifyElementDoesNotExistsByTestId(result, dataTestIds.screenReaderNotification); @@ -165,12 +152,7 @@ describe(' ', () => { it('Approve -button approves all consents when details are not shown', () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { - requiredConsent1: true, - requiredConsent2: true, - requiredConsent3: true, - optionalConsent1: true, - optionalConsent2: true, - optionalConsent3: true, + ...createCookieDataWithSelectedRejections([]), ...unknownConsents, }; clickElement(result, dataTestIds.approveButton); @@ -180,12 +162,7 @@ describe(' ', () => { it('Approve required -button approves only required consents and clears selected consents', async () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { - requiredConsent1: true, - requiredConsent2: true, - requiredConsent3: true, - optionalConsent1: false, - optionalConsent2: false, - optionalConsent3: false, + ...createCookieDataWithSelectedRejections(['optionalConsent1', 'optionalConsent2', 'optionalConsent3']), ...unknownConsents, }; await openAccordion(result, dataTestIds.settingsToggler); @@ -198,12 +175,7 @@ describe(' ', () => { it('Approve -button will approve required and selected consents when details are shown', async () => { const result = renderCookieConsent(defaultConsentData); const consentResult = { - requiredConsent1: true, - requiredConsent2: true, - requiredConsent3: true, - optionalConsent1: true, - optionalConsent2: false, - optionalConsent3: false, + ...createCookieDataWithSelectedRejections(['optionalConsent2', 'optionalConsent3']), ...unknownConsents, }; await openAccordion(result, dataTestIds.settingsToggler); @@ -249,12 +221,7 @@ describe(' ', () => { clickElement(result, dataTestIds.getConsentGroupCheckboxId(optionalGroupParent, 0)); clickElement(result, dataTestIds.approveButton); expect(JSON.parse(getSetCookieArguments().data)).toEqual({ - requiredConsent1: true, - requiredConsent2: true, - requiredConsent3: true, - optionalConsent1: false, - optionalConsent2: true, - optionalConsent3: true, + ...createCookieDataWithSelectedRejections(['optionalConsent1']), ...unknownConsents, }); verifyElementDoesNotExistsByTestId(result, dataTestIds.container); diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index f6c744ed5c..71545b18b7 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -167,6 +167,7 @@ export const commonTestProps = { settingsToggler: 'cookie-consent-settings-toggler', detailsComponent: 'cookie-consent-details', screenReaderNotification: 'cookie-consent-screen-reader-notification', + saveNotification: 'cookie-consent-save-notification', getConsentGroupCheckboxId: (parent: TestGroupParent, index: number) => `${parent}-consents-group-${index}-checkbox`, getConsentGroupDetailsTogglerId: (parent: TestGroupParent, index: number) => `${parent}-consents-group-${index}-details-toggler`, @@ -185,3 +186,33 @@ export const commonTestProps = { unknownConsent2: false, }, }; + +function createCookieData(consentList: ConsentList, source: TestConsentData, approved: boolean): ConsentObject { + const flattenArrayReducer = (acc: unknown[], val: unknown) => acc.concat(val); + const flatRequired = source.requiredConsents.reduce(flattenArrayReducer, []) as ConsentList; + const flatOptional = source.optionalConsents.reduce(flattenArrayReducer, []) as ConsentList; + const allConsents = [...flatRequired, ...flatOptional]; + const consents = allConsents.reduce((currentValue, currentConsent) => { + // eslint-disable-next-line no-param-reassign + currentValue[currentConsent] = !approved; + return currentValue; + }, {}); + consentList.forEach((consent) => { + consents[consent] = approved; + }); + return consents; +} + +export function createCookieDataWithSelectedApprovals( + approvedConsents: ConsentList, + source: TestConsentData = commonTestProps.defaultConsentData, +): ConsentObject { + return createCookieData(approvedConsents, source, true); +} + +export function createCookieDataWithSelectedRejections( + approvedConsents: ConsentList, + source: TestConsentData = commonTestProps.defaultConsentData, +): ConsentObject { + return createCookieData(approvedConsents, source, false); +} From 6f9a21258c681f262373d643c86e04f220882542 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 25 Apr 2022 14:14:22 +0300 Subject: [PATCH 073/292] cookie-consent New optional button callback. Renamed also onClick to triggerAction where actions are used. --- .../src/components/cookieConsent/buttons/Buttons.tsx | 11 +++++++---- .../cookieConsent/consentGroup/ConsentGroup.tsx | 4 ++-- .../cookieConsent/consentGroups/ConsentGroups.tsx | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx index 73230a4c2e..0667904a95 100644 --- a/packages/react/src/components/cookieConsent/buttons/Buttons.tsx +++ b/packages/react/src/components/cookieConsent/buttons/Buttons.tsx @@ -6,10 +6,11 @@ import { useCookieConsentActions, useCookieConsentUiTexts } from '../CookieConse export type Props = { detailsAreShown: boolean; + onClick?: () => void; }; -function Buttons({ detailsAreShown }: Props): React.ReactElement { - const onClick = useCookieConsentActions(); +function Buttons({ detailsAreShown, onClick = () => undefined }: Props): React.ReactElement { + const triggerAction = useCookieConsentActions(); const { approveRequiredAndSelectedConsents, approveOnlyRequiredConsents, @@ -22,7 +23,8 @@ function Buttons({ detailsAreShown }: Props): React.ReactElement { +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Host name + + Path + + Description + + Expiration +
    + Name of requiredConsent1 + + HostName of requiredConsent1 + + Path of requiredConsent1 + + Description of requiredConsent1 + + Expiration of requiredConsent1 +
    + Name of requiredConsent2 + + HostName of requiredConsent2 + + Path of requiredConsent2 + + Description of requiredConsent2 + + Expiration of requiredConsent2 +
    +
    +
    + + + + +
  • + +
  • + + + + +
    + + +
    + + + +`; From 898817c95b14b4f0b6d8e9c7096668acee4bd989 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 26 Apr 2022 09:42:00 +0300 Subject: [PATCH 075/292] cookie-consent Rename const for readability --- .../src/components/cookieConsent/modal/Modal.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/cookieConsent/modal/Modal.tsx b/packages/react/src/components/cookieConsent/modal/Modal.tsx index 534111a080..91c8ab83d2 100644 --- a/packages/react/src/components/cookieConsent/modal/Modal.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.tsx @@ -8,13 +8,13 @@ import Content from '../content/Content'; export function Modal(): React.ReactElement | null { const cookieConsentContext = useContext(CookieConsentContext); - const shouldShowCookieConsents = !cookieConsentContext.hasUserHandledAllConsents(); - const { settingsSaved } = useCookieConsentUiTexts(); - const [cookieConsentDialogIsShown] = useState(shouldShowCookieConsents); + const shouldShowModal = !cookieConsentContext.hasUserHandledAllConsents(); + const [isModalInitiallyShown] = useState(shouldShowModal); const [popupTimerComplete, setPopupTimerComplete] = useState(false); const popupDelayInMs = 500; - // if hasUserHandledAllConsents() was false at first and then later true, user must have saved them. - const showScreenReaderSaveNotification = cookieConsentDialogIsShown && !shouldShowCookieConsents; + // if hasUserHandledAllConsents() was false at first and then later true, user must have saved consents. + const showScreenReaderSaveNotification = isModalInitiallyShown && !shouldShowModal; + const { settingsSaved } = useCookieConsentUiTexts(); useEffect(() => { setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); }, []); @@ -29,7 +29,7 @@ export function Modal(): React.ReactElement | null { ); } - if (!shouldShowCookieConsents) { + if (!shouldShowModal) { return null; } From 08dd8c4c1cdfa84c30beedcc7b9db33468f8cb6b Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 26 Apr 2022 09:48:49 +0300 Subject: [PATCH 076/292] cookie-consent Adjust the modal height not to fill whole view. Added a long element to story, so the underlaying page has a scroll bar too. --- .../cookieConsent/CookieConsent.module.scss | 5 ++--- .../cookieConsent/CookieConsent.stories.tsx | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 925700160f..f3601472c2 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -13,7 +13,8 @@ z-index: 2; width: 100%; overflow-y: scroll; - max-height: 100vh; + max-height: 80vh; + border-top: 8px solid var(--color-bus); } .container .aligner { @@ -47,13 +48,11 @@ background: #ffffff; width: 100%; box-sizing: border-box; - border-top: 8px solid var(--color-bus); } .page .content { padding-top: 0; padding-bottom: 0; - border-top: 0; } .language-switcher { diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 5b25a57767..0c324d9e33 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -247,23 +247,29 @@ export const ModalVersion = () => { ); }; + const ForcePageScrollBarForModalTesting = () => { + return ( +
    +
     
    +

    Bottom page

    +
    + ); + }; + const Application = () => { const willRenderCookieConsentDialog = !hasHandledAllConsents( content.requiredConsents || [], content.optionalConsents || [], ); return ( -
    +
    {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}

    This is a dummy application

    {willRenderCookieConsentDialog ? ( <> -

    Cookie consent dialog will be shown.

    +

    Cookie consent modal will be shown.

    ) : ( <> @@ -271,6 +277,7 @@ export const ModalVersion = () => { )} +
    ); }; From b370bf8ac7a6bfd531d1cdc725682f8f10698b55 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 26 Apr 2022 10:08:54 +0300 Subject: [PATCH 077/292] cookie-consent Keep horizontal line when accordion is open Reported in accessibility audit that the line should be kept. --- .../src/components/cookieConsent/CookieConsent.module.scss | 6 ++---- .../components/cookieConsent/consentGroup/ConsentGroup.tsx | 7 +------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index f3601472c2..7068952413 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -158,9 +158,6 @@ position: relative; padding: var(--spacing-xl) 0; flex-direction: column; -} - -.consent-group-closed { border-bottom: 1px solid var(--color-black); } @@ -236,7 +233,8 @@ } } @media (min-width: 768px) { - .container { + .container, + .page { --common-spacing: var(--spacing-l); } diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx index fe8d4d0f37..7945dd2312 100644 --- a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -12,7 +12,6 @@ import { useAccordion } from '../../accordion'; import { IconAngleDown, IconAngleUp } from '../../../icons'; import { Card } from '../../card/Card'; import ConsentGroupDataTable from '../consentGroupDataTable/ConsentGroupDataTable'; -import classNames from '../../../utils/classNames'; function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: string }): React.ReactElement { const { group, isRequired, id } = props; @@ -41,14 +40,10 @@ function ConsentGroup(props: { group: ConsentGroupType; isRequired: boolean; id: '--label-font-size': 'var(--fontsize-heading-s)', } as React.CSSProperties; - const currentStyles = isOpen - ? styles['consent-group'] - : classNames(styles['consent-group'], styles['consent-group-closed']); - const getGroupIdenfier = (suffix: string) => `${id}-${suffix}`; const checkboxId = getGroupIdenfier('checkbox'); return ( -
    +
    Date: Tue, 26 Apr 2022 10:12:36 +0300 Subject: [PATCH 078/292] cookie-consent Added soft hyphen to the main header It won't fit mobile view narrower than 400px --- .../src/components/cookieConsent/CookieConsent.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx index 0c324d9e33..abafa62f34 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx +++ b/packages/react/src/components/cookieConsent/CookieConsent.stories.tsx @@ -35,7 +35,7 @@ const createContent = (options: ContentOptions): Content => { texts: { sections: { main: { - title: 'Evästesuostumukset', + title: 'Eväste\u00ADsuostumukset', text: `Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden From be6bfa9cf865e8aa3b7971ea2165149c60e156e0 Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 26 Apr 2022 10:35:43 +0300 Subject: [PATCH 079/292] cookie-consent Adjust CSS --- .../cookieConsent/CookieConsent.module.scss | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 7068952413..4ad7f72146 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -34,7 +34,6 @@ display: flex; flex-direction: column; width: 100%; - padding-top: var(--spacing-l); } .buttons > * { @@ -58,7 +57,7 @@ .language-switcher { position: absolute; top: var(--common-spacing); - left: var(--common-spacing); + left: calc(var(--common-spacing) - var(--spacing-3-xs)); } .language-selector-override { @@ -146,7 +145,7 @@ .consent-group-parent { display: flex; - padding: 0 0 var(--spacing-2-xl); + padding-bottom: var(--spacing-m); flex-direction: column; > p { margin-bottom: 0; @@ -156,14 +155,14 @@ .consent-group { display: flex; position: relative; - padding: var(--spacing-xl) 0; + padding: var(--spacing-l) 0; flex-direction: column; border-bottom: 1px solid var(--color-black); } .consent-group-content { display: flex; - padding: 0 var(--spacing-m); + padding: 0; flex-direction: column; p { padding: var(--spacing-l) 0 0; @@ -172,14 +171,14 @@ button { position: absolute; right: 0; - top: calc(var(--spacing-xl) - 1 * var(--spacing-s)); + top: var(--spacing-m); padding: var(--spacing-s); } } .title-with-checkbox { display: flex; - margin-right: calc(var(--spacing-s) * 3); + margin-right: var(--spacing-2-xl); } .page .content .consent-group-parent .title-with-checkbox label, @@ -253,6 +252,18 @@ margin-right: 200px; } + .consent-group-parent { + padding-bottom: var(--spacing-2-xl); + } + + .consent-group { + padding: var(--spacing-xl) 0; + } + + .consent-group-content { + padding: 0 var(--spacing-m); + } + .buttons { flex-direction: row; } From 6bf5a4112257217c8cbbeb3abc52138ac10fd16a Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Tue, 26 Apr 2022 11:29:21 +0300 Subject: [PATCH 080/292] cookie-consent Updated snapshots --- .../cookieConsent/modal/__snapshots__/Modal.test.tsx.snap | 8 ++++---- .../cookieConsent/page/__snapshots__/Page.test.tsx.snap | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap b/packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap index 1d9ce13a2d..755736424b 100644 --- a/packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/modal/__snapshots__/Modal.test.tsx.snap @@ -214,7 +214,7 @@ exports[` spec renders the component 1`] = ` >
  • @@ -115,18 +115,18 @@ exports[` spec renders the component 1`] = `
    @@ -302,18 +302,18 @@ exports[` spec renders the component 1`] = `
    @@ -504,18 +504,18 @@ exports[` spec renders the component 1`] = ` @@ -662,18 +662,18 @@ exports[` spec renders the component 1`] = ` - - -
  • -
    +
  • +
  • - - + + +
    - -
    -
    - checkboxAriaLabel for requiredCookieGroup1 -
    - -
    - -
  • - - -
    + + +
    - - -
    -
    - - -
      -
    • + + +
      + + + +
        +
      • - - + + +
        - -
        -
        - checkboxAriaLabel for optionalCookieGroups0 -
        - -
        - -
      • -
      • -
        +
      • +
      • - - + + +
        - -
        -
        - checkboxAriaLabel for optionalCookieGroups1 -
        - -
        - -
      • -
      +
    • +
    + - -
    - - + + + Hyväksy vain pakolliset evästeet + + +
    + `; diff --git a/packages/react/src/components/cookieConsent/modal/Modal.tsx b/packages/react/src/components/cookieConsent/modal/Modal.tsx index 2696b2a43f..5025c53f72 100644 --- a/packages/react/src/components/cookieConsent/modal/Modal.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.tsx @@ -3,7 +3,7 @@ import { VisuallyHidden } from '@react-aria/visually-hidden'; import classNames from '../../../utils/classNames'; import styles from '../CookieConsent.module.scss'; -import { CookieConsentContext, useCookieConsentUiTexts } from '../CookieConsentContext'; +import { CookieConsentContext, useCookieConsentUiTexts, useFocusShift } from '../CookieConsentContext'; import { Content } from '../content/Content'; export function Modal(): React.ReactElement | null { @@ -17,9 +17,24 @@ export function Modal(): React.ReactElement | null { // if hasUserHandledAllConsents() was false at first and then later true, user must have saved consents. const showScreenReaderSaveNotification = isModalInitiallyShown && !shouldShowModal; const { settingsSaved } = useCookieConsentUiTexts(); + const shiftFocus = useFocusShift(); + useEffect(() => { setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); }, []); + useEffect(() => { + // Set focus outside when esc is pressed. + const handleEscKey = (event: KeyboardEvent) => { + const key = event.key || event.keyCode; + if (key === 'Escape' || key === 'Esc' || key === 27) { + shiftFocus(); + } + }; + document.addEventListener('keyup', handleEscKey); + return () => { + document.removeEventListener('keyup', handleEscKey); + }; + }); if (showScreenReaderSaveNotification) { return ( diff --git a/packages/react/src/components/cookieConsent/test.util.ts b/packages/react/src/components/cookieConsent/test.util.ts index b3481c4317..ee2406d282 100644 --- a/packages/react/src/components/cookieConsent/test.util.ts +++ b/packages/react/src/components/cookieConsent/test.util.ts @@ -89,6 +89,7 @@ export const getContentSource = ( siteName: 'Test site', noCommonConsentCookie: true, currentLanguage: 'fi', + focusTargetSelector: '#focus-target', ...contentOverrides, ...contentSourceOverrides, }; @@ -149,8 +150,8 @@ export const commonTestProps = { function createConsentObject(consentList: ConsentList, source: TestConsentData, approved: boolean): ConsentObject { const flattenArrayReducer = (acc: unknown[], val: unknown) => acc.concat(val); - const flatRequired = source.requiredConsents.reduce(flattenArrayReducer, []) as ConsentList; - const flatOptional = source.optionalConsents.reduce(flattenArrayReducer, []) as ConsentList; + const flatRequired = source.requiredConsents?.reduce(flattenArrayReducer, []) as ConsentList; + const flatOptional = source.optionalConsents?.reduce(flattenArrayReducer, []) as ConsentList; const allConsents = [...flatRequired, ...flatOptional]; const consents = allConsents.reduce((currentValue, currentConsent) => { // eslint-disable-next-line no-param-reassign @@ -183,7 +184,9 @@ export async function openAllAccordions( dataTestIds: typeof commonTestProps['dataTestIds'], ): Promise { const openAccordions = async (groupParent: TestGroupParent) => { - const list = groupParent === 'required' ? content.requiredCookies.groups : content.optionalCookies.groups; + const list = (groupParent === 'required' + ? content.requiredCookies?.groups + : content.optionalCookies?.groups) as CookieGroup[]; let index = 0; /* eslint-disable no-restricted-syntax */ // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -198,3 +201,16 @@ export async function openAllAccordions( await openAccordions('required'); await openAccordions('optional'); } + +type ElementGetterResult = HTMLElement | Element | null; + +export const getActiveElement = (anyElement?: ElementGetterResult): Element | null => + anyElement ? anyElement.ownerDocument.activeElement : null; + +export const waitForElementFocus = async (elementGetter: () => ElementGetterResult): Promise => + waitFor(() => { + const target = elementGetter(); + if (target) { + expect(getActiveElement(target)).toEqual(target); + } + }); From f63ade2a24c1ec0cf0ed643a46499680723d91cf Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Fri, 17 Jun 2022 15:55:25 +0300 Subject: [PATCH 156/292] cookie-consent Safari showed focus outline for content element --- .../react/src/components/cookieConsent/CookieConsent.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/components/cookieConsent/CookieConsent.module.scss b/packages/react/src/components/cookieConsent/CookieConsent.module.scss index 115ab747fe..acf02edc8f 100644 --- a/packages/react/src/components/cookieConsent/CookieConsent.module.scss +++ b/packages/react/src/components/cookieConsent/CookieConsent.module.scss @@ -252,6 +252,7 @@ --common-spacing: var(--spacing-xs); padding-top: var(--common-spacing); padding-bottom: var(--common-spacing); + outline: none; .visuallyHiddenWithoutFocus { position: absolute; opacity: 0; From 82f4d3495991a4d2248d2ad820ce6249148d9c7d Mon Sep 17 00:00:00 2001 From: NikoHelle Date: Mon, 20 Jun 2022 10:03:20 +0300 Subject: [PATCH 157/292] cookie-consent Fix click issue with read more button. In chrome the focus is set on mouse down, and on focus the button is hidden. And if focused element is hidden, focus moves to body. The onClick won't trigger. Changed to onMousedown. --- packages/react/src/components/cookieConsent/content/Content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/cookieConsent/content/Content.tsx b/packages/react/src/components/cookieConsent/content/Content.tsx index cfd4270dfc..ea9b17bbd8 100644 --- a/packages/react/src/components/cookieConsent/content/Content.tsx +++ b/packages/react/src/components/cookieConsent/content/Content.tsx @@ -58,7 +58,7 @@ export function Content(): React.ReactElement {
    spec renders the component 1`] = ` Tietoa sivustolla käytetyistä evästeistä

    - Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta kategoriasta ja sallia tai kieltää evästeiden käytön. + Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea eri luokista ja sallia tai kieltää evästeiden käytön.

    spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` Test site käyttää evästeitä

    - Tämä sivusto käyttää välttämättömiä evästeitä suorituskyvyn varmistamiseksi sekä yleisen käytön seurantaan. Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja kohdistetun sisällön näyttämiseen. Jatkamalla sivuston käyttöä ilman asetusten muuttamista hyväksyt välttämättömien evästeiden käytön. + Tämä sivusto käyttää pakollisia evästeitä sivun perustoimintojen ja suorituskyvyn varmistamiseksi. Lisäksi käytämme kohdennusevästeitä käyttäjäkokemuksen parantamiseksi, analytiikkaan ja yksilöidyn sisällön näyttämiseen.

    spec renders the component 1`] = ` Tietoa sivustolla käytetyistä evästeistä

    - Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea tietoa jokaisesta kategoriasta ja sallia tai kieltää evästeiden käytön. + Sivustolla käytetyt evästeet on luokiteltu käyttötarkoituksen mukaan. Alla voit lukea eri luokista ja sallia tai kieltää evästeiden käytön.

    spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Osoite + Evästeen asettaja - Kuvaus + Käyttötarkoitus spec renders the component 1`] = ` - Hyväksy valitut ja pakolliset evästeet + Hyväksy valitut evästeet
    - +
    +
    + +
    +
    + `; diff --git a/packages/react/src/components/cookieConsent/modal/Modal.tsx b/packages/react/src/components/cookieConsent/modal/Modal.tsx index c3bd9355ef..cea25372eb 100644 --- a/packages/react/src/components/cookieConsent/modal/Modal.tsx +++ b/packages/react/src/components/cookieConsent/modal/Modal.tsx @@ -1,4 +1,5 @@ import React, { useContext, useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; import { VisuallyHidden } from '@react-aria/visually-hidden'; import classNames from '../../../utils/classNames'; @@ -18,11 +19,25 @@ export function Modal(): React.ReactElement | null { // if hasUserHandledAllConsents() was false at first and then later true, user must have saved consents. const showScreenReaderSaveNotification = isModalInitiallyShown && !shouldShowModal; const { settingsSaved } = useCookieConsentUiTexts(); + const containerId = 'cookieConsentContainer'; + const getContainerElement = (): HTMLElement | null => document.getElementById(containerId); + const [isDomReady, setIsDomReady] = useState(false); useEffect(() => { setTimeout(() => setPopupTimerComplete(true), popupDelayInMs); }, []); + useEffect(() => { + if (shouldShowModal && !isDomReady) { + if (!getContainerElement()) { + const htmlContainer = document.createElement('div'); + htmlContainer.id = containerId; + document.body.insertBefore(htmlContainer, document.body.firstChild); + } + setIsDomReady(true); + } + }, [shouldShowModal, isDomReady, setIsDomReady]); + useEscKey(useFocusShift()); if (showScreenReaderSaveNotification) { @@ -35,11 +50,11 @@ export function Modal(): React.ReactElement | null { ); } - if (!shouldShowModal) { + if (!shouldShowModal || !isDomReady) { return null; } - return ( + const renderModal = (): JSX.Element => (
    {popupTimerComplete && }
    ); + + return ReactDOM.createPortal(renderModal(), getContainerElement()); } From 1eea8623a69d99066542b7334a6dea942232c92d Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Wed, 6 Jul 2022 17:51:18 +0300 Subject: [PATCH 170/292] Rename HTML cookie consent container id --- .../cookieModal/__snapshots__/CookieModal.test.tsx.snap | 2 +- packages/react/src/components/cookieConsent/modal/Modal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap b/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap index 574267db5b..22c0ac1baf 100644 --- a/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap @@ -3,7 +3,7 @@ exports[` spec renders the component 1`] = `

    {text}

    - + {checkboxAriaDescription || text}
      From 0fcf9bfaed862780a218a2893039a7b894f3789e Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Fri, 8 Jul 2022 10:47:53 +0300 Subject: [PATCH 206/292] Refactor --- .../components/cookieConsent/consentGroup/ConsentGroup.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx index c0734ee1c0..25918f7e08 100644 --- a/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx +++ b/packages/react/src/components/cookieConsent/consentGroup/ConsentGroup.tsx @@ -25,6 +25,7 @@ export function ConsentGroup(props: { group: CookieGroup; isRequired: boolean; i } as React.CSSProperties; const getGroupIdentifier = (suffix: string) => `${id}-${suffix}`; const checkboxId = getGroupIdentifier('checkbox'); + const descriptionElementId = getGroupIdentifier('description'); const checkboxProps = { onChange: isRequired ? () => undefined @@ -40,7 +41,7 @@ export function ConsentGroup(props: { group: CookieGroup; isRequired: boolean; i 'data-testid': checkboxId, name: checkboxId, label: title, - 'aria-describedby': getGroupIdentifier('description'), + 'aria-describedby': descriptionElementId, style: checkboxStyle, }; @@ -51,7 +52,7 @@ export function ConsentGroup(props: { group: CookieGroup; isRequired: boolean; i

    {text}

    - {checkboxAriaDescription || text} + {checkboxAriaDescription || text}
    From 1c3e820ceb7b6a68c7b2805ae43116f6852d8d41 Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Fri, 8 Jul 2022 14:44:50 +0300 Subject: [PATCH 210/292] Fix formatting issue --- .../react/src/components/cookieConsent/category/Category.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react/src/components/cookieConsent/category/Category.tsx b/packages/react/src/components/cookieConsent/category/Category.tsx index 13cb7c6d47..7c58fb6abb 100644 --- a/packages/react/src/components/cookieConsent/category/Category.tsx +++ b/packages/react/src/components/cookieConsent/category/Category.tsx @@ -43,9 +43,7 @@ export function Category(props: { category?: CategoryType; isRequired?: boolean

    {text}

    - - {checkboxAriaDescription || text} - + {checkboxAriaDescription || text}
      {groups.map((group, index) => (
    • From 68f7dbe23432c17b78d591115e4d24b89e67ec7a Mon Sep 17 00:00:00 2001 From: Mika Nevalainen Date: Fri, 8 Jul 2022 15:32:14 +0300 Subject: [PATCH 211/292] Fix test snapshots --- .../cookieModal/__snapshots__/CookieModal.test.tsx.snap | 2 -- .../cookiePage/__snapshots__/CookiePage.test.tsx.snap | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap b/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap index 22c0ac1baf..8437f6c35e 100644 --- a/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap +++ b/packages/react/src/components/cookieConsent/cookieModal/__snapshots__/CookieModal.test.tsx.snap @@ -233,7 +233,6 @@ exports[` spec renders the component 1`] = ` Text for required cookies

    • -
    • -
      +
    • +
    • - - + + +
      - -
      -
      - checkboxAriaLabel for requiredCookieGroup1 -
      - -
      - -
    • -
    - -
    + + +
    - - -
    -
    - -
    - checkboxAriaLabel -
    -
      -
    • + + +
      + + +
      + checkboxAriaLabel +
      +
        +
      • - - + + +
        - -
        -
        - checkboxAriaLabel for optionalCookieGroups0 -
        - -
        - -
      • -
      • -
        +
      • +
      • - - + + +
        - -
        -
        - checkboxAriaLabel for optionalCookieGroups1 -
        - -
        - -
      • -
      +
    • +
    + - -
    - - + + + Hyväksy vain pakolliset evästeet + + +
    - - -
    -
    +