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
This commit is contained in:
parent
1762047bc0
commit
e7c37d77a8
|
@ -1,6 +1,6 @@
|
||||||
<h1>Features & Benefits</h1>
|
<h1>Features & Benefits</h1>
|
||||||
<article class="l-content ">
|
<article class="l-content ">
|
||||||
<div>
|
<div class="flex-center">
|
||||||
<div><h2 class="text-headline">Cross Platform</h2>
|
<div><h2 class="text-headline">Cross Platform</h2>
|
||||||
<div class="feature-row">
|
<div class="feature-row">
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { Component, DebugElement } from '@angular/core';
|
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 { CodeComponent } from './code.component';
|
||||||
import { CopierService } from 'app/shared//copier.service';
|
import { CopierService } from 'app/shared//copier.service';
|
||||||
|
@ -39,10 +41,11 @@ describe('CodeComponent', () => {
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ MdSnackBarModule, NoopAnimationsModule ],
|
||||||
declarations: [ CodeComponent, HostComponent ],
|
declarations: [ CodeComponent, HostComponent ],
|
||||||
providers: [
|
providers: [
|
||||||
PrettyPrinter,
|
PrettyPrinter,
|
||||||
{provide: CopierService, useClass: TestCopierService },
|
CopierService,
|
||||||
{provide: Logger, useClass: TestLogger }
|
{provide: Logger, useClass: TestLogger }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -65,99 +68,128 @@ describe('CodeComponent', () => {
|
||||||
expect(codeComponent).toBeTruthy('CodeComponent');
|
expect(codeComponent).toBeTruthy('CodeComponent');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format a one-line code sample', () => {
|
describe('pretty printing', () => {
|
||||||
// 'pln' spans are a tell-tale for syntax highlighing
|
it('should format a one-line code sample', () => {
|
||||||
const spans = codeComponentDe.nativeElement.querySelectorAll('span.pln');
|
// 'pln' spans are a tell-tale for syntax highlighing
|
||||||
expect(spans.length).toBeGreaterThan(0, 'formatted spans');
|
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', () => {
|
||||||
|
// `<li>`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();
|
||||||
|
|
||||||
|
// `<li>`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();
|
||||||
|
|
||||||
|
// `<li>`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();
|
||||||
|
|
||||||
|
// `<li>`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', () => {
|
describe('whitespace handling', () => {
|
||||||
// `<li>`s are a tell-tale for line numbers
|
it('should remove common indentation from the code before rendering', () => {
|
||||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
hostComponent.linenums = false;
|
||||||
expect(lis.length).toBe(0, 'should be no linenums');
|
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();
|
||||||
|
// `<li>`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', () => {
|
describe('error message', () => {
|
||||||
hostComponent.linenums = 'true';
|
it('should display error message when there is no code (after trimming)', () => {
|
||||||
fixture.detectChanges();
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
// `<li>`s are a tell-tale for line numbers
|
it('should not display "code-missing" class when there is some code', () => {
|
||||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
fixture.detectChanges();
|
||||||
expect(lis.length).toBe(1, 'has linenums');
|
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', () => {
|
describe('copy button', () => {
|
||||||
hostComponent.code = multiLineCode;
|
it('should call copier service when clicked', () => {
|
||||||
fixture.detectChanges();
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
// `<li>`s are a tell-tale for line numbers
|
it('should copy code text when clicked', () => {
|
||||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
const copierService: CopierService = TestBed.get(CopierService);
|
||||||
expect(lis.length).toBeGreaterThan(0, 'has linenums');
|
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();
|
|
||||||
|
|
||||||
// `<li>`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();
|
|
||||||
// `<li>`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 = <any> 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 = <any> 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 ////
|
//// Test helpers ////
|
||||||
|
@ -174,10 +206,6 @@ class HostComponent {
|
||||||
linenums: boolean | number | string;
|
linenums: boolean | number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCopierService {
|
|
||||||
copyText = jasmine.createSpy('copyText');
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestLogger {
|
class TestLogger {
|
||||||
log = jasmine.createSpy('log');
|
log = jasmine.createSpy('log');
|
||||||
error = jasmine.createSpy('error');
|
error = jasmine.createSpy('error');
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, OnChanges, OnDestroy, Input } from '@
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
import { PrettyPrinter } from './pretty-printer.service';
|
import { PrettyPrinter } from './pretty-printer.service';
|
||||||
import { CopierService } from 'app/shared/copier.service';
|
import { CopierService } from 'app/shared/copier.service';
|
||||||
|
import { MdSnackBar } from '@angular/material';
|
||||||
|
|
||||||
const originalLabel = 'Copy Code';
|
const originalLabel = 'Copy Code';
|
||||||
const copiedLabel = 'Copied!';
|
const copiedLabel = 'Copied!';
|
||||||
|
@ -54,17 +55,13 @@ export class CodeComponent implements OnChanges {
|
||||||
@Input()
|
@Input()
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The label to show on the copy button
|
|
||||||
*/
|
|
||||||
buttonLabel = originalLabel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The element in the template that will display the formatted code
|
* The element in the template that will display the formatted code
|
||||||
*/
|
*/
|
||||||
@ViewChild('codeContainer') codeContainer: ElementRef;
|
@ViewChild('codeContainer') codeContainer: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private snackbar: MdSnackBar,
|
||||||
private pretty: PrettyPrinter,
|
private pretty: PrettyPrinter,
|
||||||
private copier: CopierService,
|
private copier: CopierService,
|
||||||
private logger: Logger) {}
|
private logger: Logger) {}
|
||||||
|
@ -97,11 +94,16 @@ export class CodeComponent implements OnChanges {
|
||||||
const code = this.codeContainer.nativeElement.innerText;
|
const code = this.codeContainer.nativeElement.innerText;
|
||||||
if (this.copier.copyText(code)) {
|
if (this.copier.copyText(code)) {
|
||||||
this.logger.log('Copied code to clipboard:', code);
|
this.logger.log('Copied code to clipboard:', code);
|
||||||
// change the button label (for one second)
|
// success snackbar alert
|
||||||
this.buttonLabel = copiedLabel;
|
this.snackbar.open('Code Copied', '', {
|
||||||
setTimeout(() => this.buttonLabel = originalLabel, 1000);
|
duration: 800,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.logger.error('ERROR copying code to clipboard:', code);
|
this.logger.error('ERROR copying code to clipboard:', code);
|
||||||
|
// failure snackbar alert
|
||||||
|
this.snackbar.open('Copy failed. Please try again!', '', {
|
||||||
|
duration: 800,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,6 @@ l-relative {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.flex-center {
|
.flex-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -3,9 +3,15 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
aio-top-menu a.nav-link {
|
||||||
margin: 0 16px;
|
margin: 0 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: $accentblue;
|
||||||
|
outline: none;
|
||||||
|
padding: 21px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.home img {
|
.nav-link.home img {
|
||||||
|
|
|
@ -91,6 +91,7 @@ aio-code.headed-code {
|
||||||
color: $lightgray;
|
color: $lightgray;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $mediumgray;
|
color: $mediumgray;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
.l-sub-section {
|
.l-sub-section {
|
||||||
color: $darkgray;
|
color: $darkgray;
|
||||||
background-color: $lightgray;
|
background-color: rgba($blue, 0.05);
|
||||||
border-left: 8px solid $mediumgray;
|
border-left: 8px solid $blue;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: $blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue