UNPKG

monday-ui-react-core

Version:

Official monday.com UI resources for application development in React.js

350 lines (323 loc) • 9.71 kB
/* 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;