Skip to content

Commit

Permalink
Contextual consent
Browse files Browse the repository at this point in the history
@todo
* add translations
* handle focus with care
  • Loading branch information
felixgirault committed Feb 17, 2025
1 parent 2142add commit db11fa4
Show file tree
Hide file tree
Showing 35 changed files with 551 additions and 79 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ purpose:
</template>
```

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

Expand Down Expand Up @@ -389,6 +392,55 @@ Romanian, Spanish, Swedish.
> [!NOTE] Each and every translated text is overridable via
> [the configuration](#configuration).
### Contextual consent

Content embedded from other websites might be restricted by user consent (i.e. a
YouTube video).

In that case, using templates would work just like with scripts:

```js
<template data-purpose="youtube">
<iframe src="https://www.youtube.com/embed/toto"></iframe>
</template>
```

However, this won't show anything until the user consents to the related
purpose.

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.

```diff
- <template data-purpose="youtube">
+ <template data-purpose="youtube" data-contextual>
<iframe src="https://www.youtube.com/embed/toto"></iframe>
</template>
```

<details>
<summary>Integration tips</summary>

#### WordPress

Should you use Orejime in a WordPress website, you could alter the rendering of
embeds so they use contextual consent:

```php
function orejimeWrapEmbeds($content, $block) {
if ($block['blockName'] === 'core/embed') {
return '<template data-purpose="embeds" data-contextual>' . $content . '</template>';
}

return $content;
}

add_filter('render_block', 'orejimeWrapEmbeds', 10, 2);
```

</details>

## API

Functions and references are made available on the global scope:
Expand Down
9 changes: 7 additions & 2 deletions e2e/OrejimePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class OrejimePage {
public readonly context: BrowserContext
) {}

async load(config: Partial<Config>, scripts: string) {
async load(config: Partial<Config>, body: string) {
await this.page.route('/', async (route) => {
await route.fulfill({
body: `
Expand All @@ -21,11 +21,12 @@ export class OrejimePage {
</head>
<body>
${body}
<script>
window.orejimeConfig = ${JSON.stringify(config)}
</script>
<script src="orejime-standard-en.js"></script>
${scripts}
</body>
</html>
`
Expand Down Expand Up @@ -102,6 +103,10 @@ export class OrejimePage {
await this.page.keyboard.press('Escape');
}

async acceptContextualNotice() {
await this.page.locator('.orejime-ContextualNotice-button').click();
}

async expectElement(selector: string) {
await expect(this.page.locator(selector)).toBeAttached();
}
Expand Down
116 changes: 65 additions & 51 deletions e2e/orejime.spec.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,49 @@
import {test, expect} from '@playwright/test';
import {Config} from '../src/ui/types';
import {OrejimePage} from './OrejimePage';

test.describe('Orejime', () => {
const BaseConfig: Partial<Config> = {
privacyPolicyUrl: 'https://example.org/privacy',
purposes: [
{
id: 'mandatory',
title: 'Mandatory',
cookies: ['mandatory'],
isMandatory: true
},
let orejimePage: OrejimePage;

test.beforeEach(async ({page, context}) => {
orejimePage = new OrejimePage(page, context);
await orejimePage.load(
{
id: 'group',
title: 'Group',
privacyPolicyUrl: 'https://example.org/privacy',
purposes: [
{
id: 'child-1',
title: 'First child',
cookies: ['child-1']
id: 'mandatory',
title: 'Mandatory',
cookies: ['mandatory'],
isMandatory: true
},
{
id: 'child-2',
title: 'Second child',
cookies: ['child-2']
id: 'group',
title: 'Group',
purposes: [
{
id: 'contextual',
title: 'Contextual',
cookies: ['contextual']
},
{
id: 'other',
title: 'Other',
cookies: ['other']
}
]
}
]
}
]
};

const BaseScripts = `
<template data-purpose="mandatory">
<script id="mandatory"></script>
</template>
<template data-purpose="child-1">
<iframe id="child-1"></iframe>
</template>
`;

let orejimePage: OrejimePage;

test.beforeEach(async ({page, context}) => {
orejimePage = new OrejimePage(page, context);
await orejimePage.load(BaseConfig, BaseScripts);
},
`
<template data-purpose="contextual" data-contextual>
<iframe id="contextual" src=""></iframe>
</template>
<template data-purpose="mandatory">
<script id="mandatory"></script>
</template>
`
);
});

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

orejimePage.expectConsents({
'mandatory': true,
'child-1': true,
'child-2': true
'contextual': true,
'other': true
});

orejimePage.expectElement('#mandatory');
orejimePage.expectElement('#child-1');
orejimePage.expectElement('#contextual');
});

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

orejimePage.expectConsents({
'mandatory': true,
'child-1': false,
'child-2': false
'contextual': false,
'other': false
});

orejimePage.expectElement('#mandatory');
orejimePage.expectMissingElement('#child-1');
orejimePage.expectMissingElement('#contextual');
});

test('should open a modal', async () => {
Expand Down Expand Up @@ -129,39 +127,39 @@ test.describe('Orejime', () => {
test('should accept all purposes from the modal', async () => {
await orejimePage.openModalFromBanner();
await orejimePage.enableAllFromModal();
await expect(orejimePage.purposeCheckbox('child-1')).toBeChecked();
await expect(orejimePage.purposeCheckbox('contextual')).toBeChecked();
await expect(orejimePage.purposeCheckbox('mandatory')).toBeChecked();
await orejimePage.saveFromModal();

orejimePage.expectConsents({
'mandatory': true,
'child-1': true,
'child-2': true
'contextual': true,
'other': true
});
});

test('should decline all purposes from the modal', async () => {
await orejimePage.openModalFromBanner();
await orejimePage.enableAllFromModal();
await orejimePage.disableAllFromModal();
await expect(orejimePage.purposeCheckbox('child-1')).not.toBeChecked();
await expect(orejimePage.purposeCheckbox('contextual')).not.toBeChecked();
await expect(orejimePage.purposeCheckbox('mandatory')).toBeChecked();
await orejimePage.saveFromModal();

orejimePage.expectConsents({
'mandatory': true,
'child-1': false,
'child-2': false
'contextual': false,
'other': false
});
});

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

const checkbox = orejimePage.purposeCheckbox('child-1');
const checkbox = orejimePage.purposeCheckbox('contextual');
await expect(checkbox).not.toBeChecked();

const checkbox2 = orejimePage.purposeCheckbox('child-2');
const checkbox2 = orejimePage.purposeCheckbox('other');
await expect(checkbox2).not.toBeChecked();

const groupCheckbox = orejimePage.purposeCheckbox('group');
Expand All @@ -181,4 +179,20 @@ test.describe('Orejime', () => {
await expect(checkbox).not.toBeChecked();
await expect(checkbox2).not.toBeChecked();
});

test('should show a contextual consent notice', async () => {
await orejimePage.expectElement('.orejime-ContextualNotice');
});

test('should accept contextual consent from the notice', async () => {
await orejimePage.acceptContextualNotice();
await orejimePage.expectElement('#contextual');
await orejimePage.expectMissingElement('.orejime-ContextualNotice');
});

test('should accept contextual consent from the banner', async () => {
await orejimePage.acceptAllFromBanner();
await orejimePage.expectElement('#contextual');
await orejimePage.expectMissingElement('.orejime-ContextualNotice');
});
});
5 changes: 5 additions & 0 deletions rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ module.exports = {
lang: 'fr'
}),
featureTemplatePlugin({title: 'Styling', feature: 'styling'}),
featureTemplatePlugin({
title: 'Contextual consent',
feature: 'contextual',
template: 'contextual'
}),
featureTemplatePlugin({
title: "Intégration au système de design de l'état",
feature: 'dsfr',
Expand Down
5 changes: 5 additions & 0 deletions site/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ iframe {
color: var(--color-black--100);
}

.ExamplePage iframe {
aspect-ratio: 16 / 9;
min-height: 0;
}

.ExamplePage .orejime-Env {
font-size: 0.875rem;
}
Expand Down
61 changes: 61 additions & 0 deletions site/features/contextual.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!doctype html>

<html class="ExamplePage" lang="en">
<head>
<title>Orejime</title>

<link
rel="stylesheet"
href="https://boscop.fr/wp-content/themes/boscop/dist/app.css"
/>

<link rel="stylesheet" href="../assets/style.css" />
<link rel="stylesheet" href="../orejime-standard.css" />
</head>

<body>
<main class="ExampleMain" role="main">
<template data-purpose="youtube" data-contextual>
<figure role="group">
<iframe
title="YouTube video player"
src="https://www.youtube-nocookie.com/embed/pghz5vpi5q4?si=npJInLIEM7XiD0MB"
allowfullscreen
></iframe>

<figcaption class="fr-content-media__caption">
Cooking cookies with Philippe Etchebest
</figcaption>
</figure>
</template>

<button class="ExampleReset Button">Reset consent</button>
</main>

<script>
window.orejimeConfig = {
purposes: [
{
id: 'youtube',
title: 'YouTube videos'
},
{
id: 'other',
title: 'Another purpose'
}
],
privacyPolicyUrl: '#'
};
</script>

<script src="../orejime-standard-en.js"></script>

<script>
document
.querySelector('.ExampleReset')
.addEventListener('click', function () {
window.orejime.manager.clearConsents();
});
</script>
</body>
</html>
17 changes: 17 additions & 0 deletions site/features/dsfr.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ <h2>Politique de confidentialité</h2>
</li>
</ul>

<h2>Media</h2>

<template data-purpose="youtube" data-contextual>
<figure role="group" class="fr-content-media">
<iframe
class="fr-responsive-vid"
title="YouTube video player"
src="https://www.youtube-nocookie.com/embed/pghz5vpi5q4?si=npJInLIEM7XiD0MB"
allowfullscreen
></iframe>

<figcaption class="fr-content-media__caption">
Les cookies de Philippe Etchebest
</figcaption>
</figure>
</template>

<h2>Configuration</h2>

<%= js.highlightedCode %>
Expand Down
Loading

0 comments on commit db11fa4

Please sign in to comment.