Skip to content

Commit 4843f45

Browse files
committed
feat(docs-infra): implement popup to inform about the use of cookies
This commit adds a popup to angular.io to inform the user about the use of cookies. Once the user confirms having read the info, the popup will not be shown on subsequent visits. This commit is partly based on angular/material.angular.io#988. Fixes angular#42209
1 parent 8331cb5 commit 4843f45

File tree

11 files changed

+250
-2
lines changed

11 files changed

+250
-2
lines changed

aio/src/app/app.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<div id="top-of-page"></div>
22

3+
<aio-cookies-popup></aio-cookies-popup>
4+
35
<div *ngIf="isFetching" class="progress-bar-container">
46
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
57
</div>

aio/src/app/app.component.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MatSidenav } from '@angular/material/sidenav';
77
import { By, Title } from '@angular/platform-browser';
88
import { ElementsLoader } from 'app/custom-elements/elements-loader';
99
import { DocumentService } from 'app/documents/document.service';
10+
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
1011
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
1112
import { CurrentNodes } from 'app/navigation/navigation.model';
1213
import { NavigationNode, NavigationService } from 'app/navigation/navigation.service';
@@ -701,6 +702,13 @@ describe('AppComponent', () => {
701702
});
702703
});
703704

705+
describe('aio-cookies-popup', () => {
706+
it('should have a cookies popup', () => {
707+
const cookiesPopupDe = fixture.debugElement.query(By.directive(CookiesPopupComponent));
708+
expect(cookiesPopupDe.componentInstance).toBeInstanceOf(CookiesPopupComponent);
709+
});
710+
});
711+
704712
describe('deployment banner', () => {
705713
it('should show a message if the deployment mode is "archive"', async () => {
706714
createTestingModule('a/b', 'archive');

aio/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
1515
import { AppComponent } from 'app/app.component';
1616
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
1717
import { Deployment } from 'app/shared/deployment.service';
18+
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
1819
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
1920
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
2021
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
@@ -163,6 +164,7 @@ export const svgIconProviders = [
163164
],
164165
declarations: [
165166
AppComponent,
167+
CookiesPopupComponent,
166168
DocViewerComponent,
167169
DtComponent,
168170
FooterComponent,
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { LocalStorage } from 'app/shared/storage.service';
3+
import { CookiesPopupComponent, storageKey } from './cookies-popup.component';
4+
5+
describe('CookiesPopupComponent', () => {
6+
let mockLocalStorage: MockLocalStorage;
7+
let fixture: ComponentFixture<CookiesPopupComponent>;
8+
9+
beforeEach(() => {
10+
mockLocalStorage = new MockLocalStorage();
11+
12+
TestBed.configureTestingModule({
13+
declarations: [
14+
CookiesPopupComponent,
15+
],
16+
providers: [
17+
{ provide: LocalStorage, useValue: mockLocalStorage },
18+
],
19+
});
20+
21+
fixture = TestBed.createComponent(CookiesPopupComponent);
22+
});
23+
24+
it('should make the popup visible by default', () => {
25+
fixture.detectChanges();
26+
27+
expect(getCookiesPopup()).not.toBeNull();
28+
});
29+
30+
it('should include the correct content in the popup', () => {
31+
fixture.detectChanges();
32+
33+
const popup = getCookiesPopup() as Element;
34+
const infoBtn = popup.querySelector<HTMLAnchorElement>('a[mat-button]:nth-child(1)');
35+
const okBtn = popup.querySelector<HTMLButtonElement>('button[mat-button]:nth-child(2)');
36+
37+
expect(popup.textContent).toContain(
38+
'This site uses cookies from Google to deliver its services and to analyze traffic.');
39+
40+
expect(infoBtn).toBeInstanceOf(HTMLElement);
41+
expect(infoBtn?.href).toBe('https://policies.google.com/technologies/cookies');
42+
expect(infoBtn?.textContent).toMatch(/learn more/i);
43+
44+
expect(okBtn).toBeInstanceOf(HTMLElement);
45+
expect(okBtn?.textContent).toMatch(/ok, got it/i);
46+
});
47+
48+
it('should hide the cookies popup if the user has already accepted cookies', () => {
49+
mockLocalStorage.setItem(storageKey, 'true');
50+
fixture = TestBed.createComponent(CookiesPopupComponent);
51+
52+
fixture.detectChanges();
53+
54+
expect(getCookiesPopup()).toBeNull();
55+
});
56+
57+
describe('acceptCookies()', () => {
58+
it('should hide the cookies popup', () => {
59+
fixture.detectChanges();
60+
expect(getCookiesPopup()).not.toBeNull();
61+
62+
fixture.componentInstance.acceptCookies();
63+
fixture.detectChanges();
64+
expect(getCookiesPopup()).toBeNull();
65+
});
66+
67+
it('should store the user\'s confirmation', () => {
68+
fixture.detectChanges();
69+
expect(mockLocalStorage.getItem(storageKey)).toBeNull();
70+
71+
fixture.componentInstance.acceptCookies();
72+
expect(mockLocalStorage.getItem(storageKey)).toBe('true');
73+
});
74+
});
75+
76+
// Helpers
77+
function getCookiesPopup() {
78+
return (fixture.nativeElement as HTMLElement).querySelector('.cookies-popup');
79+
}
80+
81+
class MockLocalStorage implements Pick<Storage, 'getItem' | 'setItem'> {
82+
private items = new Map<string, string>();
83+
84+
getItem(key: string): string | null {
85+
return this.items.get(key) ?? null;
86+
}
87+
88+
setItem(key: string, val: string): void {
89+
this.items.set(key, val);
90+
}
91+
}
92+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Component, Inject } from '@angular/core';
2+
import { LocalStorage } from 'app/shared/storage.service';
3+
4+
export const storageKey = 'aio-accepts-cookies';
5+
6+
@Component({
7+
selector: 'aio-cookies-popup',
8+
template: `
9+
<div class="cookies-popup no-print" *ngIf="!hasAcceptedCookies">
10+
This site uses cookies from Google to deliver its services and to analyze traffic.
11+
12+
<div class="actions">
13+
<a mat-button href="https://policies.google.com/technologies/cookies" target="_blank" rel="noopener">
14+
LEARN MORE
15+
</a>
16+
<button mat-button (click)="acceptCookies()">
17+
OK, GOT IT
18+
</button>
19+
</div>
20+
</div>
21+
`,
22+
})
23+
export class CookiesPopupComponent {
24+
/** Whether the user has already accepted the cookies disclaimer. */
25+
hasAcceptedCookies: boolean;
26+
27+
constructor(@Inject(LocalStorage) private storage: Storage) {
28+
this.hasAcceptedCookies = this.storage.getItem(storageKey) === 'true';
29+
}
30+
31+
acceptCookies() {
32+
this.storage.setItem(storageKey, 'true');
33+
this.hasAcceptedCookies = true;
34+
}
35+
}

aio/src/styles/2-modules/_index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
@forward 'cli-pages/cli-pages';
1313
@forward 'code/code';
1414
@forward 'contribute/contribute';
15+
@forward 'cookies-popup/cookies-popup';
1516
@forward 'contributor/contributor';
1617
@forward 'deploy-theme/deploy-theme';
1718
@forward 'details/details';

aio/src/styles/2-modules/_theme.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@use 'card/card-theme';
88
@use 'code/code-theme';
99
@use 'contributor/contributor-theme';
10+
@use 'cookies-popup/cookies-popup-theme';
1011
@use 'deploy-theme/deploy-theme';
1112
@use 'details/details-theme';
1213
@use 'errors/errors-theme';
@@ -33,6 +34,7 @@
3334
@include card-theme.theme($theme);
3435
@include code-theme.theme($theme);
3536
@include contributor-theme.theme($theme);
37+
@include cookies-popup-theme.theme($theme);
3638
@include deploy-theme.theme($theme);
3739
@include details-theme.theme($theme);
3840
@include errors-theme.theme($theme);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@use 'sass:map';
2+
@use '../../constants' as c;
3+
@use '~@angular/material' as mat;
4+
5+
@mixin theme($theme) {
6+
$is-dark-theme: map.get($theme, is-dark);
7+
8+
aio-cookies-popup {
9+
.cookies-popup {
10+
background: if($is-dark-theme, map.get(mat.$grey-palette, 50), #252525);
11+
color: if($is-dark-theme,
12+
map.get(map.get(mat.$grey-palette, contrast), 50),
13+
map.get(mat.$dark-theme-foreground-palette, secondary-text)
14+
);
15+
16+
.actions {
17+
.mat-button {
18+
color: if($is-dark-theme, c.$blue, c.$lightblue);
19+
}
20+
}
21+
}
22+
}
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@use '~@angular/cdk' as cdk;
2+
@use '~@angular/material' as mat;
3+
4+
$inner-spacing: 16px;
5+
6+
aio-cookies-popup {
7+
.cookies-popup {
8+
@include mat.elevation(6);
9+
border-radius: 4px;
10+
bottom: 0;
11+
left: 0;
12+
position: fixed;
13+
margin: 24px;
14+
max-width: 430px;
15+
padding: $inner-spacing $inner-spacing $inner-spacing / 2;
16+
z-index: cdk.$overlay-container-z-index + 1;
17+
18+
.actions {
19+
display: flex;
20+
justify-content: flex-end;
21+
margin: $inner-spacing $inner-spacing / -2 0 0;
22+
}
23+
}
24+
}

aio/tests/e2e/src/app.po.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class SitePage {
99
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
1010
sidenav = element(by.css('mat-sidenav'));
1111
docViewer = element(by.css('aio-doc-viewer'));
12+
cookiesPopup = element(by.css('.cookies-popup'));
1213
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
1314
ghLinks = this.docViewer
1415
.all(by.css('a'))
@@ -39,10 +40,15 @@ export class SitePage {
3940
ga() { return browser.executeScript<any[][]>('return window["ga"].q'); }
4041
locationPath() { return browser.executeScript<string>('return document.location.pathname'); }
4142

42-
async navigateTo(pageUrl: string) {
43-
// Navigate to the page, disable animations, and wait for Angular.
43+
async navigateTo(pageUrl: string, keepCookiesPopup = false) {
44+
// Navigate to the page, disable animations, potentially hide the cookies popup, and wait for
45+
// Angular.
4446
await browser.get(`/${pageUrl.replace(/^\//, '')}`);
4547
await browser.executeScript('document.body.classList.add(\'no-animations\')');
48+
if (!keepCookiesPopup) {
49+
// Hide the cookies popup to prevent it from obscuring other elements.
50+
await browser.executeScript('arguments[0].remove()', this.cookiesPopup);
51+
}
4652
await browser.waitForAngular();
4753
}
4854

0 commit comments

Comments
 (0)