@@ -26,6 +26,7 @@ export interface WithWebComponentPropTypes {
2626}
2727
2828const definedWebComponents = new Set < ComponentType > ( [ ] ) ;
29+
2930/**
3031 * ⚠️ __INTERNAL__ use only! This function is not part of the public API.
3132 */
@@ -37,35 +38,42 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
3738 eventProperties : string [ ] ,
3839) => {
3940 const webComponentsSupported = parseSemVer ( version ) . major >= 19 ;
41+ const regularKebabNames = regularProperties . map ( camelToKebabCase ) ;
42+ const booleanKebabNames = booleanProperties . map ( camelToKebabCase ) ;
43+ const eventPropNames = eventProperties . map ( createEventPropName ) ;
44+ const knownKeys = new Set < string > ( [ ...regularProperties , ...slotProperties , ...booleanProperties , ...eventPropNames ] ) ;
45+ const tagNameSuffix : string = getEffectiveScopingSuffixForTag ( tagName ) ;
46+ const Component = ( tagNameSuffix ? `${ tagName } -${ tagNameSuffix } ` : tagName ) as unknown as ComponentType <
47+ CommonProps & { class ?: string ; ref ?: Ref < RefType > }
48+ > ;
49+
4050 // displayName will be assigned in the individual files
4151 // eslint-disable-next-line react/display-name
4252 return forwardRef < RefType , Props & WithWebComponentPropTypes > ( ( props , wcRef ) => {
4353 const { className, children, waitForDefine, ...rest } = props ;
4454 const [ componentRef , ref ] = useSyncRef < RefType > ( wcRef ) ;
45- const tagNameSuffix : string = getEffectiveScopingSuffixForTag ( tagName ) ;
46- const Component = ( tagNameSuffix ? `${ tagName } -${ tagNameSuffix } ` : tagName ) as unknown as ComponentType <
47- CommonProps & { class ?: string ; ref ?: Ref < RefType > }
48- > ;
4955 const [ isDefined , setIsDefined ] = useState ( definedWebComponents . has ( Component ) ) ;
5056 // regular props (no booleans, no slots and no events)
51- const regularProps = regularProperties . reduce ( ( acc , name ) => {
57+ const regularProps : Record < string , unknown > = { } ;
58+ for ( let i = 0 ; i < regularProperties . length ; i ++ ) {
59+ const name = regularProperties [ i ] ;
5260 if ( Object . prototype . hasOwnProperty . call ( rest , name ) && isPrimitiveAttribute ( rest [ name ] ) ) {
53- return { ... acc , [ camelToKebabCase ( name ) ] : rest [ name ] } ;
61+ regularProps [ regularKebabNames [ i ] ] = rest [ name ] ;
5462 }
55- return acc ;
56- } , { } ) ;
63+ }
5764
5865 // boolean properties - only attach if they are truthy
59- const booleanProps = booleanProperties . reduce ( ( acc , name ) => {
66+ const booleanProps : Record < string , unknown > = { } ;
67+ for ( let i = 0 ; i < booleanProperties . length ; i ++ ) {
68+ const name = booleanProperties [ i ] ;
6069 if ( webComponentsSupported ) {
61- return { ... acc , [ camelToKebabCase ( name ) ] : rest [ name ] } ;
70+ booleanProps [ booleanKebabNames [ i ] ] = rest [ name ] ;
6271 } else {
6372 if ( rest [ name ] === true || rest [ name ] === 'true' ) {
64- return { ... acc , [ camelToKebabCase ( name ) ] : true } ;
73+ booleanProps [ booleanKebabNames [ i ] ] = true ;
6574 }
66- return acc ;
6775 }
68- } , { } ) ;
76+ }
6977
7078 const slots = slotProperties . reduce ( ( acc , name ) => {
7179 const slotValue = rest [ name ] as ReactElement ;
@@ -117,58 +125,57 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
117125 return [ ...acc , ...slottedChildren ] ;
118126 } , [ ] ) ;
119127
120- // event binding
121- useIsomorphicLayoutEffect ( ( ) => {
122- if ( webComponentsSupported ) {
123- return ( ) => {
124- // React can handle events
125- } ;
126- }
127- const localRef = ref . current ;
128- const eventRegistry : Record < string , EventHandler > = { } ;
129- if ( ! waitForDefine || isDefined ) {
130- eventProperties . forEach ( ( eventName ) => {
131- const eventHandler = rest [ createEventPropName ( eventName ) ] as EventHandler ;
132- if ( typeof eventHandler === 'function' ) {
133- eventRegistry [ eventName ] = eventHandler ;
134- // @ts -expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
135- localRef ?. addEventListener ( eventName , eventRegistry [ eventName ] ) ;
136- }
137- } ) ;
128+ // event binding - React 19 supports this natively
129+ if ( ! webComponentsSupported ) {
130+ // React version never changes between renders
131+ // eslint-disable-next-line react-hooks/rules-of-hooks
132+ useIsomorphicLayoutEffect ( ( ) => {
133+ const localRef = ref . current ;
134+ const eventRegistry : Record < string , EventHandler > = { } ;
135+ if ( ! waitForDefine || isDefined ) {
136+ eventProperties . forEach ( ( eventName , i ) => {
137+ const eventHandler = rest [ eventPropNames [ i ] ] as EventHandler ;
138+ if ( typeof eventHandler === 'function' ) {
139+ eventRegistry [ eventName ] = eventHandler ;
140+ // @ts -expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
141+ localRef ?. addEventListener ( eventName , eventRegistry [ eventName ] ) ;
142+ }
143+ } ) ;
138144
139- return ( ) => {
140- for ( const eventName in eventRegistry ) {
141- // @ts -expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
142- localRef ?. removeEventListener ( eventName , eventRegistry [ eventName ] ) ;
143- }
144- } ;
145- }
146- } , [ ...eventProperties . map ( ( eventName ) => rest [ createEventPropName ( eventName ) ] ) , isDefined , waitForDefine ] ) ;
145+ return ( ) => {
146+ for ( const eventName in eventRegistry ) {
147+ // @ts -expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
148+ localRef ?. removeEventListener ( eventName , eventRegistry [ eventName ] ) ;
149+ }
150+ } ;
151+ }
152+ } , [ ...eventPropNames . map ( ( propName ) => rest [ propName ] ) , isDefined , waitForDefine ] ) ;
153+ }
147154
148- const eventHandlers = eventProperties . reduce ( ( events , eventName ) => {
149- const eventHandlerProp = rest [ createEventPropName ( eventName ) ] ;
150- if ( webComponentsSupported && eventHandlerProp ) {
151- events [ `on${ eventName } ` ] = eventHandlerProp ;
155+ const eventHandlers : Record < string , unknown > = { } ;
156+ if ( webComponentsSupported ) {
157+ for ( let i = 0 ; i < eventProperties . length ; i ++ ) {
158+ const eventHandlerProp = rest [ eventPropNames [ i ] ] ;
159+ if ( eventHandlerProp ) {
160+ eventHandlers [ `on${ eventProperties [ i ] } ` ] = eventHandlerProp ;
161+ }
152162 }
153- return events ;
154- } , { } ) ;
163+ }
155164
156165 // In React 19 events aren't correctly attached after hydration
157166 const [ attachEvents , setAttachEvents ] = useState ( ! webComponentsSupported || ! Object . keys ( eventHandlers ) . length ) ; // apply workaround only for React19 and if event props are defined
158167
159168 // non web component related props, just pass them
160- const nonWebComponentRelatedProps = Object . entries ( rest )
161- . filter ( ( [ key ] ) => ! regularProperties . includes ( key ) )
162- . filter ( ( [ key ] ) => ! slotProperties . includes ( key ) )
163- . filter ( ( [ key ] ) => ! booleanProperties . includes ( key ) )
164- . filter ( ( [ key ] ) => ! eventProperties . map ( ( eventName ) => createEventPropName ( eventName ) ) . includes ( key ) )
165- . reduce ( ( acc , [ key , val ] ) => {
169+ const nonWebComponentRelatedProps : Record < string , unknown > = { } ;
170+ for ( const key in rest ) {
171+ if ( Object . prototype . hasOwnProperty . call ( rest , key ) && ! knownKeys . has ( key ) ) {
172+ const val = rest [ key ] ;
166173 if ( ! key . startsWith ( 'aria-' ) && ! key . startsWith ( 'data-' ) && val === false ) {
167- return acc ;
174+ continue ;
168175 }
169- acc [ key ] = val ;
170- return acc ;
171- } , { } ) ;
176+ nonWebComponentRelatedProps [ key ] = val ;
177+ }
178+ }
172179
173180 useEffect ( ( ) => {
174181 if ( waitForDefine && ! isDefined ) {
@@ -177,20 +184,22 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
177184 definedWebComponents . add ( Component ) ;
178185 } ) ;
179186 }
180- } , [ Component , waitForDefine , isDefined ] ) ;
187+ } , [ waitForDefine , isDefined ] ) ;
181188
182- const propsToApply = regularProperties . map ( ( prop ) => ( { name : prop , value : props [ prop ] } ) ) ;
189+ const regularPropValues = regularProperties . map ( ( prop ) => props [ prop ] ) ;
183190 useEffect ( ( ) => {
184191 void customElements . whenDefined ( Component as unknown as string ) . then ( ( ) => {
185- for ( const prop of propsToApply ) {
186- if ( prop . value != null && ! isPrimitiveAttribute ( prop . value ) ) {
192+ for ( let i = 0 ; i < regularProperties . length ; i ++ ) {
193+ const value = regularPropValues [ i ] ;
194+ if ( value != null && ! isPrimitiveAttribute ( value ) ) {
187195 if ( ref . current ) {
188- ref . current [ prop . name ] = prop . value ;
196+ ref . current [ regularProperties [ i ] ] = value ;
189197 }
190198 }
191199 }
192200 } ) ;
193- } , [ Component , ...propsToApply ] ) ;
201+ // eslint-disable-next-line react-hooks/exhaustive-deps
202+ } , regularPropValues ) ;
194203
195204 useIsomorphicLayoutEffect ( ( ) => {
196205 setAttachEvents ( true ) ;
@@ -203,7 +212,6 @@ export const withWebComponent = <Props extends Record<string, any>, RefType = Ui
203212 // compatibility wrapper for ExpandableText - remove in v3
204213 if ( tagName === 'ui5-expandable-text' ) {
205214 const renderWhiteSpace = nonWebComponentRelatedProps [ 'renderWhitespace' ] ? true : undefined ;
206- // @ts -expect-error: overflowMode is available
207215 const { [ 'overflow-mode' ] : overflowMode , text, ...restRegularProps } = regularProps ;
208216 const showOverflowInPopover = nonWebComponentRelatedProps [ 'showOverflowInPopover' ] ;
209217 return (
0 commit comments