mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-12 13:38:32 +00:00
perf(lucide-react): cache static icon work and speed up rendering
- Icon: build class names, view box and child elements once per icon data instead of on every render, and build the svg props directly - Icon: render nested icon nodes, which were dropped before - createLucideIcon: drop the redundant className destructuring - DynamicIcon: remember loaded icon data so an icon that was loaded before renders right away, without the fallback - shared: cheaper mergeClasses and buildLucideIconNode with identical output - add a render benchmark (pnpm bench) Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G9u8Fgx7hqgYx7gfUQgfY6
This commit is contained in:
@@ -100,6 +100,7 @@
|
||||
"typecheck": "pnpm build:icons && tsc --noEmit",
|
||||
"typecheck:watch": "tsc -w",
|
||||
"test": "pnpm build:icons && vitest run",
|
||||
"bench": "NODE_ENV=production vitest bench --run --reporter=verbose",
|
||||
"test:watch": "vitest watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -19,6 +19,11 @@ interface DynamicIconComponentProps extends LucideProps {
|
||||
fallback?: () => JSX.Element | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon data that has been loaded before, so an icon that is used again renders right away.
|
||||
*/
|
||||
const loadedIcons = new Map<IconName, LucideIconData>();
|
||||
|
||||
async function getIconData(name: IconName) {
|
||||
if (!(name in dynamicIconImports)) {
|
||||
throw new Error('[lucide-react]: Name in Lucide DynamicIcon not found');
|
||||
@@ -28,6 +33,8 @@ async function getIconData(name: IconName) {
|
||||
const icon = (await dynamicIconImports[name]()) as DynamicIconModule;
|
||||
|
||||
if (icon.__iconData != null) {
|
||||
loadedIcons.set(name, icon.__iconData);
|
||||
|
||||
return icon.__iconData;
|
||||
}
|
||||
|
||||
@@ -52,9 +59,18 @@ async function getIconData(name: IconName) {
|
||||
*/
|
||||
const DynamicIcon = forwardRef<SVGSVGElement, DynamicIconComponentProps>(
|
||||
({ name, fallback: Fallback, ...props }, ref) => {
|
||||
const [iconData, setIconData] = useState<LucideIconData>();
|
||||
const loadedIcon = loadedIcons.get(name);
|
||||
const [lastIconData, setIconData] = useState(loadedIcon);
|
||||
// While another icon is loading, the previous one stays in place.
|
||||
const iconData = loadedIcon ?? lastIconData;
|
||||
|
||||
useEffect(() => {
|
||||
if (loadedIcon != null) {
|
||||
setIconData(loadedIcon);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let ignore = false;
|
||||
|
||||
getIconData(name)
|
||||
@@ -70,7 +86,7 @@ const DynamicIcon = forwardRef<SVGSVGElement, DynamicIconComponentProps>(
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [name]);
|
||||
}, [name, loadedIcon]);
|
||||
|
||||
if (iconData == null) {
|
||||
if (Fallback == null) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { createElement, forwardRef } from 'react';
|
||||
import { buildLucideIconForReact, hasA11yProp, mergeClasses } from '@lucide/shared';
|
||||
import { createElement, forwardRef, type ReactElement } from 'react';
|
||||
import { defaultReactAttributes, hasA11yProp, mergeClasses } from '@lucide/shared';
|
||||
import { LucideIconData, LucideIconNode, LucideProps } from './types';
|
||||
import { useLucideContext } from './context';
|
||||
|
||||
@@ -17,6 +17,80 @@ type IconComponentProps = LucideProps &
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Everything about an icon that doesn't depend on the props it is rendered with.
|
||||
*/
|
||||
type StaticIcon = {
|
||||
viewBox: string;
|
||||
classNames: string[];
|
||||
className: string;
|
||||
children: ReactElement[];
|
||||
nonScalingChildren?: ReactElement[];
|
||||
};
|
||||
|
||||
const DEFAULT_SIZE = defaultReactAttributes.width;
|
||||
const EMPTY_ICON_NODE: LucideIconNode[] = [];
|
||||
const NON_SCALING_STROKE = { vectorEffect: 'non-scaling-stroke' } as const;
|
||||
|
||||
const staticIcons = new WeakMap<LucideIconData, StaticIcon>();
|
||||
const legacyIcons = new WeakMap<LucideIconNode[], LucideIconData>();
|
||||
|
||||
const createNodeElement = (
|
||||
[tag, attrs, children]: LucideIconNode,
|
||||
index: number,
|
||||
extraAttrs?: object,
|
||||
): ReactElement =>
|
||||
createElement(
|
||||
tag,
|
||||
// The generated icons carry a key per node, custom icon nodes may not.
|
||||
{ key: index, ...extraAttrs, ...attrs },
|
||||
children?.map((child, childIndex) => createNodeElement(child, childIndex)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Icon data is static, so the class names, the view box and the child elements are created once
|
||||
* per icon instead of on every render. Handing React the very same child elements again also lets
|
||||
* it skip them when an icon re-renders.
|
||||
*/
|
||||
const getStaticIcon = (icon: LucideIconData): StaticIcon => {
|
||||
let staticIcon = staticIcons.get(icon);
|
||||
|
||||
if (staticIcon == null) {
|
||||
const classNames = ['lucide'];
|
||||
|
||||
if (icon.name) {
|
||||
classNames.push(`lucide-${icon.name}`);
|
||||
}
|
||||
|
||||
for (const alias of icon.aliases ?? []) {
|
||||
if (typeof alias === 'string' && alias.trim() !== '') {
|
||||
classNames.push(`lucide-${alias}`);
|
||||
}
|
||||
}
|
||||
|
||||
staticIcon = {
|
||||
viewBox: `0 0 ${icon.size ?? icon.width ?? DEFAULT_SIZE} ${icon.size ?? icon.height ?? DEFAULT_SIZE}`,
|
||||
classNames,
|
||||
className: mergeClasses(...classNames),
|
||||
children: icon.node.map((node, index) => createNodeElement(node, index)),
|
||||
};
|
||||
staticIcons.set(icon, staticIcon);
|
||||
}
|
||||
|
||||
return staticIcon;
|
||||
};
|
||||
|
||||
const getLegacyIcon = (iconNode: LucideIconNode[]): LucideIconData => {
|
||||
let icon = legacyIcons.get(iconNode);
|
||||
|
||||
if (icon == null) {
|
||||
icon = { node: iconNode, size: DEFAULT_SIZE };
|
||||
legacyIcons.set(iconNode, icon);
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lucide icon component
|
||||
*
|
||||
@@ -43,51 +117,56 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
|
||||
strokeWidth,
|
||||
absoluteStrokeWidth,
|
||||
nonScalingStroke,
|
||||
className = '',
|
||||
className,
|
||||
children,
|
||||
iconNode = [],
|
||||
icon = {
|
||||
node: iconNode,
|
||||
aliases: [],
|
||||
size: 24,
|
||||
},
|
||||
iconNode = EMPTY_ICON_NODE,
|
||||
icon = getLegacyIcon(iconNode),
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const {
|
||||
size: contextSize = 24,
|
||||
strokeWidth: contextStrokeWidth = 2,
|
||||
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
|
||||
nonScalingStroke: contextNonScalingStroke = false,
|
||||
color: contextColor = 'currentColor',
|
||||
className: contextClass = '',
|
||||
} = useLucideContext() ?? {};
|
||||
const context = useLucideContext();
|
||||
const staticIcon = getStaticIcon(icon);
|
||||
|
||||
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
|
||||
const resolvedWidth = width ?? size ?? context.size ?? DEFAULT_SIZE;
|
||||
const resolvedStrokeWidth =
|
||||
strokeWidth ?? context.strokeWidth ?? defaultReactAttributes.strokeWidth;
|
||||
const contextClassName = context.className;
|
||||
|
||||
const [name, svgAttributes, builtIconNode = []] = buildLucideIconForReact(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
strokeWidth: strokeWidth ?? contextStrokeWidth,
|
||||
absoluteStrokeWidth: absoluteStrokeWidth ?? contextAbsoluteStrokeWidth,
|
||||
nonScalingStroke: nonScalingStroke ?? contextNonScalingStroke,
|
||||
className: mergeClasses(contextClass, className),
|
||||
hasA11yProp: hasAccessibleProp,
|
||||
attributes: rest,
|
||||
});
|
||||
let iconChildren = staticIcon.children;
|
||||
|
||||
if (nonScalingStroke ?? context.nonScalingStroke) {
|
||||
iconChildren = staticIcon.nonScalingChildren ??= icon.node.map((node, index) =>
|
||||
createNodeElement(node, index, NON_SCALING_STROKE),
|
||||
);
|
||||
}
|
||||
|
||||
return createElement(
|
||||
name,
|
||||
'svg',
|
||||
{
|
||||
ref,
|
||||
...svgAttributes,
|
||||
...defaultReactAttributes,
|
||||
width: resolvedWidth,
|
||||
height: height ?? size ?? context.size ?? DEFAULT_SIZE,
|
||||
viewBox: staticIcon.viewBox,
|
||||
stroke: (color ?? context.color) || defaultReactAttributes.stroke,
|
||||
strokeWidth:
|
||||
(absoluteStrokeWidth ?? context.absoluteStrokeWidth)
|
||||
? (Number(resolvedStrokeWidth) * Number(icon.size ?? icon.width ?? DEFAULT_SIZE)) /
|
||||
Number(resolvedWidth)
|
||||
: resolvedStrokeWidth,
|
||||
className:
|
||||
className || contextClassName
|
||||
? mergeClasses(
|
||||
...staticIcon.classNames,
|
||||
...`${contextClassName ?? ''} ${className ?? ''}`.split(' '),
|
||||
)
|
||||
: staticIcon.className,
|
||||
...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }),
|
||||
...rest,
|
||||
},
|
||||
[
|
||||
...builtIconNode.map(([tag, attrs]) => createElement(tag, attrs)),
|
||||
...(Array.isArray(children) ? children : [children]),
|
||||
],
|
||||
iconChildren,
|
||||
children,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -35,13 +35,8 @@ function createLucideIcon(
|
||||
? toLucideIconData(iconDataOrName, iconNode, aliases)
|
||||
: iconDataOrName;
|
||||
|
||||
const Component = forwardRef<SVGSVGElement, LucideProps>(({ className, ...props }, ref) =>
|
||||
createElement(Icon, {
|
||||
ref,
|
||||
icon: iconData,
|
||||
className,
|
||||
...props,
|
||||
}),
|
||||
const Component = forwardRef<SVGSVGElement, LucideProps>((props, ref) =>
|
||||
createElement(Icon, { ...props, ref, icon: iconData }),
|
||||
);
|
||||
|
||||
if (iconData.name) {
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('Using DynamicIcon Component', () => {
|
||||
const FallbackComponent = () => <span data-testid="fallback">Loading...</span>;
|
||||
const { getByTestId } = render(
|
||||
<DynamicIcon
|
||||
name="smile"
|
||||
name="airplay"
|
||||
fallback={FallbackComponent}
|
||||
/>,
|
||||
);
|
||||
@@ -89,6 +89,37 @@ describe('Using DynamicIcon Component', () => {
|
||||
expect(getByTestId('fallback')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should render an icon that was loaded before right away', async () => {
|
||||
const FallbackComponent = () => <span data-testid="fallback">Loading...</span>;
|
||||
const first = render(<DynamicIcon name="anchor" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(first.container.firstChild).toHaveClass('lucide-anchor');
|
||||
});
|
||||
|
||||
const second = render(
|
||||
<DynamicIcon
|
||||
name="anchor"
|
||||
fallback={FallbackComponent}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(second.queryByTestId('fallback')).toBeNull();
|
||||
expect(second.container.firstChild).toHaveClass('lucide-anchor');
|
||||
});
|
||||
|
||||
it('should switch to an icon that was loaded before right away', async () => {
|
||||
const { container, rerender } = render(<DynamicIcon name="anchor" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toHaveClass('lucide-anchor');
|
||||
});
|
||||
|
||||
rerender(<DynamicIcon name="smile" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass('lucide-smile');
|
||||
});
|
||||
|
||||
it('should forward ref to the rendered svg element', async () => {
|
||||
const ref = { current: null as SVGSVGElement | null };
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, test } from 'vitest';
|
||||
import { createElement, type ReactElement } from 'react';
|
||||
// @ts-ignore react-dom ships without type declarations
|
||||
import { flushSync } from 'react-dom';
|
||||
// @ts-ignore react-dom ships without type declarations
|
||||
import { createRoot } from 'react-dom/client';
|
||||
// @ts-ignore react-dom ships without type declarations
|
||||
import { renderToString } from 'react-dom/server';
|
||||
|
||||
import { createLucideIcon, LucideProvider } from '../src/lucide-react';
|
||||
import { airVent, coffee } from './testIconNodes';
|
||||
|
||||
// Benchmarks drive React directly, outside of act().
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = false;
|
||||
|
||||
const COUNT = 1000;
|
||||
|
||||
const AirVent = createLucideIcon({ name: 'air-vent', size: 24, node: airVent });
|
||||
const Coffee = createLucideIcon({ name: 'coffee', size: 24, node: coffee, aliases: ['cup'] });
|
||||
|
||||
const list = (props: Record<string, unknown> = {}) =>
|
||||
createElement(
|
||||
'div',
|
||||
null,
|
||||
Array.from({ length: COUNT }, (_, index) =>
|
||||
createElement(index % 2 ? AirVent : Coffee, { key: index, ...props }),
|
||||
),
|
||||
);
|
||||
|
||||
const withProvider = (children: ReactElement) =>
|
||||
createElement(LucideProvider, { size: 32, color: 'red', strokeWidth: 1.5, children });
|
||||
|
||||
const mount = (element: ReactElement) => {
|
||||
const root = createRoot(document.createElement('div'));
|
||||
flushSync(() => root.render(element));
|
||||
root.unmount();
|
||||
};
|
||||
|
||||
const rerenderer = (make: () => ReactElement) => {
|
||||
const root = createRoot(document.createElement('div'));
|
||||
flushSync(() => root.render(make()));
|
||||
|
||||
return () => flushSync(() => root.render(make()));
|
||||
};
|
||||
|
||||
describe(`server render of ${COUNT} icons`, () => {
|
||||
test('default props', async ({ bench }) => {
|
||||
await bench('default props', () => {
|
||||
renderToString(list());
|
||||
}).run();
|
||||
});
|
||||
|
||||
test('with props', async ({ bench }) => {
|
||||
await bench('with props', () => {
|
||||
renderToString(list({ size: 32, color: 'red', className: 'icon', strokeWidth: 1.5 }));
|
||||
}).run();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`client mount of ${COUNT} icons`, () => {
|
||||
test('default props', async ({ bench }) => {
|
||||
await bench('default props', () => {
|
||||
mount(list());
|
||||
}).run();
|
||||
});
|
||||
|
||||
test('inside LucideProvider', async ({ bench }) => {
|
||||
await bench('inside LucideProvider', () => {
|
||||
mount(withProvider(list()));
|
||||
}).run();
|
||||
});
|
||||
|
||||
test('nonScalingStroke', async ({ bench }) => {
|
||||
await bench('nonScalingStroke', () => {
|
||||
mount(list({ nonScalingStroke: true }));
|
||||
}).run();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`client re-render of ${COUNT} icons`, () => {
|
||||
const rerenderDefault = rerenderer(() => list());
|
||||
const rerenderProps = rerenderer(() => list({ size: 32, color: 'red', className: 'icon' }));
|
||||
const rerenderProvider = rerenderer(() => withProvider(list()));
|
||||
|
||||
test('default props', async ({ bench }) => {
|
||||
await bench('default props', () => {
|
||||
rerenderDefault();
|
||||
}).run();
|
||||
});
|
||||
|
||||
test('with props', async ({ bench }) => {
|
||||
await bench('with props', () => {
|
||||
rerenderProps();
|
||||
}).run();
|
||||
});
|
||||
|
||||
test('inside LucideProvider', async ({ bench }) => {
|
||||
await bench('inside LucideProvider', () => {
|
||||
rerenderProvider();
|
||||
}).run();
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,99 @@ describe('Using Icon Component', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Icon Component static icon data', () => {
|
||||
it('should render nested icon nodes', () => {
|
||||
const { container } = render(
|
||||
<Icon
|
||||
icon={{
|
||||
name: 'nested',
|
||||
size: 24,
|
||||
node: [['g', { fill: 'red' }, [['path', { d: 'M6 8h12' }]]]],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('svg > g > path')).toHaveAttribute('d', 'M6 8h12');
|
||||
});
|
||||
|
||||
it('should add and remove the non scaling stroke on the same icon', () => {
|
||||
const { container, rerender } = render(<Icon iconNode={airVent} />);
|
||||
const getVectorEffects = () =>
|
||||
Array.from(container.querySelectorAll('svg > *')).map((child) =>
|
||||
child.getAttribute('vector-effect'),
|
||||
);
|
||||
|
||||
expect(getVectorEffects()).toEqual([null, null, null, null]);
|
||||
|
||||
rerender(
|
||||
<Icon
|
||||
iconNode={airVent}
|
||||
nonScalingStroke
|
||||
/>,
|
||||
);
|
||||
expect(getVectorEffects()).toEqual(Array(4).fill('non-scaling-stroke'));
|
||||
|
||||
rerender(<Icon iconNode={airVent} />);
|
||||
expect(getVectorEffects()).toEqual([null, null, null, null]);
|
||||
});
|
||||
|
||||
it('should keep the rendered elements when props change', () => {
|
||||
const { container, rerender } = render(<Icon iconNode={airVent} />);
|
||||
const svg = container.firstChild;
|
||||
const paths = Array.from(container.querySelectorAll('path'));
|
||||
|
||||
rerender(
|
||||
<Icon
|
||||
iconNode={airVent}
|
||||
size={48}
|
||||
className="large"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBe(svg);
|
||||
expect(Array.from(container.querySelectorAll('path'))).toEqual(paths);
|
||||
expect(svg).toHaveAttribute('width', '48');
|
||||
expect(svg).toHaveClass('lucide', 'large');
|
||||
});
|
||||
|
||||
it('should render the same icon data with different props independently', () => {
|
||||
const { container } = render(
|
||||
<>
|
||||
<Icon
|
||||
iconNode={airVent}
|
||||
size={16}
|
||||
className="small"
|
||||
/>
|
||||
<Icon
|
||||
iconNode={airVent}
|
||||
nonScalingStroke
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
const [small, nonScaling] = Array.from(container.querySelectorAll('svg'));
|
||||
|
||||
expect(small).toHaveAttribute('width', '16');
|
||||
expect(small).toHaveClass('small');
|
||||
expect(small.querySelector('[vector-effect]')).toBeNull();
|
||||
expect(nonScaling).toHaveAttribute('width', '24');
|
||||
expect(nonScaling).not.toHaveClass('small');
|
||||
expect(nonScaling.querySelectorAll('[vector-effect]')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should render user children after the icon nodes', () => {
|
||||
const { container } = render(
|
||||
<Icon iconNode={airVent}>
|
||||
<title>Air vent</title>
|
||||
<circle r="1" />
|
||||
</Icon>,
|
||||
);
|
||||
|
||||
expect(Array.from(container.querySelectorAll('svg > *')).map((child) => child.tagName)).toEqual(
|
||||
['path', 'path', 'path', 'path', 'title', 'circle'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Icon Component Accessibility', () => {
|
||||
it('should have aria-hidden prop when no aria prop is present', async () => {
|
||||
const { container } = render(
|
||||
|
||||
@@ -13,22 +13,29 @@ function isDefined<T>(value: T | null | undefined): value is T {
|
||||
* @param params Additional build parameters.
|
||||
*/
|
||||
function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode {
|
||||
const attributeNames = params.attributeNames ?? {};
|
||||
const attributeNames = params.attributeNames;
|
||||
const getAttributeName = (attributeName: string) =>
|
||||
attributeNames[attributeName] ?? attributeName;
|
||||
attributeNames?.[attributeName] ?? attributeName;
|
||||
|
||||
const viewBoxWidth = icon.size ?? icon.width ?? defaultAttributes['width'];
|
||||
const viewBoxHeight = icon.size ?? icon.height ?? defaultAttributes['height'];
|
||||
const aliasClassNames =
|
||||
icon.aliases
|
||||
?.filter((alias: string): alias is string => typeof alias === 'string' && alias.trim() !== '')
|
||||
.map((alias: string) => `lucide-${alias}`) ?? [];
|
||||
const iconClassNames = [...(icon.name ? [`lucide-${icon.name}`] : []), ...aliasClassNames];
|
||||
const classNamesFromClassName = params.className?.split(' ').filter(Boolean) ?? [];
|
||||
const className =
|
||||
params.includeDefaultClasses === false
|
||||
? mergeClasses(...classNamesFromClassName)
|
||||
: mergeClasses('lucide', ...iconClassNames, ...classNamesFromClassName);
|
||||
|
||||
const classNames: string[] = [];
|
||||
if (params.includeDefaultClasses !== false) {
|
||||
classNames.push('lucide');
|
||||
if (icon.name) {
|
||||
classNames.push(`lucide-${icon.name}`);
|
||||
}
|
||||
for (const alias of icon.aliases ?? []) {
|
||||
if (typeof alias === 'string' && alias.trim() !== '') {
|
||||
classNames.push(`lucide-${alias}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params.className) {
|
||||
classNames.push(...params.className.split(' '));
|
||||
}
|
||||
const className = mergeClasses(...classNames);
|
||||
|
||||
const calculatedStrokeWidth = params.absoluteStrokeWidth
|
||||
? (Number(params.strokeWidth ?? defaultAttributes['stroke-width']) *
|
||||
@@ -36,48 +43,48 @@ function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {
|
||||
Number(params.size ?? params.width ?? defaultAttributes['width'])
|
||||
: (params.strokeWidth ?? defaultAttributes['stroke-width']);
|
||||
|
||||
const attributes = {
|
||||
...Object.entries(defaultAttributes).reduce((attrs, [attrName, value]) => {
|
||||
attrs[getAttributeName(attrName)] = value;
|
||||
return attrs;
|
||||
}, {} as SVGProps),
|
||||
...('color' in params &&
|
||||
params.color && {
|
||||
[getAttributeName('stroke')]: params.color,
|
||||
}),
|
||||
...('size' in params &&
|
||||
isDefined(params.size) && {
|
||||
[getAttributeName('width')]: params.size,
|
||||
[getAttributeName('height')]: params.size,
|
||||
}),
|
||||
...('width' in params &&
|
||||
isDefined(params.width) && {
|
||||
[getAttributeName('width')]: params.width,
|
||||
}),
|
||||
...('height' in params &&
|
||||
isDefined(params.height) && {
|
||||
[getAttributeName('height')]: params.height,
|
||||
}),
|
||||
[getAttributeName('stroke-width')]: calculatedStrokeWidth,
|
||||
...(className && {
|
||||
[getAttributeName('class')]: className,
|
||||
}),
|
||||
[getAttributeName('viewBox')]: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
|
||||
...(params.hasA11yProp === false
|
||||
? {
|
||||
[getAttributeName('aria-hidden')]: 'true',
|
||||
}
|
||||
: {}),
|
||||
...('attributes' in params && params.attributes),
|
||||
};
|
||||
// Keys are assigned in a fixed order, so the attributes are always emitted in the same order.
|
||||
const attributes: SVGProps = {};
|
||||
|
||||
for (const attributeName in defaultAttributes) {
|
||||
attributes[getAttributeName(attributeName)] =
|
||||
defaultAttributes[attributeName as keyof typeof defaultAttributes];
|
||||
}
|
||||
|
||||
if (params.color) {
|
||||
attributes[getAttributeName('stroke')] = params.color;
|
||||
}
|
||||
if (isDefined(params.size)) {
|
||||
attributes[getAttributeName('width')] = params.size;
|
||||
attributes[getAttributeName('height')] = params.size;
|
||||
}
|
||||
if (isDefined(params.width)) {
|
||||
attributes[getAttributeName('width')] = params.width;
|
||||
}
|
||||
if (isDefined(params.height)) {
|
||||
attributes[getAttributeName('height')] = params.height;
|
||||
}
|
||||
attributes[getAttributeName('stroke-width')] = calculatedStrokeWidth;
|
||||
if (className) {
|
||||
attributes[getAttributeName('class')] = className;
|
||||
}
|
||||
attributes[getAttributeName('viewBox')] = `0 0 ${viewBoxWidth} ${viewBoxHeight}`;
|
||||
if (params.hasA11yProp === false) {
|
||||
attributes[getAttributeName('aria-hidden')] = 'true';
|
||||
}
|
||||
Object.assign(attributes, params.attributes);
|
||||
|
||||
const nonScalingStrokeAttributes = params.nonScalingStroke
|
||||
? { [getAttributeName('vector-effect')]: 'non-scaling-stroke' }
|
||||
: null;
|
||||
|
||||
return [
|
||||
'svg',
|
||||
attributes,
|
||||
icon.node.map((child): LucideIconNode => {
|
||||
const [name, attrs, children] = child;
|
||||
const nextAttrs = params.nonScalingStroke
|
||||
? { [getAttributeName('vector-effect')]: 'non-scaling-stroke', ...attrs }
|
||||
const nextAttrs = nonScalingStrokeAttributes
|
||||
? { ...nonScalingStrokeAttributes, ...attrs }
|
||||
: attrs;
|
||||
|
||||
return children ? [name, nextAttrs, children] : [name, nextAttrs];
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
* @param {array} classes
|
||||
* @returns {string} A string of classes
|
||||
*/
|
||||
export const mergeClasses = <ClassType = string | undefined | null>(...classes: ClassType[]) =>
|
||||
classes
|
||||
.filter((className, index, array) => {
|
||||
return (
|
||||
Boolean(className) &&
|
||||
(className as string).trim() !== '' &&
|
||||
array.indexOf(className) === index
|
||||
);
|
||||
})
|
||||
.join(' ')
|
||||
.trim();
|
||||
export const mergeClasses = <ClassType = string | undefined | null>(...classes: ClassType[]) => {
|
||||
const uniqueClasses: ClassType[] = [];
|
||||
|
||||
for (const className of classes) {
|
||||
if (
|
||||
Boolean(className) &&
|
||||
(className as string).trim() !== '' &&
|
||||
!uniqueClasses.includes(className)
|
||||
) {
|
||||
uniqueClasses.push(className);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueClasses.join(' ').trim();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user