Skip to content

Commit 6685892

Browse files
authored
fix(icon): icons will load when content security policies are enabled (#1141)
1 parent c37b691 commit 6685892

File tree

5 files changed

+96
-0
lines changed

5 files changed

+96
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"build.component": "stencil build",
2020
"collection.copy": "node scripts/collection-copy.js",
2121
"start": "stencil build --dev --watch --serve",
22+
"test": "npm run test.spec",
2223
"test.spec": "stencil test --spec",
2324
"release": "np --no-2fa",
2425
"version": "npm run build"

src/components/icon/icon.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Build, Component, Element, Host, Prop, State, Watch, h } from '@stencil
22
import { getSvgContent, ioniconContent } from './request';
33
import { getName, getUrl, inheritAttributes, isRTL } from './utils';
44

5+
let parser: DOMParser;
6+
57
@Component({
68
tag: 'ion-icon',
79
assetsDirs: ['svg'],
@@ -134,11 +136,27 @@ export class Icon {
134136
@Watch('icon')
135137
loadIcon() {
136138
if (Build.isBrowser && this.isVisible) {
139+
if (!parser) {
140+
/**
141+
* Create an instance of the DOM parser. This creates a single
142+
* parser instance for the entire app, which is more efficient.
143+
*/
144+
parser = new DOMParser();
145+
}
137146
const url = getUrl(this);
147+
138148
if (url) {
139149
if (ioniconContent.has(url)) {
140150
// sync if it's already loaded
141151
this.svgContent = ioniconContent.get(url);
152+
} else if (url.startsWith('data:')) {
153+
const doc = parser.parseFromString(url, 'text/html');
154+
const svgEl = doc.body.querySelector('svg');
155+
if (svgEl !== null) {
156+
this.svgContent = svgEl.outerHTML;
157+
} else {
158+
this.svgContent = '';
159+
}
142160
} else {
143161
// async if it hasn't been loaded
144162
getSvgContent(url, this.sanitize).then(() => (this.svgContent = ioniconContent.get(url)));
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect } from '@playwright/test';
2+
import { test } from '../../../utils/test/playwright';
3+
4+
test.describe('icon: csp', () => {
5+
6+
test.beforeEach(async ({ page }) => {
7+
await page.goto('/test/csp');
8+
});
9+
10+
test('should load svg', async ({ page }) => {
11+
const svg = page.locator('ion-icon#icon-usage svg');
12+
await expect(svg).toBeVisible();
13+
});
14+
15+
});

src/components/test/csp/index.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!DOCTYPE html>
2+
<html dir="ltr" lang="en" mode="ios">
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
7+
<meta http-equiv="Content-Security-Policy"
8+
content="default-src 'none'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/*; img-src 'self'; connect-src 'self'; font-src https://fonts.gstatic.com/*'">
9+
<title>IonIcon - Content Security Policy</title>
10+
<script type="module" src="../../build/ionicons.esm.js"></script>
11+
<script nomodule src="../../build/ionicons.js"></script>
12+
<style>
13+
body {
14+
margin: 0;
15+
padding: 16px;
16+
font-size: 32px;
17+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
18+
}
19+
20+
h1 {
21+
margin: 15px 0 5px;
22+
font-size: 25px;
23+
}
24+
25+
h2 {
26+
margin: 15px 0 5px;
27+
font-size: 18px;
28+
}
29+
30+
.custom {
31+
stroke: red;
32+
fill: goldenrod;
33+
color: black;
34+
}
35+
</style>
36+
</head>
37+
38+
<body>
39+
<h1>Ionicons - Test </h1>
40+
41+
<h2>Default</h2>
42+
<ion-icon src="/assets/chat.svg"></ion-icon>
43+
<ion-icon name="add"></ion-icon>
44+
45+
<ion-icon id="icon-usage"
46+
icon="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Watch</title><rect x='136' y='136' width='240' height='240' rx='8' ry='8'/><path d='M384 96h-48V16H176v80h-48a32 32 0 00-32 32v256a32 32 0 0032 32h48v80h160v-80h48a32 32 0 0032-32V128a32 32 0 00-32-32zm8 272a24 24 0 01-24 24H144a24 24 0 01-24-24V144a24 24 0 0124-24h224a24 24 0 0124 24z'/></svg>">
47+
</ion-icon>
48+
49+
<ion-icon class="custom"
50+
icon="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Watch</title><rect x='136' y='136' width='240' height='240' rx='8' ry='8'/><path d='M384 96h-48V16H176v80h-48a32 32 0 00-32 32v256a32 32 0 0032 32h48v80h160v-80h48a32 32 0 0032-32V128a32 32 0 00-32-32zm8 272a24 24 0 01-24 24H144a24 24 0 01-24-24V144a24 24 0 0124-24h224a24 24 0 0124 24z'/></svg>">
51+
</ion-icon>
52+
</body>
53+
54+
</html>

stencil.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export const config: Config = {
2222
src: './components/test/*.svg',
2323
dest: './assets/',
2424
},
25+
{
26+
src: './svg/*.svg',
27+
dest: './build/svg/',
28+
},
29+
{
30+
src: './components/test/',
31+
dest: './test/',
32+
}
2533
],
2634
empty: false,
2735
serviceWorker: false,

0 commit comments

Comments
 (0)