Skip to content

feat(icons): add swap support for mapped rh-ui icons#12245

Open
kmcfaul wants to merge 9 commits intopatternfly:mainfrom
kmcfaul:dual-icons
Open

feat(icons): add swap support for mapped rh-ui icons#12245
kmcfaul wants to merge 9 commits intopatternfly:mainfrom
kmcfaul:dual-icons

Conversation

@kmcfaul
Copy link
Contributor

@kmcfaul kmcfaul commented Feb 16, 2026

What: Closes #12220

Adds a mapping for a subset of existing PF icons with new corresponding RH-ui set icons.

Icon structure for mapped icons has been updated to the following nested structure (this does mean snapshot updates are required for icons in the mapping unless set is specified):

svg.pf-v6-svg
   svg.pf-v6-icon-default
   svg.pf-v6-icon-rh-ui

The displayed svg between icon-default and icon-rh-ui is controlled via the new set prop (which has default and rh-ui values) or by applying the pf-v6-icon-set-rh-ui class to the html element similar to theme.

Summary by CodeRabbit

  • New Features

    • Icons can expose and switch to an alternate "rh‑ui" visual variant; a new icon mapping supports these variants.
  • Bug Fixes / API

    • Improved handling of single vs. multi-path icons, variant-aware rendering, and consistent aria/title propagation; public icon data shape updated to support both variants.
  • Chores

    • Bumped design system prerelease versions across packages.

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds dynamic icon swapping: introduces a PF→RH-UI icon mapping, threads alternative rhUiIcon metadata through icon generation, updates generated exports to include rhUiIcon, and refactors the icon factory and tests to support rendering PatternFly or Red Hat UI icon variants.

Changes

Cohort / File(s) Summary
Dependency Version Bumps
packages/react-core/package.json, packages/react-docs/package.json, packages/react-icons/package.json, packages/react-styles/package.json, packages/react-tokens/package.json
Bumped devDependency @patternfly/patternfly from 6.5.0-prerelease.346.5.0-prerelease.41.
Icon Mapping Data
packages/react-icons/scripts/icons/pfToRhIcons.mjs
New exported pfToRhIcons mapping resolving PatternFly icon names to Red Hat UI icon definitions; includes safe lookup helper and many mapped entries.
Icon Generation Script
packages/react-icons/scripts/writeIcons.mjs
Imports pfToRhIcons, computes altIcon (rhUiIcon) per icon, and extended writer functions to accept and emit rhUiIcon in CJS/ESM/DTS outputs.
Icon Component Refactor
packages/react-icons/src/createIcon.tsx
API/type changes: IconDefinition now uses svgPathData; added CreateIconProps with optional rhUiIcon; SVGIconProps gains `set?: 'default'
Icon Tests
packages/react-icons/src/__tests__/createIcon.test.tsx
Tests adapted to new types and shapes (IconDefinition, CreateIconProps, SVGPathObject, svgPathData, svgClassName) and updated assertions to match new rendering/data shape.
New RH icon sources (referenced)
packages/react-icons/scripts/icons/rhIconsUI.mjs*
Mapping consumes RH UI icon definitions; ensure referenced modules exist and match expected shape.
TypeScript / Misc
packages/react-icons/tsconfig.json, packages/react-icons/scripts/...
Minor tsconfig formatting change and added script files; no functional TS semantics changed.

Sequence Diagram

sequenceDiagram
    participant Build as Build System
    participant Mapping as pfToRhIcons
    participant Script as writeIcons.mjs
    participant Registry as Generated Exports
    participant Component as createIcon
    participant Runtime as SVGIcon

    Build->>Mapping: import pfToRhIcons
    Mapping-->>Build: mapping entries (rhUiIcon or null)
    Build->>Script: run writeIcons for icon list
    Script->>Mapping: lookup pfToRhIcons[jsName]
    Mapping-->>Script: return rhUiIcon (or null)
    Script->>Registry: emit icon configs including rhUiIcon
    Registry-->>Component: createIcon invoked with icon + rhUiIcon
    Component->>Runtime: produce SVGIcon supporting 'set' prop
    Runtime->>Runtime: 'set' selects PF or RH‑UI paths for rendering
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • nicolethoen
  • mcoker
  • gitdallas
  • thatblindgeye
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(icons): add swap support for mapped rh-ui icons' clearly and specifically describes the main change: adding support for swapping between default PatternFly icons and mapped RH-ui icons. It is concise, uses conventional commit formatting, and accurately reflects the primary objective of the pull request.
Linked Issues check ✅ Passed The PR implements all coding-related requirements from issue #12220: finalizes icon mapping (pfToRhIcons.mjs with extensive mappings), implements display switching via the new 'set' prop ('default'|'rh-ui'), adds new CSS classes for nested SVG layout and global toggle support, and includes unit test updates.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the icon swapping feature: dependency updates support this work, createIcon.tsx implements core swapping logic, writeIcons.mjs enables dual icon data generation, pfToRhIcons.mjs provides the mapping, test files validate the new functionality, and tsconfig adjustments support the build.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@patternfly-build
Copy link
Collaborator

patternfly-build commented Feb 16, 2026

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react-icons/src/__tests__/createIcon.test.tsx (1)

44-47: ⚠️ Potential issue | 🔴 Critical

Bug: iconDef.svgPath is undefined — test will fail or pass vacuously.

iconDef is now typed as CreateIconProps with shape { name, icon }. There is no svgPath property on it, so iconDef.svgPath evaluates to undefined. This test either fails or incorrectly passes depending on how toHaveAttribute handles undefined.

It should reference the path data from the underlying IconDefinition:

🐛 Proposed fix
 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 as string);
 });
🤖 Fix all issues with AI agents
In `@packages/react-icons/src/createIcon.tsx`:
- Around line 123-152: The else-branch contains dead conditional checks and
produces a boolean|string for the second arg to createSvg; simplify by treating
set as undefined and rhUiIcon as non-null in this branch: assign
defaultIconClassName = 'pf-v6-icon-default' and rhUiIconClassName =
'pf-v6-icon-rh-ui' unconditionally, remove shouldRenderDefault/shouldRenderRhUi
and their impossible set checks, and call createSvg(icon, defaultIconClassName)
and createSvg(rhUiIcon, rhUiIconClassName) so the second parameter is always a
string (refer to createIcon component and the createSvg invocations).
🧹 Nitpick comments (5)
packages/react-icons/src/pfToRhIcons.js (1)

3-4: Silent null for missing/misspelled icon names could mask build-time errors.

If an rh-ui icon name in the mapping doesn't exist in rhIconsUI, getIconData silently returns null, and the generated icon module will have rhUiIcon: null — indistinguishable from an icon that intentionally has no mapping. Consider logging a warning during the build when a mapped icon name yields no data.

💡 Suggested improvement
-const getIconData = (iconName) => rhIconsUI[iconName] || null;
+const getIconData = (iconName) => {
+  const data = rhIconsUI[iconName];
+  if (!data) {
+    console.warn(`[pfToRhIcons] No rh-ui icon data found for "${iconName}"`);
+  }
+  return data || null;
+};
packages/react-icons/scripts/writeIcons.mjs (1)

54-68: Type declarations embed full icon data as literal types — consider using the type name instead.

writeDTSExport serializes the entire rhUiIcon SVG path data as a JSON literal in .d.ts files (rhUiIcon: {"svgPathData":"M0 0...","width":512,...}). This inflates .d.ts file sizes and provides no type-safety benefit over a named type. Consider using the IconDefinition | null type instead of the literal value.

💡 Suggested change for the type declaration
 const text = `import { ComponentClass } from 'react';
-import { SVGIconProps } from '../createIcon';
+import { SVGIconProps, IconDefinition } from '../createIcon';
 export declare const ${jsName}Config: {
   name: '${jsName}',
-  icon: ${JSON.stringify(icon)},
-  rhUiIcon: ${rhUiIcon ? JSON.stringify(rhUiIcon) : 'null'},
+  icon: IconDefinition,
+  rhUiIcon: IconDefinition | null,
 };
packages/react-icons/src/createIcon.tsx (3)

73-75: Unnecessary constructor — can be removed.

This constructor only calls super(props), which is the default behavior. Removing it reduces noise.

Suggested diff
-    constructor(props: SVGIconProps) {
-      super(props);
-    }
-

87-92: Branching condition is correct but hard to parse — consider simplifying.

Line 87 (set === undefined && rhUiIcon === null) || set !== undefined is equivalent to rhUiIcon === null || set !== undefined. Similarly, on Line 88, set !== undefined && set === 'rh-ui' can be just set === 'rh-ui'.

Suggested simplification
-      if ((set === undefined && rhUiIcon === null) || set !== undefined) {
-        const iconData = set !== undefined && set === 'rh-ui' && rhUiIcon !== null ? rhUiIcon : icon;
+      if (rhUiIcon === null || set !== undefined) {
+        const iconData = set === 'rh-ui' && rhUiIcon !== null ? rhUiIcon : icon;

89-105: Duplicated viewBox/path rendering logic — consider reusing createSvg or extracting shared helpers.

Lines 89–105 replicate the exact viewBox computation and path-rendering logic already in createSvg (Lines 33–55). If the if-branch needs a flat (non-nested) SVG, you could still extract the shared parts (viewBox calc, path rendering) into small helpers and compose them in both branches, reducing the maintenance surface.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/react-icons/tsconfig.json`:
- Around line 14-15: The include entry in this tsconfig references
"scripts/pfToRhIcons.mjs" which doesn't match the actual file location
("scripts/icons/pfToRhIcons.mjs") and .mjs files won't be picked up unless
allowJs is enabled; update the include to the correct path
("scripts/icons/pfToRhIcons.mjs") or remove the .mjs entry if you don't want it
type-checked, and if you do want TypeScript to check the .mjs file enable
allowJs in the base tsconfig so the file is actually processed.
🧹 Nitpick comments (3)
packages/react-icons/scripts/icons/pfToRhIcons.mjs (2)

3-4: Silent null for missing icons may hide typos in mappings.

getIconData returns null when an icon name isn't found in rhIconsUI. If any of the ~170 active mapping strings contain a typo, the build will silently produce a null icon field — resulting in a broken rh-ui icon at runtime with no build-time feedback.

Consider logging a warning (or throwing during the build) when a lookup fails:

Proposed change
-const getIconData = (iconName) => rhIconsUI[iconName] || null;
+const getIconData = (iconName) => {
+  const data = rhIconsUI[iconName];
+  if (!data) {
+    console.warn(`[pfToRhIcons] rh-ui icon "${iconName}" not found in rhIconsUI`);
+  }
+  return data || null;
+};

8-223: name is always identical to the getIconData argument — consider DRY helper.

Every entry repeats the rh-ui icon name twice (once in name, once in getIconData(…)). A small helper would eliminate the duplication and the risk of the two values drifting apart:

Proposed refactor
+const rhUiEntry = (rhUiName) => ({ name: rhUiName, icon: getIconData(rhUiName) });
+
 export const pfToRhIcons = {
-  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') },
+  AddCircleOIcon: rhUiEntry('rh-ui-add-circle'),
+  AngleDoubleLeftIcon: rhUiEntry('rh-ui-double-caret-left'),
   // ... same pattern for all entries
packages/react-icons/scripts/writeIcons.mjs (1)

124-128: Simplify with optional chaining + nullish coalescing.

Minor: the ternary can be replaced with the more concise form that the rest of the codebase seems to favor.

Proposed change
-    const altIcon = pfToRhIcons[jsName] ? pfToRhIcons[jsName].icon : null;
+    const altIcon = pfToRhIcons[jsName]?.icon ?? null;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/react-icons/src/createIcon.tsx`:
- Line 88: When consumers pass set === 'rh-ui' but no RH-UI mapping exists
(rhUiIcon === null) the code silently falls back to the default icon; update the
logic in createIcon (around the iconData computation) to emit a development-only
warning (e.g., check process.env.NODE_ENV !== 'production' or __DEV__) that
console.warns something like "Requested RH-UI icon mapping not found for <icon
name> — falling back to default" when set === 'rh-ui' && rhUiIcon === null, then
proceed to use the existing fallback assignment for iconData.
🧹 Nitpick comments (3)
packages/react-icons/src/createIcon.tsx (3)

87-122: Duplicated SVG-building logic between createSvg and the if-branch.

Lines 89–105 manually replicate the viewBox calculation and path rendering that createSvg (lines 33–61) already encapsulates. The only difference is that here the result isn't wrapped in an inner <svg>. Consider extracting the shared path-rendering and viewBox logic into a common helper, or restructuring so the if-branch can also leverage createSvg (perhaps by having createSvg return a fragment and applying viewBox at the call site).

This would eliminate the maintenance burden of keeping two copies in sync.


87-87: Simplify the branching condition for clarity.

(set === undefined && rhUiIcon === null) || set !== undefined is logically equivalent to set !== undefined || rhUiIcon === null. The current form is harder to parse at a glance.

Suggested simplification
-      if ((set === undefined && rhUiIcon === null) || set !== undefined) {
+      if (set !== undefined || rhUiIcon === null) {

73-75: No-op constructor can be removed.

This constructor only calls super(props) — the default behavior. Removing it reduces noise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/react-icons/src/createIcon.tsx (4)

73-75: Unnecessary constructor.

A constructor that only calls super(props) is implicit in class components and can be removed.

Suggested removal
-    constructor(props: SVGIconProps) {
-      super(props);
-    }
-

87-92: Warning fires on every render and is not gated to development mode.

This console.warn executes on every render cycle when the condition is met, which can flood the console. Additionally, it is not guarded by a process.env.NODE_ENV !== 'production' check, so it will appear in production builds.

Gate warning to dev mode
       if (set === 'rh-ui' && rhUiIcon === null) {
+        if (process.env.NODE_ENV !== 'production') {
         // eslint-disable-next-line no-console
         console.warn(
           `Set "rh-ui" was provided for ${name}, but no rh-ui icon data exists for this icon. The default icon will be rendered.`
         );
+        }
       }

94-129: DRY violation: if-branch duplicates createSvg logic; condition can be simplified.

Two concerns here:

  1. Condition simplification: (set === undefined && rhUiIcon === null) || set !== undefined is logically equivalent to set !== undefined || rhUiIcon === null. The simpler form reads more clearly: "use a single SVG when a set is explicitly chosen or there's no rh-ui variant."

  2. Duplicated logic: Lines 96–112 replicate the same viewBox computation, className assembly, and path-rendering logic already encapsulated in createSvg (lines 33–62). I understand the structural difference (flat vs. nested SVG), but at minimum the path-rendering portion (array-vs-string branch) could be extracted into a shared helper to avoid having two copies to maintain.

Simplify condition and extract path rendering
-      if ((set === undefined && rhUiIcon === null) || set !== undefined) {
+      if (set !== undefined || rhUiIcon === null) {

For the path-rendering duplication, consider extracting a small helper:

const renderPaths = (svgPathData: string | SVGPathObject[]) =>
  Array.isArray(svgPathData)
    ? svgPathData.map((pathObject, index) => (
        <path className={pathObject.className} key={`${pathObject.path}-${index}`} d={pathObject.path} />
      ))
    : (<path d={svgPathData as string} />);

This could then be used in both the if-branch render and inside createSvg.


95-95: Redundant set !== undefined guard.

set === 'rh-ui' already implies set !== undefined, so the check can be shortened to:

-        const iconData = set !== undefined && set === 'rh-ui' && rhUiIcon !== null ? rhUiIcon : icon;
+        const iconData = set === 'rh-ui' && rhUiIcon !== null ? rhUiIcon : icon;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Icons: dynamic swapping implementation

2 participants