mirror of
https://github.com/iconify/iconify.git
synced 2025-01-07 15:44:05 +00:00
react: add rendering modes
This commit is contained in:
parent
4bfcb4ed76
commit
9d6a3dac12
@ -4,6 +4,16 @@ import type { IconifyIconCustomisations as RawIconCustomisations } from '@iconif
|
|||||||
|
|
||||||
export { RawIconCustomisations };
|
export { RawIconCustomisations };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon render mode
|
||||||
|
*
|
||||||
|
* 'style' = 'bg' or 'mask', depending on icon content
|
||||||
|
* 'bg' = inline style using `background`
|
||||||
|
* 'mask' = inline style using `mask`
|
||||||
|
* 'inline' = inline SVG.
|
||||||
|
*/
|
||||||
|
export type IconifyRenderMode = 'style' | 'bg' | 'mask' | 'inline';
|
||||||
|
|
||||||
// Allow rotation to be string
|
// Allow rotation to be string
|
||||||
/**
|
/**
|
||||||
* Icon customisations
|
* Icon customisations
|
||||||
@ -27,6 +37,9 @@ export interface IconifyIconProps extends IconifyIconCustomisations {
|
|||||||
// Icon object or icon name (must be added to storage using addIcon for offline package)
|
// Icon object or icon name (must be added to storage using addIcon for offline package)
|
||||||
icon: IconifyIcon | string;
|
icon: IconifyIcon | string;
|
||||||
|
|
||||||
|
// Render mode
|
||||||
|
mode?: IconifyRenderMode;
|
||||||
|
|
||||||
// Style
|
// Style
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
||||||
|
@ -13,7 +13,9 @@ import {
|
|||||||
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
|
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
|
||||||
import { iconToSVG } from '@iconify/utils/lib/svg/build';
|
import { iconToSVG } from '@iconify/utils/lib/svg/build';
|
||||||
import { replaceIDs } from '@iconify/utils/lib/svg/id';
|
import { replaceIDs } from '@iconify/utils/lib/svg/id';
|
||||||
import type { IconProps, IconRef } from './props';
|
import { iconToHTML } from '@iconify/utils/lib/svg/html';
|
||||||
|
import { svgToURL } from '@iconify/utils/lib/svg/url';
|
||||||
|
import type { IconifyRenderMode, IconProps, IconRef } from './props';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default SVG attributes
|
* Default SVG attributes
|
||||||
@ -26,6 +28,39 @@ const svgDefaults: SVGProps<SVGSVGElement> = {
|
|||||||
'style': {}, // Include style if it isn't set to add verticalAlign later
|
'style': {}, // Include style if it isn't set to add verticalAlign later
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style modes
|
||||||
|
*/
|
||||||
|
const commonProps: Record<string, string> = {
|
||||||
|
display: 'inline-block',
|
||||||
|
};
|
||||||
|
|
||||||
|
const monotoneProps: Record<string, string> = {
|
||||||
|
backgroundColor: 'currentColor',
|
||||||
|
};
|
||||||
|
|
||||||
|
const coloredProps: Record<string, string> = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamically add common props to variables above
|
||||||
|
const propsToAdd: Record<string, string> = {
|
||||||
|
Image: 'var(--svg)',
|
||||||
|
Repeat: 'no-repeat',
|
||||||
|
Size: '100% 100%',
|
||||||
|
};
|
||||||
|
const propsToAddTo: Record<string, Record<string, string>> = {
|
||||||
|
webkitMask: monotoneProps,
|
||||||
|
mask: monotoneProps,
|
||||||
|
background: coloredProps,
|
||||||
|
};
|
||||||
|
for (const prefix in propsToAddTo) {
|
||||||
|
const list = propsToAddTo[prefix];
|
||||||
|
for (const prop in propsToAdd) {
|
||||||
|
list[prefix + prop] = propsToAdd[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default values for customisations for inline icon
|
* Default values for customisations for inline icon
|
||||||
*/
|
*/
|
||||||
@ -44,7 +79,7 @@ export const render = (
|
|||||||
// True if icon should have vertical-align added
|
// True if icon should have vertical-align added
|
||||||
inline: boolean,
|
inline: boolean,
|
||||||
|
|
||||||
// Optional reference for SVG, extracted by React.forwardRef()
|
// Optional reference for SVG/SPAN, extracted by React.forwardRef()
|
||||||
ref?: IconRef
|
ref?: IconRef
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
// Get default properties
|
// Get default properties
|
||||||
@ -56,14 +91,18 @@ export const render = (
|
|||||||
props as FullIconCustomisations
|
props as FullIconCustomisations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check mode
|
||||||
|
const mode: IconifyRenderMode = props.mode || 'inline';
|
||||||
|
|
||||||
// Create style
|
// Create style
|
||||||
const style =
|
const style: React.CSSProperties = {};
|
||||||
typeof props.style === 'object' && props.style !== null
|
const customStyle = props.style || {};
|
||||||
? props.style
|
|
||||||
: {};
|
|
||||||
|
|
||||||
// Create SVG component properties
|
// Create SVG component properties
|
||||||
const componentProps = { ...svgDefaults, ref, style };
|
const componentProps = {
|
||||||
|
...(mode === 'inline' ? svgDefaults : {}),
|
||||||
|
ref,
|
||||||
|
};
|
||||||
|
|
||||||
// Get element properties
|
// Get element properties
|
||||||
for (let key in props) {
|
for (let key in props) {
|
||||||
@ -77,6 +116,7 @@ export const render = (
|
|||||||
case 'style':
|
case 'style':
|
||||||
case 'children':
|
case 'children':
|
||||||
case 'onLoad':
|
case 'onLoad':
|
||||||
|
case 'mode':
|
||||||
case '_ref':
|
case '_ref':
|
||||||
case '_inline':
|
case '_inline':
|
||||||
break;
|
break;
|
||||||
@ -135,29 +175,64 @@ export const render = (
|
|||||||
|
|
||||||
// Generate icon
|
// Generate icon
|
||||||
const item = iconToSVG(icon, customisations);
|
const item = iconToSVG(icon, customisations);
|
||||||
|
const renderAttribs = item.attributes;
|
||||||
|
|
||||||
// Counter for ids based on "id" property to render icons consistently on server and client
|
// Inline display
|
||||||
let localCounter = 0;
|
if (item.inline) {
|
||||||
let id = props.id;
|
|
||||||
if (typeof id === 'string') {
|
|
||||||
// Convert '-' to '_' to avoid errors in animations
|
|
||||||
id = id.replace(/-/g, '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add icon stuff
|
|
||||||
componentProps.dangerouslySetInnerHTML = {
|
|
||||||
__html: replaceIDs(
|
|
||||||
item.body,
|
|
||||||
id ? () => id + 'ID' + localCounter++ : 'iconifyReact'
|
|
||||||
),
|
|
||||||
};
|
|
||||||
for (let key in item.attributes) {
|
|
||||||
componentProps[key] = item.attributes[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.inline && style.verticalAlign === void 0) {
|
|
||||||
style.verticalAlign = '-0.125em';
|
style.verticalAlign = '-0.125em';
|
||||||
}
|
}
|
||||||
|
|
||||||
return React.createElement('svg', componentProps);
|
if (mode === 'inline') {
|
||||||
|
// Add style
|
||||||
|
componentProps.style = {
|
||||||
|
...style,
|
||||||
|
...customStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add icon stuff
|
||||||
|
Object.assign(componentProps, renderAttribs);
|
||||||
|
|
||||||
|
// Counter for ids based on "id" property to render icons consistently on server and client
|
||||||
|
let localCounter = 0;
|
||||||
|
let id = props.id;
|
||||||
|
if (typeof id === 'string') {
|
||||||
|
// Convert '-' to '_' to avoid errors in animations
|
||||||
|
id = id.replace(/-/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add icon stuff
|
||||||
|
componentProps.dangerouslySetInnerHTML = {
|
||||||
|
__html: replaceIDs(
|
||||||
|
item.body,
|
||||||
|
id ? () => id + 'ID' + localCounter++ : 'iconifyReact'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return React.createElement('svg', componentProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render <span> with style
|
||||||
|
const { body, width, height } = icon;
|
||||||
|
const useMask =
|
||||||
|
mode === 'mask' ||
|
||||||
|
(mode === 'bg' ? false : body.indexOf('currentColor') !== -1);
|
||||||
|
|
||||||
|
// Generate SVG
|
||||||
|
const html = iconToHTML(body, {
|
||||||
|
...renderAttribs,
|
||||||
|
width: width + '',
|
||||||
|
height: height + '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate style
|
||||||
|
componentProps.style = {
|
||||||
|
...style,
|
||||||
|
'--svg': svgToURL(html),
|
||||||
|
'width': renderAttribs.width,
|
||||||
|
'height': renderAttribs.height,
|
||||||
|
...commonProps,
|
||||||
|
...(useMask ? monotoneProps : coloredProps),
|
||||||
|
...customStyle,
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
return React.createElement('span', componentProps);
|
||||||
};
|
};
|
||||||
|
45
packages/react/tests/iconify/10-style-mode.test.js
Normal file
45
packages/react/tests/iconify/10-style-mode.test.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '../../dist/iconify';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
const iconData = {
|
||||||
|
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Rendering as span', () => {
|
||||||
|
test('basic icon', () => {
|
||||||
|
const component = renderer.create(
|
||||||
|
<Icon
|
||||||
|
icon={iconData}
|
||||||
|
mode="style"
|
||||||
|
onLoad={() => {
|
||||||
|
// Should be called only for icons loaded from API
|
||||||
|
throw new Error('onLoad called for object!');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const tree = component.toJSON();
|
||||||
|
|
||||||
|
expect(tree).toMatchObject({
|
||||||
|
type: 'span',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
'--svg': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24'%3E%3Cpath d='M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z' fill='currentColor'/%3E%3C/svg%3E")`,
|
||||||
|
'width': '1em',
|
||||||
|
'height': '1em',
|
||||||
|
'display': 'inline-block',
|
||||||
|
'backgroundColor': 'currentColor',
|
||||||
|
'webkitMaskImage': 'var(--svg)',
|
||||||
|
'webkitMaskRepeat': 'no-repeat',
|
||||||
|
'webkitMaskSize': '100% 100%',
|
||||||
|
'maskImage': 'var(--svg)',
|
||||||
|
'maskRepeat': 'no-repeat',
|
||||||
|
'maskSize': '100% 100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -3,8 +3,7 @@ import { Icon, InlineIcon } from '../../dist/iconify';
|
|||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
const iconData = {
|
const iconData = {
|
||||||
body:
|
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
|
||||||
'<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
|
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
};
|
};
|
||||||
@ -67,8 +66,9 @@ describe('Passing attributes', () => {
|
|||||||
);
|
);
|
||||||
const tree = component.toJSON();
|
const tree = component.toJSON();
|
||||||
|
|
||||||
|
// `style` overrides `color`
|
||||||
expect(tree.props.style).toMatchObject({
|
expect(tree.props.style).toMatchObject({
|
||||||
color: 'red',
|
color: 'green',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,8 +3,7 @@ import { Icon, InlineIcon } from '../../dist/offline';
|
|||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
const iconData = {
|
const iconData = {
|
||||||
body:
|
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
|
||||||
'<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
|
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
};
|
};
|
||||||
@ -69,8 +68,9 @@ describe('Passing attributes', () => {
|
|||||||
);
|
);
|
||||||
const tree = component.toJSON();
|
const tree = component.toJSON();
|
||||||
|
|
||||||
|
// `style` overrides `color`
|
||||||
expect(tree.props.style).toMatchObject({
|
expect(tree.props.style).toMatchObject({
|
||||||
color: 'red',
|
color: 'green',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user