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

SAP / ui5-webcomponents-react / 10486318980

21 Aug 2024 08:28AM CUT coverage: 86.822% (+7.0%) from 79.858%
10486318980

Pull #6214

github

web-flow
Merge 1081ba16b into 7a4697321
Pull Request #6214: refactor(FilterBar): remove reference copying of filter/input elements

2769 of 3747 branches covered (73.9%)

116 of 119 new or added lines in 3 files covered. (97.48%)

22 existing lines in 3 files now uncovered.

4961 of 5714 relevant lines covered (86.82%)

72997.84 hits per line

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

78.72
/packages/main/src/internal/withWebComponent.tsx
1
'use client';
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, ReactNode, Ref } from 'react';
6
import { cloneElement, forwardRef, Fragment, isValidElement, useEffect, useState, version } from 'react';
7
import type { CommonProps, Ui5DomRef } from '../types/index.js';
8
import { useServerSideEffect } from './ssr.js';
9
import { camelToKebabCase, capitalizeFirstLetter, kebabToCamelCase, parseSemVer } from './utils.js';
10

11
const createEventPropName = (eventName: string) => `on${capitalizeFirstLetter(kebabToCamelCase(eventName))}`;
1,914,111✔
12

13
const isPrimitiveAttribute = (value: unknown): boolean => {
376✔
14
  return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
780,740✔
15
};
16

17
type EventHandler = (event: CustomEvent<unknown>) => void;
18

19
export interface WithWebComponentPropTypes {
20
  /**
21
   * 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.
22
   *
23
   * __Note:__ This adds a rendering cycle to your component.
24
   */
25
  waitForDefine?: boolean;
26
}
27

28
const definedWebComponents = new Set<ComponentType>([]);
376✔
29

30
export const withWebComponent = <Props extends Record<string, any>, RefType = Ui5DomRef>(
376✔
31
  tagName: string,
32
  regularProperties: string[],
33
  booleanProperties: string[],
34
  slotProperties: string[],
35
  eventProperties: string[],
36
  loader: () => Promise<unknown>
37
) => {
38
  const webComponentsSupported = parseSemVer(version).major >= 19;
46,248✔
39
  // displayName will be assigned in the individual files
40
  // eslint-disable-next-line react/display-name
41
  return forwardRef<RefType, Props & WithWebComponentPropTypes>((props, wcRef) => {
46,248✔
42
    const { className, children, waitForDefine, ...rest } = props;
544,202✔
43
    const [componentRef, ref] = useSyncRef<RefType>(wcRef);
544,159✔
44
    const tagNameSuffix: string = getEffectiveScopingSuffixForTag(tagName);
544,159✔
45
    const Component = (tagNameSuffix ? `${tagName}-${tagNameSuffix}` : tagName) as unknown as ComponentType<
544,159✔
46
      CommonProps & { class?: string; ref?: Ref<RefType> }
47
    >;
48

49
    const [isDefined, setIsDefined] = useState(definedWebComponents.has(Component));
544,159✔
50

51
    useServerSideEffect(() => {
544,159✔
52
      void loader();
66,804✔
53
    });
54

55
    // regular props (no booleans, no slots and no events)
56
    const regularProps = regularProperties.reduce((acc, name) => {
544,159✔
57
      if (rest.hasOwnProperty(name) && isPrimitiveAttribute(rest[name])) {
2,244,456✔
58
        return { ...acc, [camelToKebabCase(name)]: rest[name] };
372,474✔
59
      }
60
      return acc;
1,871,982✔
61
    }, {});
62

63
    // boolean properties - only attach if they are truthy
64
    const booleanProps = booleanProperties.reduce((acc, name) => {
544,159✔
65
      if (webComponentsSupported) {
1,070,920!
66
        return { ...acc, [camelToKebabCase(name)]: rest[name] };
1,070,920✔
67
      } else {
UNCOV
68
        if (rest[name] === true || rest[name] === 'true') {
×
UNCOV
69
          return { ...acc, [camelToKebabCase(name)]: true };
×
70
        }
UNCOV
71
        return acc;
×
72
      }
73
    }, {});
74

75
    const slots = slotProperties.reduce((acc, name) => {
544,159✔
76
      const slotValue = rest[name] as ReactElement;
112,369✔
77

78
      if (!slotValue) {
112,369✔
79
        return acc;
77,775✔
80
      }
81

82
      if (rest[name]?.$$typeof === Symbol.for('react.portal')) {
34,594!
UNCOV
83
        console.warn('ReactPortal is not supported for slot props.');
×
UNCOV
84
        return acc;
×
85
      }
86

87
      const slottedChildren = [];
34,594✔
88
      let index = 0;
34,594✔
89
      const removeFragments = (element: ReactNode) => {
34,594✔
90
        if (!isValidElement(element)) return;
44,404✔
91
        if (element.type === Fragment) {
41,654✔
92
          const elementChildren = (element as ReactElement<{ children?: ReactNode | ReactNode[] }>).props?.children;
5,921✔
93
          if (Array.isArray(elementChildren)) {
5,921✔
94
            elementChildren.forEach((item) => {
4,277✔
95
              if (Array.isArray(item)) {
10,943✔
96
                item.forEach(removeFragments);
5✔
97
              } else {
98
                removeFragments(item);
10,938✔
99
              }
100
            });
101
          } else {
102
            removeFragments(elementChildren);
1,644✔
103
          }
104
        } else {
105
          slottedChildren.push(
35,733✔
106
            cloneElement<Partial<HTMLElement>>(element, {
107
              key: element.key ?? `${name}-${index}`,
66,695✔
108
              slot: name
109
            })
110
          );
111
          index++;
35,733✔
112
        }
113
      };
114

115
      if (Array.isArray(slotValue)) {
34,594✔
116
        slotValue.forEach((item) => {
7,543✔
117
          removeFragments(item);
4,761✔
118
        });
119
      } else {
120
        removeFragments(slotValue);
27,051✔
121
      }
122
      return [...acc, ...slottedChildren];
34,594✔
123
    }, []);
124

125
    // event binding
126
    useIsomorphicLayoutEffect(() => {
544,159✔
127
      if (webComponentsSupported) {
236,423✔
128
        return () => {
236,423✔
129
          // React can handle events
130
        };
131
      }
UNCOV
132
      const localRef = ref.current;
×
UNCOV
133
      const eventRegistry: Record<string, EventHandler> = {};
×
UNCOV
134
      if (!waitForDefine || isDefined) {
×
UNCOV
135
        eventProperties.forEach((eventName) => {
×
UNCOV
136
          const eventHandler = rest[createEventPropName(eventName)] as EventHandler;
×
UNCOV
137
          if (typeof eventHandler === 'function') {
×
UNCOV
138
            eventRegistry[eventName] = eventHandler;
×
139
            // @ts-expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
UNCOV
140
            localRef?.addEventListener(eventName, eventRegistry[eventName]);
×
141
          }
142
        });
143

UNCOV
144
        return () => {
×
UNCOV
145
          for (const eventName in eventRegistry) {
×
146
            // @ts-expect-error: all custom events can be passed here, so `keyof HTMLElementEventMap` isn't sufficient
UNCOV
147
            localRef?.removeEventListener(eventName, eventRegistry[eventName]);
×
148
          }
149
        };
150
      }
151
    }, [...eventProperties.map((eventName) => rest[createEventPropName(eventName)]), isDefined, waitForDefine]);
361,476✔
152

153
    const eventHandlers = eventProperties.reduce((events, eventName) => {
544,159✔
154
      const eventHandlerProp = rest[createEventPropName(eventName)];
361,476✔
155
      if (webComponentsSupported && eventHandlerProp) {
361,476✔
156
        events[`on${eventName}`] = eventHandlerProp;
207,185✔
157
      }
158
      return events;
361,476✔
159
    }, {});
160

161
    // non web component related props, just pass them
162
    const nonWebComponentRelatedProps = Object.entries(rest)
544,159✔
163
      .filter(([key]) => !regularProperties.includes(key))
2,310,716✔
164
      .filter(([key]) => !slotProperties.includes(key))
1,902,533✔
165
      .filter(([key]) => !booleanProperties.includes(key))
1,863,845✔
166
      .filter(([key]) => !eventProperties.map((eventName) => createEventPropName(eventName)).includes(key))
1,559,905✔
167
      .reduce((acc, [key, val]) => {
168
        if (!key.startsWith('aria-') && !key.startsWith('data-') && val === false) {
1,341,499✔
169
          return acc;
90✔
170
        }
171
        acc[key] = val;
1,341,409✔
172
        return acc;
1,341,409✔
173
      }, {});
174

175
    useEffect(() => {
544,159✔
176
      if (waitForDefine && !isDefined) {
66,804!
UNCOV
177
        customElements.whenDefined(Component as unknown as string).then(() => {
×
UNCOV
178
          setIsDefined(true);
×
UNCOV
179
          definedWebComponents.add(Component);
×
180
        });
181
      }
182
    }, [Component, waitForDefine, isDefined]);
183

184
    const propsToApply = regularProperties.map((prop) => ({ name: prop, value: props[prop] }));
2,244,456✔
185
    useEffect(() => {
544,159✔
186
      void customElements.whenDefined(Component as unknown as string).then(() => {
522,776✔
187
        for (const prop of propsToApply) {
522,773✔
188
          if (prop.value != null && !isPrimitiveAttribute(prop.value)) {
2,244,429✔
189
            if (ref.current) {
83✔
190
              ref.current[prop.name] = prop.value;
83✔
191
            }
192
          }
193
        }
194
      });
195
    }, [Component, ...propsToApply]);
196

197
    if (waitForDefine && !isDefined) {
544,159!
UNCOV
198
      return null;
×
199
    }
200

201
    return (
544,159✔
202
      <Component
203
        ref={componentRef}
204
        {...booleanProps}
205
        {...regularProps}
206
        {...eventHandlers}
207
        {...nonWebComponentRelatedProps}
208
        class={className}
209
        suppressHydrationWarning
210
      >
211
        {slots}
212
        {children}
213
      </Component>
214
    );
215
  });
216
};
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