Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 377dba6

Browse files
authored
Merge pull request #100 from topcoder-platform/dev
[PROD] Next Release
2 parents 49b6589 + 95bf062 commit 377dba6

File tree

67 files changed

+2709
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2709
-176
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev-https": "cross-env APPMODE=development webpack-dev-server --https --port 8502 --host 0.0.0.0",
77
"build": "webpack --mode=${APPMODE:-production} --env.config=${APPENV:-prod}",
88
"analyze": "webpack --mode=production --env.analyze=true",
9-
"lint": "eslint src --ext js --ext jsx",
9+
"lint": "eslint ./src --ext .js,.jsx",
1010
"format": "prettier --write \"./**\"",
1111
"test": "cross-env BABEL_ENV=test jest",
1212
"watch-tests": "cross-env BABEL_ENV=test jest --watch",
Loading

src/components/ActionsMenu/index.jsx

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import React, { useCallback, useMemo, useState } from "react";
2+
import PT from "prop-types";
3+
import cn from "classnames";
4+
import { usePopper } from "react-popper";
5+
import Button from "components/Button";
6+
import Tooltip from "components/Tooltip";
7+
import IconArrowDown from "../../assets/images/icon-arrow-down-narrow.svg";
8+
import { useClickOutside } from "utils/hooks";
9+
import { negate, stopPropagation } from "utils/misc";
10+
import compStyles from "./styles.module.scss";
11+
12+
/**
13+
* Displays a clickable button with a menu.
14+
*
15+
* @param {Object} props component properties
16+
* @param {'primary'|'error'|'warning'} [props.handleColor] menu handle color
17+
* @param {'small'|'medium'} [props.handleSize] menu handle size
18+
* @param {string} [props.handleText] text to show inside menu handle
19+
* @param {Array} props.items menu items
20+
* @param {'absolute'|'fixed'} [props.popupStrategy] popup positioning strategy
21+
* @param {boolean} [props.stopClickPropagation] whether to stop click event propagation
22+
* @returns {JSX.Element}
23+
*/
24+
const ActionsMenu = ({
25+
handleColor = "primary",
26+
handleSize = "small",
27+
handleText,
28+
items = [],
29+
popupStrategy = "absolute",
30+
stopClickPropagation = false,
31+
}) => {
32+
const [isOpen, setIsOpen] = useState(false);
33+
const [referenceElement, setReferenceElement] = useState(null);
34+
35+
const closeMenu = useCallback(() => {
36+
setIsOpen(false);
37+
}, []);
38+
39+
const toggleMenu = useCallback(() => {
40+
setIsOpen(negate);
41+
}, []);
42+
43+
return (
44+
<div
45+
className={compStyles.container}
46+
onClick={stopClickPropagation ? stopPropagation : null}
47+
role="button"
48+
tabIndex={0}
49+
>
50+
<Button
51+
color={handleColor}
52+
size={handleSize}
53+
style={handleText ? "rounded" : "circle"}
54+
variant="contained"
55+
onClick={isOpen ? null : toggleMenu}
56+
className={cn(compStyles.handle, {
57+
[compStyles.handleMenuOpen]: isOpen,
58+
})}
59+
innerRef={setReferenceElement}
60+
>
61+
{handleText ? <span>{handleText}&nbsp;</span> : null}
62+
<IconArrowDown className={compStyles.iconArrowDown} />
63+
</Button>
64+
{isOpen && (
65+
<Menu
66+
close={closeMenu}
67+
items={items}
68+
referenceElement={referenceElement}
69+
strategy={popupStrategy}
70+
/>
71+
)}
72+
</div>
73+
);
74+
};
75+
76+
ActionsMenu.propTypes = {
77+
handleColor: PT.oneOf(["primary", "error", "warning"]),
78+
handleSize: PT.oneOf(["small", "medium"]),
79+
handleText: PT.string,
80+
items: PT.arrayOf(
81+
PT.shape({
82+
label: PT.string,
83+
action: PT.func,
84+
separator: PT.bool,
85+
disabled: PT.bool,
86+
hidden: PT.bool,
87+
})
88+
),
89+
popupStrategy: PT.oneOf(["absolute", "fixed"]),
90+
stopClickPropagation: PT.bool,
91+
};
92+
93+
export default ActionsMenu;
94+
95+
/**
96+
* Displays a list of provided action items.
97+
*
98+
* @param {Object} props component properties
99+
* @returns {JSX.Element}
100+
*/
101+
const Menu = ({ close, items, referenceElement, strategy }) => {
102+
const [popperElement, setPopperElement] = useState(null);
103+
const [arrowElement, setArrowElement] = useState(null);
104+
const { styles, attributes } = usePopper(referenceElement, popperElement, {
105+
placement: "bottom",
106+
strategy,
107+
modifiers: [
108+
{
109+
name: "flip",
110+
options: {
111+
fallbackPlacements: ["bottom"],
112+
},
113+
},
114+
{
115+
name: "offset",
116+
options: {
117+
// use offset to move the dropdown slightly down
118+
offset: [0, 5],
119+
},
120+
},
121+
{
122+
name: "arrow",
123+
// padding should be equal to border-radius of the dropdown
124+
options: { element: arrowElement, padding: 8 },
125+
},
126+
{
127+
name: "preventOverflow",
128+
options: {
129+
// padding from browser edges
130+
padding: 16,
131+
},
132+
},
133+
{
134+
name: "computeStyles",
135+
options: {
136+
// to fix bug in IE 11 https://github.com/popperjs/popper-core/issues/636
137+
gpuAcceleration: false,
138+
},
139+
},
140+
],
141+
});
142+
143+
const onClickItem = useCallback(
144+
(event) => {
145+
let targetData = event.target.dataset;
146+
let index = +targetData.actionIndex;
147+
let item = items[index];
148+
if (!item || targetData.disabled || item.separator) {
149+
return;
150+
}
151+
close();
152+
item.action?.();
153+
},
154+
[close, items]
155+
);
156+
157+
useClickOutside(popperElement, close, []);
158+
159+
const menuItems = useMemo(() => {
160+
return items.map((item, index) => {
161+
if (item.hidden) {
162+
return null;
163+
} else if (item.separator) {
164+
return <div key={index} className={compStyles.separator} />;
165+
} else {
166+
let disabled = !!item.disabled;
167+
let reasonsDisabled = Array.isArray(item.disabled)
168+
? item.disabled
169+
: null;
170+
let attrs = {
171+
key: index,
172+
"data-action-index": index,
173+
onClick: onClickItem,
174+
role: "button",
175+
tabIndex: 0,
176+
className: cn(
177+
compStyles.item,
178+
{ [compStyles.itemDisabled]: disabled },
179+
item.className
180+
),
181+
};
182+
if (disabled) {
183+
attrs["data-disabled"] = true;
184+
}
185+
return (
186+
<div {...attrs}>
187+
{reasonsDisabled ? (
188+
<Tooltip
189+
content={
190+
reasonsDisabled.length === 1 ? (
191+
reasonsDisabled[0]
192+
) : (
193+
<ul>
194+
{reasonsDisabled.map((text, index) => (
195+
<li key={index}>{text}</li>
196+
))}
197+
</ul>
198+
)
199+
}
200+
strategy="fixed"
201+
>
202+
{item.label}
203+
</Tooltip>
204+
) : (
205+
item.label
206+
)}
207+
</div>
208+
);
209+
}
210+
});
211+
}, [items, onClickItem]);
212+
213+
return (
214+
<div
215+
className={compStyles.popover}
216+
ref={setPopperElement}
217+
style={styles.popper}
218+
{...attributes.popper}
219+
>
220+
<div className={compStyles.items}>{menuItems}</div>
221+
<div
222+
ref={setArrowElement}
223+
style={styles.arrow}
224+
className={compStyles.popoverArrow}
225+
/>
226+
</div>
227+
);
228+
};
229+
230+
Menu.propTypes = {
231+
close: PT.func.isRequired,
232+
items: PT.arrayOf(
233+
PT.shape({
234+
label: PT.string,
235+
action: PT.func,
236+
checkDisabled: PT.func,
237+
disabled: PT.bool,
238+
separator: PT.bool,
239+
hidden: PT.bool,
240+
})
241+
),
242+
referenceElement: PT.object,
243+
strategy: PT.oneOf(["absolute", "fixed"]),
244+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
@import "styles/variables";
2+
3+
.container {
4+
position: relative;
5+
display: inline-block;
6+
}
7+
8+
.handle {
9+
display: inline-flex;
10+
align-items: center;
11+
12+
> span {
13+
+ .iconArrowDown {
14+
margin-left: 8px;
15+
}
16+
}
17+
}
18+
19+
.iconArrowDown {
20+
display: inline-block;
21+
width: 12px;
22+
height: 8px;
23+
}
24+
25+
.handleMenuOpen {
26+
.iconArrowDown {
27+
transform: rotate(180deg);
28+
}
29+
}
30+
31+
.popover {
32+
z-index: 100;
33+
border-radius: 8px;
34+
// min-width: 175px;
35+
background-color: #fff;
36+
box-shadow: 0px 5px 25px #c6c6c6;
37+
}
38+
39+
.popoverArrow {
40+
top: -9px;
41+
border: 10px solid transparent;
42+
border-top: none;
43+
border-bottom-color: #fff;
44+
width: 0;
45+
height: 0;
46+
}
47+
48+
.items {
49+
padding: 16px;
50+
}
51+
52+
.separator {
53+
border-top: 1px solid #e7e7e7;
54+
margin: 5px 0;
55+
}
56+
57+
.item {
58+
padding: 5px 0;
59+
font-size: 12px;
60+
font-weight: bold;
61+
letter-spacing: 0.8px;
62+
text-align: left;
63+
text-transform: uppercase;
64+
white-space: nowrap;
65+
color: $primary-text-color;
66+
cursor: pointer;
67+
}
68+
69+
.danger {
70+
color: #ef476f;
71+
}
72+
73+
.itemDisabled {
74+
color: #bbb;
75+
cursor: default;
76+
}
77+
78+
.hidden {
79+
display: none;
80+
}

src/components/Button/index.jsx

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import styles from "./styles.module.scss";
1111
* @param {string} [props.className] class name added to root element
1212
* @param {'primary'|'primary-dark'|'primary-light'|'error'|'warning'} [props.color]
1313
* button color
14+
* @param {Object|function} [props.innerRef] Ref object or function to accept the
15+
* ref for <button> element
1416
* @param {boolean} [props.isDisabled] if button is disabled
1517
* @param {boolean} [props.isSelected] if button is selected
1618
* @param {string} [props.name] button name
@@ -26,6 +28,7 @@ const Button = ({
2628
children,
2729
className,
2830
color = "primary",
31+
innerRef,
2932
isDisabled = false,
3033
isSelected = false,
3134
name,
@@ -40,6 +43,7 @@ const Button = ({
4043
data-value={value}
4144
disabled={isDisabled}
4245
name={name || ""}
46+
ref={innerRef}
4347
type={type}
4448
className={cn(
4549
styles.button,
@@ -66,6 +70,7 @@ Button.propTypes = {
6670
"error",
6771
"warning",
6872
]),
73+
innerRef: PT.oneOfType([PT.object, PT.func]),
6974
isDisabled: PT.bool,
7075
isSelected: PT.bool,
7176
name: PT.string,

0 commit comments

Comments
 (0)