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

carbon-design-system / carbon-addons-iot-react / 6485381999

11 Oct 2023 04:29PM UTC coverage: 97.443% (-0.06%) from 97.502%
6485381999

push

github

carbon-bot
v2.153.0

7777 of 8122 branches covered (0.0%)

Branch coverage included in aggregate %.

9375 of 9480 relevant lines covered (98.89%)

2510.49 hits per line

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

95.52
/packages/react/src/components/Header/HeaderAction/HeaderActionPanel.jsx
1
import React, { useEffect, useMemo, useRef } from 'react';
2
import PropTypes from 'prop-types';
3
import { settings } from 'carbon-components';
4
import classnames from 'classnames';
5
import { HeaderGlobalAction, HeaderPanel } from 'carbon-components-react/es/components/UIShell';
6
import { Close16 } from '@carbon/icons-react';
7
import { white } from '@carbon/colors';
8

9
import { APP_SWITCHER } from '../headerConstants';
10
import { handleSpecificKeyDown } from '../../../utils/componentUtilityFunctions';
11
import { HeaderActionPropTypes } from '../HeaderPropTypes';
12
import { isSafari } from '../../SuiteHeader/suiteHeaderUtils';
13

14
const { prefix: carbonPrefix } = settings;
15✔
15

16
const propTypes = {
15✔
17
  ...HeaderActionPropTypes,
18
  /** unique id for the action panel */
19
  id: PropTypes.string.isRequired,
20
  /** Ref object to be attached to the parent that should receive focus when a menu is closed */
21
  focusRef: PropTypes.oneOfType([
22
    // Either a function
23
    PropTypes.func,
24
    // Or the instance of a DOM native element (see the note about SSR)
25
    PropTypes.shape({
26
      current: typeof Element === 'undefined' ? PropTypes.any : PropTypes.instanceOf(Element),
15✔
27
    }),
28
  ]).isRequired,
29
};
30

31
const defaultProps = {
15✔
32
  // disabled b/c these are pulled in via the HeaderActionPropTypes above.
33
  /* eslint-disable react/default-props-match-prop-types */
34
  isExpanded: false,
35
  renderLabel: false,
36
  i18n: {
37
    closeMenu: 'close menu',
38
  },
39
  showCloseIconWhenPanelExpanded: false,
40
  /* eslint-enable react/default-props-match-prop-types */
41
};
42

43
/**
44
 * This component renders both a Header Action and it's child Header Panel
45
 * It has no local state.
46
 * It calls the onToggleExpansion when it should be opened or closed
47
 */
48
const HeaderActionPanel = ({
15✔
49
  item,
50
  index,
51
  onToggleExpansion,
52
  isExpanded,
53
  focusRef,
54
  renderLabel,
55
  i18n,
56
  inOverflow,
57
  showCloseIconWhenPanelExpanded,
58
  id,
59
}) => {
60
  const panelRef = useRef();
404✔
61

62
  const mergedI18n = useMemo(
404✔
63
    () => ({
237✔
64
      ...defaultProps.i18n,
65
      ...i18n,
66
    }),
67
    [i18n]
68
  );
69

70
  /**
71
   * This workaround is needed because blur event is not triggered
72
   * on button in Safari. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#clicking_and_focus
73
   */
74
  useEffect(() => {
404✔
75
    if (!isSafari()) {
404✔
76
      return;
391✔
77
    }
78

79
    const handleOutsidePanelClick = (event) => {
13✔
80
      if (
12✔
81
        (panelRef.current && panelRef.current.contains(event.target)) ||
46✔
82
        (focusRef.current && focusRef.current.contains(event.target))
83
      ) {
84
        return;
6✔
85
      }
86

87
      if (isExpanded) {
6!
88
        onToggleExpansion();
×
89
      }
90
    };
91

92
    document.addEventListener('click', handleOutsidePanelClick, { capture: true });
13✔
93

94
    // eslint-disable-next-line consistent-return
95
    return () => document.removeEventListener('click', handleOutsidePanelClick, { capture: true });
13✔
96
    // eslint-disable-next-line react-hooks/exhaustive-deps
97
  }, [isExpanded, onToggleExpansion]);
98

99
  return (
404✔
100
    <>
101
      <HeaderGlobalAction
102
        className={`${carbonPrefix}--header-action-btn action-btn__trigger`}
103
        key={`menu-item-${item.label}-global`}
104
        aria-label={item.label}
105
        aria-haspopup="menu"
106
        aria-expanded={isExpanded}
107
        onClick={() => onToggleExpansion()}
59✔
108
        ref={focusRef}
109
        id={id}
110
      >
111
        {renderLabel ? (
404!
112
          item.label
113
        ) : isExpanded && (inOverflow || showCloseIconWhenPanelExpanded) ? (
905✔
114
          <Close16 fill={white} description={mergedI18n.closeMenu} />
115
        ) : (
116
          item.btnContent
117
        )}
118
      </HeaderGlobalAction>
119
      <HeaderPanel
120
        data-testid="action-btn__panel"
121
        ref={panelRef}
122
        tabIndex="-1"
123
        key={`panel-${index}`}
124
        className={
125
          item.label !== APP_SWITCHER
404✔
126
            ? classnames('action-btn__headerpanel', {
127
                'action-btn__headerpanel--closed': !isExpanded,
128
              })
129
            : classnames(`${carbonPrefix}--app-switcher`, {
130
                [item.childContent[0].className]: item.childContent[0].className,
131
                [item.childContent[0].metaData.className]: item.childContent[0].metaData.className,
132
              })
133
        }
134
        expanded={isExpanded}
135
      >
136
        <ul aria-label={item.label}>
137
          {item.childContent.map((childItem, k) => {
138
            const { element, ...metaData } = childItem?.metaData ?? {};
602✔
139
            const ChildElement = element || 'a';
602✔
140
            const onKeyDownClick = (e) => e.target.click();
602✔
141

142
            // if the item is an A and doesn't have an onClick event
143
            // do nothing. An A tag doesn't need an onClick handler.
144
            const onClick =
145
              ChildElement === 'a' && !metaData?.onClick
602✔
146
                ? undefined
147
                : // otherwise, if an onClick exists use that, or fallback to a noop.
148
                  metaData?.onClick || (() => {});
26✔
149

150
            // if item has onKeyDown use that otherwise, fallback to onClick if it exists
151
            // or create a custom handler to trigger the click
152
            const onKeyDown = metaData?.onKeyDown ? metaData.onKeyDown : onClick || onKeyDownClick;
602✔
153

154
            return (
602✔
155
              <li key={`listitem-${item.label}-${k}`} className="action-btn__headerpanel-li">
156
                <ChildElement
157
                  key={`headerpanelmenu-item-${item.label}-${index}-child-${k}`}
158
                  {...metaData}
159
                  onClick={onClick}
160
                  onKeyDown={handleSpecificKeyDown(['Enter', ' '], onKeyDown)}
161
                >
162
                  {
163
                    // if we're working with an actual react component (not an html element) pass
164
                    // the isExpanded prop, so we can control tab-navigation on the closed AppSwitcher
165
                    React.isValidElement(childItem.content) &&
1,804✔
166
                    typeof childItem.content.type !== 'string'
167
                      ? React.cloneElement(childItem.content, {
168
                          isExpanded,
169
                        })
170
                      : childItem.content
171
                  }
172
                </ChildElement>
173
              </li>
174
            );
175
          })}
176
        </ul>
177
      </HeaderPanel>
178
    </>
179
  );
180
};
181

182
HeaderActionPanel.propTypes = propTypes;
15✔
183
HeaderActionPanel.defaultProps = defaultProps;
15✔
184

185
export default React.forwardRef((props, ref) => {
186
  return <HeaderActionPanel {...props} focusRef={ref} />;
404✔
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

© 2026 Coveralls, Inc