From e7c37d77a8d79f9cc23663319ae5f22c897f4d90 Mon Sep 17 00:00:00 2001 From: Stefanie Fluin Date: Mon, 24 Apr 2017 21:19:40 +0100 Subject: [PATCH] feat(aio): copy code snackbar and design updates - Add snackbar and pointer cursor for copy code button inside aio-code components - Flex cenetered content in features page - Removed duplicate global css class - Add styles to links inside of sub-sections - Remove focus outline on top nav bar links --- aio/content/marketing/features.html | 2 +- .../app/embedded/code/code.component.spec.ts | 206 ++++++++++-------- aio/src/app/embedded/code/code.component.ts | 18 +- aio/src/styles/1-layouts/_layout-global.scss | 3 - aio/src/styles/1-layouts/_top-menu.scss | 8 +- aio/src/styles/2-modules/_code.scss | 1 + aio/src/styles/2-modules/_subsection.scss | 9 +- 7 files changed, 143 insertions(+), 104 deletions(-) diff --git a/aio/content/marketing/features.html b/aio/content/marketing/features.html index ecc9235918..212e9a5108 100755 --- a/aio/content/marketing/features.html +++ b/aio/content/marketing/features.html @@ -1,6 +1,6 @@

Features & Benefits

-
+

Cross Platform

diff --git a/aio/src/app/embedded/code/code.component.spec.ts b/aio/src/app/embedded/code/code.component.spec.ts index 86efc25b22..2ed1effdb2 100644 --- a/aio/src/app/embedded/code/code.component.spec.ts +++ b/aio/src/app/embedded/code/code.component.spec.ts @@ -2,6 +2,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Component, DebugElement } from '@angular/core'; +import { MdSnackBarModule, MdSnackBar } from '@angular/material'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { CodeComponent } from './code.component'; import { CopierService } from 'app/shared//copier.service'; @@ -39,10 +41,11 @@ describe('CodeComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ + imports: [ MdSnackBarModule, NoopAnimationsModule ], declarations: [ CodeComponent, HostComponent ], providers: [ PrettyPrinter, - {provide: CopierService, useClass: TestCopierService }, + CopierService, {provide: Logger, useClass: TestLogger } ] }) @@ -65,99 +68,128 @@ describe('CodeComponent', () => { expect(codeComponent).toBeTruthy('CodeComponent'); }); - it('should format a one-line code sample', () => { - // 'pln' spans are a tell-tale for syntax highlighing - const spans = codeComponentDe.nativeElement.querySelectorAll('span.pln'); - expect(spans.length).toBeGreaterThan(0, 'formatted spans'); + describe('pretty printing', () => { + it('should format a one-line code sample', () => { + // 'pln' spans are a tell-tale for syntax highlighing + const spans = codeComponentDe.nativeElement.querySelectorAll('span.pln'); + expect(spans.length).toBeGreaterThan(0, 'formatted spans'); + }); + + it('should format a one-line code sample without linenums by default', () => { + // `
  • `s are a tell-tale for line numbers + const lis = codeComponentDe.nativeElement.querySelectorAll('li'); + expect(lis.length).toBe(0, 'should be no linenums'); + }); + + it('should add line numbers to one-line code sample when linenums set true', () => { + hostComponent.linenums = 'true'; + fixture.detectChanges(); + + // `
  • `s are a tell-tale for line numbers + const lis = codeComponentDe.nativeElement.querySelectorAll('li'); + expect(lis.length).toBe(1, 'has linenums'); + }); + + it('should format multi-line code with linenums by default', () => { + hostComponent.code = multiLineCode; + fixture.detectChanges(); + + // `
  • `s are a tell-tale for line numbers + const lis = codeComponentDe.nativeElement.querySelectorAll('li'); + expect(lis.length).toBeGreaterThan(0, 'has linenums'); + }); + + it('should not format multi-line code when linenums set false', () => { + hostComponent.linenums = false; + hostComponent.code = multiLineCode; + fixture.detectChanges(); + + // `
  • `s are a tell-tale for line numbers + const lis = codeComponentDe.nativeElement.querySelectorAll('li'); + expect(lis.length).toBe(0, 'should be no linenums'); + }); }); - it('should format a one-line code sample without linenums by default', () => { - // `
  • `s are a tell-tale for line numbers - const lis = codeComponentDe.nativeElement.querySelectorAll('li'); - expect(lis.length).toBe(0, 'should be no linenums'); + describe('whitespace handling', () => { + it('should remove common indentation from the code before rendering', () => { + hostComponent.linenums = false; + hostComponent.code = ' abc\n let x = text.split(\'\\n\');\n ghi\n\n jkl\n'; + fixture.detectChanges(); + const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText; + expect(codeContent).toEqual('abc\n let x = text.split(\'\\n\');\nghi\n\njkl'); + }); + + it('should trim whitespace from the code before rendering', () => { + hostComponent.linenums = false; + hostComponent.code = '\n\n\n' + multiLineCode + '\n\n\n'; + fixture.detectChanges(); + const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText; + expect(codeContent).toEqual(codeContent.trim()); + }); + + it('should trim whitespace from code before computing whether to format linenums', () => { + hostComponent.code = '\n\n\n' + hostComponent.code + '\n\n\n'; + fixture.detectChanges(); + // `
  • `s are a tell-tale for line numbers + const lis = codeComponentDe.nativeElement.querySelectorAll('li'); + expect(lis.length).toBe(0, 'should be no linenums'); + }); }); - it('should add line numbers to one-line code sample when linenums set true', () => { - hostComponent.linenums = 'true'; - fixture.detectChanges(); + describe('error message', () => { + it('should display error message when there is no code (after trimming)', () => { + hostComponent.code = ' \n '; + fixture.detectChanges(); + const missing = codeComponentDe.nativeElement.querySelector('.code-missing') as HTMLElement; + expect(missing).not.toBeNull('should have element with "code-missing" class'); + expect(missing.innerText).toContain('missing', 'error message'); + }); - // `
  • `s are a tell-tale for line numbers - const lis = codeComponentDe.nativeElement.querySelectorAll('li'); - expect(lis.length).toBe(1, 'has linenums'); + it('should not display "code-missing" class when there is some code', () => { + fixture.detectChanges(); + const missing = codeComponentDe.nativeElement.querySelector('.code-missing'); + expect(missing).toBeNull('should not have element with "code-missing" class'); + }); }); - it('should format multi-line code with linenums by default', () => { - hostComponent.code = multiLineCode; - fixture.detectChanges(); + describe('copy button', () => { + it('should call copier service when clicked', () => { + const copierService: CopierService = TestBed.get(CopierService); + const spy = spyOn(copierService, 'copyText'); + const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; + expect(spy.calls.count()).toBe(0, 'before click'); + button.click(); + expect(spy.calls.count()).toBe(1, 'after click'); + }); - // `
  • `s are a tell-tale for line numbers - const lis = codeComponentDe.nativeElement.querySelectorAll('li'); - expect(lis.length).toBeGreaterThan(0, 'has linenums'); + it('should copy code text when clicked', () => { + const copierService: CopierService = TestBed.get(CopierService); + const spy = spyOn(copierService, 'copyText'); + const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; + button.click(); + expect(spy.calls.argsFor(0)[0]).toEqual(oneLineCode, 'after click'); + }); + + it('should display a message when copy succeeds', () => { + const snackBar: MdSnackBar = TestBed.get(MdSnackBar); + const copierService: CopierService = TestBed.get(CopierService); + spyOn(snackBar, 'open'); + spyOn(copierService, 'copyText').and.returnValue(true); + const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; + button.click(); + expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 }); + }); + + it('should display an error when copy fails', () => { + const snackBar: MdSnackBar = TestBed.get(MdSnackBar); + const copierService: CopierService = TestBed.get(CopierService); + spyOn(snackBar, 'open'); + spyOn(copierService, 'copyText').and.returnValue(false); + const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; + button.click(); + expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 }); + }); }); - - it('should not format multi-line code when linenums set false', () => { - hostComponent.linenums = false; - hostComponent.code = multiLineCode; - fixture.detectChanges(); - - // `
  • `s are a tell-tale for line numbers - const lis = codeComponentDe.nativeElement.querySelectorAll('li'); - expect(lis.length).toBe(0, 'should be no linenums'); - }); - - it('should remove common indentation from the code before rendering', () => { - hostComponent.linenums = false; - hostComponent.code = ' abc\n let x = text.split(\'\\n\');\n ghi\n\n jkl\n'; - fixture.detectChanges(); - const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText; - expect(codeContent).toEqual('abc\n let x = text.split(\'\\n\');\nghi\n\njkl'); - }); - - it('should trim whitespace from the code before rendering', () => { - hostComponent.linenums = false; - hostComponent.code = '\n\n\n' + multiLineCode + '\n\n\n'; - fixture.detectChanges(); - const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText; - expect(codeContent).toEqual(codeContent.trim()); - }); - - it('should trim whitespace from code before computing whether to format linenums', () => { - hostComponent.code = '\n\n\n' + hostComponent.code + '\n\n\n'; - fixture.detectChanges(); - // `
  • `s are a tell-tale for line numbers - const lis = codeComponentDe.nativeElement.querySelectorAll('li'); - expect(lis.length).toBe(0, 'should be no linenums'); - }); - - it('should display error message when there is no code (after trimming)', () => { - hostComponent.code = ' \n '; - fixture.detectChanges(); - const missing = codeComponentDe.nativeElement.querySelector('.code-missing') as HTMLElement; - expect(missing).not.toBeNull('should have element with "code-missing" class'); - expect(missing.innerText).toContain('missing', 'error message'); - }); - - it('should not display "code-missing" class when there is some code', () => { - fixture.detectChanges(); - const missing = codeComponentDe.nativeElement.querySelector('.code-missing'); - expect(missing).toBeNull('should not have element with "code-missing" class'); - }); - - it('should call copier service when copy button clicked', () => { - const copierService: TestCopierService = codeComponentDe.injector.get(CopierService) ; - const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; - expect(copierService.copyText.calls.count()).toBe(0, 'before click'); - button.click(); - expect(copierService.copyText.calls.count()).toBe(1, 'after click'); - }); - - it('should copy code text when copy button clicked', () => { - const copierService: TestCopierService = codeComponentDe.injector.get(CopierService) ; - const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; - button.click(); - expect(copierService.copyText.calls.argsFor(0)[0]).toEqual(oneLineCode, 'after click'); - }); - }); //// Test helpers //// @@ -174,10 +206,6 @@ class HostComponent { linenums: boolean | number | string; } -class TestCopierService { - copyText = jasmine.createSpy('copyText'); -} - class TestLogger { log = jasmine.createSpy('log'); error = jasmine.createSpy('error'); diff --git a/aio/src/app/embedded/code/code.component.ts b/aio/src/app/embedded/code/code.component.ts index f994c2af6e..2f2bfe738f 100644 --- a/aio/src/app/embedded/code/code.component.ts +++ b/aio/src/app/embedded/code/code.component.ts @@ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, OnChanges, OnDestroy, Input } from '@ import { Logger } from 'app/shared/logger.service'; import { PrettyPrinter } from './pretty-printer.service'; import { CopierService } from 'app/shared/copier.service'; +import { MdSnackBar } from '@angular/material'; const originalLabel = 'Copy Code'; const copiedLabel = 'Copied!'; @@ -54,17 +55,13 @@ export class CodeComponent implements OnChanges { @Input() code: string; - /** - * The label to show on the copy button - */ - buttonLabel = originalLabel; - /** * The element in the template that will display the formatted code */ @ViewChild('codeContainer') codeContainer: ElementRef; constructor( + private snackbar: MdSnackBar, private pretty: PrettyPrinter, private copier: CopierService, private logger: Logger) {} @@ -97,11 +94,16 @@ export class CodeComponent implements OnChanges { const code = this.codeContainer.nativeElement.innerText; if (this.copier.copyText(code)) { this.logger.log('Copied code to clipboard:', code); - // change the button label (for one second) - this.buttonLabel = copiedLabel; - setTimeout(() => this.buttonLabel = originalLabel, 1000); + // success snackbar alert + this.snackbar.open('Code Copied', '', { + duration: 800, + }); } else { this.logger.error('ERROR copying code to clipboard:', code); + // failure snackbar alert + this.snackbar.open('Copy failed. Please try again!', '', { + duration: 800, + }); } } diff --git a/aio/src/styles/1-layouts/_layout-global.scss b/aio/src/styles/1-layouts/_layout-global.scss index 0b124e337e..faee1cfde1 100644 --- a/aio/src/styles/1-layouts/_layout-global.scss +++ b/aio/src/styles/1-layouts/_layout-global.scss @@ -27,9 +27,6 @@ l-relative { flex-wrap: wrap; } -.flex-center { - justify-content: center; -} .flex-center { display: flex; justify-content: center; diff --git a/aio/src/styles/1-layouts/_top-menu.scss b/aio/src/styles/1-layouts/_top-menu.scss index c33431c317..f434e73453 100644 --- a/aio/src/styles/1-layouts/_top-menu.scss +++ b/aio/src/styles/1-layouts/_top-menu.scss @@ -3,9 +3,15 @@ flex: 1 1 auto; } -.nav-link { +aio-top-menu a.nav-link { margin: 0 16px; cursor: pointer; + + &:focus { + background-color: $accentblue; + outline: none; + padding: 21px 16px; + } } .nav-link.home img { diff --git a/aio/src/styles/2-modules/_code.scss b/aio/src/styles/2-modules/_code.scss index 3c26c7007b..9021c83f4e 100644 --- a/aio/src/styles/2-modules/_code.scss +++ b/aio/src/styles/2-modules/_code.scss @@ -91,6 +91,7 @@ aio-code.headed-code { color: $lightgray; background-color: transparent; border: none; + cursor: pointer; &:hover { color: $mediumgray; } diff --git a/aio/src/styles/2-modules/_subsection.scss b/aio/src/styles/2-modules/_subsection.scss index eb81602d8a..bde0b54b24 100644 --- a/aio/src/styles/2-modules/_subsection.scss +++ b/aio/src/styles/2-modules/_subsection.scss @@ -1,11 +1,16 @@ .l-sub-section { color: $darkgray; - background-color: $lightgray; - border-left: 8px solid $mediumgray; + background-color: rgba($blue, 0.05); + border-left: 8px solid $blue; padding: 16px; margin-bottom: 8px; h3 { margin: 8px 0 0; } + + a:hover { + color: $blue; + text-decoration: underline; + } } \ No newline at end of file