diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html
index be0db3b713..a8834fc492 100644
--- a/aio/src/app/app.component.html
+++ b/aio/src/app/app.component.html
@@ -1,5 +1,7 @@
+
+
diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts
index 16967f7ce3..18385db24a 100644
--- a/aio/src/app/app.component.spec.ts
+++ b/aio/src/app/app.component.spec.ts
@@ -7,6 +7,7 @@ import { MatSidenav } from '@angular/material/sidenav';
import { By, Title } from '@angular/platform-browser';
import { ElementsLoader } from 'app/custom-elements/elements-loader';
import { DocumentService } from 'app/documents/document.service';
+import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { CurrentNodes } from 'app/navigation/navigation.model';
import { NavigationNode, NavigationService } from 'app/navigation/navigation.service';
@@ -701,6 +702,13 @@ describe('AppComponent', () => {
});
});
+ describe('aio-cookies-popup', () => {
+ it('should have a cookies popup', () => {
+ const cookiesPopupDe = fixture.debugElement.query(By.directive(CookiesPopupComponent));
+ expect(cookiesPopupDe.componentInstance).toBeInstanceOf(CookiesPopupComponent);
+ });
+ });
+
describe('deployment banner', () => {
it('should show a message if the deployment mode is "archive"', async () => {
createTestingModule('a/b', 'archive');
diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts
index 17e8bdba11..4a27d5b562 100644
--- a/aio/src/app/app.module.ts
+++ b/aio/src/app/app.module.ts
@@ -15,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { AppComponent } from 'app/app.component';
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
import { Deployment } from 'app/shared/deployment.service';
+import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
@@ -163,6 +164,7 @@ export const svgIconProviders = [
],
declarations: [
AppComponent,
+ CookiesPopupComponent,
DocViewerComponent,
DtComponent,
FooterComponent,
diff --git a/aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts b/aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts
new file mode 100644
index 0000000000..dcae1b1c23
--- /dev/null
+++ b/aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts
@@ -0,0 +1,92 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { LocalStorage } from 'app/shared/storage.service';
+import { CookiesPopupComponent, storageKey } from './cookies-popup.component';
+
+describe('CookiesPopupComponent', () => {
+ let mockLocalStorage: MockLocalStorage;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ mockLocalStorage = new MockLocalStorage();
+
+ TestBed.configureTestingModule({
+ declarations: [
+ CookiesPopupComponent,
+ ],
+ providers: [
+ { provide: LocalStorage, useValue: mockLocalStorage },
+ ],
+ });
+
+ fixture = TestBed.createComponent(CookiesPopupComponent);
+ });
+
+ it('should make the popup visible by default', () => {
+ fixture.detectChanges();
+
+ expect(getCookiesPopup()).not.toBeNull();
+ });
+
+ it('should include the correct content in the popup', () => {
+ fixture.detectChanges();
+
+ const popup = getCookiesPopup() as Element;
+ const infoBtn = popup.querySelector('a[mat-button]:nth-child(1)');
+ const okBtn = popup.querySelector('button[mat-button]:nth-child(2)');
+
+ expect(popup.textContent).toContain(
+ 'This site uses cookies from Google to deliver its services and to analyze traffic.');
+
+ expect(infoBtn).toBeInstanceOf(HTMLElement);
+ expect(infoBtn?.href).toBe('https://policies.google.com/technologies/cookies');
+ expect(infoBtn?.textContent).toMatch(/learn more/i);
+
+ expect(okBtn).toBeInstanceOf(HTMLElement);
+ expect(okBtn?.textContent).toMatch(/ok, got it/i);
+ });
+
+ it('should hide the cookies popup if the user has already accepted cookies', () => {
+ mockLocalStorage.setItem(storageKey, 'true');
+ fixture = TestBed.createComponent(CookiesPopupComponent);
+
+ fixture.detectChanges();
+
+ expect(getCookiesPopup()).toBeNull();
+ });
+
+ describe('acceptCookies()', () => {
+ it('should hide the cookies popup', () => {
+ fixture.detectChanges();
+ expect(getCookiesPopup()).not.toBeNull();
+
+ fixture.componentInstance.acceptCookies();
+ fixture.detectChanges();
+ expect(getCookiesPopup()).toBeNull();
+ });
+
+ it('should store the user\'s confirmation', () => {
+ fixture.detectChanges();
+ expect(mockLocalStorage.getItem(storageKey)).toBeNull();
+
+ fixture.componentInstance.acceptCookies();
+ expect(mockLocalStorage.getItem(storageKey)).toBe('true');
+ });
+ });
+
+ // Helpers
+ function getCookiesPopup() {
+ return (fixture.nativeElement as HTMLElement).querySelector('.cookies-popup');
+ }
+
+ class MockLocalStorage implements Pick {
+ private items = new Map();
+
+ getItem(key: string): string | null {
+ return this.items.get(key) ?? null;
+ }
+
+ setItem(key: string, val: string): void {
+ this.items.set(key, val);
+ }
+ }
+});
diff --git a/aio/src/app/layout/cookies-popup/cookies-popup.component.ts b/aio/src/app/layout/cookies-popup/cookies-popup.component.ts
new file mode 100644
index 0000000000..c398c8d9a2
--- /dev/null
+++ b/aio/src/app/layout/cookies-popup/cookies-popup.component.ts
@@ -0,0 +1,37 @@
+import { Component, Inject } from '@angular/core';
+import { LocalStorage } from 'app/shared/storage.service';
+
+export const storageKey = 'aio-accepts-cookies';
+
+@Component({
+ selector: 'aio-cookies-popup',
+ template: `
+
+ `,
+})
+export class CookiesPopupComponent {
+ /** Whether the user has already accepted the cookies disclaimer. */
+ hasAcceptedCookies: boolean;
+
+ constructor(@Inject(LocalStorage) private storage: Storage) {
+ this.hasAcceptedCookies = this.storage.getItem(storageKey) === 'true';
+ }
+
+ acceptCookies() {
+ this.storage.setItem(storageKey, 'true');
+ this.hasAcceptedCookies = true;
+ }
+}
diff --git a/aio/src/styles/2-modules/_index.scss b/aio/src/styles/2-modules/_index.scss
index 66f600a7af..913e3a9c58 100644
--- a/aio/src/styles/2-modules/_index.scss
+++ b/aio/src/styles/2-modules/_index.scss
@@ -12,6 +12,7 @@
@forward 'cli-pages/cli-pages';
@forward 'code/code';
@forward 'contribute/contribute';
+@forward 'cookies-popup/cookies-popup';
@forward 'contributor/contributor';
@forward 'deploy-theme/deploy-theme';
@forward 'details/details';
diff --git a/aio/src/styles/2-modules/_theme.scss b/aio/src/styles/2-modules/_theme.scss
index fa0e651be1..62919bd78e 100644
--- a/aio/src/styles/2-modules/_theme.scss
+++ b/aio/src/styles/2-modules/_theme.scss
@@ -7,6 +7,7 @@
@use 'card/card-theme';
@use 'code/code-theme';
@use 'contributor/contributor-theme';
+@use 'cookies-popup/cookies-popup-theme';
@use 'deploy-theme/deploy-theme';
@use 'details/details-theme';
@use 'errors/errors-theme';
@@ -33,6 +34,7 @@
@include card-theme.theme($theme);
@include code-theme.theme($theme);
@include contributor-theme.theme($theme);
+ @include cookies-popup-theme.theme($theme);
@include deploy-theme.theme($theme);
@include details-theme.theme($theme);
@include errors-theme.theme($theme);
diff --git a/aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss b/aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss
new file mode 100644
index 0000000000..a5bb9ec4a1
--- /dev/null
+++ b/aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss
@@ -0,0 +1,27 @@
+@use 'sass:map';
+@use '../../constants' as c;
+@use '~@angular/material' as mat;
+
+@mixin theme($theme) {
+ $is-dark-theme: map.get($theme, is-dark);
+
+ aio-cookies-popup {
+ .cookies-popup {
+ background: if($is-dark-theme, map.get(mat.$grey-palette, 50), #252525);
+ color: if($is-dark-theme,
+ map.get(map.get(mat.$grey-palette, contrast), 50),
+ map.get(mat.$dark-theme-foreground-palette, secondary-text)
+ );
+
+ .actions {
+ .mat-button {
+ color: if($is-dark-theme, c.$blue, c.$lightblue);
+
+ .mat-button-focus-overlay {
+ background: if($is-dark-theme, c.$black, c.$white);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss b/aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss
new file mode 100644
index 0000000000..a7ee2675f7
--- /dev/null
+++ b/aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss
@@ -0,0 +1,28 @@
+@use '~@angular/cdk' as cdk;
+@use '~@angular/material' as mat;
+
+$inner-spacing: 16px;
+
+aio-cookies-popup {
+ .cookies-popup {
+ @include mat.elevation(6);
+ border-radius: 4px;
+ bottom: 0;
+ left: 0;
+ position: fixed;
+ margin: 24px;
+ max-width: 430px;
+ padding: $inner-spacing $inner-spacing $inner-spacing / 2;
+ z-index: cdk.$overlay-container-z-index + 1;
+
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+ margin: $inner-spacing $inner-spacing / -2 0 0;
+
+ .mat-button {
+ text-transform: uppercase;
+ }
+ }
+ }
+}
diff --git a/aio/tests/e2e/src/app.po.ts b/aio/tests/e2e/src/app.po.ts
index f10ab9fe6b..b91a0ff576 100644
--- a/aio/tests/e2e/src/app.po.ts
+++ b/aio/tests/e2e/src/app.po.ts
@@ -9,6 +9,7 @@ export class SitePage {
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
sidenav = element(by.css('mat-sidenav'));
docViewer = element(by.css('aio-doc-viewer'));
+ cookiesPopup = element(by.css('.cookies-popup'));
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
ghLinks = this.docViewer
.all(by.css('a'))
@@ -39,10 +40,15 @@ export class SitePage {
ga() { return browser.executeScript('return window["ga"].q'); }
locationPath() { return browser.executeScript('return document.location.pathname'); }
- async navigateTo(pageUrl: string) {
- // Navigate to the page, disable animations, and wait for Angular.
+ async navigateTo(pageUrl: string, keepCookiesPopup = false) {
+ // Navigate to the page, disable animations, potentially hide the cookies popup, and wait for
+ // Angular.
await browser.get(`/${pageUrl.replace(/^\//, '')}`);
await browser.executeScript('document.body.classList.add(\'no-animations\')');
+ if (!keepCookiesPopup) {
+ // Hide the cookies popup to prevent it from obscuring other elements.
+ await browser.executeScript('arguments[0].remove()', this.cookiesPopup);
+ }
await browser.waitForAngular();
}
diff --git a/aio/tests/e2e/src/cookies-popup.e2e-spec.ts b/aio/tests/e2e/src/cookies-popup.e2e-spec.ts
new file mode 100644
index 0000000000..1ae42752bf
--- /dev/null
+++ b/aio/tests/e2e/src/cookies-popup.e2e-spec.ts
@@ -0,0 +1,53 @@
+import { browser, by } from 'protractor';
+import { SitePage } from './app.po';
+
+describe('cookies popup', () => {
+ const getButton = (idx: number) => page.cookiesPopup.all(by.css('.actions .mat-button')).get(idx);
+ let page: SitePage;
+
+ beforeEach(async () => {
+ page = new SitePage();
+ await page.navigateTo('', true);
+ });
+
+ afterEach(() => browser.executeScript('localStorage.clear()'));
+
+ it('should be shown by default', async () => {
+ expect(await page.cookiesPopup.isDisplayed()).toBe(true);
+ });
+
+ it('should open a new tab with more info when clicking the first button', async () => {
+ // Click the "Learn more" button.
+ await page.click(getButton(0));
+
+ // Switch to the newly opened tab.
+ const originalWindowHandle = await browser.getWindowHandle();
+ const openedWindowHandle = (await browser.getAllWindowHandles()).pop() as string;
+ await browser.switchTo().window(openedWindowHandle);
+ await browser.waitForAngularEnabled(false);
+
+ // Verify the tab's URL.
+ expect(await browser.getCurrentUrl()).toBe('https://policies.google.com/technologies/cookies');
+
+ // Close the tab and switch back to the original tab.
+ await browser.waitForAngularEnabled(true);
+ await browser.close();
+ await browser.switchTo().window(originalWindowHandle);
+ });
+
+ it('should not hide the popup when clicking the first button', async () => {
+ await page.click(getButton(0));
+
+ expect(await page.cookiesPopup.isDisplayed()).toBe(true);
+ });
+
+ it('should hide the popup when clicking the second button', async () => {
+ expect(await page.cookiesPopup.isDisplayed()).toBe(true);
+
+ await page.click(getButton(1));
+ expect(await page.cookiesPopup.isPresent()).toBe(false);
+
+ await page.navigateTo('', true);
+ expect(await page.cookiesPopup.isPresent()).toBe(false);
+ });
+});
diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json
index fa92ee0f9c..4dd3b70d74 100755
--- a/goldens/size-tracking/aio-payloads.json
+++ b/goldens/size-tracking/aio-payloads.json
@@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime-es2017": 4619,
- "main-es2017": 454003,
+ "main-es2017": 456795,
"polyfills-es2017": 55210
}
}
@@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2017": 4619,
- "main-es2017": 454138,
+ "main-es2017": 456930,
"polyfills-es2017": 55348
}
}