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
This commit is contained in:
Peter Bacon Darwin 2017-04-21 09:42:33 +01:00 committed by Pete Bacon Darwin
parent 3cad5da5a4
commit c3fa8803d3
7 changed files with 136 additions and 11 deletions

View File

@ -1,7 +1,7 @@
<md-toolbar color="primary" class="app-toolbar">
<button class="hamburger" md-button
(click)="sidenav.toggle()" title="Docs menu">
<md-icon>menu</md-icon>
<md-icon svgIcon="menu"></md-icon>
</button>
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>

View File

@ -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 `<md-icon>` component
export const svgIconProviders = [
{
provide: SVG_ICONS,
useValue: {
name: 'keyboard_arrow_right',
svgSource: '<svg xmlns="http://www.w3.org/2000/svg" focusable="false" ' +
'viewBox="0 0 24 24"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/></svg>'
},
multi: true
},
{
provide: SVG_ICONS,
useValue: {
name: 'menu',
svgSource: '<svg xmlns="http://www.w3.org/2000/svg" focusable="false" ' +
'viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>'
},
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]
})

View File

@ -9,13 +9,13 @@
<a *ngIf="node.url != null" href="{{node.url}}" [ngClass]="classes" title="{{node.tooltip}}"
(click)="headerClicked()" class="vertical-menu-item heading">
{{node.title}}
<md-icon class="rotating-icon">keyboard_arrow_right</md-icon>
<md-icon class="rotating-icon" svgIcon="keyboard_arrow_right"></md-icon>
</a>
<a *ngIf="node.url == null" [ngClass]="classes" title="{{node.tooltip}}"
(click)="headerClicked()" class="vertical-menu-item heading">
{{node.title}}
<md-icon class="rotating-icon">keyboard_arrow_right</md-icon>
<md-icon class="rotating-icon" svgIcon="keyboard_arrow_right"></md-icon>
</a>
<div class="heading-children" [ngClass]="classes">

View File

@ -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 = '<svg xmlns="http://www.w3.org/2000/svg" focusable="false" ' +
'viewBox="0 0 24 24"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/></svg>';
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 = '<svg xmlns="http://www.w3.org/2000/svg" focusable="false" ' +
'viewBox="0 0 24 24"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/></svg>';
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');
}

View File

@ -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<Array<SvgIconInfo>>('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');
});
}
}

View File

@ -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);
}
}

View File

@ -18,6 +18,7 @@
color: $offwhite;
}
.hamburger md-icon {
.hamburger .mat-icon {
position: inherit;
}
color: white;
}