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

Payment Updates #88

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev-https": "cross-env APPMODE=development webpack-dev-server --https --port 8502 --host 0.0.0.0",
"build": "webpack --mode=${APPMODE:-production} --env.config=${APPENV:-prod}",
"analyze": "webpack --mode=production --env.analyze=true",
"lint": "eslint src --ext js --ext jsx",
"lint": "eslint ./src --ext .js,.jsx",
"format": "prettier --write \"./**\"",
"test": "cross-env BABEL_ENV=test jest",
"watch-tests": "cross-env BABEL_ENV=test jest --watch",
Expand Down
5 changes: 5 additions & 0 deletions src/assets/images/icon-arrow-down-narrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
202 changes: 202 additions & 0 deletions src/components/ActionsMenu/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import React, { useCallback, useMemo, useState } from "react";
import PT from "prop-types";
import cn from "classnames";
import { usePopper } from "react-popper";
import Button from "components/Button";
import IconArrowDown from "../../assets/images/icon-arrow-down-narrow.svg";
import { useClickOutside } from "utils/hooks";
import { negate, stopPropagation } from "utils/misc";
import compStyles from "./styles.module.scss";

/**
* Displays a clickable button with a menu.
*
* @param {Object} props component properties
* @param {'primary'|'error'|'warning'} [props.handleColor] menu handle color
* @param {'small'|'medium'} [props.handleSize] menu handle size
* @param {Array} props.items menu items
* @param {'absolute'|'fixed'} [props.popupStrategy] popup positioning strategy
* @param {boolean} [props.stopClickPropagation] whether to stop click event propagation
* @returns {JSX.Element}
*/
const ActionsMenu = ({
handleColor = "primary",
handleSize = "small",
items = [],
popupStrategy = "absolute",
stopClickPropagation = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [referenceElement, setReferenceElement] = useState(null);

const closeMenu = useCallback(() => {
setIsOpen(false);
}, []);

const toggleMenu = useCallback(() => {
setIsOpen(negate);
}, []);

const onItemClick = useCallback(
(event) => {
let index = +event.target.dataset.actionIndex;
let item = items[index];
if (!item || item.disabled || item.separator) {
return;
}
closeMenu();
item.action?.();
},
[items, closeMenu]
);

const menuItems = useMemo(
() =>
items.map((item, index) => {
if (item.hidden) {
return null;
} else if (item.separator) {
return <div key={index} className={compStyles.separator} />;
} else {
return (
<div
key={index}
data-action-index={index}
onClick={onItemClick}
role="button"
tabIndex={0}
className={cn(
compStyles.item,
{ [compStyles.itemDisabled]: item.disabled },
item.className
)}
>
{item.label}
</div>
);
}
}),
[items, onItemClick]
);

return (
<div
className={compStyles.container}
onClick={stopClickPropagation ? stopPropagation : null}
role="button"
tabIndex={0}
>
<Button
color={handleColor}
size={handleSize}
variant="contained"
onClick={isOpen ? null : toggleMenu}
className={cn(compStyles.handle, {
[compStyles.handleMenuOpen]: isOpen,
})}
innerRef={setReferenceElement}
>
Actions <IconArrowDown className={compStyles.iconArrowDown} />
</Button>
{isOpen && (
<Menu
items={menuItems}
onClickOutside={closeMenu}
referenceElement={referenceElement}
strategy={popupStrategy}
/>
)}
</div>
);
};

ActionsMenu.propTypes = {
handleColor: PT.oneOf(["primary", "error", "warning"]),
handleSize: PT.oneOf(["small", "medium"]),
items: PT.arrayOf(
PT.shape({
label: PT.string,
action: PT.func,
separator: PT.bool,
hidden: PT.bool,
})
),
popupStrategy: PT.oneOf(["absolute", "fixed"]),
stopClickPropagation: PT.bool,
};

export default ActionsMenu;

/**
* Displays a list of provided action items.
*
* @param {Object} props component properties
* @returns {JSX.Element}
*/
const Menu = ({ items, onClickOutside, referenceElement, strategy }) => {
const [popperElement, setPopperElement] = useState(null);
const [arrowElement, setArrowElement] = useState(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom",
strategy,
modifiers: [
{
name: "flip",
options: {
fallbackPlacements: ["bottom"],
},
},
{
name: "offset",
options: {
// use offset to move the dropdown slightly down
offset: [0, 5],
},
},
{
name: "arrow",
// padding should be equal to border-radius of the dropdown
options: { element: arrowElement, padding: 8 },
},
{
name: "preventOverflow",
options: {
// padding from browser edges
padding: 16,
},
},
{
name: "computeStyles",
options: {
// to fix bug in IE 11 https://github.com/popperjs/popper-core/issues/636
gpuAcceleration: false,
},
},
],
});

useClickOutside(popperElement, onClickOutside, []);

return (
<div
className={compStyles.popover}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className={compStyles.items}>{items}</div>
<div
ref={setArrowElement}
style={styles.arrow}
className={compStyles.popoverArrow}
/>
</div>
);
};

Menu.propTypes = {
items: PT.array.isRequired,
onClickOutside: PT.func.isRequired,
referenceElement: PT.object,
strategy: PT.oneOf(["absolute", "fixed"]),
};
76 changes: 76 additions & 0 deletions src/components/ActionsMenu/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@import "styles/variables";

.container {
position: relative;
display: inline-block;
}

.handle {
display: inline-flex;
align-items: center;
}

.iconArrowDown {
display: inline-block;
width: 12px;
height: 8px;
margin-left: 8px;
}

.handleMenuOpen {
.iconArrowDown {
transform: rotate(180deg);
}
}

.popover {
z-index: 100;
border-radius: 8px;
// min-width: 175px;
background-color: #fff;
box-shadow: 0px 5px 25px #c6c6c6;
}

.popoverArrow {
top: -9px;
border: 10px solid transparent;
border-top: none;
border-bottom-color: #fff;
width: 0;
height: 0;
}

.items {
padding: 16px;
}

.separator {
border-top: 1px solid #e7e7e7;
margin: 5px 0;
}

.item {
padding: 5px 0;
font-size: 12px;
font-weight: bold;
letter-spacing: 0.8px;
text-align: left;
text-transform: uppercase;
white-space: nowrap;
color: $primary-text-color;
cursor: pointer;
}

.danger {
color: #ef476f;
}

.itemDisabled {
color: gray;
opacity: 0.6;
pointer-events: none;
}

.hidden {
display: none;
}
5 changes: 5 additions & 0 deletions src/components/Button/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import styles from "./styles.module.scss";
* @param {string} [props.className] class name added to root element
* @param {'primary'|'primary-dark'|'primary-light'|'error'|'warning'} [props.color]
* button color
* @param {Object|function} [props.innerRef] Ref object or function to accept the
* ref for <button> element
* @param {boolean} [props.isDisabled] if button is disabled
* @param {boolean} [props.isSelected] if button is selected
* @param {string} [props.name] button name
Expand All @@ -26,6 +28,7 @@ const Button = ({
children,
className,
color = "primary",
innerRef,
isDisabled = false,
isSelected = false,
name,
Expand All @@ -40,6 +43,7 @@ const Button = ({
data-value={value}
disabled={isDisabled}
name={name || ""}
ref={innerRef}
type={type}
className={cn(
styles.button,
Expand All @@ -66,6 +70,7 @@ Button.propTypes = {
"error",
"warning",
]),
innerRef: PT.oneOfType([PT.object, PT.func]),
isDisabled: PT.bool,
isSelected: PT.bool,
name: PT.string,
Expand Down
Loading