Skip to content

Commit 557f7ff

Browse files
committed
Contextual consent
@todo * add translations * handle focus with care
1 parent 4802c9a commit 557f7ff

23 files changed

Lines changed: 486 additions & 85 deletions

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ You can wrap many elements at once or use several templates with the same purpos
283283
</template>
284284
```
285285

286+
> [!NOTE]
287+
> There is more you can do with templates! Learn about [contextual consent](#contextual-consent).
288+
286289
<details>
287290
<summary>Integration tips</summary>
288291

@@ -361,6 +364,49 @@ Catalan, Dutch, English, Estonian, Finnish, French, German, Hungarian, Italian,
361364
> [!NOTE]
362365
> Each and every translated text is overridable via [the configuration](#configuration).
363366
367+
### Contextual consent
368+
369+
Content embedded from other websites might be restricted by user consent (i.e. a YouTube video).
370+
371+
In that case, using templates would work just like with scripts:
372+
373+
```js
374+
<template data-purpose="youtube">
375+
<iframe src="https://www.youtube.com/embed/toto"></iframe>
376+
</template>
377+
```
378+
379+
However, this won't show anything until the user consents to the related purpose.
380+
381+
To be a little more user friendly, adding the `data-contextual` attribute will display a fallback notice until consent is given, detailing the reason and offering a way to consent in place.
382+
383+
```diff
384+
- <template data-purpose="youtube">
385+
+ <template data-purpose="youtube" data-contextual>
386+
<iframe src="https://www.youtube.com/embed/toto"></iframe>
387+
</template>
388+
```
389+
390+
<details>
391+
<summary>Integration tips</summary>
392+
393+
#### WordPress
394+
395+
Should you use Orejime in a WordPress website, you could alter the rendering of embeds so they use contextual consent:
396+
397+
```php
398+
function orejimeWrapEmbeds($content, $block) {
399+
if ($block['blockName'] === 'core/embed') {
400+
return '<template data-purpose="embeds" data-contextual>' . $content . '</template>';
401+
}
402+
403+
return $content;
404+
}
405+
406+
add_filter('render_block', 'orejimeWrapEmbeds', 10, 2);
407+
```
408+
</details>
409+
364410
## API
365411

366412
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
// @see https://stackoverflow.com/a/73214414
106111
async expectElement(selector: string) {
107112
expect(await this.page.locator(selector)).toHaveCount(1);

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';
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ module.exports = {
116116
featureTemplatePlugin('Grouping', 'grouping'),
117117
featureTemplatePlugin('Internationalization', 'i18n'),
118118
featureTemplatePlugin('Styling', 'styling'),
119+
featureTemplatePlugin('Contextual consent', 'contextual', 'contextual'),
119120
featureTemplatePlugin(
120121
"Intégration au système de design de l'état",
121122
'dsfr',

site/assets/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ iframe {
6767
color: var(--color-black--100);
6868
}
6969

70+
.ExamplePage iframe {
71+
aspect-ratio: 16 / 9;
72+
min-height: 0;
73+
}
74+
7075
.ExamplePage .orejime-Env {
7176
font-size: 0.875rem;
7277
}

site/features/contextual.html

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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">
33+
Reset consent
34+
</button>
35+
</main>
36+
37+
<script>
38+
window.orejimeConfig = {
39+
purposes: [
40+
{
41+
id: 'youtube',
42+
title: 'YouTube videos'
43+
},
44+
{
45+
id: 'other',
46+
title: 'Another purpose'
47+
}
48+
],
49+
privacyPolicyUrl: '#'
50+
};
51+
</script>
52+
53+
<script src="../orejime-standard-en.js"></script>
54+
55+
<script>
56+
document
57+
.querySelector('.ExampleReset')
58+
.addEventListener('click', function () {
59+
window.orejime.manager.clearConsents();
60+
});
61+
</script>
62+
</body>
63+
</html>

site/features/dsfr.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,23 @@ <h2>Politique de confidentialité</h2>
113113
</li>
114114
</ul>
115115

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

118135
<%= js.highlightedCode %>

0 commit comments

Comments
 (0)