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

SAP / ui5-webcomponents-react / 9890609870

11 Jul 2024 11:30AM CUT coverage: 79.701% (-0.9%) from 80.604%
9890609870

Pull #6061

github

web-flow
Merge 357334feb into 3822beed4
Pull Request #6061: refactor: replace `Toolbar` with UI5 Web Component

2532 of 3761 branches covered (67.32%)

4586 of 5754 relevant lines covered (79.7%)

72781.43 hits per line

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

61.54
/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();
362✔
39

40
export const OverflowPopover: FC<OverflowPopoverProps> = (props: OverflowPopoverProps) => {
362✔
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;
177✔
54
  const [pressed, setPressed] = useState(false);
177✔
55
  const toggleBtnRef = useRef<ToggleButtonDomRef>(null);
177✔
56
  const [componentRef, popoverRef] = useSyncRef(overflowPopoverRef);
177✔
57

58
  useEffect(() => {
177✔
59
    setIsMounted(true);
43✔
60
    return () => {
43✔
61
      setIsMounted(false);
37✔
62
    };
63
  }, []);
64

65
  const handleToggleButtonClick = (e) => {
177✔
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 = () => {
177✔
79
    if (toggleBtnRef.current) {
×
80
      toggleBtnRef.current.accessibilityAttributes = { expanded: true, hasPopup: 'menu' };
×
81
    }
82
  };
83
  const handleAfterOpen = () => {
177✔
84
    setPressed(true);
×
85
  };
86

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

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

104
  const clonedOverflowButtonClick = (e) => {
177✔
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();
177✔
114

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

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

124
  return (
177✔
125
    <OverflowPopoverContextProvider value={{ inPopover: true }}>
126
      {overflowButton ? (
177!
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 &&
311✔
141
        createPortal(
142
          <Popover
143
            data-component-name="ToolbarOverflowPopover"
144
            className={clsx(classes.popover, isPhone && classes.popoverPhone)}
134!
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) {
2,496✔
162
                  // @ts-expect-error: if props is not defined, it doesn't have an id (is not a ReactElement)
163
                  if (item?.props?.id) {
1,433!
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') {
1,433✔
169
                    return cloneElement(item as ReactElement<ToolbarSeparatorPropTypes>, {
13✔
170
                      style: {
171
                        height: '0.0625rem',
172
                        margin: '0.375rem 0.1875rem',
173
                        width: '100%'
174
                      }
175
                    });
176
                  }
177
                  return item;
1,420✔
178
                }
179
                return null;
1,063✔
180
              })}
181
            </div>
182
          </Popover>,
183
          portalContainer ?? document.body
268✔
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