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:
parent
4bfcb4ed76
commit
9d6a3dac12
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
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';
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user