Skip to content

Commit db11fa4

Browse files
committed
Contextual consent
@todo * add translations * handle focus with care
1 parent 2142add commit db11fa4

35 files changed

Lines changed: 551 additions & 79 deletions

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ purpose:
298298
</template>
299299
```
300300

301+
> [!NOTE] There is more you can do with templates! Learn about
302+
> [contextual consent](#contextual-consent).
303+
301304
<details>
302305
<summary>Integration tips</summary>
303306

@@ -389,6 +392,55 @@ Romanian, Spanish, Swedish.
389392
> [!NOTE] Each and every translated text is overridable via
390393
> [the configuration](#configuration).
391394
395+
### Contextual consent
396+
397+
Content embedded from other websites might be restricted by user consent (i.e. a
398+
YouTube video).
399+
400+
In that case, using templates would work just like with scripts:
401+
402+
```js
403+
<template data-purpose="youtube">
404+
<iframe src="https://www.youtube.com/embed/toto"></iframe>
405+
</template>
406+
```
407+
408+
However, this won't show anything until the user consents to the related
409+
purpose.
410+
411+
To be a little more user friendly, adding the `data-contextual` attribute will
412+
display a fallback notice until consent is given, detailing the reason and
413+
offering a way to consent in place.
414+
415+
```diff
416+
- <template data-purpose="youtube">
417+
+ <template data-purpose="youtube" data-contextual>
418+
<iframe src="https://www.youtube.com/embed/toto"></iframe>
419+
</template>
420+
```
421+
422+
<details>
423+
<summary>Integration tips</summary>
424+
425+
#### WordPress
426+
427+
Should you use Orejime in a WordPress website, you could alter the rendering of
428+
embeds so they use contextual consent:
429+
430+
```php
431+
function orejimeWrapEmbeds($content, $block) {
432+
if ($block['blockName'] === 'core/embed') {
433+
return '<template data-purpose="embeds" data-contextual>' . $content . '</template>';
434+
}
435+
436+
return $content;
437+
}
438+
439+
add_filter('render_block', 'orejimeWrapEmbeds', 10, 2);
440+
```
441+
442+
</details>
443+
392444
## API
393445

394446
Functions and references are made available on the global scope:

e2e/OrejimePage.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class OrejimePage {
88
public readonly context: BrowserContext
99
) {}
1010

11-
async load(config: Partial<Config>, scripts: string) {
11+
async load(config: Partial<Config>, body: string) {
1212
await this.page.route('/', async (route) => {
1313
await route.fulfill({
1414
body: `
@@ -21,11 +21,12 @@ export class OrejimePage {
2121
</head>
2222
2323
<body>
24+
${body}
25+
2426
<script>
2527
window.orejimeConfig = ${JSON.stringify(config)}
2628
</script>
2729
<script src="orejime-standard-en.js"></script>
28-
${scripts}
2930
</body>
3031
</html>
3132
`
@@ -102,6 +103,10 @@ export class OrejimePage {
102103
await this.page.keyboard.press('Escape');
103104
}
104105

106+
async acceptContextualNotice() {
107+
await this.page.locator('.orejime-ContextualNotice-button').click();
108+
}
109+
105110
async expectElement(selector: string) {
106111
await expect(this.page.locator(selector)).toBeAttached();
107112
}

e2e/orejime.spec.ts

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,49 @@
11
import {test, expect} from '@playwright/test';
2-
import {Config} from '../src/ui/types';
32
import {OrejimePage} from './OrejimePage';
43

54
test.describe('Orejime', () => {
6-
const BaseConfig: Partial<Config> = {
7-
privacyPolicyUrl: 'https://example.org/privacy',
8-
purposes: [
9-
{
10-
id: 'mandatory',
11-
title: 'Mandatory',
12-
cookies: ['mandatory'],
13-
isMandatory: true
14-
},
5+
let orejimePage: OrejimePage;
6+
7+
test.beforeEach(async ({page, context}) => {
8+
orejimePage = new OrejimePage(page, context);
9+
await orejimePage.load(
1510
{
16-
id: 'group',
17-
title: 'Group',
11+
privacyPolicyUrl: 'https://example.org/privacy',
1812
purposes: [
1913
{
20-
id: 'child-1',
21-
title: 'First child',
22-
cookies: ['child-1']
14+
id: 'mandatory',
15+
title: 'Mandatory',
16+
cookies: ['mandatory'],
17+
isMandatory: true
2318
},
2419
{
25-
id: 'child-2',
26-
title: 'Second child',
27-
cookies: ['child-2']
20+
id: 'group',
21+
title: 'Group',
22+
purposes: [
23+
{
24+
id: 'contextual',
25+
title: 'Contextual',
26+
cookies: ['contextual']
27+
},
28+
{
29+
id: 'other',
30+
title: 'Other',
31+
cookies: ['other']
32+
}
33+
]
2834
}
2935
]
30-
}
31-
]
32-
};
33-
34-
const BaseScripts = `
35-
<template data-purpose="mandatory">
36-
<script id="mandatory"></script>
37-
</template>
38-
39-
<template data-purpose="child-1">
40-
<iframe id="child-1"></iframe>
41-
</template>
42-
`;
43-
44-
let orejimePage: OrejimePage;
45-
46-
test.beforeEach(async ({page, context}) => {
47-
orejimePage = new OrejimePage(page, context);
48-
await orejimePage.load(BaseConfig, BaseScripts);
36+
},
37+
`
38+
<template data-purpose="contextual" data-contextual>
39+
<iframe id="contextual" src=""></iframe>
40+
</template>
41+
42+
<template data-purpose="mandatory">
43+
<script id="mandatory"></script>
44+
</template>
45+
`
46+
);
4947
});
5048

5149
test('should show a banner', async () => {
@@ -62,12 +60,12 @@ test.describe('Orejime', () => {
6260

6361
orejimePage.expectConsents({
6462
'mandatory': true,
65-
'child-1': true,
66-
'child-2': true
63+
'contextual': true,
64+
'other': true
6765
});
6866

6967
orejimePage.expectElement('#mandatory');
70-
orejimePage.expectElement('#child-1');
68+
orejimePage.expectElement('#contextual');
7169
});
7270

7371
test('should decline all purposes from the banner', async () => {
@@ -76,12 +74,12 @@ test.describe('Orejime', () => {
7674

7775
orejimePage.expectConsents({
7876
'mandatory': true,
79-
'child-1': false,
80-
'child-2': false
77+
'contextual': false,
78+
'other': false
8179
});
8280

8381
orejimePage.expectElement('#mandatory');
84-
orejimePage.expectMissingElement('#child-1');
82+
orejimePage.expectMissingElement('#contextual');
8583
});
8684

8785
test('should open a modal', async () => {
@@ -129,39 +127,39 @@ test.describe('Orejime', () => {
129127
test('should accept all purposes from the modal', async () => {
130128
await orejimePage.openModalFromBanner();
131129
await orejimePage.enableAllFromModal();
132-
await expect(orejimePage.purposeCheckbox('child-1')).toBeChecked();
130+
await expect(orejimePage.purposeCheckbox('contextual')).toBeChecked();
133131
await expect(orejimePage.purposeCheckbox('mandatory')).toBeChecked();
134132
await orejimePage.saveFromModal();
135133

136134
orejimePage.expectConsents({
137135
'mandatory': true,
138-
'child-1': true,
139-
'child-2': true
136+
'contextual': true,
137+
'other': true
140138
});
141139
});
142140

143141
test('should decline all purposes from the modal', async () => {
144142
await orejimePage.openModalFromBanner();
145143
await orejimePage.enableAllFromModal();
146144
await orejimePage.disableAllFromModal();
147-
await expect(orejimePage.purposeCheckbox('child-1')).not.toBeChecked();
145+
await expect(orejimePage.purposeCheckbox('contextual')).not.toBeChecked();
148146
await expect(orejimePage.purposeCheckbox('mandatory')).toBeChecked();
149147
await orejimePage.saveFromModal();
150148

151149
orejimePage.expectConsents({
152150
'mandatory': true,
153-
'child-1': false,
154-
'child-2': false
151+
'contextual': false,
152+
'other': false
155153
});
156154
});
157155

158156
test('should sync grouped purposes', async () => {
159157
await orejimePage.openModalFromBanner();
160158

161-
const checkbox = orejimePage.purposeCheckbox('child-1');
159+
const checkbox = orejimePage.purposeCheckbox('contextual');
162160
await expect(checkbox).not.toBeChecked();
163161

164-
const checkbox2 = orejimePage.purposeCheckbox('child-2');
162+
const checkbox2 = orejimePage.purposeCheckbox('other');
165163
await expect(checkbox2).not.toBeChecked();
166164

167165
const groupCheckbox = orejimePage.purposeCheckbox('group');
@@ -181,4 +179,20 @@ test.describe('Orejime', () => {
181179
await expect(checkbox).not.toBeChecked();
182180
await expect(checkbox2).not.toBeChecked();
183181
});
182+
183+
test('should show a contextual consent notice', async () => {
184+
await orejimePage.expectElement('.orejime-ContextualNotice');
185+
});
186+
187+
test('should accept contextual consent from the notice', async () => {
188+
await orejimePage.acceptContextualNotice();
189+
await orejimePage.expectElement('#contextual');
190+
await orejimePage.expectMissingElement('.orejime-ContextualNotice');
191+
});
192+
193+
test('should accept contextual consent from the banner', async () => {
194+
await orejimePage.acceptAllFromBanner();
195+
await orejimePage.expectElement('#contextual');
196+
await orejimePage.expectMissingElement('.orejime-ContextualNotice');
197+
});
184198
});

rspack.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ module.exports = {
105105
lang: 'fr'
106106
}),
107107
featureTemplatePlugin({title: 'Styling', feature: 'styling'}),
108+
featureTemplatePlugin({
109+
title: 'Contextual consent',
110+
feature: 'contextual',
111+
template: 'contextual'
112+
}),
108113
featureTemplatePlugin({
109114
title: "Intégration au système de design de l'état",
110115
feature: 'dsfr',

site/assets/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ iframe {
4343
color: var(--color-black--100);
4444
}
4545

46+
.ExamplePage iframe {
47+
aspect-ratio: 16 / 9;
48+
min-height: 0;
49+
}
50+
4651
.ExamplePage .orejime-Env {
4752
font-size: 0.875rem;
4853
}

site/features/contextual.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!doctype html>
2+
3+
<html class="ExamplePage" lang="en">
4+
<head>
5+
<title>Orejime</title>
6+
7+
<link
8+
rel="stylesheet"
9+
href="https://boscop.fr/wp-content/themes/boscop/dist/app.css"
10+
/>
11+
12+
<link rel="stylesheet" href="../assets/style.css" />
13+
<link rel="stylesheet" href="../orejime-standard.css" />
14+
</head>
15+
16+
<body>
17+
<main class="ExampleMain" role="main">
18+
<template data-purpose="youtube" data-contextual>
19+
<figure role="group">
20+
<iframe
21+
title="YouTube video player"
22+
src="https://www.youtube-nocookie.com/embed/pghz5vpi5q4?si=npJInLIEM7XiD0MB"
23+
allowfullscreen
24+
></iframe>
25+
26+
<figcaption class="fr-content-media__caption">
27+
Cooking cookies with Philippe Etchebest
28+
</figcaption>
29+
</figure>
30+
</template>
31+
32+
<button class="ExampleReset Button">Reset consent</button>
33+
</main>
34+
35+
<script>
36+
window.orejimeConfig = {
37+
purposes: [
38+
{
39+
id: 'youtube',
40+
title: 'YouTube videos'
41+
},
42+
{
43+
id: 'other',
44+
title: 'Another purpose'
45+
}
46+
],
47+
privacyPolicyUrl: '#'
48+
};
49+
</script>
50+
51+
<script src="../orejime-standard-en.js"></script>
52+
53+
<script>
54+
document
55+
.querySelector('.ExampleReset')
56+
.addEventListener('click', function () {
57+
window.orejime.manager.clearConsents();
58+
});
59+
</script>
60+
</body>
61+
</html>

site/features/dsfr.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ <h2>Politique de confidentialité</h2>
118118
</li>
119119
</ul>
120120

121+
<h2>Media</h2>
122+
123+
<template data-purpose="youtube" data-contextual>
124+
<figure role="group" class="fr-content-media">
125+
<iframe
126+
class="fr-responsive-vid"
127+
title="YouTube video player"
128+
src="https://www.youtube-nocookie.com/embed/pghz5vpi5q4?si=npJInLIEM7XiD0MB"
129+
allowfullscreen
130+
></iframe>
131+
132+
<figcaption class="fr-content-media__caption">
133+
Les cookies de Philippe Etchebest
134+
</figcaption>
135+
</figure>
136+
</template>
137+
121138
<h2>Configuration</h2>
122139

123140
<%= js.highlightedCode %>

0 commit comments

Comments
 (0)