diff --git a/packages/react-icons/scripts/writeIcons.mjs b/packages/react-icons/scripts/writeIcons.mjs
index e2f2703f257..a8130e91e9e 100644
--- a/packages/react-icons/scripts/writeIcons.mjs
+++ b/packages/react-icons/scripts/writeIcons.mjs
@@ -7,9 +7,9 @@ import { pfToRhIcons } from './icons/pfToRhIcons.mjs';
import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
-// Import createIcon from compiled dist (build:esm must run first)
+// Import createIconBase from compiled dist (build:esm must run first)
const createIconModule = await import('../dist/esm/createIcon.js');
-const createIcon = createIconModule.createIcon;
+const createIconBase = createIconModule.createIconBase;
const outDir = join(__dirname, '../dist');
const staticDir = join(outDir, 'static');
@@ -27,7 +27,7 @@ exports.${jsName}Config = {
icon: ${JSON.stringify(icon)},
rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'},
};
-exports.${jsName} = require('../createIcon').createIcon(exports.${jsName}Config);
+exports.${jsName} = require('../createIcon').createIconBase(exports.${jsName}Config);
exports["default"] = exports.${jsName};
`.trim()
);
@@ -36,7 +36,7 @@ exports["default"] = exports.${jsName};
const writeESMExport = (fname, jsName, icon, rhUiIcon = null) => {
outputFileSync(
join(outDir, 'esm/icons', `${fname}.js`),
- `import { createIcon } from '../createIcon.js';
+ `import { createIconBase } from '../createIcon.js';
export const ${jsName}Config = {
name: '${jsName}',
@@ -44,7 +44,7 @@ export const ${jsName}Config = {
rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'},
};
-export const ${jsName} = createIcon(${jsName}Config);
+export const ${jsName} = createIconBase(${jsName}Config);
export default ${jsName};
`.trim()
@@ -68,7 +68,7 @@ export default ${jsName};
};
/**
- * Generates a static SVG string from icon data using createIcon
+ * Generates a static SVG string from icon data using createIconBase
* @param {string} iconName The name of the icon
* @param {object} icon The icon data object
* @returns {string} Static SVG markup
@@ -76,8 +76,8 @@ export default ${jsName};
function generateStaticSVG(iconName, icon) {
const jsName = `${toCamel(iconName)}Icon`;
- // Create icon component using createIcon
- const IconComponent = createIcon({
+ // Create icon component using createIconBase
+ const IconComponent = createIconBase({
name: jsName,
icon
});
diff --git a/packages/react-icons/src/__tests__/createIcon.test.tsx b/packages/react-icons/src/__tests__/createIcon.test.tsx
index 16f80fd8d90..aac4565e98d 100644
--- a/packages/react-icons/src/__tests__/createIcon.test.tsx
+++ b/packages/react-icons/src/__tests__/createIcon.test.tsx
@@ -1,5 +1,12 @@
import { render, screen } from '@testing-library/react';
-import { IconDefinition, CreateIconProps, createIcon, SVGPathObject } from '../createIcon';
+import {
+ IconDefinition,
+ CreateIconBaseProps,
+ CreateIconProps,
+ createIcon,
+ createIconBase,
+ SVGPathObject
+} from '../createIcon';
const multiPathIcon: IconDefinition = {
name: 'IconName',
@@ -28,24 +35,24 @@ const rhStandardIcon: IconDefinition = {
svgClassName: 'pf-v6-icon-rh-standard'
};
-const iconDef: CreateIconProps = {
+const iconDef: CreateIconBaseProps = {
name: 'SinglePathIconName',
icon: singlePathIcon
};
-const iconDefWithArrayPath: CreateIconProps = {
+const iconDefWithArrayPath: CreateIconBaseProps = {
name: 'MultiPathIconName',
icon: multiPathIcon
};
-const iconDefWithRhStandard: CreateIconProps = {
+const iconDefWithRhStandard: CreateIconBaseProps = {
name: 'RhStandardIconName',
icon: rhStandardIcon
};
-const SVGIcon = createIcon(iconDef);
-const SVGArrayIcon = createIcon(iconDefWithArrayPath);
-const RhStandardIcon = createIcon(iconDefWithRhStandard);
+const SVGIcon = createIconBase(iconDef);
+const SVGArrayIcon = createIconBase(iconDefWithArrayPath);
+const RhStandardIcon = createIconBase(iconDefWithRhStandard);
test('sets correct viewBox', () => {
render();
@@ -57,7 +64,37 @@ test('sets correct viewBox', () => {
test('sets correct svgPath if string', () => {
render();
- expect(screen.getByRole('img', { hidden: true }).querySelector('path')).toHaveAttribute('d', iconDef.svgPath);
+ expect(screen.getByRole('img', { hidden: true }).querySelector('path')).toHaveAttribute(
+ 'd',
+ singlePathIcon.svgPathData
+ );
+});
+
+test('accepts legacy flat createIcon({ svgPath }) shape', () => {
+ const legacyDef: CreateIconProps = {
+ name: 'LegacyIcon',
+ width: 10,
+ height: 20,
+ svgPath: 'legacy-path',
+ svgClassName: 'legacy-svg'
+ };
+ const LegacySVGIcon = createIcon(legacyDef);
+ render();
+ expect(screen.getByRole('img', { hidden: true }).querySelector('path')).toHaveAttribute('d', 'legacy-path');
+});
+
+test('createIconBase accepts nested icon with deprecated svgPath field', () => {
+ const nestedLegacyPath: CreateIconBaseProps = {
+ name: 'NestedLegacyPathIcon',
+ icon: {
+ width: 8,
+ height: 8,
+ svgPath: 'nested-legacy-d'
+ }
+ };
+ const NestedIcon = createIconBase(nestedLegacyPath);
+ render();
+ expect(screen.getByRole('img', { hidden: true }).querySelector('path')).toHaveAttribute('d', 'nested-legacy-d');
});
test('sets correct svgClassName by default', () => {
@@ -75,6 +112,15 @@ test('does not set svgClassName when noDefaultStyle is true', () => {
expect(screen.getByRole('img', { hidden: true })).not.toHaveClass('pf-v6-icon-rh-standard');
});
+test('throws when createIconBase omits icon', () => {
+ expect(() =>
+ createIconBase({
+ name: 'MissingDefaultIcon',
+ rhUiIcon: null
+ } as any)
+ ).toThrow('@patternfly/react-icons: createIconBase requires an `icon` definition (name: MissingDefaultIcon).');
+});
+
test('sets correct svgPath if array', () => {
render();
const paths = screen.getByRole('img', { hidden: true }).querySelectorAll('path');
@@ -127,3 +173,79 @@ test('additional props should be spread to the root svg element', () => {
render();
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
+
+describe('rh-ui mapping: nested SVGs, set prop, and warnings', () => {
+ const defaultPath = 'M0 0-default';
+ const rhUiPath = 'M0 0-rh-ui';
+
+ const defaultIconDef: IconDefinition = {
+ name: 'DefaultVariant',
+ width: 16,
+ height: 16,
+ svgPathData: defaultPath
+ };
+
+ const rhUiIconDef: IconDefinition = {
+ name: 'RhUiVariant',
+ width: 16,
+ height: 16,
+ svgPathData: rhUiPath
+ };
+
+ const dualConfig: CreateIconBaseProps = {
+ name: 'DualMappedIcon',
+ icon: defaultIconDef,
+ rhUiIcon: rhUiIconDef
+ };
+
+ const DualMappedIcon = createIconBase(dualConfig);
+
+ test('renders two nested inner svgs when rhUiIcon is set and `set` is omitted (swap layout)', () => {
+ render();
+ const root = screen.getByRole('img', { hidden: true });
+ expect(root).toHaveClass('pf-v6-svg');
+ const innerSvgs = root.querySelectorAll(':scope > svg');
+ expect(innerSvgs).toHaveLength(2);
+ expect(root?.querySelector('.pf-v6-icon-default path')).toHaveAttribute('d', defaultPath);
+ expect(root?.querySelector('.pf-v6-icon-rh-ui path')).toHaveAttribute('d', rhUiPath);
+ });
+
+ test('set="default" renders a single flat svg using the default icon paths', () => {
+ render();
+ const root = screen.getByRole('img', { hidden: true });
+ expect(root.querySelectorAll(':scope > svg')).toHaveLength(0);
+ expect(root).toHaveAttribute('viewBox', '0 0 16 16');
+ expect(root.querySelector('path')).toHaveAttribute('d', defaultPath);
+ expect(root.querySelectorAll('svg')).toHaveLength(0);
+ });
+
+ test('set="rh-ui" renders a single flat svg using the rh-ui icon paths', () => {
+ render();
+ const root = screen.getByRole('img', { hidden: true });
+ expect(root.querySelectorAll(':scope > svg')).toHaveLength(0);
+ expect(root.querySelector('path')).toHaveAttribute('d', rhUiPath);
+ expect(root.querySelectorAll('svg')).toHaveLength(0);
+ });
+
+ test('set="rh-ui" with no rhUiIcon mapping falls back to default and warns', () => {
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ try {
+ const IconNoRhMapping = createIconBase({
+ name: 'NoRhMappingIcon',
+ icon: defaultIconDef,
+ rhUiIcon: null
+ });
+
+ render();
+
+ expect(warnSpy).toHaveBeenCalledWith(
+ 'Set "rh-ui" was provided for NoRhMappingIcon, but no rh-ui icon data exists for this icon. The default icon will be rendered.'
+ );
+ const root = screen.getByRole('img', { hidden: true });
+ expect(root.querySelector('path')).toHaveAttribute('d', defaultPath);
+ expect(root.querySelectorAll('svg')).toHaveLength(0);
+ } finally {
+ warnSpy.mockRestore();
+ }
+ });
+});
diff --git a/packages/react-icons/src/createIcon.tsx b/packages/react-icons/src/createIcon.tsx
index 0286134575b..ff6a8c952e7 100644
--- a/packages/react-icons/src/createIcon.tsx
+++ b/packages/react-icons/src/createIcon.tsx
@@ -1,23 +1,49 @@
-import { Component } from 'react';
+import { Component, type ReactNode } from 'react';
export interface SVGPathObject {
path: string;
className?: string;
}
+/**
+ * Serialized / nested icon data (e.g. from the icon generator or JSON), not the flat {@link CreateIconProps}
+ * shape. `svgPathData` is preferred; deprecated `svgPath` is still supported (at least one path field is
+ * required at runtime; if both are set, `svgPathData` takes precedence).
+ */
export interface IconDefinition {
name?: string;
width: number;
height: number;
- svgPathData: string | SVGPathObject[];
xOffset?: number;
yOffset?: number;
svgClassName?: string;
+ svgPathData?: string | SVGPathObject[];
+ /** @deprecated Use `svgPathData` for nested definitions. */
+ svgPath?: string | SVGPathObject[];
}
+/**
+ * Nested (current) public API: `{ icon, rhUiIcon?, name? }` as produced by the icon generator and
+ * `createIconBase` consumers.
+ */
+export interface CreateIconBaseProps {
+ name?: string;
+ icon: IconDefinition;
+ rhUiIcon?: IconDefinition | null;
+}
+
+/**
+ * @deprecated Prefer {@link createIconBase} with a nested {@link IconDefinition} using `svgPathData` instead
+ * of this flat `createIcon` shape and legacy `svgPath` field.
+ */
export interface CreateIconProps {
name?: string;
- icon?: IconDefinition;
+ width: number;
+ height: number;
+ xOffset?: number;
+ yOffset?: number;
+ svgPath?: string | SVGPathObject[];
+ svgClassName?: string;
rhUiIcon?: IconDefinition | null;
}
@@ -32,13 +58,24 @@ export interface SVGIconProps extends Omit, 'ref'> {
let currentId = 0;
+/** Renders the same path markup as the historical `createIcon` implementation. */
+function getSvgPaths(svgPathData: string | SVGPathObject[] | undefined): ReactNode {
+ return svgPathData && Array.isArray(svgPathData) ? (
+ svgPathData.map((pathObject, index) => (
+
+ ))
+ ) : (
+
+ );
+}
+
const createSvg = (icon: IconDefinition, iconClassName: string) => {
- const { xOffset, yOffset, width, height, svgPathData, svgClassName } = icon ?? {};
+ const { xOffset, yOffset, width, height, svgPathData, svgPath, svgClassName } = icon ?? {};
const _xOffset = xOffset ?? 0;
const _yOffset = yOffset ?? 0;
const viewBox = [_xOffset, _yOffset, width, height].join(' ');
- const classNames = [];
+ const classNames: string[] = [];
if (svgClassName) {
classNames.push(svgClassName);
@@ -47,26 +84,27 @@ const createSvg = (icon: IconDefinition, iconClassName: string) => {
classNames.push(iconClassName);
}
- const svgPaths =
- svgPathData && Array.isArray(svgPathData) ? (
- svgPathData.map((pathObject, index) => (
-
- ))
- ) : (
-
- );
-
return (
);
};
/**
- * Factory to create Icon class components for consumers
+ * Factory for the nested / current icon API. Behavior matches the pre-split `createIcon` on `main` (this name
+ * replaces the original export). For path mapping from the flat `svgPath` shape, use {@link createIcon} only.
*/
-export function createIcon({ name, icon, rhUiIcon = null }: CreateIconProps): React.ComponentClass {
+export function createIconBase({
+ name,
+ icon,
+ rhUiIcon = null
+}: CreateIconBaseProps): React.ComponentClass {
+ if (icon == null) {
+ throw new Error(
+ `@patternfly/react-icons: createIconBase requires an \`icon\` definition (name: ${name ?? 'unknown'}).`
+ );
+ }
return class SVGIcon extends Component {
static displayName = name;
@@ -76,10 +114,7 @@ export function createIcon({ name, icon, rhUiIcon = null }: CreateIconProps): Re
noDefaultStyle: false
};
- constructor(props: SVGIconProps) {
- super(props);
- }
-
+ /** Renders one root `