diff --git a/packages/core/tests/30-api/30-mock-test.ts b/packages/core/tests/30-api/30-mock-test.ts
index 49d6246..b23c154 100644
--- a/packages/core/tests/30-api/30-mock-test.ts
+++ b/packages/core/tests/30-api/30-mock-test.ts
@@ -7,7 +7,7 @@ import { setAPIModule } from '../../lib/api/modules';
import { API } from '../../lib/api/';
import type { IconifyMockAPIDelayDoneCallback } from '../../lib/api/modules/mock';
import { mockAPIModule, mockAPIData } from '../../lib/api/modules/mock';
-import { allowSimpleNames } from '../../lib/storage/functions';
+import { getStorage, iconExists } from '../../lib/storage/storage';
describe('Testing mock API module', () => {
let prefixCounter = 0;
@@ -238,4 +238,46 @@ describe('Testing mock API module', () => {
}
);
});
+
+ // This is useful for testing component where loadIcons() cannot be accessed
+ it('Using timer in callback for second test', (done) => {
+ const prefix = nextPrefix();
+ const name = 'test1';
+
+ // Mock data
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: {
+ body: '',
+ },
+ },
+ },
+ delay: (next) => {
+ // Icon should not be loaded yet
+ const storage = getStorage(provider, prefix);
+ expect(iconExists(storage, name)).to.be.equal(false);
+
+ // Set data
+ next();
+
+ // Icon should be loaded now
+ expect(iconExists(storage, name)).to.be.equal(true);
+
+ done();
+ },
+ });
+
+ // Load icons
+ API.loadIcons([
+ {
+ provider,
+ prefix,
+ name,
+ },
+ ]);
+ });
});
diff --git a/packages/react-demo/package.json b/packages/react-demo/package.json
index 72b572d..6e2e784 100644
--- a/packages/react-demo/package.json
+++ b/packages/react-demo/package.json
@@ -30,6 +30,7 @@
"devDependencies": {
"@iconify-icons/mdi-light": "^1.1.0",
"@iconify-icons/uil": "^1.1.1",
+ "@iconify/core": "^1.0.0-rc.4",
"@iconify/react": "^3.0.0-dev"
}
}
diff --git a/packages/react-demo/src/App.js b/packages/react-demo/src/App.js
index a263c4a..5fad717 100644
--- a/packages/react-demo/src/App.js
+++ b/packages/react-demo/src/App.js
@@ -6,6 +6,7 @@ import {
import {
addIcon as addOnlineIcon,
addCollection as addOnlineCollection,
+ disableCache,
} from '@iconify/react/dist/iconify';
import presentationPlay from '@iconify-icons/mdi-light/presentation-play';
import playIcon from '@iconify-icons/mdi-light/play';
@@ -14,11 +15,16 @@ import { Checkbox } from './demo-components/Checkbox';
import { InlineDemo } from './demo-components/Inline';
import { OfflineUsageDemo } from './demo-components/UsageOffline';
import { FullOfflineUsageDemo } from './demo-components/UsageFullOffline';
+import { FullUsageDemo } from './demo-components/UsageFull';
import { TestsOffline } from './test-components/TestsOffline';
import { TestsFullOffline } from './test-components/TestsFullOffline';
+import { TestsFull } from './test-components/TestsFull';
import './App.css';
+// Disable cache
+disableCache('all');
+
// Add 'mdi-light:presentation-play' as 'demo' for offline module
addOfflineIcon('demo', presentationPlay);
@@ -76,6 +82,7 @@ function App() {
+
Checkbox
@@ -97,6 +104,7 @@ function App() {
+
);
}
diff --git a/packages/react-demo/src/demo-components/UsageFull.jsx b/packages/react-demo/src/demo-components/UsageFull.jsx
new file mode 100644
index 0000000..cc9020e
--- /dev/null
+++ b/packages/react-demo/src/demo-components/UsageFull.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Icon } from '@iconify/react/dist/iconify';
+
+export function FullUsageDemo() {
+ return (
+
+ Usage (full module)
+
+ Icon referenced by name:
+
+
+
+ Important notice with alert icon!
+
+
+ );
+}
diff --git a/packages/react-demo/src/test-components/TestsFull.jsx b/packages/react-demo/src/test-components/TestsFull.jsx
new file mode 100644
index 0000000..ddffcc9
--- /dev/null
+++ b/packages/react-demo/src/test-components/TestsFull.jsx
@@ -0,0 +1,243 @@
+import React from 'react';
+import { InlineIcon, addAPIProvider, _api } from '@iconify/react/dist/iconify';
+import { mockAPIModule, mockAPIData } from '@iconify/core/lib/api/modules/mock';
+import { TestIcons, toggleTest } from './TestIcons';
+import playIcon from '@iconify-icons/mdi-light/map-marker';
+
+// API provider for tests
+const provider = 'mock-api';
+const prefix = 'demo';
+
+// Set API module for provider
+addAPIProvider(provider, {
+ resources: 'http://localhost',
+ rotate: 10000,
+ timeout: 10000,
+});
+_api.setAPIModule(provider, mockAPIModule);
+
+// Set mock data
+mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ icon: playIcon,
+ },
+ },
+ delay: 2000,
+});
+
+export function TestsFull() {
+ const icon = `@${provider}:${prefix}:icon`;
+
+ return (
+
+ Tests (full module, with API)
+
+ References
+
+ Icons should load 2 seconds after page load
+
+
+
+ Getting reference
+ {
+ const key = 'full-ref1';
+ if (element && element.tagName === 'svg') {
+ toggleTest(key, 'success');
+ } else {
+ toggleTest(key, 'failed');
+ }
+ }}
+ />
+
+
+
+
+ Getting reference for empty icon
+ {
+ // Cannot be called because there is no SVG to render!
+ toggleTest('full-ref-missing', 'failed');
+ }}
+ />
+
+
+
+
+ Getting reference for missing icon with fallback text{' '}
+ {
+ // Cannot be called because there is no SVG to render!
+ toggleTest('full-ref-missing2', 'failed');
+ }}
+ >
+ 😀
+
+
+
+ Style
+
+
+
+ Inline style for icon
+ {
+ const key = 'full-style';
+ if (element && element.tagName === 'svg') {
+ let errors = false;
+
+ // Get style
+ const style = element.style;
+
+ switch (style.color.toLowerCase()) {
+ case 'rgb(23, 105, 170)':
+ case '#1769aa':
+ break;
+
+ default:
+ console.log('Invalid color:', style.color);
+ errors = true;
+ }
+
+ if (style.fontSize !== '24px') {
+ console.log(
+ 'Invalid font-size:',
+ style.fontSize
+ );
+ errors = true;
+ }
+
+ if (style.verticalAlign !== '-0.25em') {
+ console.log(
+ 'Invalid vertical-align:',
+ style.verticalAlign
+ );
+ errors = true;
+ }
+
+ toggleTest(key, !errors);
+ } else {
+ toggleTest(key, 'failed');
+ }
+ }}
+ />
+
+
+
+
+ Green color from attribute:{' '}
+ {
+ const key = 'full-color1';
+ if (element && element.tagName === 'svg') {
+ let errors = false;
+
+ // Get style
+ const style = element.style;
+
+ switch (style.color.toLowerCase()) {
+ case 'rgb(0, 128, 0)':
+ case '#008000':
+ case 'green':
+ break;
+
+ default:
+ console.log('Invalid color:', style.color);
+ errors = true;
+ }
+
+ toggleTest(key, !errors);
+ } else {
+ toggleTest(key, 'failed');
+ }
+ }}
+ />
+
+
+
+
+ Green color from style:{' '}
+ {
+ const key = 'full-color2';
+ if (element && element.tagName === 'svg') {
+ let errors = false;
+
+ // Get style
+ const style = element.style;
+
+ switch (style.color.toLowerCase()) {
+ case 'rgb(0, 128, 0)':
+ case '#008000':
+ case 'green':
+ break;
+
+ default:
+ console.log('Invalid color:', style.color);
+ errors = true;
+ }
+
+ toggleTest(key, !errors);
+ } else {
+ toggleTest(key, 'failed');
+ }
+ }}
+ />
+
+
+
+
+ Green color from attribute (overrides style) + red from style:{' '}
+ {
+ const key = 'full-color3';
+ if (element && element.tagName === 'svg') {
+ let errors = false;
+
+ // Get style
+ const style = element.style;
+
+ switch (style.color.toLowerCase()) {
+ case 'rgb(0, 128, 0)':
+ case '#008000':
+ case 'green':
+ break;
+
+ default:
+ console.log('Invalid color:', style.color);
+ errors = true;
+ }
+
+ toggleTest(key, !errors);
+ } else {
+ toggleTest(key, 'failed');
+ }
+ }}
+ />
+
+
+ );
+}
diff --git a/packages/react/src/iconify.ts b/packages/react/src/iconify.ts
index b705d7b..aaf42c6 100644
--- a/packages/react/src/iconify.ts
+++ b/packages/react/src/iconify.ts
@@ -74,6 +74,7 @@ import type {
// Render SVG
import { render } from './render';
+import { merge } from '@iconify/core/lib/misc/merge';
/**
* Export required types
@@ -292,37 +293,154 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
/**
* Component
*/
-function component(
- props: IconProps,
- inline: boolean,
- ref?: IconRef
-): JSX.Element {
- const icon = props.icon;
+interface InternalIconProps extends IconProps {
+ _ref?: IconRef;
+ _inline: boolean;
+}
- // Check if icon is an object
- if (typeof icon === 'object' && typeof icon.body === 'string') {
- return render(fullIcon(icon), props, inline, ref);
+type IconComponentData = Required | null;
+
+interface IconComponentState {
+ data: IconComponentData;
+}
+
+interface ComponentAbortData {
+ name: string;
+ abort: IconifyIconLoaderAbort;
+}
+
+class IconComponent extends React.Component<
+ InternalIconProps,
+ IconComponentState
+> {
+ protected _icon: string;
+ protected _loading: ComponentAbortData | null;
+
+ constructor(props: InternalIconProps) {
+ super(props);
+ this.state = {
+ // Render placeholder before component is mounted
+ data: null,
+ };
}
- // Check if icon is a string
- if (typeof icon === 'string') {
- const iconName = stringToIcon(icon, true, true);
- if (iconName) {
- // Valid icon name
- const iconData = getIconData(iconName);
- if (iconData) {
- // Icon is available
- return render(iconData, props, inline, ref);
- }
-
- // TODO: icon is missing
+ /**
+ * Abort loading icon
+ */
+ _abortLoading() {
+ if (this._loading) {
+ this._loading.abort();
+ this._loading = null;
}
}
- // Error
- return props.children
- ? (props.children as JSX.Element)
- : React.createElement('span', {});
+ /**
+ * Update state
+ */
+ _setData(data: IconComponentData) {
+ if (this.state.data !== data) {
+ this.setState({
+ data,
+ });
+ }
+ }
+
+ /**
+ * Check if icon should be loaded
+ */
+ _checkIcon(changed: boolean) {
+ const state = this.state;
+ const icon = this.props.icon;
+
+ // Icon is an object
+ if (typeof icon === 'object' && typeof icon.body === 'string') {
+ // Stop loading
+ this._icon = '';
+ this._abortLoading();
+
+ if (changed || state.data === null) {
+ // Set data if it was changed
+ this._setData(fullIcon(icon));
+ }
+ return;
+ }
+
+ // Invalid icon?
+ if (typeof icon !== 'string') {
+ this._abortLoading();
+ this._setData(null);
+ return;
+ }
+
+ // Load icon
+ const data = getIconData(icon);
+ if (data === null) {
+ // Icon needs to be loaded
+ if (!this._loading || this._loading.name !== icon) {
+ // New icon to load
+ this._abortLoading();
+ this._icon = '';
+ this._setData(null);
+ this._loading = {
+ name: icon,
+ abort: API.loadIcons(
+ [icon],
+ this._checkIcon.bind(this, false)
+ ),
+ };
+ }
+ return;
+ }
+
+ // Icon data is available
+ if (this._icon !== icon || state.data === null) {
+ // New icon or icon has been loaded
+ this._abortLoading();
+ this._icon = icon;
+ this._setData(data);
+ }
+ }
+
+ /**
+ * Component mounted
+ */
+ componentDidMount() {
+ this._checkIcon(false);
+ }
+
+ /**
+ * Component updated
+ */
+ componentDidUpdate(oldProps) {
+ if (oldProps.icon !== this.props.icon) {
+ this._checkIcon(true);
+ }
+ }
+
+ /**
+ * Abort loading
+ */
+ componentWillUnmount() {
+ this._abortLoading();
+ }
+
+ /**
+ * Render
+ */
+ render() {
+ const props = this.props;
+ const data = this.state.data;
+
+ if (data === null) {
+ // Render placeholder
+ return props.children
+ ? (props.children as JSX.Element)
+ : React.createElement('span', {});
+ }
+
+ // Render icon
+ return render(data, props, props._inline, props._ref);
+ }
}
/**
@@ -336,7 +454,13 @@ export type Component = (props: IconProps) => JSX.Element;
* @param props - Component properties
*/
export const Icon: Component = React.forwardRef(
- (props: IconProps, ref?: IconRef) => component(props, false, ref)
+ (props: IconProps, ref?: IconRef) => {
+ const newProps = merge(props as Partial, {
+ _ref: ref,
+ _inline: false,
+ }) as InternalIconProps;
+ return React.createElement(IconComponent, newProps);
+ }
);
/**
@@ -345,5 +469,11 @@ export const Icon: Component = React.forwardRef(
* @param props - Component properties
*/
export const InlineIcon: Component = React.forwardRef(
- (props: IconProps, ref?: IconRef) => component(props, true, ref)
+ (props: IconProps, ref?: IconRef) => {
+ const newProps = merge(props as Partial, {
+ _ref: ref,
+ _inline: true,
+ }) as InternalIconProps;
+ return React.createElement(IconComponent, newProps);
+ }
);
diff --git a/packages/react/src/render.ts b/packages/react/src/render.ts
index 1c01c36..637c9e8 100644
--- a/packages/react/src/render.ts
+++ b/packages/react/src/render.ts
@@ -75,6 +75,9 @@ export const render = (
// Properties to ignore
case 'icon':
case 'style':
+ case 'children':
+ case '_ref':
+ case '_inline':
break;
// Flip as string: 'horizontal,vertical'
diff --git a/packages/react/tests/api/20-rendering-from-api.js b/packages/react/tests/api/20-rendering-from-api.js
deleted file mode 100644
index 333034c..0000000
--- a/packages/react/tests/api/20-rendering-from-api.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import { Icon, loadIcons, iconExists } from '../../lib/iconify';
-import { mockAPIData } from '@iconify/core/lib/api/modules/mock';
-import { provider, nextPrefix } from './load';
-
-const iconData = {
- body:
- '',
- width: 24,
- height: 24,
-};
-
-describe('Rendering icon', () => {
- test('rendering icon after loading it', (done) => {
- const prefix = nextPrefix();
- const name = 'mock-test';
- const iconName = `@${provider}:${prefix}:${name}`;
- mockAPIData({
- provider,
- prefix,
- response: {
- prefix,
- icons: {
- [name]: iconData,
- },
- },
- });
-
- // Check if icon has been loaded
- expect(iconExists(iconName)).toEqual(false);
-
- // Load icon
- loadIcons([iconName], (loaded, missing, pending) => {
- // Make sure icon has been loaded
- expect(loaded).toMatchObject([
- {
- provider,
- prefix,
- name,
- },
- ]);
- expect(missing).toMatchObject([]);
- expect(pending).toMatchObject([]);
- expect(iconExists(iconName)).toEqual(true);
-
- // Render component
- const component = renderer.create();
- const tree = component.toJSON();
-
- expect(tree).toMatchObject({
- type: 'svg',
- props: {
- 'xmlns': 'http://www.w3.org/2000/svg',
- 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
- 'aria-hidden': true,
- 'role': 'img',
- 'style': {},
- 'dangerouslySetInnerHTML': {
- __html: iconData.body,
- },
- 'width': '1em',
- 'height': '1em',
- 'preserveAspectRatio': 'xMidYMid meet',
- 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height,
- },
- children: null,
- });
-
- done();
- });
- });
-});
diff --git a/packages/react/tests/api/20-rendering-from-api.test.js b/packages/react/tests/api/20-rendering-from-api.test.js
new file mode 100644
index 0000000..0f66f69
--- /dev/null
+++ b/packages/react/tests/api/20-rendering-from-api.test.js
@@ -0,0 +1,197 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { Icon, loadIcons, iconExists } from '../../lib/iconify';
+import { mockAPIData } from '@iconify/core/lib/api/modules/mock';
+import { provider, nextPrefix } from './load';
+
+const iconData = {
+ body:
+ '',
+ width: 24,
+ height: 24,
+};
+
+describe('Rendering icon', () => {
+ test('rendering icon after loading it', (done) => {
+ const prefix = nextPrefix();
+ const name = 'render-test';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: iconData,
+ },
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Load icon
+ loadIcons([iconName], (loaded, missing, pending) => {
+ // Make sure icon has been loaded
+ expect(loaded).toMatchObject([
+ {
+ provider,
+ prefix,
+ name,
+ },
+ ]);
+ expect(missing).toMatchObject([]);
+ expect(pending).toMatchObject([]);
+ expect(iconExists(iconName)).toEqual(true);
+
+ // Render component
+ const component = renderer.create();
+ const tree = component.toJSON();
+
+ expect(tree).toMatchObject({
+ type: 'svg',
+ props: {
+ 'xmlns': 'http://www.w3.org/2000/svg',
+ 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
+ 'aria-hidden': true,
+ 'role': 'img',
+ 'style': {},
+ 'dangerouslySetInnerHTML': {
+ __html: iconData.body,
+ },
+ 'width': '1em',
+ 'height': '1em',
+ 'preserveAspectRatio': 'xMidYMid meet',
+ 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height,
+ },
+ children: null,
+ });
+
+ done();
+ });
+ });
+
+ test('rendering icon before loading it', (done) => {
+ const prefix = nextPrefix();
+ const name = 'mock-test';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: iconData,
+ },
+ },
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName)).toEqual(true);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ const tree = component.toJSON();
+
+ expect(tree).toMatchObject({
+ type: 'svg',
+ props: {
+ 'xmlns': 'http://www.w3.org/2000/svg',
+ 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
+ 'aria-hidden': true,
+ 'role': 'img',
+ 'style': {},
+ 'dangerouslySetInnerHTML': {
+ __html: iconData.body,
+ },
+ 'width': '1em',
+ 'height': '1em',
+ 'preserveAspectRatio': 'xMidYMid meet',
+ 'viewBox':
+ '0 0 ' +
+ iconData.width +
+ ' ' +
+ iconData.height,
+ },
+ children: null,
+ });
+
+ done();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Render component
+ const component = renderer.create();
+ const tree = component.toJSON();
+
+ // Should render placeholder
+ expect(tree).toMatchObject({
+ type: 'span',
+ props: {},
+ children: null,
+ });
+ });
+
+ test('missing icon', (done) => {
+ const prefix = nextPrefix();
+ const name = 'missing-icon';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ mockAPIData({
+ provider,
+ prefix,
+ response: 404,
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ const tree = component.toJSON();
+
+ expect(tree).toMatchObject({
+ type: 'span',
+ props: {},
+ children: null,
+ });
+
+ done();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Render component
+ const component = renderer.create();
+ const tree = component.toJSON();
+
+ // Should render placeholder
+ expect(tree).toMatchObject({
+ type: 'span',
+ props: {},
+ children: null,
+ });
+ });
+});
diff --git a/packages/react/tests/api/30-changing-props.test.js b/packages/react/tests/api/30-changing-props.test.js
new file mode 100644
index 0000000..734a509
--- /dev/null
+++ b/packages/react/tests/api/30-changing-props.test.js
@@ -0,0 +1,256 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { Icon, iconExists } from '../../lib/iconify';
+import { mockAPIData } from '@iconify/core/lib/api/modules/mock';
+import { provider, nextPrefix } from './load';
+
+const iconData = {
+ body:
+ '',
+ width: 24,
+ height: 24,
+};
+
+const iconData2 = {
+ body:
+ '',
+ width: 32,
+ height: 32,
+};
+
+describe('Rendering icon', () => {
+ test('changing icon property', (done) => {
+ const prefix = nextPrefix();
+ const name = 'changing-prop';
+ const name2 = 'changing-prop2';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ const iconName2 = `@${provider}:${prefix}:${name2}`;
+
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: iconData,
+ },
+ },
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName)).toEqual(true);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ const tree = component.toJSON();
+
+ expect(tree).toMatchObject({
+ type: 'svg',
+ props: {
+ 'xmlns': 'http://www.w3.org/2000/svg',
+ 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
+ 'aria-hidden': true,
+ 'role': 'img',
+ 'style': {},
+ 'dangerouslySetInnerHTML': {
+ __html: iconData.body,
+ },
+ 'width': '1em',
+ 'height': '1em',
+ 'preserveAspectRatio': 'xMidYMid meet',
+ 'viewBox':
+ '0 0 ' +
+ iconData.width +
+ ' ' +
+ iconData.height,
+ },
+ children: null,
+ });
+
+ component.update();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name2]: iconData2,
+ },
+ },
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName2)).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName2)).toEqual(true);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ const tree = component.toJSON();
+
+ expect(tree).toMatchObject({
+ type: 'svg',
+ props: {
+ 'xmlns': 'http://www.w3.org/2000/svg',
+ 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
+ 'aria-hidden': true,
+ 'role': 'img',
+ 'style': {},
+ 'dangerouslySetInnerHTML': {
+ __html: iconData2.body,
+ },
+ 'width': '1em',
+ 'height': '1em',
+ 'preserveAspectRatio': 'xMidYMid meet',
+ 'viewBox':
+ '0 0 ' +
+ iconData2.width +
+ ' ' +
+ iconData2.height,
+ },
+ children: null,
+ });
+
+ done();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Render component
+ const component = renderer.create();
+ const tree = component.toJSON();
+
+ // Should render placeholder
+ expect(tree).toMatchObject({
+ type: 'span',
+ props: {},
+ children: null,
+ });
+ });
+
+ test('changing multiple properties', (done) => {
+ const prefix = nextPrefix();
+ const name = 'multiple-props';
+ const iconName = `@${provider}:${prefix}:${name}`;
+
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: iconData,
+ },
+ },
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName)).toEqual(true);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ let tree = component.toJSON();
+ expect(tree).toMatchObject({
+ type: 'svg',
+ props: {
+ 'xmlns': 'http://www.w3.org/2000/svg',
+ 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
+ 'aria-hidden': true,
+ 'role': 'img',
+ 'style': {},
+ 'dangerouslySetInnerHTML': {
+ __html: iconData.body,
+ },
+ 'width': '1em',
+ 'height': '1em',
+ 'preserveAspectRatio': 'xMidYMid meet',
+ 'viewBox':
+ '0 0 ' +
+ iconData.width +
+ ' ' +
+ iconData.height,
+ },
+ children: null,
+ });
+
+ // Add horizontal flip and style
+ component.update(
+
+ );
+
+ tree = component.toJSON();
+ expect(tree).toMatchObject({
+ type: 'svg',
+ props: {
+ 'xmlns': 'http://www.w3.org/2000/svg',
+ 'xmlnsXlink': 'http://www.w3.org/1999/xlink',
+ 'aria-hidden': true,
+ 'role': 'img',
+ 'style': {
+ color: 'red',
+ },
+ 'dangerouslySetInnerHTML': {
+ __html: `${iconData.body}`,
+ },
+ 'width': '1em',
+ 'height': '1em',
+ 'preserveAspectRatio': 'xMidYMid meet',
+ 'viewBox':
+ '0 0 ' +
+ iconData.width +
+ ' ' +
+ iconData.height,
+ },
+ children: null,
+ });
+ done();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Render component with placeholder text
+ const component = renderer.create(
+ loading...
+ );
+ const tree = component.toJSON();
+
+ // Should render placeholder
+ expect(tree).toEqual('loading...');
+ });
+});
diff --git a/packages/react/tests/api/30-ref.test.js b/packages/react/tests/api/30-ref.test.js
new file mode 100644
index 0000000..6ea39a9
--- /dev/null
+++ b/packages/react/tests/api/30-ref.test.js
@@ -0,0 +1,188 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { Icon, InlineIcon, loadIcons, iconExists } from '../../lib/iconify';
+import { mockAPIData } from '@iconify/core/lib/api/modules/mock';
+import { provider, nextPrefix } from './load';
+
+const iconData = {
+ body:
+ '',
+ width: 24,
+ height: 24,
+};
+
+describe('Testing references', () => {
+ test('reference for preloaded icon', (done) => {
+ const prefix = nextPrefix();
+ const name = 'render-test';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: iconData,
+ },
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Load icon
+ loadIcons([iconName], (loaded, missing, pending) => {
+ let gotRef = false;
+ let gotInlineRef = false;
+
+ // Make sure icon has been loaded
+ expect(loaded).toMatchObject([
+ {
+ provider,
+ prefix,
+ name,
+ },
+ ]);
+ expect(missing).toMatchObject([]);
+ expect(pending).toMatchObject([]);
+ expect(iconExists(iconName)).toEqual(true);
+
+ // Render components
+ renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ renderer.create(
+ {
+ gotInlineRef = true;
+ }}
+ />
+ );
+
+ // References should be called immediately in test
+ expect(gotRef).toEqual(true);
+ expect(gotInlineRef).toEqual(true);
+
+ done();
+ });
+ });
+
+ test('reference to pending icon', (done) => {
+ const prefix = nextPrefix();
+ const name = 'mock-test';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ let gotRef = false;
+
+ mockAPIData({
+ provider,
+ prefix,
+ response: {
+ prefix,
+ icons: {
+ [name]: iconData,
+ },
+ },
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Reference should not have been called yet
+ expect(gotRef).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName)).toEqual(true);
+ expect(gotRef).toEqual(false);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ expect(gotRef).toEqual(true);
+
+ done();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Render component
+ renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Reference should not have been called yet
+ expect(gotRef).toEqual(false);
+ });
+
+ test('missing icon', (done) => {
+ const prefix = nextPrefix();
+ const name = 'missing-icon';
+ const iconName = `@${provider}:${prefix}:${name}`;
+ let gotRef = false;
+
+ mockAPIData({
+ provider,
+ prefix,
+ response: 404,
+ delay: (next) => {
+ // Icon should not have loaded yet
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Reference should not have been called
+ expect(gotRef).toEqual(false);
+
+ // Send icon data
+ next();
+
+ // Test it again
+ expect(iconExists(iconName)).toEqual(false);
+ expect(gotRef).toEqual(false);
+
+ // Check if state was changed
+ // Wrapped in double setTimeout() because re-render takes 2 ticks
+ setTimeout(() => {
+ setTimeout(() => {
+ // Reference should not have been called
+ expect(gotRef).toEqual(false);
+
+ done();
+ }, 0);
+ }, 0);
+ },
+ });
+
+ // Check if icon has been loaded
+ expect(iconExists(iconName)).toEqual(false);
+
+ // Render component
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ >
+ );
+
+ // Reference should not have been called
+ expect(gotRef).toEqual(false);
+ });
+});
diff --git a/packages/react/tests/iconify/20-ref.test.js b/packages/react/tests/iconify/20-ref.test.js
new file mode 100644
index 0000000..01b2931
--- /dev/null
+++ b/packages/react/tests/iconify/20-ref.test.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Icon, InlineIcon } from '../../lib/iconify';
+import renderer from 'react-test-renderer';
+
+const iconData = {
+ body:
+ '',
+ width: 24,
+ height: 24,
+};
+
+describe('Testing references', () => {
+ test('basic icon reference', () => {
+ let gotRef = false;
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Ref should have been called by now
+ expect(gotRef).toEqual(true);
+ });
+
+ test('inline icon reference', () => {
+ let gotRef = false;
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Ref should have been called by now
+ expect(gotRef).toEqual(true);
+ });
+
+ test('placeholder reference', () => {
+ let gotRef = false;
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Ref should not have been called
+ expect(gotRef).toEqual(false);
+ });
+});
diff --git a/packages/react/tests/offline/20-ref.test.js b/packages/react/tests/offline/20-ref.test.js
new file mode 100644
index 0000000..3e3d6e2
--- /dev/null
+++ b/packages/react/tests/offline/20-ref.test.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Icon, InlineIcon } from '../../lib/offline';
+import renderer from 'react-test-renderer';
+
+const iconData = {
+ body:
+ '',
+ width: 24,
+ height: 24,
+};
+
+describe('Testing references', () => {
+ test('basic icon reference', () => {
+ let gotRef = false;
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Ref should have been called by now
+ expect(gotRef).toEqual(true);
+ });
+
+ test('inline icon reference', () => {
+ let gotRef = false;
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Ref should have been called by now
+ expect(gotRef).toEqual(true);
+ });
+
+ test('placeholder reference', () => {
+ let gotRef = false;
+ const component = renderer.create(
+ {
+ gotRef = true;
+ }}
+ />
+ );
+
+ // Ref should not have been called
+ expect(gotRef).toEqual(false);
+ });
+});