Skip to content
Merged
16 changes: 8 additions & 8 deletions packages/react-icons/scripts/writeIcons.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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()
);
Expand All @@ -36,15 +36,15 @@ 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}',
icon: ${JSON.stringify(icon)},
rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'},
};

export const ${jsName} = createIcon(${jsName}Config);
export const ${jsName} = createIconBase(${jsName}Config);

export default ${jsName};
`.trim()
Expand All @@ -68,16 +68,16 @@ 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
*/
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
});
Expand Down
138 changes: 130 additions & 8 deletions packages/react-icons/src/__tests__/createIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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(<SVGIcon />);
Expand All @@ -57,7 +64,37 @@ test('sets correct viewBox', () => {

test('sets correct svgPath if string', () => {
render(<SVGIcon />);
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(<LegacySVGIcon />);
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(<NestedIcon />);
expect(screen.getByRole('img', { hidden: true }).querySelector('path')).toHaveAttribute('d', 'nested-legacy-d');
});

test('sets correct svgClassName by default', () => {
Expand All @@ -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(<SVGArrayIcon />);
const paths = screen.getByRole('img', { hidden: true }).querySelectorAll('path');
Expand Down Expand Up @@ -127,3 +173,79 @@ test('additional props should be spread to the root svg element', () => {
render(<SVGIcon data-testid="icon" />);
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(<DualMappedIcon />);
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(<DualMappedIcon set="default" />);
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(<DualMappedIcon set="rh-ui" />);
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(<IconNoRhMapping set="rh-ui" />);

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();
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading
Loading