monday-ui-react-core
Version:
Official monday.com UI resources for application development in React.js
350 lines (323 loc) • 9.71 kB
JSX
/* eslint-disable react/jsx-props-no-spreading,react/button-has-type */
import React, { forwardRef, useCallback, useEffect, useMemo, useRef } 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 "./ButtonContstants";
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
},
ref
) => {
const buttonRef = useRef(null);
const updateCssVariables = useMemo(() => {
const callback = ({ 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`);
};
return callback;
}, [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;
}
button.blur();
}, [buttonRef]);
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}`,
{
"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
]);
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-labelledby": ariaLabeledBy,
"aria-label": ariaLabel,
"aria-busy": loading
};
}, [
disabled,
classNames,
name,
onMouseUp,
style,
onButtonClicked,
id,
type,
onMouseDownClicked,
ariaLabel,
loading,
onFocus,
onBlur,
mergedRef,
ariaLabeledBy
]);
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}
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}
className={cx({ "monday-style-button--left-icon": !!children })}
ignoreFocusStyle
/>
) : null}
{children}
{rightIcon ? (
<Icon
iconType={Icon.type.ICON_FONT}
clickable={false}
icon={rightIcon}
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,
/** 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
]),
/** 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.string,
/** Icon to place on the left */
leftIcon: PropTypes.string,
/** 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.string,
/** 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,
/** 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,
name: "",
size: SIZES.MEDIUM,
color: BUTTON_COLORS.PRIMARY,
disabled: false,
className: "",
rightIcon: null,
leftIcon: null,
successIcon: "",
successText: "",
success: false,
loading: false,
active: false,
id: "",
marginRight: false,
marginLeft: false,
type: BUTTON_INPUT_TYPE.BUTTON,
ariaLabel: "",
ariaLabeledBy: "",
rightFlat: false,
leftFlat: false,
preventClickAnimation: false,
noSidePadding: false,
onFocus: NOOP,
onBlur: NOOP,
defaultTextColorOnPrimaryColor: TRANSPARENT_COLOR
};
export default Button;