@@ -4,45 +4,96 @@ export interface SVGPathObject {
44 path : string ;
55 className ?: string ;
66}
7+
78export interface IconDefinition {
89 name ?: string ;
910 width : number ;
1011 height : number ;
11- svgPath : string | SVGPathObject [ ] ;
12+ svgPathData : string | SVGPathObject [ ] ;
1213 xOffset ?: number ;
1314 yOffset ?: number ;
1415 svgClassName ?: string ;
1516}
1617
18+ export interface CreateIconProps extends IconDefinition {
19+ name ?: string ;
20+ icon ?: IconDefinition ;
21+ rhUiIcon ?: IconDefinition | null ;
22+ }
23+
1724export interface SVGIconProps extends Omit < React . HTMLProps < SVGElement > , 'ref' > {
1825 title ?: string ;
1926 className ?: string ;
27+ /* Indicates the icon should render using alternate svg data for unified theme */
28+ set ?: 'default' | 'unified' ;
2029}
2130
2231let currentId = 0 ;
32+ const canUseDOM = ! ! ( typeof window !== 'undefined' && window . document && window . document . createElement ) ;
2333
2434/**
2535 * Factory to create Icon class components for consumers
2636 */
27- export function createIcon ( {
28- name,
29- xOffset = 0 ,
30- yOffset = 0 ,
31- width,
32- height,
33- svgPath,
34- svgClassName
35- } : IconDefinition ) : React . ComponentClass < SVGIconProps > {
36- return class SVGIcon extends Component < SVGIconProps > {
37+ export function createIcon ( { name, icon, rhUiIcon = null } : CreateIconProps ) : React . ComponentClass < SVGIconProps > {
38+ return class SVGIcon extends Component < SVGIconProps , { themeClassVersion : number } > {
3739 static displayName = name ;
3840
3941 id = `icon-title-${ currentId ++ } ` ;
42+ private observer : MutationObserver | null = null ;
43+
44+ constructor ( props : SVGIconProps ) {
45+ super ( props ) ;
46+ this . state = { themeClassVersion : 0 } ;
47+ }
48+
49+ componentDidMount ( ) {
50+ if ( rhUiIcon !== null && canUseDOM ) {
51+ this . observer = new MutationObserver ( ( mutations ) => {
52+ for ( const mutation of mutations ) {
53+ if ( mutation . type === 'attributes' && mutation . attributeName === 'class' ) {
54+ const target = mutation . target as HTMLElement ;
55+ const hadClass = ( mutation . oldValue || '' ) . includes ( 'pf-v6-theme-unified' ) ;
56+ const hasClass = target . classList . contains ( 'pf-v6-theme-unified' ) ;
57+
58+ if ( hadClass !== hasClass && this . props . set === undefined ) {
59+ this . setState ( ( prevState ) => ( {
60+ themeClassVersion : prevState . themeClassVersion + 1
61+ } ) ) ;
62+ }
63+ }
64+ }
65+ } ) ;
66+
67+ this . observer . observe ( document . documentElement , {
68+ attributes : true ,
69+ attributeFilter : [ 'class' ] ,
70+ attributeOldValue : true
71+ } ) ;
72+ }
73+ }
74+
75+ componentWillUnmount ( ) {
76+ if ( this . observer ) {
77+ this . observer . disconnect ( ) ;
78+ this . observer = null ;
79+ }
80+ }
4081
4182 render ( ) {
42- const { title, className : propsClassName , ...props } = this . props ;
83+ const { title, className : propsClassName , set, ...props } = this . props ;
84+
85+ const shouldUseAltData =
86+ rhUiIcon !== null &&
87+ ( set === 'unified' ||
88+ ( set === undefined && canUseDOM && document . documentElement . classList . contains ( 'pf-v6-theme-unified' ) ) ) ;
89+
90+ const iconData = shouldUseAltData ? rhUiIcon : icon ;
91+ const { xOffset, yOffset, width, height, svgClassName, svgPathData } = iconData ?? { } ;
92+ const _xOffset = xOffset ?? 0 ;
93+ const _yOffset = yOffset ?? 0 ;
4394
4495 const hasTitle = Boolean ( title ) ;
45- const viewBox = [ xOffset , yOffset , width , height ] . join ( ' ' ) ;
96+ const viewBox = [ _xOffset , _yOffset , width , height ] . join ( ' ' ) ;
4697
4798 const classNames = [ 'pf-v6-svg' ] ;
4899 if ( svgClassName ) {
@@ -52,6 +103,14 @@ export function createIcon({
52103 classNames . push ( propsClassName ) ;
53104 }
54105
106+ const svgPaths = Array . isArray ( svgPathData ) ? (
107+ svgPathData . map ( ( pathObject , index ) => (
108+ < path className = { pathObject . className } key = { `${ pathObject . path } -${ index } ` } d = { pathObject . path } />
109+ ) )
110+ ) : (
111+ < path d = { svgPathData as string } />
112+ ) ;
113+
55114 return (
56115 < svg
57116 className = { classNames . join ( ' ' ) }
@@ -65,15 +124,76 @@ export function createIcon({
65124 { ...( props as Omit < React . SVGProps < SVGElement > , 'ref' > ) } // Lie.
66125 >
67126 { hasTitle && < title id = { this . id } > { title } </ title > }
68- { Array . isArray ( svgPath ) ? (
69- svgPath . map ( ( pathObject , index ) => (
70- < path className = { pathObject . className } key = { `${ pathObject . path } -${ index } ` } d = { pathObject . path } />
71- ) )
72- ) : (
73- < path d = { svgPath } />
74- ) }
127+ { svgPaths }
75128 </ svg >
76129 ) ;
130+
131+ // Alternate CSS method tinkering
132+ // TODO: remove or refactor to use this method
133+ // Below works for paths, but not viewbox without needing the MutationObserver which would be nice to not have when using CSS method
134+ // May be able to use two separate svgs instead of paths instead if going this route
135+
136+ // const defaultSvgPathData = Array.isArray(icon?.svgPathData) ? (
137+ // icon?.svgPathData.map((pathObject, index) => (
138+ // <path className={pathObject.className} key={`${pathObject.path}-${index}`} d={pathObject.path} />
139+ // ))
140+ // ) : (
141+ // <path d={icon?.svgPathData as string} />
142+ // );
143+
144+ // const rhUiSvgPathData =
145+ // rhUiIcon?.svgPathData && Array.isArray(rhUiIcon?.svgPathData) ? (
146+ // rhUiIcon?.svgPathData.map((pathObject, index) => (
147+ // <path className={pathObject.className} key={`${pathObject.path}-${index}`} d={pathObject.path} />
148+ // ))
149+ // ) : (
150+ // <path d={rhUiIcon?.svgPathData as string} />
151+ // );
152+
153+ // const finalSvgPath =
154+ // rhUiIcon !== null ? (
155+ // <>
156+ // <g className="pf-icon">{defaultSvgPathData}</g>
157+ // <g className="rh-icon">{rhUiSvgPathData}</g>
158+ // </>
159+ // ) : (
160+ // pfSvgPathData
161+ // );
162+
163+ // return (
164+ // <svg
165+ // className={classNames.join(' ')}
166+ // viewBox={viewBox}
167+ // fill="currentColor"
168+ // aria-labelledby={hasTitle ? this.id : null}
169+ // aria-hidden={hasTitle ? null : true}
170+ // role="img"
171+ // width="1em"
172+ // height="1em"
173+ // {...(props as Omit<React.SVGProps<SVGElement>, 'ref'>)} // Lie.
174+ // >
175+ // {hasTitle && <title id={this.id}>{title}</title> }
176+ // <style type="text/css">
177+ // {/* Testing CSS visibility switching */}
178+ // {/* TODO: move to css file, add to global styles */}
179+ // {`
180+ // .pf-v6-theme-unified {
181+ // .pf-icon {
182+ // display: none;
183+ // }
184+ // .rh-icon {
185+ // display: block;
186+ // }
187+ // }
188+ // .rh-icon {
189+ // display: none;
190+ // }
191+
192+ // `}
193+ // </style>
194+ // {finalSvgPath}
195+ // </svg>
196+ // );
77197 }
78198 } ;
79199}
0 commit comments