• 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

94.67
/packages/react/src/components/Header/HeaderAction/HeaderActionMenu.jsx
1
import { ChevronDown32 } from '@carbon/icons-react';
2
import { settings } from 'carbon-components';
3
import classnames from 'classnames';
4
import React from 'react';
5
import PropTypes from 'prop-types';
6
import { HeaderMenuItem } from 'carbon-components-react/es/components/UIShell';
7

8
import { ChildContentPropTypes } from '../HeaderPropTypes';
9
import { handleSpecificKeyDown } from '../../../utils/componentUtilityFunctions';
10
import Button from '../../Button/Button';
11
import { isSafari } from '../../SuiteHeader/suiteHeaderUtils';
12

13
const { prefix } = settings;
16✔
14

15
// eslint-disable-next-line react/prop-types
16
const defaultRenderMenuContent = ({ ariaLabel }) => (
16✔
17
  <>
5✔
18
    {ariaLabel}
19
    <ChevronDown32 className={`${prefix}--header__menu-arrow`} />
20
  </>
21
);
22

23
/**
24
 * `HeaderActionMenu` is used to render submenu's in the `Header`. Most often children
25
 * will be a `HeaderActionMenuItem`. It handles certain keyboard events to help
26
 * with managing focus. It also passes along refs to each child so that it can
27
 * help manage focus state of its children.
28
 */
29
class HeaderActionMenu extends React.Component {
30
  static propTypes = {
16✔
31
    /** Ref object to be attached to the parent that should receive focus when a menu is closed */
32
    focusRef: PropTypes.oneOfType([
33
      // Either a function
34
      PropTypes.func,
35
      // Or the instance of a DOM native element (see the note about SSR)
36
      PropTypes.shape({
37
        current: typeof Element === 'undefined' ? PropTypes.any : PropTypes.instanceOf(Element),
16✔
38
      }),
39
    ]).isRequired,
40
    /** Optionally provide a tabIndex for the underlying menu button */
41
    tabIndex: PropTypes.number,
42
    /** Optional component to render instead of string */
43
    renderMenuContent: PropTypes.func,
44
    /** Determines if the header panel should be rendered which is decided by Header */
45
    isExpanded: PropTypes.bool,
46
    /** Hides/unhides the header panel logic */
47
    onToggleExpansion: PropTypes.func.isRequired,
48
    /** Unique name used by handleExpandedState */
49
    label: PropTypes.string.isRequired,
50
    /** MenuItem's to be rendered as children */
51
    childContent: PropTypes.arrayOf(PropTypes.shape(ChildContentPropTypes)).isRequired,
52
    /** Unique id for action menu */
53
    id: PropTypes.string.isRequired,
54
  };
55

56
  static defaultProps = {
16✔
57
    renderMenuContent: defaultRenderMenuContent,
58
    isExpanded: false,
59
    tabIndex: null,
60
  };
61

62
  state = {
387✔
63
    isOverflowing: [],
64
  };
65

66
  constructor(props) {
67
    super(props);
387✔
68
    this.menuItemRefs = props.childContent.map(() => React.createRef(null));
1,370✔
69
    this.menuRef = React.createRef(null);
387✔
70
    this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
387✔
71
  }
72

73
  componentDidMount() {
74
    const { isExpanded } = this.props;
387✔
75
    if (isExpanded) {
387✔
76
      this.checkForOverflows();
6✔
77
    }
78

79
    document.addEventListener('click', this.handleOutsideMenuClick, { capture: true });
387✔
80
  }
81

82
  componentDidUpdate(prevProps) {
83
    const { isExpanded } = this.props;
166✔
84
    if (isExpanded && !prevProps.isExpanded) {
166✔
85
      this.checkForOverflows();
24✔
86
    }
87
  }
88

89
  componentWillUnmount() {
90
    document.removeEventListener('click', this.handleOutsideMenuClick, { capture: true });
382✔
91
  }
92

93
  handleOutsideMenuClick(evt) {
94
    if (!isSafari()) {
265✔
95
      return;
241✔
96
    }
97

98
    const { isExpanded, onToggleExpansion } = this.props;
24✔
99
    if (this.menuRef.current && this.menuRef.current.contains(evt.target)) {
24!
100
      return;
×
101
    }
102

103
    if (isExpanded) {
24!
104
      onToggleExpansion();
×
105
    }
106
  }
107

108
  checkForOverflows() {
109
    this.setState({
30✔
110
      isOverflowing: this.menuItemRefs.map((ref) => {
111
        const element = ref.current.firstChild;
63✔
112
        return (
63✔
113
          element.offsetHeight < element.scrollHeight || element.offsetWidth < element.scrollWidth
126✔
114
        );
115
      }),
116
    });
117
  }
118

119
  render() {
120
    const {
121
      // eslint-disable-next-line react/prop-types
122
      'aria-label': ariaLabel,
123
      // eslint-disable-next-line react/prop-types
124
      'aria-labelledby': ariaLabelledBy,
125
      className: customClassName,
126
      renderMenuContent: MenuContent,
127
      childContent,
128
      onToggleExpansion,
129
      label,
130
      focusRef,
131
      isExpanded,
132
      id,
133
    } = this.props;
553✔
134

135
    const accessibilityLabel = {
553✔
136
      'aria-label': ariaLabel,
137
      'aria-labelledby': ariaLabelledBy,
138
    };
139

140
    const className = classnames(`${prefix}--header__submenu`, customClassName);
553✔
141

142
    // Notes on eslint comments and based on the examples in:
143
    // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-1/menubar-1.html#
144
    // - The focus is handled by the <a> menuitem, onMouseOver is for mouse
145
    // users
146
    // - aria-haspopup can definitely have the value "menu"
147
    // - aria-expanded is on their example node with role="menuitem"
148

149
    return (
553✔
150
      // TODO: CAN WE REMOVE THIS DIV WRAPPER AND ATTACH THE CLASS DIRECTLY
151
      <div className={className}>
152
        <Button
153
          hasIconOnly
154
          iconDescription={ariaLabel}
155
          tooltipPosition="bottom"
156
          aria-haspopup="menu"
157
          aria-expanded={isExpanded}
158
          className={classnames(`${prefix}--header__menu-item`, `${prefix}--header__menu-title`)}
159
          onClick={onToggleExpansion}
160
          ref={focusRef}
161
          testId="menuitem"
162
          aria-label={ariaLabel}
163
          id={id}
164
        >
165
          <MenuContent ariaLabel={ariaLabel} />
166
        </Button>
167
        <ul
168
          {...accessibilityLabel}
169
          ref={this.menuRef}
170
          className={`${prefix}--header__menu`}
171
          role="menu"
172
        >
173
          {childContent.map((childItem, index) => {
174
            const { isOverflowing } = this.state;
1,747✔
175
            const childIsOverflowing = isOverflowing[index];
1,747✔
176
            const fallbackTitle = this.menuItemRefs?.[index]?.current?.textContent ?? '';
1,747✔
177
            const title =
178
              childItem.metaData?.title ?? (childIsOverflowing ? fallbackTitle : undefined);
1,747✔
179
            const onKeyDownClick = (e) => e.target.click();
1,747✔
180

181
            // if the item is an A and doesn't have an onClick event
182
            // do nothing. An A tag doesn't need an onClick handler.
183
            const onClick =
184
              childItem.metaData?.element === 'a' && !childItem.metaData?.onClick
1,747✔
185
                ? undefined
186
                : // otherwise, if an onClick exists use that, or fallback to a noop.
187
                  childItem.metaData?.onClick || (() => {});
1,885✔
188

189
            // if item has onKeyDown use that otherwise, fallback to onClick if it exists
190
            // or create a custom handler to trigger the click
191
            const onKeyDown = childItem?.metaData?.onKeyDown
1,747✔
192
              ? childItem.metaData.onKeyDown
193
              : onClick || onKeyDownClick;
1,941✔
194

195
            return (
1,747✔
196
              <HeaderMenuItem
197
                ref={this.menuItemRefs[index]}
198
                key={`menu-item-${label + index}-child`}
199
                {...childItem.metaData}
200
                title={title}
201
                onClick={onClick}
202
                onKeyDown={handleSpecificKeyDown(['Enter', ' '], onKeyDown)}
203
              >
204
                {childItem.content}
205
              </HeaderMenuItem>
206
            );
207
          })}
208
        </ul>
209
      </div>
210
    );
211
  }
212
}
213

214
// eslint-disable-next-line react/no-multi-comp
215
export default React.forwardRef((props, ref) => {
216
  return <HeaderActionMenu {...props} focusRef={ref} />;
523✔
217
});
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