monday-ui-react-core
Version:
Official monday.com UI resources for application development in React.js
269 lines (236 loc) • 8.42 kB
JSX
/* eslint-disable react/jsx-props-no-spreading */
// Libraries import
import React, { useCallback, useMemo, useRef, useState } from "react";
import cx from "classnames";
import PropTypes from "prop-types";
// Constants import
import { keyCodes } from "../../constants/KeyCodes";
// Utils import
import { NOOP } from "../../utils/function-utils";
import { isInsideClass } from "../../utils/dom-utils";
// Hooks import
import useKeyEvent from "../../hooks/useKeyEvent";
// Components import
import Button from "../Button/Button";
import Dialog from "../Dialog/Dialog";
import useEventListener from "../../hooks/useEventListener";
import DropdownChevronDown from "../Icon/Icons/components/DropdownChevronDown";
// SCSS import
import "./SplitButton.scss";
import DialogContentContainer from "../DialogContentContainer/DialogContentContainer";
// Constants
const DIALOG_MOVE_BY = { main: 8, secondary: 0 };
const DEFAULT_DIALOG_SHOW_TRIGGER = "click";
const DEFAULT_DIALOG_HIDE_TRIGGER = ["clickoutside", "click", "esckey"];
const SECONDARY_BUTTON_WRAPPER_CLASSNAME = "monday-style-split-button__secondary-button-wrapper";
const EMPTY_ARR = [];
const SECONDARY_CONTENT_POSITIONS = {
BOTTOM_START: "bottom-start",
BOTTOM_MIDDLE: "bottom",
BOTTOM_END: "bottom-end"
};
export const SECONDARY_BUTTON_ARIA_LABEL = "additional actions";
const SplitButton = ({
marginLeft,
marginRight,
success,
loading,
children,
leftIcon,
rightIcon,
color,
kind,
className,
onClick,
secondaryDialogContent,
onSecondaryDialogDidShow,
onSecondaryDialogDidHide,
disabled,
shouldCloseOnClickInsideDialog,
zIndex,
secondaryDialogClassName,
secondaryDialogPosition,
...buttonProps
}) => {
// State //
const [isDialogOpen, setDialogOpen] = useState(false);
const [isHovered, setIsHover] = useState(false);
const [isActive, setIsActive] = useState(false);
// Refs //
const ref = useRef(null);
// Callbacks //
const setHovered = useCallback(() => setIsHover(true), [setIsHover]);
const setNotHovered = useCallback(() => setIsHover(false), [setIsHover]);
const shouldSetActive = useCallback(
e => {
if (disabled) return false;
return !isInsideClass(e.target, SECONDARY_BUTTON_WRAPPER_CLASSNAME);
},
[disabled]
);
const setActive = useCallback(
e => {
if (!shouldSetActive(e)) return;
setIsActive(true);
},
[setIsActive, shouldSetActive]
);
const setNotActive = useCallback(() => setIsActive(false), [setIsActive]);
const setActiveOnEnter = useCallback(
e => {
if (!shouldSetActive(e)) return;
setIsActive(true);
},
[setIsActive, shouldSetActive]
);
const showDialog = useCallback(() => {
setDialogOpen(true);
onSecondaryDialogDidShow();
}, [setDialogOpen, onSecondaryDialogDidShow]);
const hideDialog = useCallback(() => {
setDialogOpen(false);
onSecondaryDialogDidHide();
}, [setDialogOpen, onSecondaryDialogDidHide]);
// Event listeners //
// Used to set both buttons as hovered no matter what button was hovered
useEventListener({ eventName: "mouseenter", callback: setHovered, ref });
useEventListener({ eventName: "mouseleave", callback: setNotHovered, ref });
useEventListener({ eventName: "mousedown", callback: setActive, ref });
useEventListener({ eventName: "mouseup", callback: setNotActive, ref });
// Used to finish active transition if clicked on enter
useEventListener({ eventName: "transitionend", callback: setNotActive, ref });
// Key events
useKeyEvent({ keys: [keyCodes.ENTER], ref, callback: setActiveOnEnter });
// We won't show the secondary button in case of success or loading
const shouldRenderSplitContent = !(success || loading);
const classNames = useMemo(
() =>
cx(
"monday-style-split-button",
`monday-style-split-button--kind-${kind}`,
`monday-style-split-button--color-${color}`,
{
"monday-style-split-button--active": isActive,
"monday-style-split-button--split-content-open": isDialogOpen,
"monday-style-split-button--hovered": isHovered,
"monday-style-split-button--disabled": disabled
},
className
),
[className, kind, color, isActive, isDialogOpen, isHovered, disabled]
);
const dialogShowTrigger = useMemo(() => (disabled ? EMPTY_ARR : DEFAULT_DIALOG_SHOW_TRIGGER), [disabled]);
const dialogHideTrigger = useMemo(() => {
if (shouldCloseOnClickInsideDialog) return [...DEFAULT_DIALOG_HIDE_TRIGGER, "onContentClick"];
return DEFAULT_DIALOG_HIDE_TRIGGER;
}, [shouldCloseOnClickInsideDialog]);
const actionsContent = useCallback(() => {
const content = typeof secondaryDialogContent === "function" ? secondaryDialogContent() : secondaryDialogContent;
return (
<DialogContentContainer type={DialogContentContainer.types.POPOVER} size={DialogContentContainer.sizes.MEDIUM}>
{content}
</DialogContentContainer>
);
}, [secondaryDialogContent]);
const animationEdgePosition = useMemo(() => {
if (secondaryDialogPosition === SECONDARY_CONTENT_POSITIONS.BOTTOM_MIDDLE) {
return "";
}
if (secondaryDialogPosition === SECONDARY_CONTENT_POSITIONS.BOTTOM_START) {
return "bottom";
}
return "top";
}, [secondaryDialogPosition]);
return (
<div className={classNames} ref={ref} role="button">
<Button
{
...buttonProps /* We are enriching button with other props so we must use spreading */
}
preventClickAnimation
leftIcon={leftIcon}
rightIcon={rightIcon}
rightFlat
color={color}
kind={kind}
onClick={onClick}
className="monday-style-split-button__main-button"
marginLeft={marginLeft}
onFocus={setHovered}
onBlur={setNotHovered}
disabled={disabled}
>
{children}
</Button>
{shouldRenderSplitContent && (
<div className={SECONDARY_BUTTON_WRAPPER_CLASSNAME}>
<Dialog
wrapperClassName={secondaryDialogClassName}
zIndex={zIndex}
content={actionsContent}
position={secondaryDialogPosition}
startingEdge={animationEdgePosition}
animationType="expand"
moveBy={DIALOG_MOVE_BY}
onDialogDidShow={showDialog}
onDialogDidHide={hideDialog}
showTrigger={dialogShowTrigger}
hideTrigger={dialogHideTrigger}
>
<Button
{...buttonProps}
preventClickAnimation
leftFlat
noSidePadding
color={color}
kind={kind}
className="monday-style-split-button__secondary-button"
active={isDialogOpen}
marginRight={marginRight}
onFocus={setHovered}
onBlur={setNotHovered}
disabled={disabled}
ariaLabel={SECONDARY_BUTTON_ARIA_LABEL}
aria-haspopup="true"
aria-expanded={isDialogOpen}
>
<div className="monday-style-split-button__secondary-button-icon-wrapper">
<DropdownChevronDown />
</div>
</Button>
</Dialog>
</div>
)}
</div>
);
};
SplitButton.secondaryPositions = SECONDARY_CONTENT_POSITIONS;
SplitButton.sizes = Button.sizes;
SplitButton.colors = Button.colors;
SplitButton.kinds = Button.kinds;
SplitButton.inputTags = Button.inputTags;
SplitButton.defaultProps = {
...Button.defaultProps,
onSecondaryDialogDidShow: NOOP,
onSecondaryDialogDidHide: NOOP,
zIndex: null,
secondaryDialogClassName: "",
secondaryDialogPosition: SECONDARY_CONTENT_POSITIONS.BOTTOM_START
};
SplitButton.propTypes = {
...Button.propTypes,
secondaryDialogContent: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
onSecondaryDialogDidShow: PropTypes.func,
onSecondaryDialogDidHide: PropTypes.func,
zIndex: PropTypes.number,
/*
* Class name to provide the element which wraps the popover/modal/dialog
*/
secondaryDialogClassName: PropTypes.string,
secondaryDialogPosition: PropTypes.oneOf([
SplitButton.secondaryPositions.BOTTOM_START,
SplitButton.secondaryPositions.BOTTOM_MIDDLE,
SplitButton.secondaryPositions.BOTTOM_END
])
};
export default SplitButton;