|
1 | 1 | /* |
2 | | - * Copyright 2017-2024 Elyra Authors |
| 2 | + * Copyright 2017-2025 Elyra Authors |
3 | 3 | * |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | * you may not use this file except in compliance with the License. |
|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
17 | | -import React from "react"; |
| 17 | +import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef } from "react"; |
18 | 18 | import PropTypes from "prop-types"; |
19 | 19 |
|
20 | | -import { v4 as uuid4 } from "uuid"; |
21 | 20 | import { Button } from "@carbon/react"; |
22 | 21 | import { OverflowMenuVertical } from "@carbon/react/icons"; |
23 | 22 | import KeyboardUtils from "../common-canvas/keyboard-utils.js"; |
24 | 23 | import ToolbarSubMenu from "./toolbar-sub-menu.jsx"; |
25 | 24 |
|
26 | | -class ToolbarOverflowItem extends React.Component { |
27 | | - constructor(props) { |
28 | | - super(props); |
| 25 | +const ToolbarOverflowItem = forwardRef(({ |
| 26 | + index, |
| 27 | + action, |
| 28 | + label, |
| 29 | + size, |
| 30 | + subMenuActions, |
| 31 | + setOverflowIndex, |
| 32 | + toolbarActionHandler, |
| 33 | + instanceId, |
| 34 | + containingDivId, |
| 35 | + toolbarFocusAction, |
| 36 | + setToolbarFocusAction, |
| 37 | + isFocusInToolbar, |
| 38 | + closeAnyOpenSubArea |
| 39 | +}, ref) => { |
| 40 | + |
| 41 | + const [showExtendedMenu, setShowExtendedMenu] = useState(false); |
| 42 | + |
| 43 | + const buttonRef = useRef(null); |
| 44 | + |
| 45 | + // Manage button focus |
| 46 | + useEffect(() => { |
| 47 | + if (toolbarFocusAction === action && isFocusInToolbar && !showExtendedMenu) { |
| 48 | + buttonRef.current.focus(); |
| 49 | + } |
| 50 | + }, [toolbarFocusAction, isFocusInToolbar, showExtendedMenu]); |
| 51 | + |
| 52 | + |
| 53 | + // Manage event listener for clicking outside the overflow menu. |
| 54 | + useEffect(() => { |
| 55 | + document.addEventListener("click", clickOutside, false); |
29 | 56 |
|
30 | | - this.state = { |
31 | | - showExtendedMenu: false |
| 57 | + return () => { |
| 58 | + document.removeEventListener("click", clickOutside, false); |
32 | 59 | }; |
33 | 60 |
|
34 | | - this.buttonRef = React.createRef(); |
| 61 | + }, [toolbarFocusAction, isFocusInToolbar, showExtendedMenu]); |
35 | 62 |
|
36 | | - this.uuid = uuid4(); |
37 | | - this.toggleExtendedMenu = this.toggleExtendedMenu.bind(this); |
38 | | - this.clickOutside = this.clickOutside.bind(this); |
39 | | - this.closeSubArea = this.closeSubArea.bind(this); |
40 | | - this.onKeyDown = this.onKeyDown.bind(this); |
41 | | - } |
42 | 63 |
|
43 | | - componentDidUpdate() { |
44 | | - if (this.props.toolbarFocusAction === this.props.action && this.props.isFocusInToolbar && !this.state.showExtendedMenu) { |
45 | | - this.buttonRef.current.focus(); |
46 | | - } |
47 | | - } |
| 64 | + // Expose methods to toolbar.jsx |
| 65 | + useImperativeHandle(ref, () => ({ |
| 66 | + getAction: () => action, |
48 | 67 |
|
49 | | - // We must remove the eventListener in case this class is unmounted due |
50 | | - // to the toolbar getting redrawn. |
51 | | - componentWillUnmount() { |
52 | | - document.removeEventListener("click", this.clickOutside, false); |
53 | | - } |
| 68 | + isSubAreaDisplayed: () => showExtendedMenu, |
| 69 | + |
| 70 | + closeSubArea: () => closeSubArea() |
| 71 | + })); |
54 | 72 |
|
55 | | - onKeyDown(evt) { |
| 73 | + function onKeyDown(evt) { |
56 | 74 | if (KeyboardUtils.closeSubArea(evt)) { |
57 | | - this.closeSubArea(); |
| 75 | + closeSubArea(); |
58 | 76 |
|
59 | 77 | } else if (KeyboardUtils.openSubArea(evt)) { |
60 | | - this.openSubArea(); |
| 78 | + openSubArea(); |
61 | 79 | } |
62 | 80 | // Left and Right arrow clicks are caught in the |
63 | 81 | // toolbar.jsx onKeyDown method. |
64 | 82 | } |
65 | 83 |
|
66 | | - |
67 | | - // Called by toolbar.jsx |
68 | | - getAction() { |
69 | | - return this.props.action; |
70 | | - } |
71 | | - |
72 | | - // Called by toolbar.jsx |
73 | | - isSubAreaDisplayed() { |
74 | | - return this.state.showExtendedMenu; |
75 | | - } |
76 | | - |
77 | | - // Called by toolbar.jsx and internally |
78 | | - closeSubArea() { |
79 | | - document.removeEventListener("click", this.clickOutside, false); |
80 | | - this.props.setOverflowIndex(null); // Clear the indexes |
81 | | - this.setState({ showExtendedMenu: false }); |
82 | | - this.props.setToolbarFocusAction(this.props.action); // This will not set focus on this item |
83 | | - } |
84 | | - |
85 | | - openSubArea() { |
86 | | - document.addEventListener("click", this.clickOutside, false); |
87 | | - this.props.closeAnyOpenSubArea(); |
88 | | - this.props.setOverflowIndex(this.props.index); |
89 | | - this.setState({ showExtendedMenu: true }); |
90 | | - this.props.setToolbarFocusAction(this.props.action); |
| 84 | + function closeSubArea() { |
| 85 | + setOverflowIndex(null); // Clear the indexes |
| 86 | + setShowExtendedMenu(false); |
| 87 | + setToolbarFocusAction(action); // This will not set focus on this item |
91 | 88 | } |
92 | 89 |
|
93 | | - genOverflowButtonClassName() { |
94 | | - return "toolbar-overflow-container " + this.genIndexClassName() + " " + this.genUuidClassName(); |
| 90 | + function openSubArea() { |
| 91 | + closeAnyOpenSubArea(); |
| 92 | + setOverflowIndex(index); |
| 93 | + setShowExtendedMenu(true); |
| 94 | + setToolbarFocusAction(action); |
95 | 95 | } |
96 | 96 |
|
97 | | - genIndexClassName() { |
98 | | - return "toolbar-index-" + this.props.index; |
| 97 | + function genOverflowButtonClassName() { |
| 98 | + return "toolbar-overflow-container " + genIndexClassName(); |
99 | 99 | } |
100 | 100 |
|
101 | | - genUuidClassName() { |
102 | | - return "toolbar-uuid-" + this.uuid; |
| 101 | + function genIndexClassName() { |
| 102 | + return "toolbar-index-" + index; |
103 | 103 | } |
104 | 104 |
|
105 | 105 | // When the overflow item is clicked to open the overflow menu we must set the |
106 | 106 | // index of the overflow items so the overflow menu can be correctly constructed. |
107 | 107 | // The overflow index values are used to split out the overflow menu action items |
108 | 108 | // from the left bar and right bar. |
109 | 109 | // When the overflow menu is closed we set the overflow index values to null. |
110 | | - toggleExtendedMenu() { |
111 | | - if (this.state.showExtendedMenu) { |
112 | | - this.closeSubArea(); |
| 110 | + function toggleExtendedMenu() { |
| 111 | + if (showExtendedMenu) { |
| 112 | + closeSubArea(); |
113 | 113 |
|
114 | 114 | } else { |
115 | | - this.openSubArea(); |
| 115 | + openSubArea(); |
116 | 116 | } |
117 | 117 | } |
118 | 118 |
|
119 | | - clickOutside(evt) { |
120 | | - if (this.state.showExtendedMenu) { |
| 119 | + function clickOutside(evt) { |
| 120 | + if (showExtendedMenu) { |
121 | 121 | // Selector for the overflow-container that contains the overflow icon |
122 | 122 | // and submenu (if submenu is open). |
123 | | - const selector = "." + this.genIndexClassName(); |
| 123 | + const selector = "." + genIndexClassName(); |
124 | 124 | const isClickInOverflowContainer = evt.target.closest(selector); |
125 | 125 | if (!isClickInOverflowContainer) { |
126 | | - this.setState({ showExtendedMenu: false }); |
| 126 | + setShowExtendedMenu(false); |
127 | 127 | } |
128 | 128 | } |
129 | 129 | } |
130 | 130 |
|
131 | | - render() { |
132 | | - let overflowMenu = null; |
133 | | - if (this.state.showExtendedMenu) { |
134 | | - const actionItemRect = this.buttonRef.current.getBoundingClientRect(); |
135 | | - overflowMenu = ( |
136 | | - <ToolbarSubMenu |
137 | | - ref={this.subMenuRef} |
138 | | - subMenuActions={this.props.subMenuActions} |
139 | | - instanceId={this.props.instanceId} |
140 | | - toolbarActionHandler={this.props.toolbarActionHandler} |
141 | | - closeSubArea={this.closeSubArea} |
142 | | - setToolbarFocusAction={this.props.setToolbarFocusAction} |
143 | | - actionItemRect={actionItemRect} |
144 | | - expandDirection={"vertical"} |
145 | | - containingDivId={this.props.containingDivId} |
146 | | - parentSelector={".toolbar-overflow-container"} |
147 | | - isOverflowMenu |
148 | | - isCascadeMenu={false} |
149 | | - size={this.props.size} |
150 | | - /> |
151 | | - ); |
152 | | - } |
| 131 | + let overflowMenu = null; |
| 132 | + if (showExtendedMenu) { |
| 133 | + const actionItemRect = buttonRef.current.getBoundingClientRect(); |
| 134 | + overflowMenu = ( |
| 135 | + <ToolbarSubMenu |
| 136 | + subMenuActions={subMenuActions} |
| 137 | + instanceId={instanceId} |
| 138 | + toolbarActionHandler={toolbarActionHandler} |
| 139 | + closeSubArea={closeSubArea} |
| 140 | + setToolbarFocusAction={setToolbarFocusAction} |
| 141 | + actionItemRect={actionItemRect} |
| 142 | + expandDirection={"vertical"} |
| 143 | + containingDivId={containingDivId} |
| 144 | + parentSelector={".toolbar-overflow-container"} |
| 145 | + isOverflowMenu |
| 146 | + isCascadeMenu={false} |
| 147 | + size={size} |
| 148 | + /> |
| 149 | + ); |
| 150 | + } |
153 | 151 |
|
154 | | - const tabIndex = this.props.toolbarFocusAction === this.props.action ? 0 : -1; |
155 | | - |
156 | | - return ( |
157 | | - <div className={this.genOverflowButtonClassName()} data-toolbar-action={this.props.action}> |
158 | | - <div className={"toolbar-overflow-item"}> |
159 | | - <Button |
160 | | - ref={this.buttonRef} |
161 | | - kind="ghost" |
162 | | - tabIndex={tabIndex} |
163 | | - onClick={this.toggleExtendedMenu} |
164 | | - onKeyDown={this.onKeyDown} |
165 | | - aria-label={this.props.label} |
166 | | - size={this.props.size} |
167 | | - > |
168 | | - <div className="toolbar-item-content default"> |
169 | | - <div className="toolbar-icon overflow-item"> |
170 | | - <OverflowMenuVertical /> |
171 | | - </div> |
| 152 | + const tabIndex = toolbarFocusAction === action ? 0 : -1; |
| 153 | + |
| 154 | + return ( |
| 155 | + <div className={genOverflowButtonClassName()} data-toolbar-action={action}> |
| 156 | + <div className={"toolbar-overflow-item"}> |
| 157 | + <Button |
| 158 | + ref={buttonRef} |
| 159 | + kind="ghost" |
| 160 | + tabIndex={tabIndex} |
| 161 | + onClick={toggleExtendedMenu} |
| 162 | + onKeyDown={onKeyDown} |
| 163 | + aria-label={label} |
| 164 | + size={size} |
| 165 | + > |
| 166 | + <div className="toolbar-item-content default"> |
| 167 | + <div className="toolbar-icon overflow-item"> |
| 168 | + <OverflowMenuVertical /> |
172 | 169 | </div> |
173 | | - </Button> |
174 | | - </div> |
175 | | - {overflowMenu} |
| 170 | + </div> |
| 171 | + </Button> |
176 | 172 | </div> |
177 | | - ); |
178 | | - } |
179 | | -} |
| 173 | + {overflowMenu} |
| 174 | + </div> |
| 175 | + ); |
| 176 | +}); |
180 | 177 |
|
181 | 178 | ToolbarOverflowItem.propTypes = { |
182 | 179 | index: PropTypes.number.isRequired, |
|
0 commit comments