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:
parent
3cad5da5a4
commit
c3fa8803d3
|
@ -1,7 +1,7 @@
|
||||||
<md-toolbar color="primary" class="app-toolbar">
|
<md-toolbar color="primary" class="app-toolbar">
|
||||||
<button class="hamburger" md-button
|
<button class="hamburger" md-button
|
||||||
(click)="sidenav.toggle()" title="Docs menu">
|
(click)="sidenav.toggle()" title="Docs menu">
|
||||||
<md-icon>menu</md-icon>
|
<md-icon svgIcon="menu"></md-icon>
|
||||||
</button>
|
</button>
|
||||||
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
|
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
|
||||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
|
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:
|
// Temporary fix for MdSidenavModule issue:
|
||||||
// crashes with "missing first" operator when SideNav.mode is "over"
|
// 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 { SearchResultsComponent } from './search/search-results/search-results.component';
|
||||||
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
||||||
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -69,6 +93,8 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
SearchService,
|
SearchService,
|
||||||
Platform,
|
Platform,
|
||||||
AutoScrollService,
|
AutoScrollService,
|
||||||
|
{ provide: MdIconRegistry, useClass: CustomMdIconRegistry },
|
||||||
|
svgIconProviders
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
<a *ngIf="node.url != null" href="{{node.url}}" [ngClass]="classes" title="{{node.tooltip}}"
|
<a *ngIf="node.url != null" href="{{node.url}}" [ngClass]="classes" title="{{node.tooltip}}"
|
||||||
(click)="headerClicked()" class="vertical-menu-item heading">
|
(click)="headerClicked()" class="vertical-menu-item heading">
|
||||||
{{node.title}}
|
{{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>
|
||||||
|
|
||||||
<a *ngIf="node.url == null" [ngClass]="classes" title="{{node.tooltip}}"
|
<a *ngIf="node.url == null" [ngClass]="classes" title="{{node.tooltip}}"
|
||||||
(click)="headerClicked()" class="vertical-menu-item heading">
|
(click)="headerClicked()" class="vertical-menu-item heading">
|
||||||
{{node.title}}
|
{{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>
|
||||||
|
|
||||||
<div class="heading-children" [ngClass]="classes">
|
<div class="heading-children" [ngClass]="classes">
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ md-sidenav-container div.mat-sidenav-content {
|
||||||
}
|
}
|
||||||
|
|
||||||
//icons _within_ nav
|
//icons _within_ nav
|
||||||
.material-icons {
|
.mat-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
@ -83,11 +83,12 @@ md-sidenav-container div.mat-sidenav-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
.mat-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
color: $mediumgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-children.expanded {
|
.heading-children.expanded {
|
||||||
|
@ -138,11 +139,11 @@ a.selected.level-1,
|
||||||
padding-left: 30px;
|
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);
|
@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);
|
@include rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
color: $offwhite;
|
color: $offwhite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger md-icon {
|
.hamburger .mat-icon {
|
||||||
position: inherit;
|
position: inherit;
|
||||||
|
color: white;
|
||||||
}
|
}
|
Loading…
Reference in New Issue