Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
createIcon,
createIconBase,
LegacyFlatIconDefinition,
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: LegacyFlatIconDefinition = {
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