diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json
index d6d3253ca8..9f1df56be2 100755
--- a/aio/scripts/_payload-limits.json
+++ b/aio/scripts/_payload-limits.json
@@ -3,17 +3,17 @@
"master": {
"gzip7": {
"inline": 961,
- "main": 115381,
+ "main": 116924,
"polyfills": 12962
},
"gzip9": {
"inline": 961,
- "main": 115186,
+ "main": 116712,
"polyfills": 12958
},
"uncompressed": {
"inline": 1602,
- "main": 453905,
+ "main": 459119,
"polyfills": 40264
}
}
diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html
index 733800c29d..bad7f69913 100644
--- a/aio/src/app/app.component.html
+++ b/aio/src/app/app.component.html
@@ -5,15 +5,29 @@
-
-
-
-
-
-
-
+
+
+ Help Angular by taking a 1 minute survey!
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts
index 53c8ac4075..d35f9b57b5 100644
--- a/aio/src/app/app.component.ts
+++ b/aio/src/app/app.component.ts
@@ -6,6 +6,7 @@ import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'ap
import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { Deployment } from 'app/shared/deployment.service';
import { LocationService } from 'app/shared/location.service';
+import { NotificationComponent } from 'app/layout/notification/notification.component';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResults } from 'app/search/interfaces';
@@ -90,6 +91,10 @@ export class AppComponent implements OnInit {
@ViewChild(MatSidenav)
sidenav: MatSidenav;
+ @ViewChild(NotificationComponent)
+ notification: NotificationComponent;
+ notificationAnimating = false;
+
constructor(
public deployment: Deployment,
private documentService: DocumentService,
@@ -273,14 +278,33 @@ export class AppComponent implements OnInit {
this.folderId = (id === 'index') ? 'home' : id.split('/', 1)[0];
}
+ notificationDismissed() {
+ this.notificationAnimating = true;
+ // this should be kept in sync with the animation durations in:
+ // - aio/src/styles/2-modules/_notification.scss
+ // - aio/src/app/layout/notification/notification.component.ts
+ setTimeout(() => this.notificationAnimating = false, 250);
+ this.updateHostClasses();
+ }
+
updateHostClasses() {
const mode = `mode-${this.deployment.mode}`;
const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`;
const pageClass = `page-${this.pageId}`;
const folderClass = `folder-${this.folderId}`;
const viewClasses = Object.keys(this.currentNodes || {}).map(view => `view-${view}`).join(' ');
+ const notificationClass = `aio-notification-${this.notification.showNotification}`;
+ const notificationAnimatingClass = this.notificationAnimating ? 'aio-notification-animating' : '';
- this.hostClasses = `${mode} ${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`;
+ this.hostClasses = [
+ mode,
+ sideNavOpen,
+ pageClass,
+ folderClass,
+ viewClasses,
+ notificationClass,
+ notificationAnimatingClass
+ ].join(' ');
}
updateHostClassesForDoc(doc: DocumentContents) {
diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts
index a7a7d9b83d..c14ce329d3 100644
--- a/aio/src/app/app.module.ts
+++ b/aio/src/app/app.module.ts
@@ -34,8 +34,10 @@ import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
+import { NotificationComponent } from 'app/layout/notification/notification.component';
import { TocComponent } from 'app/layout/toc/toc.component';
import { TocService } from 'app/shared/toc.service';
+import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
import { WindowToken, windowProvider } from 'app/shared/window';
import { EmbedComponentsModule } from 'app/embed-components/embed-components.module';
@@ -65,6 +67,30 @@ export const svgIconProviders = [
'viewBox="0 0 24 24">'
},
multi: true
+ },
+ {
+ provide: SVG_ICONS,
+ useValue: {
+ name: 'insert_comment',
+ svgSource:
+ ''
+ },
+ multi: true
+ },
+ {
+ provide: SVG_ICONS,
+ useValue: {
+ name: 'close',
+ svgSource:
+ ''
+ },
+ multi: true
}
];
@@ -91,6 +117,7 @@ export const svgIconProviders = [
NavMenuComponent,
NavItemComponent,
SearchBoxComponent,
+ NotificationComponent,
TocComponent,
TopMenuComponent,
],
@@ -109,6 +136,7 @@ export const svgIconProviders = [
SearchService,
svgIconProviders,
TocService,
+ { provide: CurrentDateToken, useFactory: currentDateProvider },
{ provide: WindowToken, useFactory: windowProvider },
{
diff --git a/aio/src/app/layout/notification/notification.component.html b/aio/src/app/layout/notification/notification.component.html
new file mode 100644
index 0000000000..5db9b72caf
--- /dev/null
+++ b/aio/src/app/layout/notification/notification.component.html
@@ -0,0 +1,9 @@
+
+
+
+ {{buttonText}}
+
+
+
diff --git a/aio/src/app/layout/notification/notification.component.spec.ts b/aio/src/app/layout/notification/notification.component.spec.ts
new file mode 100644
index 0000000000..f54335e8a7
--- /dev/null
+++ b/aio/src/app/layout/notification/notification.component.spec.ts
@@ -0,0 +1,123 @@
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { CurrentDateToken } from 'app/shared/current-date';
+import { NotificationComponent } from './notification.component';
+import { WindowToken } from 'app/shared/window';
+
+describe('NotificationComponent', () => {
+ let element: HTMLElement;
+ let component: NotificationComponent;
+ let fixture: ComponentFixture;
+
+ function configTestingModule(now = new Date('2018-01-20')) {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent, NotificationComponent],
+ providers: [
+ { provide: WindowToken, useClass: MockWindow },
+ { provide: CurrentDateToken, useValue: now },
+ ],
+ imports: [NoopAnimationsModule],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+ }
+
+ function createComponent() {
+ fixture = TestBed.createComponent(TestComponent);
+ const debugElement = fixture.debugElement.query(By.directive(NotificationComponent));
+ element = debugElement.nativeElement;
+ component = debugElement.componentInstance;
+ component.ngOnInit();
+ fixture.detectChanges();
+ }
+
+ it('should display the message', () => {
+ configTestingModule();
+ createComponent();
+ expect(fixture.nativeElement.innerHTML).toContain('Help Angular by taking a 1 minute survey!');
+ });
+
+ it('should display an icon', () => {
+ configTestingModule();
+ createComponent();
+ const iconElement = fixture.debugElement.query(By.css('.icon'));
+ expect(iconElement.properties['svgIcon']).toEqual('insert_comment');
+ expect(iconElement.attributes['aria-label']).toEqual('Survey');
+ });
+
+ it('should display a button', () => {
+ configTestingModule();
+ createComponent();
+ const button = fixture.debugElement.query(By.css('.action-button'));
+ expect(button.nativeElement.textContent).toEqual('Go to survey');
+ });
+
+ it('should call dismiss when the message link is clicked', () => {
+ configTestingModule();
+ createComponent();
+ spyOn(component, 'dismiss');
+ fixture.debugElement.query(By.css('a')).triggerEventHandler('click', null);
+ fixture.detectChanges();
+ expect(component.dismiss).toHaveBeenCalled();
+ });
+
+ it('should call dismiss when the close button is clicked', () => {
+ configTestingModule();
+ createComponent();
+ spyOn(component, 'dismiss');
+ fixture.debugElement.query(By.css('button')).triggerEventHandler('click', null);
+ fixture.detectChanges();
+ expect(component.dismiss).toHaveBeenCalled();
+ });
+
+ it('should hide the notification when dismiss is called', () => {
+ configTestingModule();
+ createComponent();
+ expect(component.showNotification).toBe('show');
+ component.dismiss();
+ expect(component.showNotification).toBe('hide');
+ });
+
+ it('should update localStorage key when dismiss is called', () => {
+ configTestingModule();
+ createComponent();
+ const setItemSpy: jasmine.Spy = TestBed.get(WindowToken).localStorage.setItem;
+ component.dismiss();
+ expect(setItemSpy).toHaveBeenCalledWith('aio-notification/survey-january-2018', 'hide');
+ });
+
+ it('should not show the notification if the date is after the expiry date', () => {
+ configTestingModule(new Date('2018-01-23'));
+ createComponent();
+ expect(component.showNotification).toBe('hide');
+ });
+
+ it('should not show the notification if the there is a "hide" flag in localStorage', () => {
+ configTestingModule();
+ const getItemSpy: jasmine.Spy = TestBed.get(WindowToken).localStorage.getItem;
+ getItemSpy.and.returnValue('hide');
+ createComponent();
+ expect(getItemSpy).toHaveBeenCalledWith('aio-notification/survey-january-2018');
+ expect(component.showNotification).toBe('hide');
+ });
+});
+
+@Component({
+ template: `
+
+ Help Angular by taking a 1 minute survey!
+ `
+})
+class TestComponent {
+}
+
+class MockWindow {
+ localStorage = jasmine.createSpyObj('localStorage', ['getItem', 'setItem']);
+}
diff --git a/aio/src/app/layout/notification/notification.component.ts b/aio/src/app/layout/notification/notification.component.ts
new file mode 100644
index 0000000000..1346c7ae6e
--- /dev/null
+++ b/aio/src/app/layout/notification/notification.component.ts
@@ -0,0 +1,52 @@
+import { animate, state, style, trigger, transition } from '@angular/animations';
+import { Component, EventEmitter, HostBinding, Inject, Input, OnInit, Output } from '@angular/core';
+import { CurrentDateToken } from 'app/shared/current-date';
+import { WindowToken } from 'app/shared/window';
+
+const LOCAL_STORAGE_NAMESPACE = 'aio-notification/';
+
+@Component({
+ selector: 'aio-notification',
+ templateUrl: 'notification.component.html',
+ animations: [
+ trigger('hideAnimation', [
+ state('show', style({height: '*'})),
+ state('hide', style({height: 0})),
+ // this should be kept in sync with the animation durations in:
+ // - aio/src/styles/2-modules/_notification.scss
+ // - aio/src/app/app.component.ts : notificationDismissed()
+ transition('show => hide', animate(250))
+ ])
+ ]
+})
+export class NotificationComponent implements OnInit {
+ private get localStorage() { return this.window.localStorage; }
+
+ @Input() icon: string;
+ @Input() iconLabel: string;
+ @Input() buttonText: string;
+ @Input() actionUrl: string;
+ @Input() notificationId: string;
+ @Input() expirationDate: string;
+ @Output() dismissed = new EventEmitter();
+
+ @HostBinding('@hideAnimation')
+ showNotification: 'show'|'hide';
+
+ constructor(
+ @Inject(WindowToken) private window: Window,
+ @Inject(CurrentDateToken) private currentDate: Date
+ ) {}
+
+ ngOnInit() {
+ const previouslyHidden = this.localStorage.getItem(LOCAL_STORAGE_NAMESPACE + this.notificationId) === 'hide';
+ const expired = this.currentDate > new Date(this.expirationDate);
+ this.showNotification = previouslyHidden || expired ? 'hide' : 'show';
+ }
+
+ dismiss() {
+ this.localStorage.setItem(LOCAL_STORAGE_NAMESPACE + this.notificationId, 'hide');
+ this.showNotification = 'hide';
+ this.dismissed.next();
+ }
+}
diff --git a/aio/src/app/shared/current-date.ts b/aio/src/app/shared/current-date.ts
new file mode 100644
index 0000000000..ab02331bff
--- /dev/null
+++ b/aio/src/app/shared/current-date.ts
@@ -0,0 +1,4 @@
+import { InjectionToken } from '@angular/core';
+
+export const CurrentDateToken = new InjectionToken('CurrentDate');
+export function currentDateProvider() { return new Date(); }
diff --git a/aio/src/styles/1-layouts/_top-menu.scss b/aio/src/styles/1-layouts/_top-menu.scss
index fdfb4e5f54..5fc6611596 100644
--- a/aio/src/styles/1-layouts/_top-menu.scss
+++ b/aio/src/styles/1-layouts/_top-menu.scss
@@ -10,9 +10,12 @@ mat-toolbar.mat-toolbar {
right: 0;
left: 0;
z-index: 10;
- padding: 0 16px 0 0;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.30);
+ mat-toolbar-row {
+ padding: 0 16px 0 0;
+ }
+
mat-icon {
color: $white;
}
diff --git a/aio/src/styles/2-modules/_modules-dir.scss b/aio/src/styles/2-modules/_modules-dir.scss
index 3535f8ae5a..066d020812 100644
--- a/aio/src/styles/2-modules/_modules-dir.scss
+++ b/aio/src/styles/2-modules/_modules-dir.scss
@@ -29,3 +29,4 @@
@import 'toc';
@import 'select-menu';
@import 'deploy-theme';
+ @import 'notification';
diff --git a/aio/src/styles/2-modules/_notification.scss b/aio/src/styles/2-modules/_notification.scss
new file mode 100644
index 0000000000..f5fd0f91c4
--- /dev/null
+++ b/aio/src/styles/2-modules/_notification.scss
@@ -0,0 +1,106 @@
+$notificationHeight: 56px;
+
+// we need to override some of the toolbar styling
+.mat-toolbar mat-toolbar-row.notification-container {
+ padding: 0;
+ height: auto;
+ overflow: hidden;
+}
+
+aio-notification {
+ background: $darkgray;
+ display: flex;
+ position: relative;
+ align-items: center;
+ width: 100%;
+ height: $notificationHeight;
+ justify-content: center;
+
+ @media (max-width: 430px) {
+ justify-content: flex-start;
+ padding-left: 10px;
+ }
+
+ .close-button {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: $notificationHeight;
+ height: $notificationHeight;
+ background: $darkgray;
+ }
+
+ .content {
+ display: flex;
+ max-width: calc(100% - #{$notificationHeight});
+ text-transform: none;
+ padding: 0;
+
+ .icon {
+ margin-right: 10px;
+ @media (max-width: 464px) {
+ display: none;
+ }
+ }
+
+ .message {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .action-button {
+ margin-left: 10px;
+ background: $brightred;
+ border-radius: 15px;
+ text-transform: uppercase;
+ padding: 0 10px;
+ font-size: 12px;
+ @media (max-width: 780px) {
+ display: none;
+ }
+ }
+ }
+}
+
+// Here are all the hacks to make the content and sidebars the right height
+// when the notification is visible
+.aio-notification-show {
+ .sidenav-content {
+ padding-top: 80px + $notificationHeight;
+ }
+
+ mat-sidenav.mat-sidenav.sidenav {
+ top: 56px + $notificationHeight;
+
+ @media (max-width: 600px) {
+ top: 56px + $notificationHeight;
+ }
+ }
+
+ .toc-container {
+ top: 76px + $notificationHeight;
+ }
+
+ .search-results {
+ padding-top: 68px + $notificationHeight;
+ }
+
+ &.page-home, &.page-resources, &.page-events, &.page-features, &.page-presskit, &.page-contribute {
+ section {
+ padding-top: $notificationHeight;
+ }
+ }
+}
+
+// Animate the content when the notification bar is dismissed
+// this should be kept in sync with the animation durations in
+// - aio/src/app/layout/notification/notification.component.ts
+// - aio/src/app/app.component.ts : notificationDismissed()
+.aio-notification-animating {
+ .sidenav-content {
+ transition: padding-top 250ms ease;
+ }
+ mat-sidenav.mat-sidenav.sidenav, .toc-container {
+ transition: top 250ms ease;
+ }
+}