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

SAP / ui5-webcomponents-react / 10004391615

19 Jul 2024 07:35AM CUT coverage: 79.91% (+0.2%) from 79.67%
10004391615

Pull #6089

github

web-flow
Merge 7cb3d1a69 into 44c90daf1
Pull Request #6089: feat(ObjectPage): refactor component to support ui5wc v2

2522 of 4036 branches covered (62.49%)

42 of 45 new or added lines in 5 files covered. (93.33%)

13 existing lines in 4 files now uncovered.

4598 of 5754 relevant lines covered (79.91%)

74465.57 hits per line

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

57.69
/packages/main/src/components/Toolbar/OverflowPopover.tsx
1
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
2
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
3
import PopupAccessibleRole from '@ui5/webcomponents/dist/types/PopupAccessibleRole.js';
4
import iconOverflow from '@ui5/webcomponents-icons/dist/overflow.js';
5
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
6
import { clsx } from 'clsx';
7
import type { Dispatch, FC, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
8
import { cloneElement, useEffect, useRef, useState } from 'react';
9
import { createPortal } from 'react-dom';
10
import { getOverflowPopoverContext } from '../../internal/OverflowPopoverContext.js';
11
import { useCanRenderPortal } from '../../internal/ssr.js';
12
import { stopPropagation } from '../../internal/stopPropagation.js';
13
import { getUi5TagWithSuffix } from '../../internal/utils.js';
14
import type {
15
  ButtonPropTypes,
16
  PopoverDomRef,
17
  ToggleButtonDomRef,
18
  ToggleButtonPropTypes
19
} from '../../webComponents/index.js';
20
import { Popover, ToggleButton } from '../../webComponents/index.js';
21
import type { ToolbarSeparatorPropTypes } from '../ToolbarSeparator/index.js';
22
import type { ToolbarPropTypes } from './index.js';
23

24
interface OverflowPopoverProps {
25
  lastVisibleIndex: number;
26
  classes: Record<string, string>;
27
  children: ReactNode[];
28
  portalContainer: Element;
29
  overflowContentRef: Ref<HTMLDivElement>;
30
  numberOfAlwaysVisibleItems?: number;
31
  showMoreText: string;
32
  overflowPopoverRef?: Ref<PopoverDomRef>;
33
  overflowButton?: ReactElement<ToggleButtonPropTypes> | ReactElement<ButtonPropTypes>;
34
  setIsMounted: Dispatch<SetStateAction<boolean>>;
35
  a11yConfig?: ToolbarPropTypes['a11yConfig'];
36
}
37

38
const isPhone = Device.isPhone();
452✔
39

40
export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopoverProps) => {
452✔
41
  const {
42
    lastVisibleIndex,
43
    classes,
44
    children,
45
    portalContainer,
46
    overflowContentRef,
47
    numberOfAlwaysVisibleItems,
48
    showMoreText,
49
    overflowButton,
50
    overflowPopoverRef,
51
    setIsMounted,
52
    a11yConfig
53
  } = props;
42✔
54
  const [pressed, setPressed] = useState(false);
42✔
55
  const toggleBtnRef = useRef<ToggleButtonDomRef>(null);
42✔
56
  const [componentRef, popoverRef] = useSyncRef(overflowPopoverRef);
42✔
57

58
  useEffect(() => {
42✔
59
    setIsMounted(true);
14✔
60
    return () => {
14✔
61
      setIsMounted(false);
13✔
62
    };
63
  }, []);
64

65
  const handleToggleButtonClick = (e) => {
42✔
66
    e.stopPropagation();
×
67
    setPressed((prev) => {
×
68
      if (!prev) {
×
69
        if (popoverRef.current) {
×
70
          popoverRef.current.opener = e.target;
×
71
        }
72
        return true;
×
73
      }
74
      return false;
×
75
    });
76
  };
77

78
  const handleBeforeOpen = () => {
42✔
79
    if (toggleBtnRef.current) {
×
80
      toggleBtnRef.current.accessibilityAttributes = { expanded: true, hasPopup: 'menu' };
×
81
    }
82
  };
83
  const handleAfterOpen = () => {
42✔
84
    setPressed(true);
×
85
  };
86

87
  const handleClose = (e) => {
42✔
88
    if (toggleBtnRef.current) {
×
89
      toggleBtnRef.current.accessibilityAttributes = { expanded: false, hasPopup: 'menu' };
×
90
    }
91
    stopPropagation(e);
×
92
    setPressed(false);
×
93
  };
94

95
  useEffect(() => {
42✔
96
    const tagName = getUi5TagWithSuffix('ui5-toggle-button');
14✔
97
    void customElements.whenDefined(tagName).then(() => {
14✔
98
      if (toggleBtnRef.current) {
14✔
99
        toggleBtnRef.current.accessibilityAttributes = { expanded: pressed, hasPopup: 'menu' };
14✔
100
      }
101
    });
102
  }, []);
103

104
  const clonedOverflowButtonClick = (e) => {
42✔
105
    if (typeof overflowButton?.props?.onClick === 'function') {
×
106
      overflowButton.props.onClick(e);
×
107
    }
108
    if (!e.defaultPrevented) {
×
109
      handleToggleButtonClick(e);
×
110
    }
111
  };
112

113
  const canRenderPortal = useCanRenderPortal();
42✔
114

115
  const accessibleRole = (() => {
42✔
116
    if (a11yConfig?.overflowPopover?.contentRole) {
42!
117
      return PopupAccessibleRole.None;
×
118
    }
119
    return a11yConfig?.overflowPopover?.role;
42✔
120
  })();
121

122
  const OverflowPopoverContextProvider = getOverflowPopoverContext().Provider;
42✔
123

124
  return (
42✔
125
    <OverflowPopoverContextProvider value={{ inPopover: true }}>
126
      {overflowButton ? (
42!
127
        cloneElement(overflowButton, { onClick: clonedOverflowButtonClick })
128
      ) : (
129
        <ToggleButton
130
          ref={toggleBtnRef}
131
          design={ButtonDesign.Transparent}
132
          icon={iconOverflow}
133
          onClick={handleToggleButtonClick}
134
          pressed={pressed}
135
          accessibleName={showMoreText}
136
          tooltip={showMoreText}
137
          data-component-name="ToolbarOverflowButton"
138
        />
139
      )}
140
      {canRenderPortal &&
70✔
141
        createPortal(
142
          <Popover
143
            data-component-name="ToolbarOverflowPopover"
144
            className={clsx(classes.popover, isPhone && classes.popoverPhone)}
28!
145
            placement={PopoverPlacement.Bottom}
146
            ref={componentRef}
147
            open={pressed}
148
            onClose={handleClose}
149
            onBeforeOpen={handleBeforeOpen}
150
            onOpen={handleAfterOpen}
151
            hideArrow
152
            accessibleRole={accessibleRole}
153
          >
154
            <div
155
              className={classes.popoverContent}
156
              ref={overflowContentRef}
157
              role={a11yConfig?.overflowPopover?.contentRole}
158
              data-component-name="ToolbarOverflowPopoverContent"
159
            >
160
              {children.map((item, index) => {
161
                if (index > lastVisibleIndex && index > numberOfAlwaysVisibleItems - 1) {
28✔
162
                  // @ts-expect-error: if props is not defined, it doesn't have an id (is not a ReactElement)
163
                  if (item?.props?.id) {
28!
164
                    // @ts-expect-error: item is ReactElement
165
                    return cloneElement(item, { id: `${item.props.id}-overflow` });
×
166
                  }
167
                  // @ts-expect-error: if type is not defined, it's not a spacer
168
                  if (item.type?.displayName === 'ToolbarSeparator') {
28!
UNCOV
169
                    return cloneElement(item as ReactElement<ToolbarSeparatorPropTypes>, {
×
170
                      style: {
171
                        height: '0.0625rem',
172
                        margin: '0.375rem 0.1875rem',
173
                        width: '100%'
174
                      }
175
                    });
176
                  }
177
                  return item;
28✔
178
                }
UNCOV
179
                return null;
×
180
              })}
181
            </div>
182
          </Popover>,
183
          portalContainer ?? document.body
56✔
184
        )}
185
    </OverflowPopoverContextProvider>
186
  );
187
};
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