diff --git a/packages/react-icons/src/__tests__/createIcon.test.tsx b/packages/react-icons/src/__tests__/createIcon.test.tsx
index 16f80fd8d90..a462664db45 100644
--- a/packages/react-icons/src/__tests__/createIcon.test.tsx
+++ b/packages/react-icons/src/__tests__/createIcon.test.tsx
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
-import { IconDefinition, CreateIconProps, createIcon, SVGPathObject } from '../createIcon';
+import { IconDefinition, CreateIconProps, createIcon, LegacyFlatIconDefinition, SVGPathObject } from '../createIcon';
const multiPathIcon: IconDefinition = {
name: 'IconName',
@@ -57,7 +57,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: LegacyFlatIconDefinition = {
+ 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('accepts CreateIconProps with nested icon using deprecated svgPath field', () => {
+ const nestedLegacyPath: CreateIconProps = {
+ name: 'NestedLegacyPathIcon',
+ icon: {
+ width: 8,
+ height: 8,
+ svgPath: 'nested-legacy-d'
+ }
+ };
+ const NestedIcon = createIcon(nestedLegacyPath);
+ render();
+ expect(screen.getByRole('img', { hidden: true }).querySelector('path')).toHaveAttribute('d', 'nested-legacy-d');
});
test('sets correct svgClassName by default', () => {
@@ -75,6 +105,17 @@ 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 nested CreateIconProps omits icon', () => {
+ expect(() =>
+ createIcon({
+ name: 'MissingDefaultIcon',
+ rhUiIcon: null
+ })
+ ).toThrow(
+ '@patternfly/react-icons: createIcon requires an `icon` definition when using nested CreateIconProps (name: MissingDefaultIcon).'
+ );
+});
+
test('sets correct svgPath if array', () => {
render();
const paths = screen.getByRole('img', { hidden: true }).querySelectorAll('path');
@@ -127,3 +168,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: CreateIconProps = {
+ name: 'DualMappedIcon',
+ icon: defaultIconDef,
+ rhUiIcon: rhUiIconDef
+ };
+
+ const DualMappedIcon = createIcon(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 = createIcon({
+ 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..6268eb778d0 100644
--- a/packages/react-icons/src/createIcon.tsx
+++ b/packages/react-icons/src/createIcon.tsx
@@ -5,22 +5,49 @@ export interface SVGPathObject {
className?: string;
}
-export interface IconDefinition {
+export interface IconDefinitionBase {
name?: string;
width: number;
height: number;
- svgPathData: string | SVGPathObject[];
xOffset?: number;
yOffset?: number;
svgClassName?: string;
}
+/**
+ * SVG path content for one icon variant (default or rh-ui). At runtime at least one of
+ * `svgPathData` or `svgPath` must be set; if both are present, `svgPathData` is used.
+ */
+export interface IconDefinition extends IconDefinitionBase {
+ svgPathData?: string | SVGPathObject[];
+ /**
+ * @deprecated Use {@link IconDefinition.svgPathData} instead.
+ */
+ svgPath?: string | SVGPathObject[];
+}
+
+/** Narrows {@link IconDefinition} to the preferred shape with required `svgPathData`. */
+export type IconDefinitionWithSvgPathData = Required> & IconDefinition;
+
+/**
+ * @deprecated Use {@link IconDefinition} with `svgPathData` instead.
+ * Narrows {@link IconDefinition} to the legacy shape with required `svgPath`.
+ */
+export type IconDefinitionWithSvgPath = Required> & IconDefinition;
+
+/** When passing `icon` or `rhUiIcon` keys (nested form), `icon` is required at runtime. */
export interface CreateIconProps {
name?: string;
icon?: IconDefinition;
rhUiIcon?: IconDefinition | null;
}
+/**
+ * @deprecated The previous `createIcon` accepted a flat {@link IconDefinition} with top-level
+ * `svgPath`. Pass {@link CreateIconProps} with a nested `icon` field instead.
+ */
+export type LegacyFlatIconDefinition = IconDefinition;
+
export interface SVGIconProps extends Omit, 'ref'> {
title?: string;
className?: string;
@@ -32,7 +59,70 @@ export interface SVGIconProps extends Omit, 'ref'> {
let currentId = 0;
-const createSvg = (icon: IconDefinition, iconClassName: string) => {
+/** Returns path data from `svgPathData` or deprecated `svgPath` (prefers `svgPathData` when both exist). */
+function resolveSvgPathData(icon: IconDefinition): string | SVGPathObject[] {
+ if ('svgPathData' in icon && icon.svgPathData !== undefined) {
+ return icon.svgPathData;
+ }
+ if ('svgPath' in icon && icon.svgPath !== undefined) {
+ return icon.svgPath;
+ }
+ throw new Error('@patternfly/react-icons: IconDefinition must define svgPathData or svgPath');
+}
+
+/** Produces a single {@link IconDefinitionWithSvgPathData} for internal rendering. */
+function normalizeIconDefinition(icon: IconDefinition): IconDefinitionWithSvgPathData {
+ return {
+ name: icon.name,
+ width: icon.width,
+ height: icon.height,
+ svgPathData: resolveSvgPathData(icon),
+ xOffset: icon.xOffset,
+ yOffset: icon.yOffset,
+ svgClassName: icon.svgClassName
+ };
+}
+
+/** True when the argument uses the nested `CreateIconProps` shape (`icon` and/or `rhUiIcon` keys). */
+function isNestedCreateIconProps(arg: object): arg is CreateIconProps {
+ return 'icon' in arg || 'rhUiIcon' in arg;
+}
+
+/** Props after resolving legacy `svgPath` and flat `createIcon` arguments. */
+interface NormalizedCreateIconProps {
+ name?: string;
+ icon?: IconDefinitionWithSvgPathData;
+ rhUiIcon: IconDefinitionWithSvgPathData | null;
+}
+
+/**
+ * Coerces legacy flat or nested props into normalized {@link NormalizedCreateIconProps}.
+ * Nested input must include a non-null `icon` or throws.
+ */
+function normalizeCreateIconArg(arg: CreateIconProps | LegacyFlatIconDefinition): NormalizedCreateIconProps {
+ if (isNestedCreateIconProps(arg)) {
+ const p = arg as CreateIconProps;
+ if (p.icon == null) {
+ const label = p.name != null ? ` (name: ${String(p.name)})` : '';
+ throw new Error(
+ `@patternfly/react-icons: createIcon requires an \`icon\` definition when using nested CreateIconProps${label}.`
+ );
+ }
+ return {
+ name: p.name,
+ icon: normalizeIconDefinition(p.icon),
+ rhUiIcon: p.rhUiIcon != null ? normalizeIconDefinition(p.rhUiIcon) : null
+ };
+ }
+ return {
+ name: (arg as LegacyFlatIconDefinition).name,
+ icon: normalizeIconDefinition(arg as IconDefinition),
+ rhUiIcon: null
+ };
+}
+
+/** Renders an inner `