feat(aio): code copy button has tooltip and aria-label
Based on optional title passed in from parent element such as CodeExample or CodeTabs. Darkens uncovered copy button slightly as recommended for a11y. PR #16808
This commit is contained in:
parent
4ccb2269a5
commit
a42322da0c
|
@ -18,7 +18,9 @@ import { getBoolFromAttribute } from 'app/shared/attribute-utils';
|
||||||
template: `
|
template: `
|
||||||
<header *ngIf="title">{{title}}</header>
|
<header *ngIf="title">{{title}}</header>
|
||||||
<aio-code [ngClass]="classes" [code]="code"
|
<aio-code [ngClass]="classes" [code]="code"
|
||||||
[language]="language" [linenums]="linenums" [path]="path" [region]="region" [hideCopy]="hideCopy"></aio-code>
|
[language]="language" [linenums]="linenums"
|
||||||
|
[path]="path" [region]="region"
|
||||||
|
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class CodeExampleComponent implements OnInit {
|
export class CodeExampleComponent implements OnInit {
|
||||||
|
|
|
@ -27,7 +27,8 @@ export interface TabInfo {
|
||||||
<span class="{{tab.class}}">{{ tab.title }}</span>
|
<span class="{{tab.class}}">{{ tab.title }}</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums"
|
<aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums"
|
||||||
[path]="tab.path" [region]="tab.region" class="{{ tab.class }}"></aio-code>
|
[path]="tab.path" [region]="tab.region" [title]="tab.title"
|
||||||
|
class="{{ tab.class }}"></aio-code>
|
||||||
</md-tab>
|
</md-tab>
|
||||||
</md-tab-group>
|
</md-tab-group>
|
||||||
`
|
`
|
||||||
|
|
|
@ -179,26 +179,45 @@ describe('CodeComponent', () => {
|
||||||
|
|
||||||
describe('copy button', () => {
|
describe('copy button', () => {
|
||||||
|
|
||||||
|
function getButton() {
|
||||||
|
const btnDe = fixture.debugElement.query(By.css('button'));
|
||||||
|
return btnDe ? btnDe.nativeElement : null;
|
||||||
|
}
|
||||||
|
|
||||||
it('should be hidden if the `hideCopy` input is true', () => {
|
it('should be hidden if the `hideCopy` input is true', () => {
|
||||||
hostComponent.hideCopy = true;
|
hostComponent.hideCopy = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(fixture.debugElement.query(By.css('button'))).toBe(null);
|
expect(getButton()).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have title', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getButton().title).toBe('Copy code snippet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no aria-label by default', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getButton().getAttribute('aria-label')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have aria-label explaining what is being copied when title passed in', () => {
|
||||||
|
hostComponent.title = 'a/b/c/foo.ts';
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getButton().getAttribute('aria-label')).toContain(hostComponent.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call copier service when clicked', () => {
|
it('should call copier service when clicked', () => {
|
||||||
const copierService: CopierService = TestBed.get(CopierService);
|
const copierService: CopierService = TestBed.get(CopierService);
|
||||||
const spy = spyOn(copierService, 'copyText');
|
const spy = spyOn(copierService, 'copyText');
|
||||||
const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
|
|
||||||
expect(spy.calls.count()).toBe(0, 'before click');
|
expect(spy.calls.count()).toBe(0, 'before click');
|
||||||
button.click();
|
getButton().click();
|
||||||
expect(spy.calls.count()).toBe(1, 'after click');
|
expect(spy.calls.count()).toBe(1, 'after click');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should copy code text when clicked', () => {
|
it('should copy code text when clicked', () => {
|
||||||
const copierService: CopierService = TestBed.get(CopierService);
|
const copierService: CopierService = TestBed.get(CopierService);
|
||||||
const spy = spyOn(copierService, 'copyText');
|
const spy = spyOn(copierService, 'copyText');
|
||||||
const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
|
getButton().click();
|
||||||
button.click();
|
|
||||||
expect(spy.calls.argsFor(0)[0]).toEqual(oneLineCode, 'after click');
|
expect(spy.calls.argsFor(0)[0]).toEqual(oneLineCode, 'after click');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -207,8 +226,7 @@ describe('CodeComponent', () => {
|
||||||
const copierService: CopierService = TestBed.get(CopierService);
|
const copierService: CopierService = TestBed.get(CopierService);
|
||||||
spyOn(snackBar, 'open');
|
spyOn(snackBar, 'open');
|
||||||
spyOn(copierService, 'copyText').and.returnValue(true);
|
spyOn(copierService, 'copyText').and.returnValue(true);
|
||||||
const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
|
getButton().click();
|
||||||
button.click();
|
|
||||||
expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 });
|
expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -217,8 +235,7 @@ describe('CodeComponent', () => {
|
||||||
const copierService: CopierService = TestBed.get(CopierService);
|
const copierService: CopierService = TestBed.get(CopierService);
|
||||||
spyOn(snackBar, 'open');
|
spyOn(snackBar, 'open');
|
||||||
spyOn(copierService, 'copyText').and.returnValue(false);
|
spyOn(copierService, 'copyText').and.returnValue(false);
|
||||||
const button = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
|
getButton().click();
|
||||||
button.click();
|
|
||||||
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
|
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -230,16 +247,18 @@ describe('CodeComponent', () => {
|
||||||
selector: 'aio-host-comp',
|
selector: 'aio-host-comp',
|
||||||
template: `
|
template: `
|
||||||
<aio-code md-no-ink [code]="code" [language]="language"
|
<aio-code md-no-ink [code]="code" [language]="language"
|
||||||
[linenums]="linenums" [path]="path" [region]="region" [hideCopy]="hideCopy"></aio-code>
|
[linenums]="linenums" [path]="path" [region]="region"
|
||||||
|
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
class HostComponent {
|
class HostComponent {
|
||||||
code = oneLineCode;
|
code = oneLineCode;
|
||||||
|
hideCopy: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
linenums: boolean | number | string;
|
linenums: boolean | number | string;
|
||||||
path: string;
|
path: string;
|
||||||
region: string;
|
region: string;
|
||||||
hideCopy: boolean;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestLogger {
|
class TestLogger {
|
||||||
|
|
|
@ -31,19 +31,32 @@ const defaultLineNumsCount = 10; // by default, show linenums over this number
|
||||||
selector: 'aio-code',
|
selector: 'aio-code',
|
||||||
template: `
|
template: `
|
||||||
<pre class="prettyprint lang-{{language}}">
|
<pre class="prettyprint lang-{{language}}">
|
||||||
<button *ngIf="!hideCopy" class="material-icons copy-button" (click)="doCopy()">content_copy</button>
|
<button *ngIf="!hideCopy" class="material-icons copy-button"
|
||||||
|
title="Copy code snippet"
|
||||||
|
[attr.aria-label]="ariaLabel"
|
||||||
|
(click)="doCopy()">
|
||||||
|
<span aria-hidden="true">content_copy</span>
|
||||||
|
</button>
|
||||||
<code class="animated fadeIn" #codeContainer></code>
|
<code class="animated fadeIn" #codeContainer></code>
|
||||||
</pre>
|
</pre>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class CodeComponent implements OnChanges {
|
export class CodeComponent implements OnChanges {
|
||||||
|
|
||||||
|
ariaLabel = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The code to be formatted, this should already be HTML encoded
|
* The code to be formatted, this should already be HTML encoded
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set to true if the copy button is not to be shown
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
hideCopy: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The language of the code to render
|
* The language of the code to render
|
||||||
* (could be javascript, dart, typescript, etc)
|
* (could be javascript, dart, typescript, etc)
|
||||||
|
@ -73,10 +86,10 @@ export class CodeComponent implements OnChanges {
|
||||||
region: string;
|
region: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set to true if the copy button is not to be shown
|
* title for this snippet (optional)
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
hideCopy: boolean;
|
title: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The element in the template that will display the formatted code
|
* The element in the template that will display the formatted code
|
||||||
|
@ -91,6 +104,7 @@ export class CodeComponent implements OnChanges {
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
this.code = this.code && leftAlign(this.code);
|
this.code = this.code && leftAlign(this.code);
|
||||||
|
this.ariaLabel = this.title ? `Copy code snippet from ${this.title}` : '';
|
||||||
|
|
||||||
if (!this.code) {
|
if (!this.code) {
|
||||||
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
|
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
|
||||||
|
|
|
@ -104,7 +104,7 @@ code ol {
|
||||||
top: 6px;
|
top: 6px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
|
||||||
color: $lightgray;
|
color: $blue-grey-200;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
Loading…
Reference in New Issue