Skip to content

Commit 59a38e6

Browse files
committed
Contextual consent
@todo * add translations * handle focus with care
1 parent 1d6b729 commit 59a38e6

35 files changed

Lines changed: 670 additions & 103 deletions

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ purpose:
303303
</template>
304304
```
305305

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

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

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

e2e/OrejimePage.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {expect, BrowserContext, Page} from '@playwright/test';
1+
import {expect, BrowserContext, Page, Locator} from '@playwright/test';
22
import Cookie from 'js-cookie';
33
import {Config} from '../src/ui/types';
44

@@ -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
`
@@ -36,61 +37,69 @@ export class OrejimePage {
3637
}
3738

3839
get banner() {
39-
return this.page.locator('.orejime-Banner');
40+
return this.locator('.orejime-Banner');
4041
}
4142

4243
get learnMoreBannerButton() {
43-
return this.page.locator('.orejime-Banner-learnMoreButton');
44+
return this.locator('.orejime-Banner-learnMoreButton');
4445
}
4546

4647
get firstFocusableElementFromBanner() {
47-
return this.page.locator('.orejime-Banner :is(a, button)').first();
48+
return this.locator('.orejime-Banner :is(a, button)').first();
4849
}
4950

5051
get modal() {
51-
return this.page.locator('.orejime-Modal');
52+
return this.locator('.orejime-Modal');
5253
}
5354

54-
purposeCheckbox(purposeId: string) {
55-
return this.page.locator(`#orejime-purpose-${purposeId}`);
55+
get contextualNotice() {
56+
return this.locator('.orejime-ContextualNotice');
57+
}
58+
59+
get contextualNoticePlaceholder() {
60+
return this.locator('.orejime-ContextualNotice-placeholder');
5661
}
5762

58-
async focusNext() {
59-
await this.page.keyboard.press('Tab');
63+
locator(selector: string) {
64+
return this.page.locator(selector);
65+
}
66+
67+
purposeCheckbox(purposeId: string) {
68+
return this.locator(`#orejime-purpose-${purposeId}`);
6069
}
6170

6271
async acceptAllFromBanner() {
63-
await this.page.locator('.orejime-Banner-saveButton').click();
72+
await this.locator('.orejime-Banner-saveButton').click();
6473
}
6574

6675
async declineAllFromBanner() {
67-
await this.page.locator('.orejime-Banner-declineButton').click();
76+
await this.locator('.orejime-Banner-declineButton').click();
6877
}
6978

7079
async openModalFromBanner() {
7180
await this.learnMoreBannerButton.click();
7281
}
7382

7483
async enableAllFromModal() {
75-
await this.page.locator('.orejime-PurposeToggles-enableAll').click();
84+
await this.locator('.orejime-PurposeToggles-enableAll').click();
7685
}
7786

7887
async disableAllFromModal() {
79-
await this.page.locator('.orejime-PurposeToggles-disableAll').click();
88+
await this.locator('.orejime-PurposeToggles-disableAll').click();
8089
}
8190

8291
async saveFromModal() {
83-
await this.page.locator('.orejime-Modal-saveButton').click();
92+
await this.locator('.orejime-Modal-saveButton').click();
8493
}
8594

8695
async closeModalByClickingButton() {
87-
await this.page.locator('.orejime-Modal-closeButton').click();
96+
await this.locator('.orejime-Modal-closeButton').click();
8897
}
8998

9099
async closeModalByClickingOutside() {
91100
// We're clicking in a corner to avoid clicking on the
92101
// modal itself, which has no effect.
93-
await this.page.locator('.orejime-ModalOverlay').click({
102+
await this.locator('.orejime-ModalOverlay').click({
94103
position: {
95104
x: 1,
96105
y: 1
@@ -102,12 +111,8 @@ export class OrejimePage {
102111
await this.page.keyboard.press('Escape');
103112
}
104113

105-
async expectElement(selector: string) {
106-
await expect(this.page.locator(selector)).toBeAttached();
107-
}
108-
109-
async expectMissingElement(selector: string) {
110-
await expect(this.page.locator(selector)).not.toBeAttached();
114+
async acceptContextualNotice() {
115+
await this.locator('.orejime-ContextualNotice-button').click();
111116
}
112117

113118
async expectConsents(consents: Record<string, unknown>) {
@@ -120,4 +125,13 @@ export class OrejimePage {
120125
const {value} = cookies.find((cookie) => cookie.name === name)!;
121126
return JSON.parse(Cookie.converter.read(value, name));
122127
}
128+
129+
// In specific conditions, browser events can get queued
130+
// up and won't be fired until some interaction with the
131+
// page.
132+
// We're using a dummy click to trigger queued events.
133+
// @see https://github.com/microsoft/playwright/issues/979
134+
emptyEventQueue() {
135+
return this.page.mouse.click(0, 0);
136+
}
123137
}

0 commit comments

Comments
 (0)