Skip to content

Commit 68b1e2b

Browse files
committed
fixes from review comments
1 parent fe6db88 commit 68b1e2b

2 files changed

Lines changed: 88 additions & 71 deletions

File tree

packages/react-icons/src/__tests__/createIcon.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { render, screen } from '@testing-library/react';
22
import {
33
IconDefinition,
44
CreateIconBaseProps,
5+
CreateIconLegacyProps,
56
createIcon,
67
createIconBase,
7-
LegacyFlatIconDefinition,
88
SVGPathObject
99
} from '../createIcon';
1010

@@ -71,7 +71,7 @@ test('sets correct svgPath if string', () => {
7171
});
7272

7373
test('accepts legacy flat createIcon({ svgPath }) shape', () => {
74-
const legacyDef: LegacyFlatIconDefinition = {
74+
const legacyDef: CreateIconLegacyProps = {
7575
name: 'LegacyIcon',
7676
width: 10,
7777
height: 20,

packages/react-icons/src/createIcon.tsx

Lines changed: 86 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from 'react';
1+
import { Component, type ReactNode } from 'react';
22

33
export interface SVGPathObject {
44
path: string;
@@ -51,18 +51,28 @@ export interface CreateIconBaseProps {
5151
export type CreateIconProps = CreateIconBaseProps;
5252

5353
/**
54-
* Props for {@link createIcon} — flat {@link IconDefinition} fields at the top level, optionally with
55-
* `rhUiIcon`, matching the pre–nested-config API.
54+
* The **flat (legacy) public API** for {@link createIcon}: the same path/view fields as
55+
* {@link IconDefinition} at the top level, plus optional `rhUiIcon` for the dual-`set` layout.
56+
* `createIcon` is the only entry that accepts this shape; it maps to {@link CreateIconBaseProps} and calls
57+
* {@link createIconBase} (see {@link createIcon}).
5658
*/
57-
export type CreateIconLegacyProps = IconDefinition & {
59+
export interface CreateIconLegacyProps {
60+
name?: string;
61+
width: number;
62+
height: number;
63+
xOffset?: number;
64+
yOffset?: number;
65+
svgPathData?: string | SVGPathObject[];
66+
/**
67+
* @deprecated Use {@link CreateIconLegacyProps.svgPathData} instead.
68+
*/
69+
svgPath?: string | SVGPathObject[];
70+
svgClassName?: string;
71+
/**
72+
* Optional second variant for the `set="rh-ui"` / nested-inner-SVG layout, matching {@link createIconBase}.
73+
*/
5874
rhUiIcon?: IconDefinition | null;
59-
};
60-
61-
/**
62-
* @deprecated The previous `createIcon` accepted only a flat {@link IconDefinition}. Use {@link createIcon}
63-
* for that shape, or {@link createIconBase} with nested `icon` / `rhUiIcon`.
64-
*/
65-
export type LegacyFlatIconDefinition = IconDefinition;
75+
}
6676

6777
export interface SVGIconProps extends Omit<React.HTMLProps<SVGElement>, 'ref'> {
6878
title?: string;
@@ -99,14 +109,25 @@ function normalizeIconDefinition(icon: IconDefinition): IconDefinitionWithSvgPat
99109
};
100110
}
101111

112+
/** Renders <path> element(s) from resolved (normalized) path data. */
113+
function pathElementsFromResolvedData(svgPathData: string | SVGPathObject[]): ReactNode {
114+
return Array.isArray(svgPathData) ? (
115+
svgPathData.map((pathObject, index) => (
116+
<path className={pathObject.className} key={`${pathObject.path}-${index}`} d={pathObject.path} />
117+
))
118+
) : (
119+
<path d={svgPathData} />
120+
);
121+
}
122+
102123
/** Renders an inner `<svg>` with viewBox and path(s) for the dual-SVG (CSS swap) layout. */
103124
const createSvg = (icon: IconDefinitionWithSvgPathData, iconClassName: string) => {
104-
const { xOffset, yOffset, width, height, svgPathData, svgClassName } = icon ?? {};
125+
const { xOffset, yOffset, width, height, svgPathData, svgClassName } = icon;
105126
const _xOffset = xOffset ?? 0;
106127
const _yOffset = yOffset ?? 0;
107128
const viewBox = [_xOffset, _yOffset, width, height].join(' ');
108129

109-
const classNames = [];
130+
const classNames: string[] = [];
110131

111132
if (svgClassName) {
112133
classNames.push(svgClassName);
@@ -115,18 +136,9 @@ const createSvg = (icon: IconDefinitionWithSvgPathData, iconClassName: string) =
115136
classNames.push(iconClassName);
116137
}
117138

118-
const svgPaths =
119-
svgPathData && Array.isArray(svgPathData) ? (
120-
svgPathData.map((pathObject, index) => (
121-
<path className={pathObject.className} key={`${pathObject.path}-${index}`} d={pathObject.path} />
122-
))
123-
) : (
124-
<path d={svgPathData as string} />
125-
);
126-
127139
return (
128140
<svg viewBox={viewBox} className={classNames.join(' ')}>
129-
{svgPaths}
141+
{pathElementsFromResolvedData(svgPathData)}
130142
</svg>
131143
);
132144
};
@@ -155,10 +167,33 @@ export function createIconBase({
155167

156168
id = `icon-title-${currentId++}`;
157169

170+
private warnedMissingRhUi = false;
171+
158172
static defaultProps: SVGIconProps = {
159173
noDefaultStyle: false
160174
};
161175

176+
private warnIfMissingRhUiMapping = () => {
177+
if (this.warnedMissingRhUi) {
178+
return;
179+
}
180+
if (this.props.set === 'rh-ui' && normalizedRhUiIcon === null) {
181+
this.warnedMissingRhUi = true;
182+
// eslint-disable-next-line no-console -- intentional dev-facing warning for invalid set/rh-ui pairing
183+
console.warn(
184+
`Set "rh-ui" was provided for ${displayName}, but no rh-ui icon data exists for this icon. The default icon will be rendered.`
185+
);
186+
}
187+
};
188+
189+
componentDidMount() {
190+
this.warnIfMissingRhUiMapping();
191+
}
192+
193+
componentDidUpdate() {
194+
this.warnIfMissingRhUiMapping();
195+
}
196+
162197
/** Renders one root `<svg>`; either a single variant or nested inner SVGs for RH UI swap. */
163198
render() {
164199
const { title, className: propsClassName, set, noDefaultStyle, ...props } = this.props;
@@ -170,18 +205,10 @@ export function createIconBase({
170205
classNames.push(propsClassName);
171206
}
172207

173-
if (set === 'rh-ui' && normalizedRhUiIcon === null) {
174-
// eslint-disable-next-line no-console
175-
console.warn(
176-
`Set "rh-ui" was provided for ${displayName}, but no rh-ui icon data exists for this icon. The default icon will be rendered.`
177-
);
178-
}
179-
180-
if ((set === undefined && normalizedRhUiIcon === null) || set !== undefined) {
181-
const iconData: IconDefinitionWithSvgPathData | undefined =
182-
set !== undefined && set === 'rh-ui' && normalizedRhUiIcon !== null ? normalizedRhUiIcon : normalizedIcon;
183-
const { xOffset, yOffset, width, height, svgPathData, svgClassName } =
184-
iconData ?? ({} as Partial<IconDefinitionWithSvgPathData>);
208+
if (set !== undefined || normalizedRhUiIcon === null) {
209+
const iconData: IconDefinitionWithSvgPathData =
210+
set === 'rh-ui' && normalizedRhUiIcon !== null ? normalizedRhUiIcon : normalizedIcon;
211+
const { xOffset, yOffset, width, height, svgPathData, svgClassName } = iconData;
185212
const _xOffset = xOffset ?? 0;
186213
const _yOffset = yOffset ?? 0;
187214
const viewBox = [_xOffset, _yOffset, width, height].join(' ');
@@ -190,14 +217,7 @@ export function createIconBase({
190217
classNames.push(svgClassName);
191218
}
192219

193-
const svgPaths =
194-
svgPathData && Array.isArray(svgPathData) ? (
195-
svgPathData.map((pathObject, index) => (
196-
<path className={pathObject.className} key={`${pathObject.path}-${index}`} d={pathObject.path} />
197-
))
198-
) : (
199-
<path d={svgPathData as string} />
200-
);
220+
const svgPaths = pathElementsFromResolvedData(svgPathData);
201221

202222
return (
203223
<svg
@@ -215,37 +235,34 @@ export function createIconBase({
215235
{svgPaths}
216236
</svg>
217237
);
218-
} else {
219-
return (
220-
<svg
221-
className={classNames.join(' ')}
222-
fill="currentColor"
223-
aria-labelledby={hasTitle ? this.id : null}
224-
aria-hidden={hasTitle ? null : true}
225-
role="img"
226-
width="1em"
227-
height="1em"
228-
{...(props as Omit<React.SVGProps<SVGElement>, 'ref'>)} // Lie.
229-
>
230-
{hasTitle && <title id={this.id}>{title}</title>}
231-
{normalizedIcon && createSvg(normalizedIcon, 'pf-v6-icon-default')}
232-
{normalizedRhUiIcon && createSvg(normalizedRhUiIcon, 'pf-v6-icon-rh-ui')}
233-
</svg>
234-
);
235238
}
239+
return (
240+
<svg
241+
className={classNames.join(' ')}
242+
fill="currentColor"
243+
aria-labelledby={hasTitle ? this.id : null}
244+
aria-hidden={hasTitle ? null : true}
245+
role="img"
246+
width="1em"
247+
height="1em"
248+
{...(props as Omit<React.SVGProps<SVGElement>, 'ref'>)} // Lie.
249+
>
250+
{hasTitle && <title id={this.id}>{title}</title>}
251+
{createSvg(normalizedIcon, 'pf-v6-icon-default')}
252+
{normalizedRhUiIcon && createSvg(normalizedRhUiIcon, 'pf-v6-icon-rh-ui')}
253+
</svg>
254+
);
236255
}
237256
};
238257
}
239258

240259
/**
241-
* Legacy-friendly factory: **flat** {@link IconDefinition} fields (plus optional `rhUiIcon`) and delegates to
242-
* {@link createIconBase}. For nested configs, use {@link createIconBase} directly.
260+
* Legacy **flat** factory: maps {@link CreateIconLegacyProps} to {@link CreateIconBaseProps} and calls
261+
* {@link createIconBase} (all remapping lives here; `createIconBase` only implements rendering). For the
262+
* nested `icon` / `rhUiIcon` shape, use {@link createIconBase} directly.
243263
*/
244-
export function createIcon(props: CreateIconLegacyProps): React.ComponentClass<SVGIconProps> {
245-
const { rhUiIcon, ...icon } = props;
246-
return createIconBase({
247-
name: icon.name,
248-
icon,
249-
rhUiIcon: rhUiIcon ?? null
250-
});
264+
export function createIcon(legacy: CreateIconLegacyProps): React.ComponentClass<SVGIconProps> {
265+
const { rhUiIcon = null, ...iconFields } = legacy;
266+
const icon: IconDefinition = iconFields;
267+
return createIconBase({ name: icon.name, icon, rhUiIcon });
251268
}

0 commit comments

Comments
 (0)