diff --git a/src/lib/icon/icon.spec.ts b/src/lib/icon/icon.spec.ts
index d8df8da37346..0e265e0ee8ef 100644
--- a/src/lib/icon/icon.spec.ts
+++ b/src/lib/icon/icon.spec.ts
@@ -2,7 +2,7 @@ import {inject, async, fakeAsync, tick, TestBed} from '@angular/core/testing';
import {SafeResourceUrl, DomSanitizer, SafeHtml} from '@angular/platform-browser';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {Component} from '@angular/core';
-import {MatIconModule} from './index';
+import {MatIconModule, MAT_ICON_LOCATION} from './index';
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
import {FAKE_SVGS} from './fake-svgs';
import {wrappedErrorMessage} from '@angular/cdk/testing';
@@ -52,7 +52,11 @@ describe('MatIcon', () => {
IconWithBindingAndNgIf,
InlineIcon,
SvgIconWithUserContent,
- ]
+ ],
+ providers: [{
+ provide: MAT_ICON_LOCATION,
+ useValue: {pathname: '/fake-path'}
+ }]
});
TestBed.compileComponents();
@@ -580,6 +584,30 @@ describe('MatIcon', () => {
tick();
}));
+
+ it('should prepend the current path to attributes with `url()` references', fakeAsync(() => {
+ iconRegistry.addSvgIconLiteral('fido', trustHtml(`
+
+ `));
+
+ const fixture = TestBed.createComponent(IconFromSvgName);
+ fixture.componentInstance.iconName = 'fido';
+ fixture.detectChanges();
+ const circle = fixture.nativeElement.querySelector('mat-icon svg circle');
+
+ // We use a regex to match here, rather than the exact value, because different browsers
+ // return different quotes through `getAttribute`, while some even omit the quotes altogether.
+ expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/fake-path#blur['"]?\)$/);
+
+ tick();
+ }));
+
});
describe('custom fonts', () => {
diff --git a/src/lib/icon/icon.ts b/src/lib/icon/icon.ts
index 2c5f67d554ef..a091d25191e7 100644
--- a/src/lib/icon/icon.ts
+++ b/src/lib/icon/icon.ts
@@ -17,7 +17,12 @@ import {
OnInit,
SimpleChanges,
ViewEncapsulation,
+ Optional,
+ InjectionToken,
+ inject,
+ Inject,
} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {MatIconRegistry} from './icon-registry';
@@ -31,6 +36,53 @@ export class MatIconBase {
export const _MatIconMixinBase: CanColorCtor & typeof MatIconBase =
mixinColor(MatIconBase);
+/**
+ * Injection token used to provide the current location to `MatIcon`.
+ * Used to handle server-side rendering and to stub out during unit tests.
+ * @docs-private
+ */
+export const MAT_ICON_LOCATION = new InjectionToken('mat-icon-location', {
+ providedIn: 'root',
+ factory: MAT_ICON_LOCATION_FACTORY
+});
+
+/**
+ * Stubbed out location for `MatIcon`.
+ * @docs-private
+ */
+export interface MatIconLocation {
+ pathname: string;
+}
+
+/** @docs-private */
+export function MAT_ICON_LOCATION_FACTORY(): MatIconLocation {
+ const _document = inject(DOCUMENT);
+ const pathname = (_document && _document.location && _document.location.pathname) || '';
+ return {pathname};
+}
+
+
+/** SVG attributes that accept a FuncIRI (e.g. `url()`). */
+const funcIriAttributes = [
+ 'clip-path',
+ 'color-profile',
+ 'src',
+ 'cursor',
+ 'fill',
+ 'filter',
+ 'marker',
+ 'marker-start',
+ 'marker-mid',
+ 'marker-end',
+ 'mask',
+ 'stroke'
+];
+
+/** Selector that can be used to find all elements that are using a `FuncIRI`. */
+const funcIriAttributeSelector = funcIriAttributes.map(attr => `[${attr}]`).join(', ');
+
+/** Regex that can be used to extract the id out of a FuncIRI. */
+const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
/**
* Component to display an icon. It can be used in the following ways:
@@ -113,7 +165,12 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
constructor(
elementRef: ElementRef,
private _iconRegistry: MatIconRegistry,
- @Attribute('aria-hidden') ariaHidden: string) {
+ @Attribute('aria-hidden') ariaHidden: string,
+ /**
+ * @deprecated `location` parameter to be made required.
+ * @breaking-change 8.0.0
+ */
+ @Optional() @Inject(MAT_ICON_LOCATION) private _location?: MatIconLocation) {
super(elementRef);
// If the user has not explicitly set aria-hidden, mark the icon as hidden, as this is
@@ -192,6 +249,9 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
styleTags[i].textContent += ' ';
}
+ // Note: we do this fix here, rather than the icon registry, because the
+ // references have to point to the URL at the time that the icon was created.
+ this._prependCurrentPathToReferences(svg);
this._elementRef.nativeElement.appendChild(svg);
}
@@ -251,4 +311,32 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
private _cleanupFontValue(value: string) {
return typeof value === 'string' ? value.trim().split(' ')[0] : value;
}
+
+ /**
+ * Prepends the current path to all elements that have an attribute pointing to a `FuncIRI`
+ * reference. This is required because WebKit browsers require references to be prefixed with
+ * the current path, if the page has a `base` tag.
+ */
+ private _prependCurrentPathToReferences(element: SVGElement) {
+ // @breaking-change 8.0.0 Remove this null check once `_location` parameter is required.
+ if (!this._location) {
+ return;
+ }
+
+ const elementsWithFuncIri = element.querySelectorAll(funcIriAttributeSelector);
+ const path = this._location.pathname ? this._location.pathname.split('#')[0] : '';
+
+ for (let i = 0; i < elementsWithFuncIri.length; i++) {
+ funcIriAttributes.forEach(attr => {
+ const value = elementsWithFuncIri[i].getAttribute(attr);
+ const match = value ? value.match(funcIriPattern) : null;
+
+ if (match) {
+ // Note the quotes inside the `url()`. They're important, because URLs pointing to named
+ // router outlets can contain parentheses which will break if they aren't quoted.
+ elementsWithFuncIri[i].setAttribute(attr, `url('${path}#${match[1]}')`);
+ }
+ });
+ }
+ }
}