mirror of
https://github.com/iconify/iconify.git
synced 2024-12-04 18:23:17 +00:00
chore: rewrite react component
This commit is contained in:
parent
59bf4012b5
commit
e244ddea3d
@ -12,7 +12,11 @@ export function InlineDemo() {
|
||||
id="inline-demo-block-offline"
|
||||
icon="experiment2"
|
||||
/>
|
||||
<FullIcon id="inline-demo-block-full" icon="experiment2" />
|
||||
<FullIcon
|
||||
id="inline-demo-block-full"
|
||||
icon="experiment2"
|
||||
ssr={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Inline icon (behaving line text / icon font):
|
||||
@ -27,6 +31,7 @@ export function InlineDemo() {
|
||||
icon="experiment2"
|
||||
inline={true}
|
||||
mode="style"
|
||||
ssr={true}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -9,8 +9,6 @@ const commands = [];
|
||||
const compile = {
|
||||
// Compile TypeScript src -> lib
|
||||
lib: true,
|
||||
// Fix types for icon components
|
||||
cleanup: true,
|
||||
// Generate bundle from compiled files lib -> dist
|
||||
dist: true,
|
||||
// Generate TypeScript definitions in dist
|
||||
|
@ -1,48 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const reactSearch = "import React from 'react';";
|
||||
const reactImport = 'React';
|
||||
|
||||
function fixTypes(filename) {
|
||||
let content = fs.readFileSync(__dirname + filename, 'utf8');
|
||||
if (content.indexOf(reactSearch) === -1) {
|
||||
throw new Error(`Missing React import in ${filename}`);
|
||||
}
|
||||
let replaced = false;
|
||||
|
||||
['Icon', 'InlineIcon'].forEach((name) => {
|
||||
const searchStart = `export declare const ${name}: ${reactImport}.ForwardRefExoticComponent`;
|
||||
const searchEnd = `& ${reactImport}.RefAttributes<IconRef>>;`;
|
||||
const replace = `export declare const ${name}: (props: IconProps) => ${reactImport}.ReactElement<IconProps, string | ${reactImport}.JSXElementConstructor<any>>;`;
|
||||
|
||||
if (content.indexOf(replace) !== -1) {
|
||||
// Already replaced
|
||||
return;
|
||||
}
|
||||
const parts = content.split(searchStart);
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`Error replacing types for component ${name} in ${filename}`
|
||||
);
|
||||
}
|
||||
const contentStart = parts.shift();
|
||||
const parts2 = parts.shift().split(searchEnd);
|
||||
if (parts2.length < 2) {
|
||||
throw new Error(
|
||||
`Error replacing types for component ${name} in ${filename}`
|
||||
);
|
||||
}
|
||||
parts2.shift();
|
||||
|
||||
content = contentStart + replace + parts2.join(searchEnd);
|
||||
replaced = true;
|
||||
});
|
||||
|
||||
if (replaced) {
|
||||
fs.writeFileSync(__dirname + filename, content, 'utf8');
|
||||
console.log(`Fixed component types in ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
fixTypes('/lib/iconify.d.ts');
|
||||
fixTypes('/lib/offline.d.ts');
|
@ -2,7 +2,11 @@
|
||||
"name": "@iconify/react",
|
||||
"description": "Iconify icon component for React.",
|
||||
"author": "Vjacheslav Trushkin",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0-beta.1",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "next"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/iconify/iconify/issues",
|
||||
"homepage": "https://iconify.design/",
|
||||
@ -20,7 +24,6 @@
|
||||
"build:dist": "rollup -c rollup.config.mjs",
|
||||
"prebuild:api": "api-extractor run --local --verbose --config api-extractor.offline.json",
|
||||
"build:api": "api-extractor run --local --verbose --config api-extractor.iconify.json",
|
||||
"build:cleanup": "node cleanup",
|
||||
"test": "jest --runInBand"
|
||||
},
|
||||
"main": "dist/iconify.js",
|
||||
|
@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
createElement,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
import type { IconifyJSON, IconifyIcon } from '@iconify/types';
|
||||
|
||||
// Core
|
||||
import type { IconifyIconName } from '@iconify/utils/lib/icon/name';
|
||||
import { stringToIcon } from '@iconify/utils/lib/icon/name';
|
||||
import type { IconifyIconSize } from '@iconify/utils/lib/customisations/defaults';
|
||||
import type { IconifyStorageFunctions } from '@iconify/core/lib/storage/functions';
|
||||
import {
|
||||
@ -73,7 +76,7 @@ import type {
|
||||
IconifyIconProps,
|
||||
IconifyRenderMode,
|
||||
IconProps,
|
||||
IconRef,
|
||||
IconElement,
|
||||
} from './props';
|
||||
|
||||
// Render SVG
|
||||
@ -218,240 +221,129 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
|
||||
* Component
|
||||
*/
|
||||
interface InternalIconProps extends IconProps {
|
||||
_ref?: IconRef;
|
||||
_inline: boolean;
|
||||
_ref?: Ref<IconElement> | null;
|
||||
}
|
||||
|
||||
interface IconComponentData {
|
||||
data: IconifyIcon;
|
||||
classes?: string[];
|
||||
}
|
||||
function IconComponent(props: InternalIconProps): JSX.Element {
|
||||
interface State {
|
||||
// Currently rendered icon
|
||||
name: string;
|
||||
|
||||
interface IconComponentState {
|
||||
icon: IconComponentData | null;
|
||||
}
|
||||
|
||||
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
|
||||
icon: null,
|
||||
};
|
||||
// Icon data, null if missing
|
||||
data?: IconifyIcon | null;
|
||||
}
|
||||
const [mounted, setMounted] = useState(!!props.ssr);
|
||||
|
||||
/**
|
||||
* Abort loading icon
|
||||
*/
|
||||
_abortLoading() {
|
||||
if (this._loading) {
|
||||
this._loading.abort();
|
||||
this._loading = null;
|
||||
interface AbortState {
|
||||
callback?: IconifyIconLoaderAbort;
|
||||
}
|
||||
const [abort, setAbort] = useState<AbortState>({});
|
||||
const [state, setState] = useState<State>({
|
||||
name: '',
|
||||
});
|
||||
|
||||
// Cancel loading
|
||||
function cleanup() {
|
||||
const callback = abort.callback;
|
||||
if (callback) {
|
||||
callback();
|
||||
setAbort({});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state
|
||||
*/
|
||||
_setData(icon: IconComponentData | null) {
|
||||
if (this.state.icon !== icon) {
|
||||
this.setState({
|
||||
icon,
|
||||
// Update state
|
||||
function updateState() {
|
||||
const name = props.icon;
|
||||
if (typeof name === 'object') {
|
||||
// Icon as object
|
||||
cleanup();
|
||||
setState({
|
||||
name: '',
|
||||
data: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' &&
|
||||
icon !== null &&
|
||||
typeof icon.body === 'string'
|
||||
) {
|
||||
// Stop loading
|
||||
this._icon = '';
|
||||
this._abortLoading();
|
||||
|
||||
if (changed || state.icon === null) {
|
||||
// Set data if it was changed
|
||||
this._setData({
|
||||
data: icon,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid icon?
|
||||
let iconName: IconifyIconName | null;
|
||||
if (
|
||||
typeof icon !== 'string' ||
|
||||
(iconName = stringToIcon(icon, false, true)) === null
|
||||
) {
|
||||
this._abortLoading();
|
||||
this._setData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load icon
|
||||
const data = getIconData(iconName);
|
||||
if (!data) {
|
||||
// Icon data is not available
|
||||
if (!this._loading || this._loading.name !== icon) {
|
||||
// New icon to load
|
||||
this._abortLoading();
|
||||
this._icon = '';
|
||||
this._setData(null);
|
||||
if (data !== null) {
|
||||
// Icon was not loaded
|
||||
this._loading = {
|
||||
name: icon,
|
||||
abort: loadIcons(
|
||||
[iconName],
|
||||
this._checkIcon.bind(this, false)
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Icon data is available
|
||||
if (this._icon !== icon || state.icon === null) {
|
||||
// New icon or icon has been loaded
|
||||
this._abortLoading();
|
||||
this._icon = icon;
|
||||
|
||||
// Add classes
|
||||
const classes: string[] = ['iconify'];
|
||||
if (iconName.prefix !== '') {
|
||||
classes.push('iconify--' + iconName.prefix);
|
||||
}
|
||||
if (iconName.provider !== '') {
|
||||
classes.push('iconify--' + iconName.provider);
|
||||
}
|
||||
|
||||
// Set data
|
||||
this._setData({
|
||||
// New icon or got icon data
|
||||
const data = getIconData(name);
|
||||
if (state.name !== name || data !== state.data) {
|
||||
cleanup();
|
||||
setState({
|
||||
name,
|
||||
data,
|
||||
classes,
|
||||
});
|
||||
if (this.props.onLoad) {
|
||||
this.props.onLoad(icon);
|
||||
if (data === undefined) {
|
||||
// Load icon, update state when done
|
||||
const callback = loadIcons([name], updateState);
|
||||
setAbort({
|
||||
callback,
|
||||
});
|
||||
} else if (data) {
|
||||
// Icon data is available: trigger onLoad callback if present
|
||||
props.onLoad?.(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component mounted
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._checkIcon(false);
|
||||
}
|
||||
// Mounted state, cleanup for loader
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
updateState();
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Component updated
|
||||
*/
|
||||
componentDidUpdate(oldProps) {
|
||||
if (oldProps.icon !== this.props.icon) {
|
||||
this._checkIcon(true);
|
||||
// Icon changed
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
updateState();
|
||||
}
|
||||
}, [props.icon]);
|
||||
|
||||
// Render icon
|
||||
const { name, data } = state;
|
||||
if (!data) {
|
||||
return props.children
|
||||
? (props.children as JSX.Element)
|
||||
: createElement('span', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort loading
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._abortLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render
|
||||
*/
|
||||
render() {
|
||||
const props = this.props;
|
||||
const icon = this.state.icon;
|
||||
|
||||
if (icon === null) {
|
||||
// Render placeholder
|
||||
return props.children
|
||||
? (props.children as JSX.Element)
|
||||
: React.createElement('span', {});
|
||||
}
|
||||
|
||||
// Add classes
|
||||
let newProps = props;
|
||||
if (icon.classes) {
|
||||
newProps = {
|
||||
...props,
|
||||
className:
|
||||
(typeof props.className === 'string'
|
||||
? props.className + ' '
|
||||
: '') + icon.classes.join(' '),
|
||||
};
|
||||
}
|
||||
|
||||
// Render icon
|
||||
return render(
|
||||
{
|
||||
...defaultIconProps,
|
||||
...icon.data,
|
||||
},
|
||||
newProps,
|
||||
props._inline,
|
||||
props._ref
|
||||
);
|
||||
}
|
||||
return render(
|
||||
{
|
||||
...defaultIconProps,
|
||||
...data,
|
||||
},
|
||||
props,
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Component type
|
||||
type IconComponentType = (props: IconProps) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Block icon
|
||||
*
|
||||
* @param props - Component properties
|
||||
*/
|
||||
export const Icon = React.forwardRef<IconRef, IconProps>(function Icon(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const newProps = {
|
||||
export const Icon = forwardRef<IconElement, IconProps>((props, ref) =>
|
||||
IconComponent({
|
||||
...props,
|
||||
_ref: ref,
|
||||
_inline: false,
|
||||
} as InternalIconProps;
|
||||
return React.createElement(IconComponent, newProps);
|
||||
});
|
||||
})
|
||||
) as IconComponentType;
|
||||
|
||||
/**
|
||||
* Inline icon (has negative verticalAlign that makes it behave like icon font)
|
||||
*
|
||||
* @param props - Component properties
|
||||
*/
|
||||
export const InlineIcon = React.forwardRef<IconRef, IconProps>(
|
||||
function InlineIcon(props, ref) {
|
||||
const newProps = {
|
||||
...props,
|
||||
_ref: ref,
|
||||
_inline: true,
|
||||
} as InternalIconProps;
|
||||
return React.createElement(IconComponent, newProps);
|
||||
}
|
||||
);
|
||||
export const InlineIcon = forwardRef<IconElement, IconProps>((props, ref) =>
|
||||
IconComponent({
|
||||
inline: true,
|
||||
...props,
|
||||
_ref: ref,
|
||||
})
|
||||
) as IconComponentType;
|
||||
|
||||
/**
|
||||
* Internal API
|
||||
|
@ -1,6 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { forwardRef, createElement, type Ref } from 'react';
|
||||
import type { IconifyIcon, IconifyJSON } from '@iconify/types';
|
||||
import type { IconifyIconSize } from '@iconify/utils/lib/customisations/defaults';
|
||||
import { defaultIconProps } from '@iconify/utils/lib/icon/defaults';
|
||||
@ -11,7 +9,7 @@ import type {
|
||||
IconifyIconProps,
|
||||
IconifyRenderMode,
|
||||
IconProps,
|
||||
IconRef,
|
||||
IconElement,
|
||||
} from './props';
|
||||
import { render } from './render';
|
||||
|
||||
@ -31,67 +29,59 @@ export { IconifyIcon, IconifyJSON, IconifyIconSize, IconifyRenderMode };
|
||||
const storage: Record<string, IconifyIcon> = Object.create(null);
|
||||
|
||||
/**
|
||||
* Generate icon
|
||||
* Component
|
||||
*/
|
||||
function component(
|
||||
props: IconProps,
|
||||
inline: boolean,
|
||||
ref?: React.ForwardedRef<IconRef>
|
||||
): JSX.Element {
|
||||
// Split properties
|
||||
const propsIcon = props.icon;
|
||||
const icon =
|
||||
typeof propsIcon === 'string'
|
||||
? storage[propsIcon]
|
||||
: typeof propsIcon === 'object'
|
||||
? propsIcon
|
||||
: null;
|
||||
interface InternalIconProps extends IconProps {
|
||||
_ref?: Ref<IconElement> | null;
|
||||
}
|
||||
|
||||
// Validate icon object
|
||||
if (
|
||||
icon === null ||
|
||||
typeof icon !== 'object' ||
|
||||
typeof icon.body !== 'string'
|
||||
) {
|
||||
function IconComponent(props: InternalIconProps): JSX.Element {
|
||||
const icon = props.icon;
|
||||
const data = typeof icon === 'string' ? storage[icon] : icon;
|
||||
|
||||
if (!data) {
|
||||
return props.children
|
||||
? (props.children as JSX.Element)
|
||||
: React.createElement('span', {});
|
||||
: createElement('span', {});
|
||||
}
|
||||
|
||||
// Valid icon: render it
|
||||
return render(
|
||||
{
|
||||
...defaultIconProps,
|
||||
...icon,
|
||||
...data,
|
||||
},
|
||||
props,
|
||||
inline,
|
||||
ref as IconRef
|
||||
typeof icon === 'string' ? icon : undefined
|
||||
);
|
||||
}
|
||||
|
||||
// Component type
|
||||
type IconComponentType = (props: IconProps) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Block icon
|
||||
*
|
||||
* @param props - Component properties
|
||||
*/
|
||||
export const Icon = React.forwardRef<IconRef, IconProps>(function Icon(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return component(props, false, ref);
|
||||
});
|
||||
export const Icon = forwardRef<IconElement, IconProps>((props, ref) =>
|
||||
IconComponent({
|
||||
...props,
|
||||
_ref: ref,
|
||||
})
|
||||
) as IconComponentType;
|
||||
|
||||
/**
|
||||
* Inline icon (has negative verticalAlign that makes it behave like icon font)
|
||||
*
|
||||
* @param props - Component properties
|
||||
*/
|
||||
export const InlineIcon = React.forwardRef<IconRef, IconProps>(
|
||||
function InlineIcon(props, ref) {
|
||||
return component(props, true, ref);
|
||||
}
|
||||
);
|
||||
export const InlineIcon = forwardRef<IconElement, IconProps>((props, ref) =>
|
||||
IconComponent({
|
||||
inline: true,
|
||||
...props,
|
||||
_ref: ref,
|
||||
})
|
||||
) as IconComponentType;
|
||||
|
||||
/**
|
||||
* Add icon to storage, allowing to call it by name
|
||||
|
@ -53,6 +53,9 @@ export interface IconifyIconProps extends IconifyIconCustomisations {
|
||||
// Unique id, used as base for ids for shapes. Use it to get consistent ids for server side rendering
|
||||
id?: string;
|
||||
|
||||
// If true, icon will be rendered without waiting for component to mount, such as when rendering on server side
|
||||
ssr?: boolean;
|
||||
|
||||
// Callback to call when icon data has been loaded. Used only for icons loaded from API
|
||||
onLoad?: IconifyIconOnLoad;
|
||||
}
|
||||
@ -62,13 +65,12 @@ export interface IconifyIconProps extends IconifyIconCustomisations {
|
||||
*/
|
||||
type IconifyElementProps = SVGProps<SVGSVGElement>;
|
||||
|
||||
export type IconRef = RefAttributes<SVGSVGElement>;
|
||||
|
||||
export interface ReactRefProp {
|
||||
ref?: IconRef;
|
||||
}
|
||||
/**
|
||||
* Reference for SVG element
|
||||
*/
|
||||
export type IconElement = SVGSVGElement | HTMLSpanElement;
|
||||
|
||||
/**
|
||||
* Mix of icon properties and SVGSVGElement properties
|
||||
*/
|
||||
export type IconProps = IconifyElementProps & IconifyIconProps & ReactRefProp;
|
||||
export type IconProps = IconifyElementProps & IconifyIconProps;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
import { createElement } from 'react';
|
||||
import type { SVGProps, CSSProperties } from 'react';
|
||||
import type { IconifyIcon } from '@iconify/types';
|
||||
import { mergeCustomisations } from '@iconify/utils/lib/customisations/merge';
|
||||
import { flipFromString } from '@iconify/utils/lib/customisations/flip';
|
||||
@ -13,9 +13,9 @@ import type {
|
||||
IconifyIconCustomisations,
|
||||
IconifyRenderMode,
|
||||
IconProps,
|
||||
IconRef,
|
||||
} from './props';
|
||||
import { defaultExtendedIconCustomisations } from './props';
|
||||
import { stringToIcon } from '@iconify/utils/lib/icon/name';
|
||||
|
||||
/**
|
||||
* Default SVG attributes
|
||||
@ -85,14 +85,11 @@ export const render = (
|
||||
// Partial properties
|
||||
props: IconProps,
|
||||
|
||||
// True if icon should have vertical-align added
|
||||
inline: boolean,
|
||||
|
||||
// Optional reference for SVG/SPAN, extracted by React.forwardRef()
|
||||
ref?: IconRef
|
||||
// Icon name
|
||||
name?: string
|
||||
): JSX.Element => {
|
||||
// Get default properties
|
||||
const defaultProps = inline
|
||||
const defaultProps = props.inline
|
||||
? inlineDefaults
|
||||
: defaultExtendedIconCustomisations;
|
||||
|
||||
@ -103,14 +100,29 @@ export const render = (
|
||||
const mode: IconifyRenderMode = props.mode || 'svg';
|
||||
|
||||
// Create style
|
||||
const style: React.CSSProperties = {};
|
||||
const style: CSSProperties = {};
|
||||
const customStyle = props.style || {};
|
||||
|
||||
// Create SVG component properties
|
||||
const componentProps = {
|
||||
...(mode === 'svg' ? svgDefaults : {}),
|
||||
ref,
|
||||
};
|
||||
if (name) {
|
||||
const iconName = stringToIcon(name, false, true);
|
||||
if (iconName) {
|
||||
const classNames: string[] = ['iconify'];
|
||||
const props: (keyof typeof iconName)[] = [
|
||||
'provider',
|
||||
'prefix',
|
||||
] as const;
|
||||
for (const prop of props) {
|
||||
if (iconName[prop]) {
|
||||
classNames.push('iconify--' + iconName[prop]);
|
||||
}
|
||||
}
|
||||
componentProps.className = classNames.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Get element properties
|
||||
for (let key in props) {
|
||||
@ -125,8 +137,19 @@ export const render = (
|
||||
case 'children':
|
||||
case 'onLoad':
|
||||
case 'mode':
|
||||
case 'ssr':
|
||||
break;
|
||||
|
||||
// Forward ref
|
||||
case '_ref':
|
||||
case '_inline':
|
||||
componentProps.ref = value;
|
||||
break;
|
||||
|
||||
// Merge class names
|
||||
case 'className':
|
||||
componentProps[key] =
|
||||
(componentProps[key] ? componentProps[key] + ' ' : '') +
|
||||
value;
|
||||
break;
|
||||
|
||||
// Boolean attributes
|
||||
@ -210,7 +233,7 @@ export const render = (
|
||||
)
|
||||
),
|
||||
};
|
||||
return React.createElement('svg', componentProps);
|
||||
return createElement('svg', componentProps);
|
||||
}
|
||||
|
||||
// Render <span> with style
|
||||
@ -235,7 +258,7 @@ export const render = (
|
||||
...commonProps,
|
||||
...(useMask ? monotoneProps : coloredProps),
|
||||
...customStyle,
|
||||
} as React.CSSProperties;
|
||||
} as CSSProperties;
|
||||
|
||||
return React.createElement('span', componentProps);
|
||||
return createElement('span', componentProps);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user