mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-16 07:37:42 +01:00
feat(icon-component): Creating icons with iconNodes (#1997)
* Add useIconComponent, lucide-react * Add concept useIconComponent * add useIconComponents to packages * Add icon component * Add icon component * Add tests for react packages * Reset changes in icons * Add types * Add support for Icon components in Lucide Vue Next * update tests * Update tests * Enable Svelte component * Fix lucide-react-native tests * Update Solid package * update snapshots * Add docs * add docs * Update tests * Formatting * Formatting * Update package lock * Remove `useIconComponent` * Update guides * Update exports preact and solid package * Formatting * Format createIcons.ts * Add lucide lab repo link in docs
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"devDependencies": {
|
||||
"@lucide/rollup-plugins": "workspace:*",
|
||||
"@lucide/build-icons": "workspace:*",
|
||||
"@lucide/shared": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.6",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
|
||||
69
packages/lucide-react-native/src/Icon.ts
Normal file
69
packages/lucide-react-native/src/Icon.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createElement, forwardRef, type FunctionComponent } from 'react';
|
||||
import * as NativeSvg from 'react-native-svg';
|
||||
import defaultAttributes, { childDefaultAttributes } from './defaultAttributes';
|
||||
import { IconNode, LucideProps } from './types';
|
||||
|
||||
interface IconComponentProps extends LucideProps {
|
||||
iconNode: IconNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lucide icon component
|
||||
*
|
||||
* @component Icon
|
||||
* @param {object} props
|
||||
* @param {string} props.color - The color of the icon
|
||||
* @param {number} props.size - The size of the icon
|
||||
* @param {number} props.strokeWidth - The stroke width of the icon
|
||||
* @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
|
||||
* @param {string} props.className - The class name of the icon
|
||||
* @param {IconNode} props.children - The children of the icon
|
||||
* @param {IconNode} props.iconNode - The icon node of the icon
|
||||
*
|
||||
* @returns {ForwardRefExoticComponent} LucideIcon
|
||||
*/
|
||||
const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
|
||||
(
|
||||
{
|
||||
color = 'currentColor',
|
||||
size = 24,
|
||||
strokeWidth = 2,
|
||||
absoluteStrokeWidth,
|
||||
children,
|
||||
iconNode,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const customAttrs = {
|
||||
stroke: color,
|
||||
strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return createElement(
|
||||
NativeSvg.Svg as unknown as string,
|
||||
{
|
||||
ref,
|
||||
...defaultAttributes,
|
||||
width: size,
|
||||
height: size,
|
||||
...customAttrs,
|
||||
},
|
||||
[
|
||||
...iconNode.map(([tag, attrs]) => {
|
||||
const upperCasedTag = (tag.charAt(0).toUpperCase() +
|
||||
tag.slice(1)) as keyof typeof NativeSvg;
|
||||
// duplicating the attributes here because generating the OTA update bundles don't inherit the SVG properties from parent (codepush, expo-updates)
|
||||
return createElement(
|
||||
NativeSvg[upperCasedTag] as FunctionComponent<LucideProps>,
|
||||
{ ...childDefaultAttributes, ...customAttrs, ...attrs } as LucideProps,
|
||||
);
|
||||
}),
|
||||
...((Array.isArray(children) ? children : [children]) || []),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Icon;
|
||||
@@ -7,17 +7,7 @@ import {
|
||||
} from 'react';
|
||||
import * as NativeSvg from 'react-native-svg';
|
||||
import defaultAttributes, { childDefaultAttributes } from './defaultAttributes';
|
||||
import type { SvgProps } from 'react-native-svg';
|
||||
|
||||
export type IconNode = [elementName: keyof ReactSVG, attrs: Record<string, string>][];
|
||||
|
||||
export interface LucideProps extends SvgProps {
|
||||
size?: string | number;
|
||||
absoluteStrokeWidth?: boolean;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export type LucideIcon = ForwardRefExoticComponent<LucideProps>;
|
||||
import { IconNode, LucideIcon, LucideProps } from './types';
|
||||
|
||||
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
|
||||
const Component = forwardRef(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
export * from './icons';
|
||||
export * as icons from './icons';
|
||||
export * from './aliases';
|
||||
export {
|
||||
default as createLucideIcon,
|
||||
type IconNode,
|
||||
type LucideProps,
|
||||
type LucideIcon,
|
||||
} from './createLucideIcon';
|
||||
export * from './types';
|
||||
|
||||
export { default as createLucideIcon } from './createLucideIcon';
|
||||
export { default as Icon } from './Icon';
|
||||
|
||||
12
packages/lucide-react-native/src/types.ts
Normal file
12
packages/lucide-react-native/src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ForwardRefExoticComponent, ReactSVG } from 'react';
|
||||
import type { SvgProps } from 'react-native-svg';
|
||||
|
||||
export type IconNode = [elementName: keyof ReactSVG, attrs: Record<string, string>][];
|
||||
|
||||
export interface LucideProps extends SvgProps {
|
||||
size?: string | number;
|
||||
absoluteStrokeWidth?: boolean;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export type LucideIcon = ForwardRefExoticComponent<LucideProps>;
|
||||
35
packages/lucide-react-native/tests/Icon.spec.tsx
Normal file
35
packages/lucide-react-native/tests/Icon.spec.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { airVent } from './testIconNodes';
|
||||
import { Icon } from '../src/lucide-react-native';
|
||||
|
||||
vi.mock('react-native-svg');
|
||||
|
||||
describe('Using Icon Component', () => {
|
||||
it('should render icon based on a iconNode', async () => {
|
||||
const { container } = render(
|
||||
<Icon
|
||||
iconNode={airVent}
|
||||
size={48}
|
||||
stroke="red"
|
||||
absoluteStrokeWidth
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render icon and match snapshot', async () => {
|
||||
const { container } = render(
|
||||
<Icon
|
||||
iconNode={airVent}
|
||||
size={48}
|
||||
stroke="red"
|
||||
absoluteStrokeWidth
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
|
||||
<svg
|
||||
fill="none"
|
||||
height="48"
|
||||
stroke="red"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
viewBox="0 0 24 24"
|
||||
width="48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
|
||||
fill="none"
|
||||
stroke="red"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<path
|
||||
d="M6 8h12"
|
||||
fill="none"
|
||||
stroke="red"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<path
|
||||
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
|
||||
fill="none"
|
||||
stroke="red"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<path
|
||||
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
|
||||
fill="none"
|
||||
stroke="red"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -10,7 +10,6 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-testid="grid-icon"
|
||||
>
|
||||
<rect fill="none"
|
||||
stroke="red"
|
||||
@@ -128,7 +127,6 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
data-testid="grid-icon"
|
||||
>
|
||||
<rect fill="none"
|
||||
stroke="red"
|
||||
|
||||
@@ -14,21 +14,20 @@ describe('Using lucide icon components', () => {
|
||||
});
|
||||
|
||||
it('should adjust the size, stroke color and stroke width', () => {
|
||||
const testId = 'grid-icon';
|
||||
const { container, getByTestId } = render(
|
||||
const { container } = render(
|
||||
<Grid
|
||||
data-testid={testId}
|
||||
size={48}
|
||||
stroke="red"
|
||||
strokeWidth={4}
|
||||
/>,
|
||||
);
|
||||
|
||||
const { attributes } = getByTestId(testId);
|
||||
expect((attributes as unknown as Attributes).stroke.value).toBe('red');
|
||||
expect((attributes as unknown as Attributes).width.value).toBe('48');
|
||||
expect((attributes as unknown as Attributes).height.value).toBe('48');
|
||||
expect((attributes as unknown as Attributes)['stroke-width'].value).toBe('4');
|
||||
const SVGElement = container.firstElementChild;
|
||||
|
||||
expect(SVGElement).toHaveAttribute('stroke', 'red');
|
||||
expect(SVGElement).toHaveAttribute('width', '48');
|
||||
expect(SVGElement).toHaveAttribute('height', '48');
|
||||
expect(SVGElement).toHaveAttribute('stroke-width', '4');
|
||||
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
@@ -61,23 +60,20 @@ describe('Using lucide icon components', () => {
|
||||
});
|
||||
|
||||
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
|
||||
const testId = 'grid-icon';
|
||||
const { container, getByTestId } = render(
|
||||
const { container } = render(
|
||||
<Grid
|
||||
data-testid={testId}
|
||||
size={48}
|
||||
stroke="red"
|
||||
absoluteStrokeWidth
|
||||
/>,
|
||||
);
|
||||
|
||||
const { attributes } = getByTestId(testId) as unknown as {
|
||||
attributes: Record<string, { value: string }>;
|
||||
};
|
||||
expect(attributes.stroke.value).toBe('red');
|
||||
expect(attributes.width.value).toBe('48');
|
||||
expect(attributes.height.value).toBe('48');
|
||||
expect(attributes['stroke-width'].value).toBe('1');
|
||||
const SVGElement = container.firstElementChild;
|
||||
|
||||
expect(SVGElement).toHaveAttribute('stroke', 'red');
|
||||
expect(SVGElement).toHaveAttribute('width', '48');
|
||||
expect(SVGElement).toHaveAttribute('height', '48');
|
||||
expect(SVGElement).toHaveAttribute('stroke-width', '1');
|
||||
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
@@ -91,8 +87,8 @@ describe('Using lucide icon components', () => {
|
||||
<Grid data-testid={childId} />
|
||||
</Grid>,
|
||||
);
|
||||
const { children } = getByTestId(testId) as unknown as { children: HTMLCollection };
|
||||
const lastChild = children[children.length - 1];
|
||||
const { children } = container.firstElementChild ?? {};
|
||||
const lastChild = children?.[children.length - 1];
|
||||
|
||||
expect(lastChild).toEqual(getByTestId(childId));
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
|
||||
22
packages/lucide-react-native/tests/testIconNodes.ts
Normal file
22
packages/lucide-react-native/tests/testIconNodes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IconNode } from '../src/types';
|
||||
|
||||
export const airVent: IconNode = [
|
||||
[
|
||||
'path',
|
||||
{
|
||||
d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2',
|
||||
key: 'larmp2',
|
||||
},
|
||||
],
|
||||
['path', { d: 'M6 8h12', key: '6g4wlu' }],
|
||||
['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12', key: '1bo8pg' }],
|
||||
['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5', key: 't9h90c' }],
|
||||
];
|
||||
|
||||
export const coffee: IconNode = [
|
||||
['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1', key: 'jx4kbh' }],
|
||||
['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z', key: '1bxrl0' }],
|
||||
['line', { x1: '6', x2: '6', y1: '2', y2: '4', key: '1cr9l3' }],
|
||||
['line', { x1: '10', x2: '10', y1: '2', y2: '4', key: '170wym' }],
|
||||
['line', { x1: '14', x2: '14', y1: '2', y2: '4', key: '1c5f70' }],
|
||||
];
|
||||
Reference in New Issue
Block a user