From a42322da0c98383bcfb3d4230bccb14523214391 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Mon, 15 May 2017 18:03:56 -0700 Subject: [PATCH] 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 --- .../embedded/code/code-example.component.ts | 4 +- .../app/embedded/code/code-tabs.component.ts | 3 +- .../app/embedded/code/code.component.spec.ts | 41 ++++++++++++++----- aio/src/app/embedded/code/code.component.ts | 20 +++++++-- aio/src/styles/2-modules/_code.scss | 4 +- 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/aio/src/app/embedded/code/code-example.component.ts b/aio/src/app/embedded/code/code-example.component.ts index 9e9cad1dbb..b35c824e85 100644 --- a/aio/src/app/embedded/code/code-example.component.ts +++ b/aio/src/app/embedded/code/code-example.component.ts @@ -18,7 +18,9 @@ import { getBoolFromAttribute } from 'app/shared/attribute-utils'; template: `
{{title}}
+ [language]="language" [linenums]="linenums" + [path]="path" [region]="region" + [hideCopy]="hideCopy" [title]="title"> ` }) export class CodeExampleComponent implements OnInit { diff --git a/aio/src/app/embedded/code/code-tabs.component.ts b/aio/src/app/embedded/code/code-tabs.component.ts index a7f0556cdd..0d98f912b9 100644 --- a/aio/src/app/embedded/code/code-tabs.component.ts +++ b/aio/src/app/embedded/code/code-tabs.component.ts @@ -27,7 +27,8 @@ export interface TabInfo { {{ tab.title }} + [path]="tab.path" [region]="tab.region" [title]="tab.title" + class="{{ tab.class }}"> ` diff --git a/aio/src/app/embedded/code/code.component.spec.ts b/aio/src/app/embedded/code/code.component.spec.ts index acec0d2e8b..51fffd6278 100644 --- a/aio/src/app/embedded/code/code.component.spec.ts +++ b/aio/src/app/embedded/code/code.component.spec.ts @@ -179,26 +179,45 @@ describe('CodeComponent', () => { 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', () => { hostComponent.hideCopy = true; 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', () => { 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(); + getButton().click(); expect(spy.calls.count()).toBe(1, 'after click'); }); 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(); + getButton().click(); expect(spy.calls.argsFor(0)[0]).toEqual(oneLineCode, 'after click'); }); @@ -207,8 +226,7 @@ describe('CodeComponent', () => { 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(); + getButton().click(); expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 }); }); @@ -217,8 +235,7 @@ describe('CodeComponent', () => { 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(); + getButton().click(); expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 }); }); }); @@ -230,16 +247,18 @@ describe('CodeComponent', () => { selector: 'aio-host-comp', template: ` + [linenums]="linenums" [path]="path" [region]="region" + [hideCopy]="hideCopy" [title]="title"> ` }) class HostComponent { code = oneLineCode; + hideCopy: boolean; language: string; linenums: boolean | number | string; path: string; region: string; - hideCopy: boolean; + title: string; } class TestLogger { diff --git a/aio/src/app/embedded/code/code.component.ts b/aio/src/app/embedded/code/code.component.ts index 3c678fc8d6..280fc0a33c 100644 --- a/aio/src/app/embedded/code/code.component.ts +++ b/aio/src/app/embedded/code/code.component.ts @@ -31,19 +31,32 @@ const defaultLineNumsCount = 10; // by default, show linenums over this number selector: 'aio-code', template: `
-      
+      
       
     
` }) export class CodeComponent implements OnChanges { + ariaLabel = ''; + /** * The code to be formatted, this should already be HTML encoded */ @Input() code: string; + /** + * set to true if the copy button is not to be shown + */ + @Input() + hideCopy: boolean; + /** * The language of the code to render * (could be javascript, dart, typescript, etc) @@ -73,10 +86,10 @@ export class CodeComponent implements OnChanges { region: string; /** - * set to true if the copy button is not to be shown + * title for this snippet (optional) */ @Input() - hideCopy: boolean; + title: string; /** * The element in the template that will display the formatted code @@ -91,6 +104,7 @@ export class CodeComponent implements OnChanges { ngOnChanges() { this.code = this.code && leftAlign(this.code); + this.ariaLabel = this.title ? `Copy code snippet from ${this.title}` : ''; if (!this.code) { const src = this.path ? this.path + (this.region ? '#' + this.region : '') : ''; diff --git a/aio/src/styles/2-modules/_code.scss b/aio/src/styles/2-modules/_code.scss index 4015777436..26f2418850 100644 --- a/aio/src/styles/2-modules/_code.scss +++ b/aio/src/styles/2-modules/_code.scss @@ -104,7 +104,7 @@ code ol { top: 6px; right: 8px; - color: $lightgray; + color: $blue-grey-200; background-color: transparent; border: none; cursor: pointer; @@ -239,4 +239,4 @@ code-tabs md-tab-group *.mat-ripple-element, code-tabs md-tab-group *.mat-tab-bo .sidenav-content code a { color: inherit; font-size: inherit; -} \ No newline at end of file +}