From faf45e4700360413c8248f7371a6a572534e29a1 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 16 Jan 2026 13:38:43 -0500 Subject: [PATCH 1/2] feat(icons): investigating pf-rh icon hotswapping --- packages/react-icons/scripts/writeIcons.mjs | 40 +-- packages/react-icons/src/createIcon.tsx | 160 ++++++++-- packages/react-icons/src/pfToRhIcons.js | 316 ++++++++++++++++++++ 3 files changed, 471 insertions(+), 45 deletions(-) create mode 100644 packages/react-icons/src/pfToRhIcons.js diff --git a/packages/react-icons/scripts/writeIcons.mjs b/packages/react-icons/scripts/writeIcons.mjs index a4c58a700f4..3ae962c3ce6 100644 --- a/packages/react-icons/scripts/writeIcons.mjs +++ b/packages/react-icons/scripts/writeIcons.mjs @@ -3,7 +3,7 @@ import { outputFileSync, ensureDirSync } from 'fs-extra/esm'; import { generateIcons } from './generateIcons.mjs'; import { createElement } from 'react'; import { renderToString } from 'react-dom/server'; - +import { pfToRhIcons } from '../src/pfToRhIcons.js'; import * as url from 'url'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); @@ -17,19 +17,15 @@ const staticDir = join(outDir, 'static'); const removeSnake = (s) => s.toUpperCase().replace('-', '').replace('_', ''); const toCamel = (s) => `${s[0].toUpperCase()}${s.substr(1).replace(/([-_][\w])/gi, removeSnake)}`; -const writeCJSExport = (fname, jsName, icon) => { +const writeCJSExport = (fname, jsName, icon, rhUiIcon = null) => { outputFileSync( join(outDir, 'js/icons', `${fname}.js`), `"use strict" exports.__esModule = true; exports.${jsName}Config = { name: '${jsName}', - height: ${icon.height}, - width: ${icon.width}, - svgPath: ${JSON.stringify(icon.svgPathData)}, - yOffset: ${icon.yOffset || 0}, - xOffset: ${icon.xOffset || 0}, - svgClassName: ${JSON.stringify(icon.svgClassName)}, + icon: ${JSON.stringify(icon)}, + rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'}, }; exports.${jsName} = require('../createIcon').createIcon(exports.${jsName}Config); exports["default"] = exports.${jsName}; @@ -37,19 +33,15 @@ exports["default"] = exports.${jsName}; ); }; -const writeESMExport = (fname, jsName, icon) => { +const writeESMExport = (fname, jsName, icon, rhUiIcon = null) => { outputFileSync( join(outDir, 'esm/icons', `${fname}.js`), `import { createIcon } from '../createIcon'; export const ${jsName}Config = { name: '${jsName}', - height: ${icon.height}, - width: ${icon.width}, - svgPath: ${JSON.stringify(icon.svgPathData)}, - yOffset: ${icon.yOffset || 0}, - xOffset: ${icon.xOffset || 0}, - svgClassName: ${JSON.stringify(icon.svgClassName)}, + icon: ${JSON.stringify(icon)}, + rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'}, }; export const ${jsName} = createIcon(${jsName}Config); @@ -59,17 +51,13 @@ export default ${jsName}; ); }; -const writeDTSExport = (fname, jsName, icon) => { +const writeDTSExport = (fname, jsName, icon, rhUiIcon = null) => { const text = `import { ComponentClass } from 'react'; import { SVGIconProps } from '../createIcon'; export declare const ${jsName}Config: { name: '${jsName}', - height: ${icon.height}, - width: ${icon.width}, - svgPath: ${JSON.stringify(icon.svgPathData)}, - yOffset: ${icon.yOffset || 0}, - xOffset: ${icon.xOffset || 0}, - svgClassName: ${JSON.stringify(icon.svgClassName)}, + icon: ${JSON.stringify(icon)}, + rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'}, }; export declare const ${jsName}: ComponentClass; export default ${jsName}; @@ -133,9 +121,11 @@ function writeIcons(icons) { Object.entries(icons).forEach(([iconName, icon]) => { const fname = `${iconName}-icon`; const jsName = `${toCamel(iconName)}Icon`; - writeESMExport(fname, jsName, icon); - writeCJSExport(fname, jsName, icon); - writeDTSExport(fname, jsName, icon); + + const altIcon = pfToRhIcons[jsName] ? pfToRhIcons[jsName].icon : null; + writeESMExport(fname, jsName, icon, altIcon); + writeCJSExport(fname, jsName, icon, altIcon); + writeDTSExport(fname, jsName, icon, altIcon); index.push({ fname, jsName }); }); diff --git a/packages/react-icons/src/createIcon.tsx b/packages/react-icons/src/createIcon.tsx index b447c039b72..60b0c668d05 100644 --- a/packages/react-icons/src/createIcon.tsx +++ b/packages/react-icons/src/createIcon.tsx @@ -4,45 +4,96 @@ export interface SVGPathObject { path: string; className?: string; } + export interface IconDefinition { name?: string; width: number; height: number; - svgPath: string | SVGPathObject[]; + svgPathData: string | SVGPathObject[]; xOffset?: number; yOffset?: number; svgClassName?: string; } +export interface CreateIconProps extends IconDefinition { + name?: string; + icon?: IconDefinition; + rhUiIcon?: IconDefinition | null; +} + export interface SVGIconProps extends Omit, 'ref'> { title?: string; className?: string; + /* Indicates the icon should render using alternate svg data for unified theme */ + set?: 'default' | 'unified'; } let currentId = 0; +const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement); /** * Factory to create Icon class components for consumers */ -export function createIcon({ - name, - xOffset = 0, - yOffset = 0, - width, - height, - svgPath, - svgClassName -}: IconDefinition): React.ComponentClass { - return class SVGIcon extends Component { +export function createIcon({ name, icon, rhUiIcon = null }: CreateIconProps): React.ComponentClass { + return class SVGIcon extends Component { static displayName = name; id = `icon-title-${currentId++}`; + private observer: MutationObserver | null = null; + + constructor(props: SVGIconProps) { + super(props); + this.state = { themeClassVersion: 0 }; + } + + componentDidMount() { + if (rhUiIcon !== null && canUseDOM) { + this.observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const target = mutation.target as HTMLElement; + const hadClass = (mutation.oldValue || '').includes('pf-v6-theme-unified'); + const hasClass = target.classList.contains('pf-v6-theme-unified'); + + if (hadClass !== hasClass && this.props.set === undefined) { + this.setState((prevState) => ({ + themeClassVersion: prevState.themeClassVersion + 1 + })); + } + } + } + }); + + this.observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + attributeOldValue: true + }); + } + } + + componentWillUnmount() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + } render() { - const { title, className: propsClassName, ...props } = this.props; + const { title, className: propsClassName, set, ...props } = this.props; + + const shouldUseAltData = + rhUiIcon !== null && + (set === 'unified' || + (set === undefined && canUseDOM && document.documentElement.classList.contains('pf-v6-theme-unified'))); + + const iconData = shouldUseAltData ? rhUiIcon : icon; + const { xOffset, yOffset, width, height, svgClassName, svgPathData } = iconData ?? {}; + const _xOffset = xOffset ?? 0; + const _yOffset = yOffset ?? 0; const hasTitle = Boolean(title); - const viewBox = [xOffset, yOffset, width, height].join(' '); + const viewBox = [_xOffset, _yOffset, width, height].join(' '); const classNames = ['pf-v6-svg']; if (svgClassName) { @@ -52,6 +103,14 @@ export function createIcon({ classNames.push(propsClassName); } + const svgPaths = Array.isArray(svgPathData) ? ( + svgPathData.map((pathObject, index) => ( + + )) + ) : ( + + ); + return ( , 'ref'>)} // Lie. > {hasTitle && {title}} - {Array.isArray(svgPath) ? ( - svgPath.map((pathObject, index) => ( - - )) - ) : ( - - )} + {svgPaths} ); + + // Alternate CSS method tinkering + // TODO: remove or refactor to use this method + // Below works for paths, but not viewbox without needing the MutationObserver which would be nice to not have when using CSS method + // May be able to use two separate svgs instead of paths instead if going this route + + // const defaultSvgPathData = Array.isArray(icon?.svgPathData) ? ( + // icon?.svgPathData.map((pathObject, index) => ( + // + // )) + // ) : ( + // + // ); + + // const rhUiSvgPathData = + // rhUiIcon?.svgPathData && Array.isArray(rhUiIcon?.svgPathData) ? ( + // rhUiIcon?.svgPathData.map((pathObject, index) => ( + // + // )) + // ) : ( + // + // ); + + // const finalSvgPath = + // rhUiIcon !== null ? ( + // <> + // {defaultSvgPathData} + // {rhUiSvgPathData} + // + // ) : ( + // pfSvgPathData + // ); + + // return ( + // , 'ref'>)} // Lie. + // > + // {hasTitle && {title}} + // + // {finalSvgPath} + // + // ); } }; } diff --git a/packages/react-icons/src/pfToRhIcons.js b/packages/react-icons/src/pfToRhIcons.js new file mode 100644 index 00000000000..339c54eb9d6 --- /dev/null +++ b/packages/react-icons/src/pfToRhIcons.js @@ -0,0 +1,316 @@ +import rhIconsUI from '../scripts/icons/rhIconsUI.mjs'; + +// Helper function to get icon data, handling cases where icon might not exist +const getIconData = (iconName) => rhIconsUI[iconName] || null; + +// Generated using Cursor from design's google sheet mapping data. +// Additional mappings were added based on rh-ui icon names that matched existing icons. +export const pfToRhIcons = { + // design sheet mappings + AddCircleOIcon: { name: 'rh-ui-add-circle', icon: getIconData('rh-ui-add-circle') }, + AngleDoubleLeftIcon: { name: 'rh-ui-double-caret-left', icon: getIconData('rh-ui-double-caret-left') }, + AngleDoubleRightIcon: { name: 'rh-ui-double-caret-right', icon: getIconData('rh-ui-double-caret-right') }, + AngleDownIcon: { name: 'rh-ui-caret-down', icon: getIconData('rh-ui-caret-down') }, + AngleLeftIcon: { name: 'rh-ui-caret-left', icon: getIconData('rh-ui-caret-left') }, + AngleRightIcon: { name: 'rh-ui-caret-right', icon: getIconData('rh-ui-caret-right') }, + AngleUpIcon: { name: 'rh-ui-caret-up', icon: getIconData('rh-ui-caret-up') }, + ArrowCircleDownIcon: { name: 'rh-ui-arrow-circle-down', icon: getIconData('rh-ui-arrow-circle-down') }, + ArrowCircleUpIcon: { name: 'rh-ui-arrow-circle-up', icon: getIconData('rh-ui-arrow-circle-up') }, + ArrowRightIcon: { name: 'rh-ui-arrow-right', icon: getIconData('rh-ui-arrow-right') }, + ArrowsAltVIcon: { name: 'rh-ui-arrow-up-down', icon: getIconData('rh-ui-arrow-up-down') }, + AsleepIcon: { name: 'rh-ui-asleep', icon: getIconData('rh-ui-asleep') }, + AttentionBellIcon: { name: 'rh-ui-attention-bell', icon: getIconData('rh-ui-attention-bell') }, + AutomationIcon: { name: 'rh-ui-automation', icon: getIconData('rh-ui-automation') }, + BalanceScaleIcon: { name: 'rh-ui-scale-balanced', icon: getIconData('rh-ui-scale-balanced') }, + BanIcon: { name: 'rh-ui-ban', icon: getIconData('rh-ui-ban') }, + BarsIcon: { name: 'rh-ui-menu-bars', icon: getIconData('rh-ui-menu-bars') }, + BellIcon: { name: 'rh-ui-notification', icon: getIconData('rh-ui-notification') }, + BlueprintIcon: { name: 'rh-ui-blueprint', icon: getIconData('rh-ui-blueprint') }, + BugIcon: { name: 'rh-ui-bug', icon: getIconData('rh-ui-bug') }, + BundleIcon: { name: 'rh-ui-cubes', icon: getIconData('rh-ui-cubes') }, + CaretDownIcon: { name: 'rh-ui-caret-down', icon: getIconData('rh-ui-caret-down') }, + CatalogIcon: { name: 'rh-ui-catalog-alt', icon: getIconData('rh-ui-catalog-alt') }, + CheckCircleIcon: { name: 'rh-ui-check-circle', icon: getIconData('rh-ui-check-circle') }, + CheckIcon: { name: 'rh-ui-check', icon: getIconData('rh-ui-check') }, + ClipboardCheckIcon: { name: 'rh-ui-check-clipboard', icon: getIconData('rh-ui-check-clipboard') }, + CloudSecurityIcon: { name: 'rh-ui-cloud-security', icon: getIconData('rh-ui-cloud-security') }, + CloudTenantIcon: { name: 'rh-ui-cloud-tenant', icon: getIconData('rh-ui-cloud-tenant') }, + ClusterIcon: { name: 'rh-ui-server-stack', icon: getIconData('rh-ui-server-stack') }, + CodeBranchIcon: { name: 'rh-ui-branch', icon: getIconData('rh-ui-branch') }, + CodeIcon: { name: 'rh-ui-code', icon: getIconData('rh-ui-code') }, + CogIcon: { name: 'rh-ui-settings', icon: getIconData('rh-ui-settings') }, + ColumnsIcon: { name: 'rh-ui-columns', icon: getIconData('rh-ui-columns') }, + CompressArrowsAltIcon: { name: 'rh-ui-compress-arrows', icon: getIconData('rh-ui-compress-arrows') }, + CompressIcon: { name: 'rh-ui-compress', icon: getIconData('rh-ui-compress') }, + ConnectedIcon: { name: 'rh-ui-connected', icon: getIconData('rh-ui-connected') }, + CopyIcon: { name: 'rh-ui-copy', icon: getIconData('rh-ui-copy') }, + CriticalRiskIcon: { name: 'rh-ui-severity-critical-fill', icon: getIconData('rh-ui-severity-critical-fill') }, + CubeIcon: { name: 'rh-ui-container', icon: getIconData('rh-ui-container') }, + CubesIcon: { name: 'rh-ui-module', icon: getIconData('rh-ui-module') }, + DatabaseIcon: { name: 'rh-ui-storage', icon: getIconData('rh-ui-storage') }, + DataProcessorIcon: { name: 'rh-ui-data-processor', icon: getIconData('rh-ui-data-processor') }, + DataSinkIcon: { name: 'rh-ui-data-sink', icon: getIconData('rh-ui-data-sink') }, + DataSourceIcon: { name: 'rh-ui-data-source', icon: getIconData('rh-ui-data-source') }, + DegradedIcon: { name: 'rh-ui-degraded', icon: getIconData('rh-ui-degraded') }, + DesktopIcon: { name: 'rh-ui-desktop', icon: getIconData('rh-ui-desktop') }, + DisconnectedIcon: { name: 'rh-ui-disconnected', icon: getIconData('rh-ui-disconnected') }, + DownloadIcon: { name: 'rh-ui-download', icon: getIconData('rh-ui-download') }, + EllipsisVIcon: { name: 'rh-ui-ellipsis-vertical', icon: getIconData('rh-ui-ellipsis-vertical') }, + EnhancementIcon: { name: 'rh-ui-enhancement', icon: getIconData('rh-ui-enhancement') }, + EnterpriseIcon: { name: 'rh-ui-enterprise', icon: getIconData('rh-ui-enterprise') }, + ExclamationCircleIcon: { name: 'rh-ui-error', icon: getIconData('rh-ui-error') }, + ExclamationTriangleIcon: { name: 'rh-ui-warning', icon: getIconData('rh-ui-warning') }, + ExpandArrowsAltIcon: { name: 'rh-ui-expand-arrows', icon: getIconData('rh-ui-expand-arrows') }, + ExpandIcon: { name: 'rh-ui-expand', icon: getIconData('rh-ui-expand') }, + ExportIcon: { name: 'rh-ui-export', icon: getIconData('rh-ui-export') }, + ExternalLinkAltIcon: { name: 'rh-ui-external-link', icon: getIconData('rh-ui-external-link') }, + EyeIcon: { name: 'rh-ui-view', icon: getIconData('rh-ui-view') }, + EyeSlashIcon: { name: 'rh-ui-view-off', icon: getIconData('rh-ui-view-off') }, + FileIcon: { name: 'rh-ui-document', icon: getIconData('rh-ui-document') }, + FilterIcon: { name: 'rh-ui-filter', icon: getIconData('rh-ui-filter') }, + FlagIcon: { name: 'rh-ui-flag', icon: getIconData('rh-ui-flag') }, + FolderIcon: { name: 'rh-ui-folder', icon: getIconData('rh-ui-folder') }, + FolderOpenIcon: { name: 'rh-ui-folder-open', icon: getIconData('rh-ui-folder-open') }, + GripHorizontalIcon: { name: 'rh-ui-grip-horizontal', icon: getIconData('rh-ui-grip-horizontal') }, + GripVerticalIcon: { name: 'rh-ui-grip-vertical', icon: getIconData('rh-ui-grip-vertical') }, + HistoryIcon: { name: 'rh-ui-history', icon: getIconData('rh-ui-history') }, + HomeIcon: { name: 'rh-ui-home', icon: getIconData('rh-ui-home') }, + ImageIcon: { name: 'rh-ui-image', icon: getIconData('rh-ui-image') }, + ImportIcon: { name: 'rh-ui-import', icon: getIconData('rh-ui-import') }, + InfoCircleIcon: { name: 'rh-ui-information', icon: getIconData('rh-ui-information') }, + InfrastructureIcon: { name: 'rh-ui-infrastructure', icon: getIconData('rh-ui-infrastructure') }, + InProgressIcon: { name: 'rh-ui-in-progress', icon: getIconData('rh-ui-in-progress') }, + IntegrationIcon: { name: 'rh-ui-puzzle-piece', icon: getIconData('rh-ui-puzzle-piece') }, + KeyIcon: { name: 'rh-ui-key', icon: getIconData('rh-ui-key') }, + LockIcon: { name: 'rh-ui-lock', icon: getIconData('rh-ui-lock') }, + LockOpenIcon: { name: 'rh-ui-unlock', icon: getIconData('rh-ui-unlock') }, + LongArrowAltDownIcon: { name: 'rh-ui-long-arrow-down', icon: getIconData('rh-ui-long-arrow-down') }, + LongArrowAltUpIcon: { name: 'rh-ui-long-arrow-up', icon: getIconData('rh-ui-long-arrow-up') }, + MemoryIcon: { name: 'rh-ui-memory', icon: getIconData('rh-ui-memory') }, + MicrochipIcon: { name: 'rh-ui-circuit', icon: getIconData('rh-ui-circuit') }, + MiddlewareIcon: { name: 'rh-ui-path', icon: getIconData('rh-ui-path') }, + MigrationIcon: { name: 'rh-ui-migrate', icon: getIconData('rh-ui-migrate') }, + MinusCircleIcon: { name: 'rh-ui-minus-circle', icon: getIconData('rh-ui-minus-circle') }, + MinusIcon: { name: 'rh-ui-minus', icon: getIconData('rh-ui-minus') }, + ModuleIcon: { name: 'rh-ui-module', icon: getIconData('rh-ui-module') }, + MonitoringIcon: { name: 'rh-ui-monitoring', icon: getIconData('rh-ui-monitoring') }, + MulticlusterIcon: { name: 'rh-ui-cluster', icon: getIconData('rh-ui-cluster') }, + NetworkIcon: { name: 'rh-ui-network', icon: getIconData('rh-ui-network') }, + OffIcon: { name: 'rh-ui-off', icon: getIconData('rh-ui-off') }, + OpenDrawerRightIcon: { name: 'rh-ui-open-drawer-right', icon: getIconData('rh-ui-open-drawer-right') }, + OptimizeIcon: { name: 'rh-ui-optimize', icon: getIconData('rh-ui-optimize') }, + OutlinedCalendarAltIcon: { name: 'rh-ui-calendar', icon: getIconData('rh-ui-calendar') }, + OutlinedClockIcon: { name: 'rh-ui-clock', icon: getIconData('rh-ui-clock') }, + OutlinedCommentsIcon: { name: 'rh-ui-comments', icon: getIconData('rh-ui-comments') }, + OutlinedHddIcon: { name: 'rh-ui-hard-drive', icon: getIconData('rh-ui-hard-drive') }, + OutlinedQuestionCircleIcon: { name: 'rh-ui-question-mark-circle', icon: getIconData('rh-ui-question-mark-circle') }, + OutlinedWindowRestoreIcon: { name: 'rh-ui-restore-window', icon: getIconData('rh-ui-restore-window') }, + PackageIcon: { name: 'rh-ui-package', icon: getIconData('rh-ui-package') }, + PauseCircleIcon: { name: 'rh-ui-pause-circle', icon: getIconData('rh-ui-pause-circle') }, + PauseIcon: { name: 'rh-ui-pause', icon: getIconData('rh-ui-pause') }, + PencilAltIcon: { name: 'rh-ui-edit', icon: getIconData('rh-ui-edit') }, + PendingIcon: { name: 'rh-ui-pending', icon: getIconData('rh-ui-pending') }, + PficonNetworkRangeIcon: { name: 'rh-ui-connected', icon: getIconData('rh-ui-connected') }, + PficonTemplateIcon: { name: 'rh-ui-template', icon: getIconData('rh-ui-template') }, + PficonVcenterIcon: { name: 'rh-ui-virtual-machine-center', icon: getIconData('rh-ui-virtual-machine-center') }, + PlayIcon: { name: 'rh-ui-play', icon: getIconData('rh-ui-play') }, + PlusIcon: { name: 'rh-ui-add', icon: getIconData('rh-ui-add') }, + PortIcon: { name: 'rh-ui-port', icon: getIconData('rh-ui-port') }, + PowerOffIcon: { name: 'rh-ui-power', icon: getIconData('rh-ui-power') }, + PrintIcon: { name: 'rh-ui-print', icon: getIconData('rh-ui-print') }, + PrivateIcon: { name: 'rh-ui-lock', icon: getIconData('rh-ui-lock') }, + ProcessAutomationIcon: { name: 'rh-ui-process-automation', icon: getIconData('rh-ui-process-automation') }, + QuestionCircleIcon: { name: 'rh-ui-question-mark-circle', icon: getIconData('rh-ui-question-mark-circle') }, + RedoIcon: { name: 'rh-ui-redo', icon: getIconData('rh-ui-redo') }, + RegionsIcon: { name: 'rh-ui-regions', icon: getIconData('rh-ui-regions') }, + RegistryIcon: { name: 'rh-ui-registry', icon: getIconData('rh-ui-registry') }, + ReplicatorIcon: { name: 'rh-ui-replicator', icon: getIconData('rh-ui-replicator') }, + RepositoryIcon: { name: 'rh-ui-folders', icon: getIconData('rh-ui-folders') }, + ResourcesAlmostEmptyIcon: { name: 'rh-ui-resources-almost-empty', icon: getIconData('rh-ui-resources-almost-empty') }, + ResourcesAlmostFullIcon: { name: 'rh-ui-resources-almost-full', icon: getIconData('rh-ui-resources-almost-full') }, + ResourcesEmptyIcon: { name: 'rh-ui-resources-empty', icon: getIconData('rh-ui-resources-empty') }, + ResourcesFullIcon: { name: 'rh-ui-resources-full', icon: getIconData('rh-ui-resources-full') }, + RouteIcon: { name: 'rh-ui-route', icon: getIconData('rh-ui-route') }, + RunningIcon: { name: 'rh-ui-running', icon: getIconData('rh-ui-running') }, + SaveIcon: { name: 'rh-ui-save', icon: getIconData('rh-ui-save') }, + SearchIcon: { name: 'rh-ui-search', icon: getIconData('rh-ui-search') }, + SearchMinusIcon: { name: 'rh-ui-zoom-out', icon: getIconData('rh-ui-zoom-out') }, + SearchPlusIcon: { name: 'rh-ui-zoom-in', icon: getIconData('rh-ui-zoom-in') }, + ServerGroupIcon: { name: 'rh-ui-server-stack', icon: getIconData('rh-ui-server-stack') }, + ServiceCatalogIcon: { name: 'rh-ui-catalog-alt', icon: getIconData('rh-ui-catalog-alt') }, + ServiceIcon: { name: 'rh-ui-kubernetes-service', icon: getIconData('rh-ui-kubernetes-service') }, + SeverityCriticalIcon: { name: 'rh-ui-severity-critical-fill', icon: getIconData('rh-ui-severity-critical-fill') }, + SeverityImportantIcon: { name: 'rh-ui-severity-important-fill', icon: getIconData('rh-ui-severity-important-fill') }, + SeverityMinorIcon: { name: 'rh-ui-severity-minor-fill', icon: getIconData('rh-ui-severity-minor-fill') }, + SeverityModerateIcon: { name: 'rh-ui-severity-moderate-fill', icon: getIconData('rh-ui-severity-moderate-fill') }, + SeverityNoneIcon: { name: 'rh-ui-severity-none-fill', icon: getIconData('rh-ui-severity-none-fill') }, + SeverityUndefinedIcon: { name: 'rh-ui-severity-undefined-fill', icon: getIconData('rh-ui-severity-undefined-fill') }, + ShareSquareIcon: { name: 'rh-ui-share-alt', icon: getIconData('rh-ui-share-alt') }, + SortAmountDownAltIcon: { + name: 'rh-ui-sort-down-small-to-large', + icon: getIconData('rh-ui-sort-down-small-to-large') + }, + SortAmountDownIcon: { name: 'rh-ui-sort-down-large-to-small', icon: getIconData('rh-ui-sort-down-large-to-small') }, + StarIcon: { name: 'rh-ui-star', icon: getIconData('rh-ui-star') }, + StorageDomainIcon: { name: 'rh-ui-storage-domain', icon: getIconData('rh-ui-storage-domain') }, + SyncAltIcon: { name: 'rh-ui-sync', icon: getIconData('rh-ui-sync') }, + TableIcon: { name: 'rh-ui-table', icon: getIconData('rh-ui-table') }, + TachometerAltIcon: { name: 'rh-ui-speedometer', icon: getIconData('rh-ui-speedometer') }, + TagIcon: { name: 'rh-ui-tag', icon: getIconData('rh-ui-tag') }, + TaskIcon: { name: 'rh-ui-task', icon: getIconData('rh-ui-task') }, + TenantIcon: { name: 'rh-ui-tenant', icon: getIconData('rh-ui-tenant') }, + ThIcon: { name: 'rh-ui-thumbnail-view-small', icon: getIconData('rh-ui-thumbnail-view-small') }, + ThLargeIcon: { name: 'rh-ui-thumbnail-view-large', icon: getIconData('rh-ui-thumbnail-view-large') }, + ThumbtackIcon: { name: 'rh-ui-thumbtack', icon: getIconData('rh-ui-thumbtack') }, + TimesCircleIcon: { name: 'rh-ui-close-circle', icon: getIconData('rh-ui-close-circle') }, + TimesIcon: { name: 'rh-ui-close', icon: getIconData('rh-ui-close') }, + TopologyIcon: { name: 'rh-ui-topology', icon: getIconData('rh-ui-topology') }, + TrashIcon: { name: 'rh-ui-trash', icon: getIconData('rh-ui-trash') }, + TrendDownIcon: { name: 'rh-ui-trend-down', icon: getIconData('rh-ui-trend-down') }, + TrendUpIcon: { name: 'rh-ui-trend-up', icon: getIconData('rh-ui-trend-up') }, + UndoIcon: { name: 'rh-ui-undo', icon: getIconData('rh-ui-undo') }, + UnknownIcon: { name: 'rh-ui-unknown', icon: getIconData('rh-ui-unknown') }, + UploadIcon: { name: 'rh-ui-upload', icon: getIconData('rh-ui-upload') }, + UserIcon: { name: 'rh-ui-profile', icon: getIconData('rh-ui-profile') }, + UsersIcon: { name: 'rh-ui-users', icon: getIconData('rh-ui-users') }, + VirtualMachineIcon: { name: 'rh-ui-virtual-server', icon: getIconData('rh-ui-virtual-server') }, + VolumeIcon: { name: 'rh-ui-storage', icon: getIconData('rh-ui-storage') }, + WrenchIcon: { name: 'rh-ui-build', icon: getIconData('rh-ui-build') }, + ZoneIcon: { name: 'rh-ui-zone', icon: getIconData('rh-ui-zone') }, + + // Cursor-generated direct mappings + AngleDoubleDownIcon: { name: 'rh-ui-double-caret-down', icon: getIconData('rh-ui-double-caret-down') }, + AngleDoubleUpIcon: { name: 'rh-ui-double-caret-up', icon: getIconData('rh-ui-double-caret-up') }, + ApplicationsIcon: { name: 'rh-ui-applications', icon: getIconData('rh-ui-applications') }, + ArrowCircleLeftIcon: { name: 'rh-ui-arrow-circle-left', icon: getIconData('rh-ui-arrow-circle-left') }, + ArrowCircleRightIcon: { name: 'rh-ui-arrow-circle-right', icon: getIconData('rh-ui-arrow-circle-right') }, + ArrowDownIcon: { name: 'rh-ui-arrow-down', icon: getIconData('rh-ui-arrow-down') }, + ArrowLeftIcon: { name: 'rh-ui-arrow-left', icon: getIconData('rh-ui-arrow-left') }, + ArrowUpIcon: { name: 'rh-ui-arrow-up', icon: getIconData('rh-ui-arrow-up') }, + BookmarkIcon: { name: 'rh-ui-bookmark', icon: getIconData('rh-ui-bookmark') }, + CarIcon: { name: 'rh-ui-car', icon: getIconData('rh-ui-car') }, + CaretLeftIcon: { name: 'rh-ui-caret-left', icon: getIconData('rh-ui-caret-left') }, + CaretRightIcon: { name: 'rh-ui-caret-right', icon: getIconData('rh-ui-caret-right') }, + CaretUpIcon: { name: 'rh-ui-caret-up', icon: getIconData('rh-ui-caret-up') }, + ClipboardIcon: { name: 'rh-ui-clipboard', icon: getIconData('rh-ui-clipboard') }, + CloudIcon: { name: 'rh-ui-cloud', icon: getIconData('rh-ui-cloud') }, + CommentIcon: { name: 'rh-ui-comment', icon: getIconData('rh-ui-comment') }, + EthernetIcon: { name: 'rh-ui-ethernet', icon: getIconData('rh-ui-ethernet') }, + FingerprintIcon: { name: 'rh-ui-fingerprint', icon: getIconData('rh-ui-fingerprint') }, + ForwardIcon: { name: 'rh-ui-forward', icon: getIconData('rh-ui-forward') }, + HourglassIcon: { name: 'rh-ui-hourglass', icon: getIconData('rh-ui-hourglass') }, + LanguageIcon: { name: 'rh-ui-language', icon: getIconData('rh-ui-language') }, + LinkIcon: { name: 'rh-ui-link', icon: getIconData('rh-ui-link') }, + ListIcon: { name: 'rh-ui-list', icon: getIconData('rh-ui-list') }, + MicrophoneIcon: { name: 'rh-ui-microphone', icon: getIconData('rh-ui-microphone') }, + NewProcessIcon: { name: 'rh-ui-new-process', icon: getIconData('rh-ui-new-process') }, + NotStartedIcon: { name: 'rh-ui-not-started', icon: getIconData('rh-ui-not-started') }, + PanelCloseIcon: { name: 'rh-ui-panel-close', icon: getIconData('rh-ui-panel-close') }, + PanelOpenIcon: { name: 'rh-ui-panel-open', icon: getIconData('rh-ui-panel-open') }, + PlayCircleIcon: { name: 'rh-ui-play-circle', icon: getIconData('rh-ui-play-circle') }, + PlugIcon: { name: 'rh-ui-plug', icon: getIconData('rh-ui-plug') }, + RobotIcon: { name: 'rh-ui-robot', icon: getIconData('rh-ui-robot') }, + RocketIcon: { name: 'rh-ui-rocket', icon: getIconData('rh-ui-rocket') }, + RssIcon: { name: 'rh-ui-rss', icon: getIconData('rh-ui-rss') }, + SecurityIcon: { name: 'rh-ui-security', icon: getIconData('rh-ui-security') }, + ServerIcon: { name: 'rh-ui-server', icon: getIconData('rh-ui-server') }, + ShareIcon: { name: 'rh-ui-share', icon: getIconData('rh-ui-share') }, + StopCircleIcon: { name: 'rh-ui-stop-circle', icon: getIconData('rh-ui-stop-circle') }, + StopIcon: { name: 'rh-ui-stop', icon: getIconData('rh-ui-stop') }, + TruckIcon: { name: 'rh-ui-truck', icon: getIconData('rh-ui-truck') }, + VolumeDownIcon: { name: 'rh-ui-volume-down', icon: getIconData('rh-ui-volume-down') }, + VolumeUpIcon: { name: 'rh-ui-volume-up', icon: getIconData('rh-ui-volume-up') }, + WindowsIcon: { name: 'rh-ui-windows', icon: getIconData('rh-ui-windows') } + + // Cursor-generated best-guess indirect mappings, need to verify before using + // BackwardIcon: { name: 'rh-ui-backwards', icon: getIconData('rh-ui-backwards') }, + // BuildIcon: { name: 'rh-ui-build-fill', icon: getIconData('rh-ui-build-fill') }, + // CalendarIcon: { name: 'rh-ui-calendar-fill', icon: getIconData('rh-ui-calendar-fill') }, + // CaretLeftIcon: { name: 'rh-ui-caret-left-fill', icon: getIconData('rh-ui-caret-left-fill') }, + // CaretRightIcon: { name: 'rh-ui-caret-right-fill', icon: getIconData('rh-ui-caret-right-fill') }, + // CaretUpIcon: { name: 'rh-ui-caret-up-fill', icon: getIconData('rh-ui-caret-up-fill') }, + // ClockIcon: { name: 'rh-ui-clock-fill', icon: getIconData('rh-ui-clock-fill') }, + // CloudDownloadAltIcon: { name: 'rh-ui-cloud-download', icon: getIconData('rh-ui-cloud-download') }, + // CloudUploadAltIcon: { name: 'rh-ui-cloud-upload', icon: getIconData('rh-ui-cloud-upload') }, + // CommentsIcon: { name: 'rh-ui-comments-fill', icon: getIconData('rh-ui-comments-fill') }, + // EditIcon: { name: 'rh-ui-edit-fill', icon: getIconData('rh-ui-edit-fill') }, + // GitlabIcon: { name: 'rh-ui-lab', icon: getIconData('rh-ui-lab') }, + // GraduationCapIcon: { name: 'rh-ui-graduation', icon: getIconData('rh-ui-graduation') }, + // MailBulkIcon: { name: 'rh-ui-mail', icon: getIconData('rh-ui-mail') }, + // MailchimpIcon: { name: 'rh-ui-mail', icon: getIconData('rh-ui-mail') }, + // PuzzlePieceIcon: { name: 'rh-ui-puzzle-piece-fill', icon: getIconData('rh-ui-puzzle-piece-fill') }, + // QuestionIcon: { name: 'rh-ui-question-mark', icon: getIconData('rh-ui-question-mark') }, + // ResourcePoolIcon: { name: 'rh-ui-resource', icon: getIconData('rh-ui-resource') }, + // ShareAltIcon: { name: 'rh-ui-share-alt-fill', icon: getIconData('rh-ui-share-alt-fill') }, + // SquareIcon: { name: 'rh-ui-add-square', icon: getIconData('rh-ui-add-square') }, + // SyncIcon: { name: 'rh-ui-sync-alt', icon: getIconData('rh-ui-sync-alt') }, + // TreeIcon: { name: 'rh-ui-tree-view', icon: getIconData('rh-ui-tree-view') }, + // UnlockIcon: { name: 'rh-ui-unlock-fill', icon: getIconData('rh-ui-unlock-fill') }, + // VoicemailIcon: { name: 'rh-ui-mail', icon: getIconData('rh-ui-mail') }, + // WindIcon: { name: 'rh-ui-window', icon: getIconData('rh-ui-window') } + + // Non-rh-ui icon mappings (commented out for now) + // AnchorIcon: { name: 'rh-standard-anchor', icon: getIconData('rh-standard-anchor') }, + // AtomIcon: { name: 'rh-standard-atom', icon: getIconData('rh-standard-atom') }, + // BinocularsIcon: { name: 'rh-standard-binoculars', icon: getIconData('rh-standard-binoculars') }, + // BlogIcon: { name: 'rh-standard-blog', icon: getIconData('rh-standard-blog') }, + // BookIcon: { name: 'rh-standard-book', icon: getIconData('rh-standard-book') }, + // BoxIcon: { name: 'rh-standard-box', icon: getIconData('rh-standard-box') }, + // BrainIcon: { name: 'rh-standard-brain', icon: getIconData('rh-standard-brain') }, + // CalculatorIcon: { name: 'rh-standard-calculator', icon: getIconData('rh-standard-calculator') }, + // CalendarIcon: { name: 'rh-standard-calendar', icon: getIconData('rh-standard-calendar') }, + // CameraIcon: { name: 'rh-standard-camera', icon: getIconData('rh-standard-camera') }, + // CaretLeftIcon: { name: 'rh-microns-caret-left', icon: getIconData('rh-microns-caret-left') }, + // CaretRightIcon: { name: 'rh-microns-caret-right', icon: getIconData('rh-microns-caret-right') }, + // CaretUpIcon: { name: 'rh-microns-caret-up', icon: getIconData('rh-microns-caret-up') }, + // CarrotIcon: { name: 'rh-standard-carrot', icon: getIconData('rh-standard-carrot') }, + // CityIcon: { name: 'rh-standard-city', icon: getIconData('rh-standard-city') }, + // ClockIcon: { name: 'rh-standard-clock', icon: getIconData('rh-standard-clock') }, + // CloseIcon: { name: 'rh-microns-close', icon: getIconData('rh-microns-close') }, + // CompassIcon: { name: 'rh-standard-compass', icon: getIconData('rh-standard-compass') }, + // CreditCardIcon: { name: 'rh-standard-credit-card', icon: getIconData('rh-standard-credit-card') }, + // CrossIcon: { name: 'rh-standard-cross', icon: getIconData('rh-standard-cross') }, + // DollarSignIcon: { name: 'rh-standard-dollar-sign', icon: getIconData('rh-standard-dollar-sign') }, + // EdgeIcon: { name: 'rh-standard-edge', icon: getIconData('rh-standard-edge') }, + // EnvelopeIcon: { name: 'rh-standard-envelope', icon: getIconData('rh-standard-envelope') }, + // FastForwardIcon: { name: 'rh-standard-fast-forward', icon: getIconData('rh-standard-fast-forward') }, + // FireExtinguisherIcon: { name: 'rh-standard-fire-extinguisher', icon: getIconData('rh-standard-fire-extinguisher') }, + // FishIcon: { name: 'rh-standard-fish', icon: getIconData('rh-standard-fish') }, + // GlobeIcon: { name: 'rh-standard-globe', icon: getIconData('rh-standard-globe') }, + // GraduationCapIcon: { name: 'rh-standard-graduation-cap', icon: getIconData('rh-standard-graduation-cap') }, + // HandshakeIcon: { name: 'rh-standard-handshake', icon: getIconData('rh-standard-handshake') }, + // HeadphonesIcon: { name: 'rh-standard-headphones', icon: getIconData('rh-standard-headphones') }, + // HeartIcon: { name: 'rh-standard-heart', icon: getIconData('rh-standard-heart') }, + // IndustryIcon: { name: 'rh-standard-industry', icon: getIconData('rh-standard-industry') }, + // InfoIcon: { name: 'rh-standard-info', icon: getIconData('rh-standard-info') }, + // KeyboardIcon: { name: 'rh-standard-keyboard', icon: getIconData('rh-standard-keyboard') }, + // LaptopIcon: { name: 'rh-standard-laptop', icon: getIconData('rh-standard-laptop') }, + // LungsIcon: { name: 'rh-standard-lungs', icon: getIconData('rh-standard-lungs') }, + // MapIcon: { name: 'rh-standard-map', icon: getIconData('rh-standard-map') }, + // MouseIcon: { name: 'rh-standard-mouse', icon: getIconData('rh-standard-mouse') }, + // NewspaperIcon: { name: 'rh-standard-newspaper', icon: getIconData('rh-standard-newspaper') }, + // PaintRollerIcon: { name: 'rh-standard-paint-roller', icon: getIconData('rh-standard-paint-roller') }, + // PenIcon: { name: 'rh-standard-pen', icon: getIconData('rh-standard-pen') }, + // PhoneIcon: { name: 'rh-standard-phone', icon: getIconData('rh-standard-phone') }, + // PiggyBankIcon: { name: 'rh-standard-piggy-bank', icon: getIconData('rh-standard-piggy-bank') }, + // PuzzlePieceIcon: { name: 'rh-standard-puzzle-piece', icon: getIconData('rh-standard-puzzle-piece') }, + // RainbowIcon: { name: 'rh-standard-rainbow', icon: getIconData('rh-standard-rainbow') }, + // RecycleIcon: { name: 'rh-standard-recycle', icon: getIconData('rh-standard-recycle') }, + // RibbonIcon: { name: 'rh-standard-ribbon', icon: getIconData('rh-standard-ribbon') }, + // SchoolIcon: { name: 'rh-standard-school', icon: getIconData('rh-standard-school') }, + // ShowerIcon: { name: 'rh-standard-shower', icon: getIconData('rh-standard-shower') }, + // SnowflakeIcon: { name: 'rh-standard-snowflake', icon: getIconData('rh-standard-snowflake') }, + // StopwatchIcon: { name: 'rh-standard-stopwatch', icon: getIconData('rh-standard-stopwatch') }, + // SunIcon: { name: 'rh-standard-sun', icon: getIconData('rh-standard-sun') }, + // TabletIcon: { name: 'rh-standard-tablet', icon: getIconData('rh-standard-tablet') }, + // ToolboxIcon: { name: 'rh-standard-toolbox', icon: getIconData('rh-standard-toolbox') }, + // TreeIcon: { name: 'rh-standard-tree', icon: getIconData('rh-standard-tree') }, + // TrophyIcon: { name: 'rh-standard-trophy', icon: getIconData('rh-standard-trophy') }, + // TshirtIcon: { name: 'rh-standard-tshirt', icon: getIconData('rh-standard-tshirt') }, + // UmbrellaIcon: { name: 'rh-standard-umbrella', icon: getIconData('rh-standard-umbrella') }, + // UtensilsIcon: { name: 'rh-standard-utensils', icon: getIconData('rh-standard-utensils') }, + // VideoIcon: { name: 'rh-standard-video', icon: getIconData('rh-standard-video') }, + // VolumeMuteIcon: { name: 'rh-standard-volume-mute', icon: getIconData('rh-standard-volume-mute') }, + // WifiIcon: { name: 'rh-standard-wifi', icon: getIconData('rh-standard-wifi') }, + // WindIcon: { name: 'rh-standard-wind', icon: getIconData('rh-standard-wind') } +}; From c85b84ecc59749fe128de9295e9a7e05dfd57ba3 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 16 Jan 2026 14:47:49 -0500 Subject: [PATCH 2/2] update createIcon tests for new structure --- .../src/__tests__/createIcon.test.tsx | 39 ++++++++++++------- packages/react-icons/src/createIcon.tsx | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/react-icons/src/__tests__/createIcon.test.tsx b/packages/react-icons/src/__tests__/createIcon.test.tsx index 2b0e36a6b6f..cfc8dd2115f 100644 --- a/packages/react-icons/src/__tests__/createIcon.test.tsx +++ b/packages/react-icons/src/__tests__/createIcon.test.tsx @@ -1,24 +1,35 @@ import { render, screen } from '@testing-library/react'; -import { createIcon } from '../createIcon'; +import { IconDefinition, CreateIconProps, createIcon, SVGPathObject } from '../createIcon'; -const iconDef = { +const multiPathIcon: IconDefinition = { name: 'IconName', width: 10, height: 20, - svgPath: 'svgPath' + svgPathData: [ + { path: 'svgPath1', className: 'class1' }, + { path: 'svgPath2', className: 'class2' } + ], + svgClassName: 'test' }; -const iconDefWithArrayPath = { +const singlePathIcon: IconDefinition = { name: 'IconName', width: 10, height: 20, - svgPath: [ - { path: 'svgPath1', className: 'class1' }, - { path: 'svgPath2', className: 'class2' } - ], + svgPathData: 'svgPath', svgClassName: 'test' }; +const iconDef: CreateIconProps = { + name: 'SinglePathIconName', + icon: singlePathIcon +}; + +const iconDefWithArrayPath: CreateIconProps = { + name: 'MultiPathIconName', + icon: multiPathIcon +}; + const SVGIcon = createIcon(iconDef); const SVGArrayIcon = createIcon(iconDefWithArrayPath); @@ -26,7 +37,7 @@ test('sets correct viewBox', () => { render(); expect(screen.getByRole('img', { hidden: true })).toHaveAttribute( 'viewBox', - `0 0 ${iconDef.width} ${iconDef.height}` + `0 0 ${singlePathIcon.width} ${singlePathIcon.height}` ); }); @@ -39,16 +50,16 @@ test('sets correct svgPath if array', () => { render(); const paths = screen.getByRole('img', { hidden: true }).querySelectorAll('path'); expect(paths).toHaveLength(2); - expect(paths[0]).toHaveAttribute('d', iconDefWithArrayPath.svgPath[0].path); - expect(paths[1]).toHaveAttribute('d', iconDefWithArrayPath.svgPath[1].path); - expect(paths[0]).toHaveClass(iconDefWithArrayPath.svgPath[0].className); - expect(paths[1]).toHaveClass(iconDefWithArrayPath.svgPath[1].className); + expect(paths[0]).toHaveAttribute('d', (multiPathIcon.svgPathData as SVGPathObject[])[0].path); + expect(paths[1]).toHaveAttribute('d', (multiPathIcon.svgPathData as SVGPathObject[])[1].path); + expect(paths[0]).toHaveClass((multiPathIcon.svgPathData as SVGPathObject[])[0].className ?? ''); + expect(paths[1]).toHaveClass((multiPathIcon.svgPathData as SVGPathObject[])[1].className ?? ''); }); test('sets correct svgClassName', () => { render(); const paths = screen.getByRole('img', { hidden: true }); - expect(paths).toHaveClass(iconDefWithArrayPath.svgClassName); + expect(paths).toHaveClass(multiPathIcon.svgClassName ?? ''); }); test('aria-hidden is true if no title is specified', () => { diff --git a/packages/react-icons/src/createIcon.tsx b/packages/react-icons/src/createIcon.tsx index 60b0c668d05..169675fab6f 100644 --- a/packages/react-icons/src/createIcon.tsx +++ b/packages/react-icons/src/createIcon.tsx @@ -15,7 +15,7 @@ export interface IconDefinition { svgClassName?: string; } -export interface CreateIconProps extends IconDefinition { +export interface CreateIconProps { name?: string; icon?: IconDefinition; rhUiIcon?: IconDefinition | null;