Skip to content

Commit 4911d4b

Browse files
committed
fix(icon): handle references for pages with base tag
Prepends the current path to any SVG elements with attributes pointing to something by id. If the reference isn't prefixed, it won't work on Safari if the page has a `base` tag (which is used by most Angular apps that are using the router). Fixes #9276.
1 parent 877de56 commit 4911d4b

File tree

2 files changed

+90
-2
lines changed

2 files changed

+90
-2
lines changed

src/lib/icon/icon.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {inject, async, fakeAsync, tick, TestBed} from '@angular/core/testing';
22
import {SafeResourceUrl, DomSanitizer, SafeHtml} from '@angular/platform-browser';
33
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
44
import {Component} from '@angular/core';
5+
import {Location} from '@angular/common';
56
import {MatIconModule} from './index';
67
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
78
import {FAKE_SVGS} from './fake-svgs';
@@ -52,7 +53,11 @@ describe('MatIcon', () => {
5253
IconWithBindingAndNgIf,
5354
InlineIcon,
5455
SvgIconWithUserContent,
55-
]
56+
],
57+
providers: [{
58+
provide: Location,
59+
useValue: {path: () => '/fake-path'}
60+
}]
5661
});
5762

5863
TestBed.compileComponents();
@@ -580,6 +585,30 @@ describe('MatIcon', () => {
580585

581586
tick();
582587
}));
588+
589+
it('should prepend the current path to attributes with `url()` references', fakeAsync(() => {
590+
iconRegistry.addSvgIconLiteral('fido', trustHtml(`
591+
<svg>
592+
<filter id="blur">
593+
<feGaussianBlur in="SourceGraphic" stdDeviation="5" />
594+
</filter>
595+
596+
<circle cx="170" cy="60" r="50" fill="green" filter="url('#blur')" />
597+
</svg>
598+
`));
599+
600+
const fixture = TestBed.createComponent(IconFromSvgName);
601+
fixture.componentInstance.iconName = 'fido';
602+
fixture.detectChanges();
603+
const circle = fixture.nativeElement.querySelector('mat-icon svg circle');
604+
605+
// We use a regex to match here, rather than the exact value, because different browsers
606+
// return different quotes through `getAttribute`, while some even omit the quotes altogether.
607+
expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/fake-path#blur['"]?\)$/);
608+
609+
tick();
610+
}));
611+
583612
});
584613

585614
describe('custom fonts', () => {

src/lib/icon/icon.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
OnInit,
1818
SimpleChanges,
1919
ViewEncapsulation,
20+
Optional,
2021
} from '@angular/core';
22+
import {Location} from '@angular/common';
2123
import {CanColor, mixinColor} from '@angular/material/core';
2224
import {coerceBooleanProperty} from '@angular/cdk/coercion';
2325
import {MatIconRegistry} from './icon-registry';
@@ -30,6 +32,27 @@ export class MatIconBase {
3032
}
3133
export const _MatIconMixinBase = mixinColor(MatIconBase);
3234

35+
/** SVG attributes that accept a FuncIRI (e.g. `url(<something>)`). */
36+
const funcIriAttributes = [
37+
'clip-path',
38+
'color-profile',
39+
'src',
40+
'cursor',
41+
'fill',
42+
'filter',
43+
'marker',
44+
'marker-start',
45+
'marker-mid',
46+
'marker-end',
47+
'mask',
48+
'stroke'
49+
];
50+
51+
/** Selector that can be used to find all elements that are using a `FuncIRI`. */
52+
const funcIriAttributeSelector = funcIriAttributes.map(attr => `[${attr}]`).join(', ');
53+
54+
/** Regex that can be used to extract the id out of a FuncIRI. */
55+
const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
3356

3457
/**
3558
* Component to display an icon. It can be used in the following ways:
@@ -112,7 +135,12 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
112135
constructor(
113136
elementRef: ElementRef,
114137
private _iconRegistry: MatIconRegistry,
115-
@Attribute('aria-hidden') ariaHidden: string) {
138+
@Attribute('aria-hidden') ariaHidden: string,
139+
/**
140+
* @deprecated `location` parameter to be made required.
141+
* @breaking-change 8.0.0
142+
*/
143+
@Optional() private _location?: Location) {
116144
super(elementRef);
117145

118146
// If the user has not explicitly set aria-hidden, mark the icon as hidden, as this is
@@ -191,6 +219,9 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
191219
styleTags[i].textContent += ' ';
192220
}
193221

222+
// Note: we do this fix here, rather than the icon registry, because the
223+
// references have to point to the URL at the time that the icon was created.
224+
this._prependCurrentPathToReferences(svg);
194225
this._elementRef.nativeElement.appendChild(svg);
195226
}
196227

@@ -250,4 +281,32 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
250281
private _cleanupFontValue(value: string) {
251282
return typeof value === 'string' ? value.trim().split(' ')[0] : value;
252283
}
284+
285+
/**
286+
* Prepends the current path to all elements that have an attribute pointing to a `FuncIRI`
287+
* reference. This is required because WebKit browsers require references to be prefixed with
288+
* the current path, if the page has a `base` tag.
289+
*/
290+
private _prependCurrentPathToReferences(element: SVGElement) {
291+
// @breaking-change 8.0.0 Remove this null check once `_location` parameter is required.
292+
if (!this._location) {
293+
return;
294+
}
295+
296+
const elementsWithFuncIri = element.querySelectorAll(funcIriAttributeSelector);
297+
const path = this._location.path();
298+
299+
for (let i = 0; i < elementsWithFuncIri.length; i++) {
300+
funcIriAttributes.forEach(attr => {
301+
const value = elementsWithFuncIri[i].getAttribute(attr);
302+
const match = value ? value.match(funcIriPattern) : null;
303+
304+
if (match) {
305+
// Note the quotes inside the `url()`. They're important, because URLs pointing to named
306+
// router outlets can contain parentheses which will break if they aren't quoted.
307+
elementsWithFuncIri[i].setAttribute(attr, `url('${path}#${match[1]}')`);
308+
}
309+
});
310+
}
311+
}
253312
}

0 commit comments

Comments
 (0)