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 10, 2025
1 parent 4802c9a commit 918915e
Show file tree
Hide file tree
Showing 21 changed files with 389 additions and 32 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ You can wrap many elements at once or use several templates with the same purpos
</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 @@ -361,6 +364,29 @@ Catalan, Dutch, English, Estonian, Finnish, French, German, Hungarian, Italian,
> [!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>
```

## API

Functions and references are made available on the global scope:
Expand Down
1 change: 1 addition & 0 deletions rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ module.exports = {
featureTemplatePlugin('Grouping', 'grouping'),
featureTemplatePlugin('Internationalization', 'i18n'),
featureTemplatePlugin('Styling', 'styling'),
featureTemplatePlugin('Contextual consent', 'contextual', 'contextual'),
featureTemplatePlugin(
"Intégration au système de design de l'état",
'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 @@ -67,6 +67,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
59 changes: 59 additions & 0 deletions site/features/contextual.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!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'
}
],
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 @@ -113,6 +113,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
21 changes: 4 additions & 17 deletions site/features/dsfr.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,13 @@ window.orejimeConfig = {
{
id: 'mandatory',
title: 'Cookies techniques',
description:
'Cookies nécéssaires au bon fonctionnement du site.',
description: 'Cookies nécéssaires au bon fonctionnement du site.',
isMandatory: true
},
{
id: 'third-party',
title: 'Suivi tiers',
description:
'Cookies déposés par des partenaires',
purposes: [
{
id: 'analytics',
title: "Analyse d'audience"
},
{
id: 'social',
title: 'Réseaux sociaux',
description: 'Médias et partage'
}
]
id: 'youtube',
title: 'YouTube',
description: 'Vidéos YouTube'
}
],
privacyPolicyUrl: '#privacyPolicy'
Expand Down
20 changes: 20 additions & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ <h3 id="styling" data-example="styling-example">Styling</h3>
</em>
</p>

<h3 id="contextual" data-example="contextual-example">
Contextual consent
</h3>

<p>
Any content can be handled by Orejime. Instead of simply
hiding everything the user hasn't consented to, Orejime can
show a fallback notice with a way to consent in place.
</p>

<details id="contextual-example">
<summary>Example</summary>
<iframe
style="min-height: 20lh"
data-src="./features/contextual.html"
title="Example of contextual consent"
loading="lazy"
></iframe>
</details>

<h3>Themes</h3>

<p>
Expand Down
20 changes: 7 additions & 13 deletions src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {Manager, setup as setupManager} from './core';
import {Config, setup as setupUi} from './ui';
import {assertConfigValidity, DefaultConfig, purposesOnly} from './ui/utils/config';
import {
assertConfigValidity,
DefaultConfig,
purposesOnly
} from './ui/utils/config';
import {deepMerge} from './ui/utils/objects';

export interface OrejimeInstance {
Expand All @@ -17,21 +21,11 @@ export default (partialConfig: Partial<Config>): OrejimeInstance => {
cookie: config.cookie
});

const {show, openModal} = setupUi(config, manager)

manager.on('dirty', (isDirty) => {
if (isDirty) {
show();
}
});

if (manager.isDirty()) {
show();
}
const {openModal} = setupUi(config, manager);

return {
config,
manager,
prompt: openModal
};
}
};
5 changes: 5 additions & 0 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export default {
save: 'Save',
saveTitle: 'Save my configuration on collected information'
},
contextual: {
title: '"{purpose}" is inactive',
description: 'Allow cookies to access this functionality.',
accept: 'Allow'
},
purpose: {
mandatory: 'always required',
mandatoryTitle: 'This application is always required',
Expand Down
5 changes: 5 additions & 0 deletions src/translations/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export default {
save: 'Sauvegarder',
saveTitle: 'Sauvegarder ma configuration sur les informations collectées'
},
contextual: {
title: '"{purpose}" est désactivé',
description: 'Autorisez le dépôt de cookies pour accèder à cette fonctionnalité.',
accept: 'Autoriser'
},
purpose: {
mandatory: 'toujours requis',
mandatoryTitle: 'Cette application est toujours requise',
Expand Down
40 changes: 40 additions & 0 deletions src/ui/components/ContextualNoticeContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {purposesOnly} from '../utils/config';
import {useConfig, useManager, useTheme} from '../utils/hooks';

interface ContextualNoticeContainerProps {
data: Record<string, string>;
}

const ContextualNoticeContainer = ({data}: ContextualNoticeContainerProps) => {
const config = useConfig();
const manager = useManager();
const {ContextualNotice} = useTheme();

if (!data?.purpose) {
return null;
}

const purpose = purposesOnly(config.purposes).find(
({id}) => id === data.purpose
);

if (!purpose) {
return null;
}

const handleAccept = () => {
manager.setConsent(purpose.id, true);
};

return (
<div className="orejime-Env">
<ContextualNotice
purpose={purpose}
data={data}
onAccept={handleAccept}
></ContextualNotice>
</div>
);
};

export default ContextualNoticeContainer;
14 changes: 14 additions & 0 deletions src/ui/components/types/ContextualNotice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {FunctionComponent} from 'preact';
import {Purpose} from '../../types';

export type ContextualNoticeOptions = Record<string, string>;

export interface ContextualNoticeProps<Data extends ContextualNoticeOptions> {
purpose: Purpose;
data: Data;
onAccept: () => void;
}

export type ContextualNoticeComponent<
Data extends ContextualNoticeOptions
> = FunctionComponent<ContextualNoticeProps<Data>>;
2 changes: 2 additions & 0 deletions src/ui/components/types/Theme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {BannerComponent} from './Banner';
import {ContextualNoticeComponent} from './ContextualNotice';
import {GlobalConsentComponent} from './GlobalConsent';
import {ModalComponent} from './Modal';
import {ModalBannerComponent} from './ModalBanner';
Expand All @@ -7,6 +8,7 @@ import {PurposeListComponent} from './PurposeList';

export interface Theme {
Banner: BannerComponent;
ContextualNotice: ContextualNoticeComponent;
GlobalConsent: GlobalConsentComponent;
Modal: ModalComponent;
ModalBanner: ModalBannerComponent;
Expand Down
38 changes: 38 additions & 0 deletions src/ui/contextualConsentsEffect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {render} from 'preact';
import {ConsentsMap, Manager} from '../core';
import {Config} from './types';
import Context from './components/Context';
import ContextualNoticeContainer from './components/ContextualNoticeContainer';

export const contextualConsentsEffect = (config: Config, manager: Manager) => {
const templates = new WeakMap();

return (consents: ConsentsMap) => {
Object.entries(consents).forEach(([id, state]) => {
document
.querySelectorAll(`template[data-contextual][data-purpose="${id}"]`)
.forEach((template: HTMLTemplateElement) => {
if (!templates.has(template)) {
const container = document.createElement('div');
container.style.display = 'contents';
template.insertAdjacentElement('afterend', container);
templates.set(template, container);
}

render(
state ? null : (
<Context.Provider
value={{
config,
manager
}}
>
<ContextualNoticeContainer data={{...template.dataset}} />
</Context.Provider>
),
templates.get(template)
);
});
});
};
};
Loading

0 comments on commit 918915e

Please sign in to comment.