monday-ui-react-core
Version:
Official monday.com UI resources for application development in React.js
403 lines (374 loc) • 11.7 kB
JSX
/* eslint-disable react/jsx-props-no-spreading,react/button-has-type */
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import useResizeObserver from "../../hooks/useResizeObserver";
import useMergeRefs from "../../hooks/useMergeRefs";
import "./Button.scss";
import { BUTTON_COLORS, BUTTON_INPUT_TYPE, BUTTON_TYPES, getActualSize } from "./ButtonConstants";
import { NOOP } from "../../utils/function-utils";
import Icon from "../Icon/Icon";
import Loader from "../Loader/Loader";
import { SIZES } from "../../constants/sizes";
import { getParentBackgroundColorNotTransparent, TRANSPARENT_COLOR } from "./helper/dom-helpers";
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
// min button width
const MIN_BUTTON_HEIGHT_PX = isIE11 ? 32 : 6;
const UPDATE_CSS_VARIABLES_DEBOUNCE = 200;
const Button = forwardRef(
(
{
className,
children,
kind,
onClick,
name,
size,
color,
disabled,
rightIcon,
leftIcon,
success,
successText,
successIcon,
style,
loading,
active,
id,
marginRight,
marginLeft,
type,
onMouseDown,
ariaLabel,
rightFlat,
leftFlat,
preventClickAnimation,
noSidePadding,
onFocus,
onBlur,
ariaLabeledBy,
defaultTextColorOnPrimaryColor,
ariaHasPopup,
ariaExpanded,
ariaControls,
blurOnMouseUp
},
ref
) => {
const buttonRef = useRef(null);
const [hasSizeStyle, setHasSizeStyle] = useState(false);
const updateCssVariables = useMemo(() => {
return ({ borderBoxSize }) => {
const { blockSize, inlineSize } = borderBoxSize;
const width = Math.max(inlineSize, MIN_BUTTON_HEIGHT_PX);
const height = Math.max(blockSize, MIN_BUTTON_HEIGHT_PX);
if (!buttonRef.current) return;
buttonRef.current.style.setProperty("--element-width", `${width}px`);
buttonRef.current.style.setProperty("--element-height", `${height}px`);
setHasSizeStyle(true);
};
}, [buttonRef]);
useResizeObserver({
ref: buttonRef,
callback: updateCssVariables,
debounceTime: UPDATE_CSS_VARIABLES_DEBOUNCE
});
useEffect(() => {
if (color !== BUTTON_COLORS.ON_PRIMARY_COLOR) return;
if (kind !== BUTTON_TYPES.PRIMARY) return;
if (!buttonRef.current) return;
const buttonElement = buttonRef.current;
buttonElement.style.color = getParentBackgroundColorNotTransparent(buttonElement, defaultTextColorOnPrimaryColor);
}, [kind, buttonRef, color, defaultTextColorOnPrimaryColor]);
const onMouseUp = useCallback(() => {
const button = buttonRef.current;
if (!button) {
return;
}
if (blurOnMouseUp) {
button.blur();
}
}, [buttonRef, blurOnMouseUp]);
const onButtonClicked = useCallback(
event => {
if (disabled || loading || success) {
event.preventDefault();
return;
}
if (onClick) {
onClick(event);
}
},
[onClick, disabled, loading, success]
);
const onMouseDownClicked = useCallback(
event => {
if (disabled || loading || success) {
event.preventDefault();
return;
}
if (onMouseDown) {
onMouseDown(event);
}
},
[onMouseDown, disabled, loading, success]
);
const classNames = useMemo(() => {
const calculatedColor = success ? BUTTON_COLORS.POSITIVE : color;
return cx(
className,
"monday-style-button",
`monday-style-button--size-${getActualSize(size)}`,
`monday-style-button--kind-${kind}`,
`monday-style-button--color-${calculatedColor}`,
{
"has-style-size": hasSizeStyle,
"monday-style-button--loading": loading,
[`monday-style-button--color-${calculatedColor}-active`]: active,
"monday-style-button--margin-right": marginRight,
"monday-style-button--margin-left": marginLeft,
"monday-style-button--right-flat": rightFlat,
"monday-style-button--left-flat": leftFlat,
"monday-style-button--prevent-click-animation": preventClickAnimation,
"monday-style-button--no-side-padding": noSidePadding
}
);
}, [
size,
kind,
color,
className,
success,
loading,
active,
marginRight,
marginLeft,
noSidePadding,
preventClickAnimation,
leftFlat,
rightFlat,
hasSizeStyle
]);
const mergedRef = useMergeRefs({ refs: [ref, buttonRef] });
const buttonProps = useMemo(() => {
return {
disabled,
ref: mergedRef,
type,
className: classNames,
name,
onMouseUp,
style,
onClick: onButtonClicked,
id,
onFocus,
onBlur,
onMouseDown: onMouseDownClicked,
"aria-disabled": disabled,
"aria-labelledby": ariaLabeledBy,
"aria-label": ariaLabel,
"aria-busy": loading ? "true" : undefined,
"aria-haspopup": ariaHasPopup,
"aria-expanded": ariaExpanded,
"aria-controls": ariaControls
};
}, [
disabled,
classNames,
name,
onMouseUp,
style,
onButtonClicked,
id,
type,
onMouseDownClicked,
ariaLabel,
loading,
onFocus,
onBlur,
mergedRef,
ariaLabeledBy,
ariaControls,
ariaExpanded,
ariaHasPopup
]);
const leftIconSize = useMemo(() => {
if (typeof leftIcon !== "function") return;
if (size === SIZES.SMALL) return "20";
if (size === SIZES.MEDIUM) return "24";
return "24";
}, [leftIcon, size]);
const rightIconSize = useMemo(() => {
if (typeof rightIcon !== "function") return;
if (size === SIZES.SMALL) return "20";
if (size === SIZES.MEDIUM) return "24";
return "24";
}, [rightIcon, size]);
const successIconSize = useMemo(() => {
if (typeof successIcon !== "function") return;
if (size === SIZES.SMALL) return "20";
if (size === SIZES.MEDIUM) return "24";
return "24";
}, [successIcon, size]);
if (loading) {
return (
<button {...buttonProps}>
<span className="monday-style-button__loader">
<Loader svgClassName="monday-style-button-loader-svg" />
</span>
</button>
);
}
if (success) {
return (
<button {...buttonProps}>
{successIcon ? (
<Icon
iconType={Icon.type.ICON_FONT}
clickable={false}
icon={successIcon}
iconSize={successIconSize}
className={cx({
"monday-style-button--left-icon": !!successText
})}
ignoreFocusStyle
/>
) : null}
{successText}
</button>
);
}
return (
<button {...buttonProps}>
{leftIcon ? (
<Icon
iconType={Icon.type.ICON_FONT}
clickable={false}
icon={leftIcon}
iconSize={leftIconSize}
className={cx({ "monday-style-button--left-icon": !!children })}
ignoreFocusStyle
/>
) : null}
{children}
{rightIcon ? (
<Icon
iconType={Icon.type.ICON_FONT}
clickable={false}
icon={rightIcon}
iconSize={rightIconSize}
className={cx({ "monday-style-button--right-icon": !!children })}
ignoreFocusStyle
/>
) : null}
</button>
);
}
);
Button.sizes = SIZES;
Button.colors = BUTTON_COLORS;
Button.kinds = BUTTON_TYPES;
Button.inputTags = BUTTON_INPUT_TYPE;
Button.propTypes = {
className: PropTypes.string,
/** The kind of a button is exposed on the component */
kind: PropTypes.oneOf([Button.kinds.PRIMARY, Button.kinds.SECONDARY, Button.kinds.TERTIARY]),
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
/** Blur on button click */
blurOnMouseUp: PropTypes.bool,
/** Name of the button - for form submit usages */
name: PropTypes.string,
/** The size of a button is exposed on the component */
size: PropTypes.oneOf([Button.sizes.SMALL, Button.sizes.MEDIUM, Button.sizes.LARGE]),
/** The color of a button is exposed on the component */
color: PropTypes.oneOf([
Button.colors.PRIMARY,
Button.colors.NEGATIVE,
Button.colors.POSITIVE,
Button.colors.ON_PRIMARY_COLOR,
Button.colors.ON_INVERTED_BACKGROUND
]),
/** The type of a button is exposed on the component */
type: PropTypes.oneOf([Button.inputTags.BUTTON, Button.inputTags.SUBMIT, Button.inputTags.RESET]),
/** Disabled property which causes the button to be disabled */
disabled: PropTypes.bool,
/** Icon to place on the right */
rightIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/** Icon to place on the left */
leftIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/** the success props are used when you have async action and wants to display a success message */
success: PropTypes.bool,
/** Success icon name */
successIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/** Success text */
successText: PropTypes.string,
/** loading boolean which switches the text to a loader */
loading: PropTypes.bool,
// eslint-disable-next-line react/forbid-prop-types
style: PropTypes.object,
/** displays the active state */
active: PropTypes.bool,
/** id to pass to the button */
id: PropTypes.string,
/** adds 8px margin to the right */
marginRight: PropTypes.bool,
/** adds 8px margin to the left */
marginLeft: PropTypes.bool,
/** element id to describe the button accordingly */
ariaLabeledBy: PropTypes.string,
/** aria label to provide important when providing only Icon */
ariaLabel: PropTypes.string,
/** aria for a button popup */
ariaHasPopup: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
/** aria to be set if the popup is open */
ariaExpanded: PropTypes.bool,
/** aria controls - receives id for the controlled region */
ariaControls: PropTypes.string,
/** On Button Focus callback */
onFocus: PropTypes.func,
/** On Button Blur callback */
onBlur: PropTypes.func,
rightFlat: PropTypes.bool,
leftFlat: PropTypes.bool,
preventClickAnimation: PropTypes.bool,
noSidePadding: PropTypes.bool,
/** default color for text color in ON_PRIMARY_COLOR kind (should be any type of css color (rbg, var, hex...) */
defaultTextColorOnPrimaryColor: PropTypes.string
};
Button.defaultProps = {
kind: BUTTON_TYPES.PRIMARY,
onClick: NOOP,
onMouseDown: NOOP,
blurOnMouseUp: true,
name: undefined,
style: undefined,
size: SIZES.MEDIUM,
color: BUTTON_COLORS.PRIMARY,
disabled: false,
className: "",
rightIcon: null,
leftIcon: null,
successIcon: "",
successText: "",
success: false,
loading: false,
active: false,
id: undefined,
marginRight: false,
marginLeft: false,
type: BUTTON_INPUT_TYPE.BUTTON,
rightFlat: false,
leftFlat: false,
preventClickAnimation: false,
noSidePadding: false,
onFocus: NOOP,
onBlur: NOOP,
defaultTextColorOnPrimaryColor: TRANSPARENT_COLOR,
ariaHasPopup: undefined,
ariaExpanded: undefined,
ariaControls: undefined,
ariaLabel: undefined,
ariaLabeledBy: undefined
};
export default Button;