• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 5241066828

pending completion
5241066828

Pull #4716

github

web-flow
Merge 036786fdc into 2a21ccd05
Pull Request #4716: chore(deps): update nextjs monorepo to v13.4.5 (patch)

2643 of 3618 branches covered (73.05%)

5126 of 5923 relevant lines covered (86.54%)

15152.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.04
/packages/main/src/internal/withWebComponent.tsx
1
'use client';
1,897✔
2

3
import { getEffectiveScopingSuffixForTag } from '@ui5/webcomponents-base/dist/CustomElementsScope.js';
4
import { useIsomorphicLayoutEffect, useSyncRef } from '@ui5/webcomponents-react-base';
5
import type { ComponentType, ReactElement, Ref } from 'react';
6
import React, { Children, cloneElement, forwardRef, Fragment, useEffect, useState } from 'react';
7
import type { CommonProps, Ui5DomRef } from '../interfaces/index.js';
8
import { useServerSideEffect } from './ssr.js';
9
import { camelToKebabCase, capitalizeFirstLetter, kebabToCamelCase } from './utils.js';
10

11
const createEventPropName = (eventName) => `on${capitalizeFirstLetter(kebabToCamelCase(eventName))}`;
817,050✔
12

13
type EventHandler = (event: CustomEvent<unknown>) => void;
14

15
export interface WithWebComponentPropTypes {
16
  /**
17
   * Defines whether the component should wait for the underlying custom element of the web component to be defined. This can be useful, for example, for using instance methods when mounting the component.
18
   *
19
   * __Note:__ This adds a rendering cycle to your component.
20
   */
21
  waitForDefine?: boolean;
22
}
23

24
const definedWebComponents = new Set<ComponentType>([]);
271✔
25

26
export const withWebComponent = <Props extends Record<string, any>, RefType = Ui5DomRef>(
271✔
27
  tagName: string,
28
  regularProperties: string[],
29
  booleanProperties: string[],
30
  slotProperties: string[],
31
  eventProperties: string[],
32
  loader: () => Promise<unknown>
33
) => {
27,642✔
34
  // displayName will be assigned in the individual files
35
  // eslint-disable-next-line react/display-name
36
  return forwardRef<RefType, Props & WithWebComponentPropTypes>((props, wcRef) => {
187,467✔
37
    const { className, children, waitForDefine, ...rest } = props;
186,428✔
38
    const [componentRef, ref] = useSyncRef<RefType>(wcRef);
186,368✔
39
    const tagNameSuffix: string = getEffectiveScopingSuffixForTag(tagName);
186,368✔
40
    const Component = (tagNameSuffix ? `${tagName}-${tagNameSuffix}` : tagName) as unknown as ComponentType<
186,368!
41
      CommonProps & { class?: string; ref?: Ref<RefType> }
42
    >;
43

44
    const [isDefined, setIsDefined] = useState(definedWebComponents.has(Component));
186,368✔
45

46
    useServerSideEffect(() => {
186,368✔
47
      void loader();
29,922✔
48
    });
49

50
    // regular props (no booleans, no slots and no events)
51
    const regularProps = regularProperties.reduce((acc, name) => {
186,368✔
52
      if (rest.hasOwnProperty(name)) {
900,288✔
53
        return { ...acc, [camelToKebabCase(name)]: rest[name] };
309,059✔
54
      }
55
      return acc;
591,229✔
56
    }, {});
57

58
    // boolean properties - only attach if they are truthy
59
    const booleanProps = booleanProperties.reduce((acc, name) => {
186,368✔
60
      if (rest[name] === true || rest[name] === 'true') {
576,151✔
61
        return { ...acc, [camelToKebabCase(name)]: true };
68,803✔
62
      }
63
      return acc;
507,348✔
64
    }, {});
65

66
    const slots = slotProperties.reduce((acc, name) => {
186,368✔
67
      const slotValue = rest[name] as ReactElement;
68,686✔
68

69
      if (!slotValue) {
68,686✔
70
        return acc;
52,533✔
71
      }
72

73
      if (rest[name]?.$$typeof === Symbol.for('react.portal')) {
16,153!
74
        console.warn('ReactPortal is not supported for slot props.');
75
        return acc;
76
      }
77

78
      const slottedChildren = [];
16,153✔
79
      let index = 0;
16,153✔
80
      const removeFragments = (element) => {
16,153✔
81
        if (!element) return;
23,899!
82
        if (element.type === Fragment) {
23,899✔
83
          Children.toArray(element.props?.children)
3,639✔
84
            .filter(Boolean)
85
            .forEach((item) => {
86
              removeFragments(item);
9,347✔
87
            });
88
        } else {
89
          slottedChildren.push(
20,260✔
90
            cloneElement(element, {
91
              key: `${name}-${index}`,
92
              slot: name
93
            })
94
          );
95
          index++;
20,260✔
96
        }
97
      };
98

99
      if (Array.isArray(slotValue)) {
16,153✔
100
        slotValue.forEach((item) => {
4,792✔
101
          removeFragments(item);
3,191✔
102
        });
103
      } else {
104
        removeFragments(slotValue);
11,361✔
105
      }
106
      return [...acc, ...slottedChildren];
16,153✔
107
    }, []);
108

109
    // event binding
110
    useIsomorphicLayoutEffect(() => {
186,368✔
111
      const localRef = ref.current;
104,710✔
112
      const eventRegistry: Record<string, EventHandler> = {};
104,710✔
113
      if (!waitForDefine || isDefined) {
104,710!
114
        eventProperties.forEach((eventName) => {
104,710✔
115
          const eventHandler = rest[createEventPropName(eventName)] as EventHandler;
143,822✔
116
          if (typeof eventHandler === 'function') {
143,822✔
117
            eventRegistry[eventName] = eventHandler;
93,874✔
118
            // @ts-expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
119
            localRef?.addEventListener(eventName, eventRegistry[eventName]);
93,874✔
120
          }
121
        });
122

123
        return () => {
104,710✔
124
          for (const eventName in eventRegistry) {
103,451✔
125
            // @ts-expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
126
            localRef?.removeEventListener(eventName, eventRegistry[eventName]);
93,273✔
127
          }
128
        };
129
      }
130
    }, [...eventProperties.map((eventName) => rest[createEventPropName(eventName)]), isDefined, waitForDefine]);
176,532✔
131

132
    // non web component related props, just pass them
133
    const nonWebComponentRelatedProps = Object.entries(rest)
186,368✔
134
      .filter(([key]) => !regularProperties.includes(key))
958,276✔
135
      .filter(([key]) => !slotProperties.includes(key))
649,217✔
136
      .filter(([key]) => !booleanProperties.includes(key))
631,457✔
137
      .filter(([key]) => !eventProperties.map((eventName) => createEventPropName(eventName)).includes(key))
496,724✔
138
      .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {});
368,167✔
139

140
    useEffect(() => {
186,368✔
141
      if (waitForDefine && !isDefined) {
29,922!
142
        customElements.whenDefined(Component as unknown as string).then(() => {
143
          setIsDefined(true);
144
          definedWebComponents.add(Component);
145
        });
146
      }
147
    }, [Component, waitForDefine, isDefined]);
148
    if (waitForDefine && !isDefined) {
186,368!
149
      return null;
150
    }
151

152
    return (
186,368✔
153
      <Component
154
        ref={componentRef}
155
        {...booleanProps}
156
        {...regularProps}
157
        {...nonWebComponentRelatedProps}
158
        class={className}
159
      >
160
        {slots}
161
        {children}
162
      </Component>
163
    );
164
  });
165
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc