2
0
mirror of https://github.com/iconify/iconify.git synced 2025-01-06 07:20:40 +00:00

react: add rendering modes

This commit is contained in:
Vjacheslav Trushkin 2022-04-13 09:55:07 +03:00
parent 4bfcb4ed76
commit 9d6a3dac12
5 changed files with 167 additions and 34 deletions

View File

@ -4,6 +4,16 @@ import type { IconifyIconCustomisations as RawIconCustomisations } from '@iconif
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
/**
* 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: IconifyIcon | string;
// Render mode
mode?: IconifyRenderMode;
// Style
color?: string;

View File

@ -13,7 +13,9 @@ import {
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
import { iconToSVG } from '@iconify/utils/lib/svg/build';
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
@ -26,6 +28,39 @@ const svgDefaults: SVGProps<SVGSVGElement> = {
'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
*/
@ -44,7 +79,7 @@ export const render = (
// True if icon should have vertical-align added
inline: boolean,
// Optional reference for SVG, extracted by React.forwardRef()
// Optional reference for SVG/SPAN, extracted by React.forwardRef()
ref?: IconRef
): JSX.Element => {
// Get default properties
@ -56,14 +91,18 @@ export const render = (
props as FullIconCustomisations
);
// Check mode
const mode: IconifyRenderMode = props.mode || 'inline';
// Create style
const style =
typeof props.style === 'object' && props.style !== null
? props.style
: {};
const style: React.CSSProperties = {};
const customStyle = props.style || {};
// Create SVG component properties
const componentProps = { ...svgDefaults, ref, style };
const componentProps = {
...(mode === 'inline' ? svgDefaults : {}),
ref,
};
// Get element properties
for (let key in props) {
@ -77,6 +116,7 @@ export const render = (
case 'style':
case 'children':
case 'onLoad':
case 'mode':
case '_ref':
case '_inline':
break;
@ -135,29 +175,64 @@ export const render = (
// Generate icon
const item = iconToSVG(icon, customisations);
const renderAttribs = item.attributes;
// 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'
),
};
for (let key in item.attributes) {
componentProps[key] = item.attributes[key];
}
if (item.inline && style.verticalAlign === void 0) {
// Inline display
if (item.inline) {
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);
};

View 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,
});
});
});

View File

@ -3,8 +3,7 @@ import { Icon, InlineIcon } from '../../dist/iconify';
import renderer from 'react-test-renderer';
const iconData = {
body:
'<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
width: 24,
height: 24,
};
@ -67,8 +66,9 @@ describe('Passing attributes', () => {
);
const tree = component.toJSON();
// `style` overrides `color`
expect(tree.props.style).toMatchObject({
color: 'red',
color: 'green',
});
});

View File

@ -3,8 +3,7 @@ import { Icon, InlineIcon } from '../../dist/offline';
import renderer from 'react-test-renderer';
const iconData = {
body:
'<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
body: '<path d="M4 19h16v2H4zm5-4h11v2H9zm-5-4h16v2H4zm0-8h16v2H4zm5 4h11v2H9z" fill="currentColor"/>',
width: 24,
height: 24,
};
@ -69,8 +68,9 @@ describe('Passing attributes', () => {
);
const tree = component.toJSON();
// `style` overrides `color`
expect(tree.props.style).toMatchObject({
color: 'red',
color: 'green',
});
});