From c3fa8803d3ec7b0eb9c99a66156db550bb0c8e65 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 21 Apr 2017 09:42:33 +0100 Subject: [PATCH] fix(aio): use SVG icons for page load sensitive UI The side nav and menu buttons need to appear early on in the loading of the page. Currently we are using icon fonts with ligatures to get icons for these areas. This can result in a flash of unstyled font. By replacing these with SVG icons, we get a better user experience. By overriding the `MdIconRegistry` we can inline the SVG source, which means that there will never by a delay in rendering the icons. The new `CustomMdIconRegistry` expects a multi-provider containing an array of `SvgIconInfo` objects. These objects hold the name and SVG source of the icon. When `MdIconComponent` requests an SVG icon we will get it from the pre-loading cache, if available, before delegating back to the original `MdIconRegistry`. Note that SVG versions of `md-icon` do not apply the `material-icons` CSS class to the element, so the styling for the icons that we are preloading has been changed to use `.mat-icon` instead. Closes #16100 --- aio/src/app/app.component.html | 2 +- aio/src/app/app.module.ts | 28 ++++++++- .../layout/nav-item/nav-item.component.html | 4 +- .../shared/custom-md-icon-registry.spec.ts | 39 +++++++++++++ aio/src/app/shared/custom-md-icon-registry.ts | 58 +++++++++++++++++++ aio/src/styles/1-layouts/_sidenav.scss | 11 ++-- aio/src/styles/2-modules/_hamburger.scss | 5 +- 7 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 aio/src/app/shared/custom-md-icon-registry.spec.ts create mode 100644 aio/src/app/shared/custom-md-icon-registry.ts diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index fcad3b428c..8622e550ca 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -1,7 +1,7 @@ Home diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 2e57d85970..707712daea 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -5,7 +5,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; -import { MdToolbarModule, MdButtonModule, MdIconModule, MdInputModule, MdSidenavModule, MdTabsModule, Platform} from '@angular/material'; +import { MdToolbarModule, MdButtonModule, MdIconModule, MdInputModule, MdSidenavModule, MdTabsModule, Platform, + MdIconRegistry } from '@angular/material'; // Temporary fix for MdSidenavModule issue: // crashes with "missing first" operator when SideNav.mode is "over" @@ -31,6 +32,29 @@ import { NavItemComponent } from 'app/layout/nav-item/nav-item.component'; import { SearchResultsComponent } from './search/search-results/search-results.component'; import { SearchBoxComponent } from './search/search-box/search-box.component'; import { AutoScrollService } from 'app/shared/auto-scroll.service'; +import { CustomMdIconRegistry, SVG_ICONS } from 'app/shared/custom-md-icon-registry'; + +// These are the hardcoded inline svg sources to be used by the `` component +export const svgIconProviders = [ + { + provide: SVG_ICONS, + useValue: { + name: 'keyboard_arrow_right', + svgSource: '' + }, + multi: true + }, + { + provide: SVG_ICONS, + useValue: { + name: 'menu', + svgSource: '' + }, + multi: true + } +]; @NgModule({ imports: [ @@ -69,6 +93,8 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service'; SearchService, Platform, AutoScrollService, + { provide: MdIconRegistry, useClass: CustomMdIconRegistry }, + svgIconProviders ], bootstrap: [AppComponent] }) diff --git a/aio/src/app/layout/nav-item/nav-item.component.html b/aio/src/app/layout/nav-item/nav-item.component.html index 0fde70cbbd..3894100686 100644 --- a/aio/src/app/layout/nav-item/nav-item.component.html +++ b/aio/src/app/layout/nav-item/nav-item.component.html @@ -9,13 +9,13 @@ {{node.title}} - keyboard_arrow_right + {{node.title}} - keyboard_arrow_right +
diff --git a/aio/src/app/shared/custom-md-icon-registry.spec.ts b/aio/src/app/shared/custom-md-icon-registry.spec.ts new file mode 100644 index 0000000000..d7a1db03b0 --- /dev/null +++ b/aio/src/app/shared/custom-md-icon-registry.spec.ts @@ -0,0 +1,39 @@ +import { MdIconRegistry } from '@angular/material'; +import { CustomMdIconRegistry, SVG_ICONS, SvgIconInfo } from './custom-md-icon-registry'; + +describe('CustomMdIconRegistry', () => { + it('should get the SVG element for a preloaded icon from the cache', () => { + const mockHttp: any = {}; + const mockSanitizer: any = {}; + const svgSrc = ''; + const svgIcons: SvgIconInfo[] = [ + { name: 'test_icon', svgSource: svgSrc } + ]; + const registry = new CustomMdIconRegistry(mockHttp, mockSanitizer, svgIcons); + let svgElement: SVGElement; + registry.getNamedSvgIcon('test_icon', null).subscribe(el => svgElement = el as SVGElement); + expect(svgElement).toEqual(createSvg(svgSrc)); + }); + + it('should call through to the MdIconRegistry if the icon name is not in the preloaded cache', () => { + const mockHttp: any = {}; + const mockSanitizer: any = {}; + const svgSrc = ''; + const svgIcons: SvgIconInfo[] = [ + { name: 'test_icon', svgSource: svgSrc } + ]; + spyOn(MdIconRegistry.prototype, 'getNamedSvgIcon'); + + const registry = new CustomMdIconRegistry(mockHttp, mockSanitizer, svgIcons); + registry.getNamedSvgIcon('other_icon', null); + expect(MdIconRegistry.prototype.getNamedSvgIcon).toHaveBeenCalledWith('other_icon', null); + }); +}); + +function createSvg(svgSrc) { + const div = document.createElement('div'); + div.innerHTML = svgSrc; + return div.querySelector('svg'); +} diff --git a/aio/src/app/shared/custom-md-icon-registry.ts b/aio/src/app/shared/custom-md-icon-registry.ts new file mode 100644 index 0000000000..77b05d9169 --- /dev/null +++ b/aio/src/app/shared/custom-md-icon-registry.ts @@ -0,0 +1,58 @@ +import { InjectionToken, Inject, Injectable } from '@angular/core'; +import { of } from 'rxjs/observable/of'; +import { MdIconRegistry } from '@angular/material'; +import { Http } from '@angular/http'; +import { DomSanitizer } from '@angular/platform-browser'; + +/** + * Use SVG_ICONS (and SvgIconInfo) as "multi" providers to provide the SVG source + * code for the icons that you wish to have preloaded in the `CustomMdIconRegistry` + * For compatibility with the MdIconComponent, please ensure that the SVG source has + * the following attributes: + * + * * `xmlns="http://www.w3.org/2000/svg"` + * * `focusable="false"` (disable IE11 default behavior to make SVGs focusable) + * * `height="100%"` (the default) + * * `width="100%"` (the default) + * * `preserveAspectRatio="xMidYMid meet"` (the default) + * + */ +export const SVG_ICONS = new InjectionToken>('SvgIcons'); +export interface SvgIconInfo { + name: string; + svgSource: string; +} + +interface SvgIconMap { + [iconName: string]: SVGElement; +} + +/** + * A custom replacement for Angular Material's `MdIconRegistry`, which allows + * us to provide preloaded icon SVG sources. + */ +@Injectable() +export class CustomMdIconRegistry extends MdIconRegistry { + private preloadedSvgElements: SvgIconMap = {}; + + constructor(http: Http, sanitizer: DomSanitizer, @Inject(SVG_ICONS) svgIcons: SvgIconInfo[]) { + super(http, sanitizer); + this.loadSvgElements(svgIcons); + } + + getNamedSvgIcon(iconName, namespace) { + if (this.preloadedSvgElements[iconName]) { + return of(this.preloadedSvgElements[iconName].cloneNode(true)); + } + return super.getNamedSvgIcon(iconName, namespace); + } + + private loadSvgElements(svgIcons: SvgIconInfo[]) { + const div = document.createElement('DIV'); + svgIcons.forEach(icon => { + // SECURITY: the source for the SVG icons is provided in code by trusted developers + div.innerHTML = icon.svgSource; + this.preloadedSvgElements[icon.name] = div.querySelector('svg'); + }); + } +} diff --git a/aio/src/styles/1-layouts/_sidenav.scss b/aio/src/styles/1-layouts/_sidenav.scss index 2749bba3d0..a6ccfa1362 100644 --- a/aio/src/styles/1-layouts/_sidenav.scss +++ b/aio/src/styles/1-layouts/_sidenav.scss @@ -58,7 +58,7 @@ md-sidenav-container div.mat-sidenav-content { } //icons _within_ nav - .material-icons { + .mat-icon { position: absolute; top: 6px; margin-left: 10px; @@ -83,11 +83,12 @@ md-sidenav-container div.mat-sidenav-content { width: 100%; } -.material-icons { +.mat-icon { display: inline-block; position: absolute; top: 6px; right: 8px; + color: $mediumgray; } .heading-children.expanded { @@ -138,11 +139,11 @@ a.selected.level-1, padding-left: 30px; } -.level-1.expanded .material-icons, .level-2.expanded .material-icons { +.level-1.expanded .mat-icon, .level-2.expanded .mat-icon { @include rotate(90deg); } -.level-1:not(.expanded) .material-icons, .level-2:not(.expanded) .material-icons { +.level-1:not(.expanded) .mat-icon, .level-2:not(.expanded) .mat-icon { @include rotate(0deg); } @@ -161,5 +162,5 @@ aio-nav-menu.top-menu { aio-nav-item:last-child div { border-bottom: 2px solid rgba($mediumgray, 0.5); } - + } \ No newline at end of file diff --git a/aio/src/styles/2-modules/_hamburger.scss b/aio/src/styles/2-modules/_hamburger.scss index 1380f1637b..a628062f7b 100644 --- a/aio/src/styles/2-modules/_hamburger.scss +++ b/aio/src/styles/2-modules/_hamburger.scss @@ -18,6 +18,7 @@ color: $offwhite; } -.hamburger md-icon { +.hamburger .mat-icon { position: inherit; -} \ No newline at end of file + color: white; +}