import React, { RefObject, createRef } from "react"; import { sortBy } from "lodash"; import { Alignment, Icon, Menu, MenuItem, Classes as CoreClass, Spinner, } from "@blueprintjs/core"; import { Classes, Popover2 } from "@blueprintjs/popover2"; import { IconName } from "@blueprintjs/icons"; import tinycolor from "tinycolor2"; import { darkenActive, darkenHover } from "constants/DefaultTheme"; import { ButtonStyleType, ButtonVariant, ButtonVariantTypes, ButtonPlacement, } from "components/constants"; import styled, { createGlobalStyle } from "styled-components"; import { getCustomBackgroundColor, getCustomBorderColor, getCustomJustifyContent, getComplementaryGrayscaleColor, } from "widgets/WidgetUtils"; import { RenderMode, RenderModes } from "constants/WidgetConstants"; import { DragContainer } from "widgets/ButtonWidget/component/DragContainer"; import { buttonHoverActiveStyles } from "../../ButtonWidget/component/utils"; import { THEMEING_TEXT_SIZES } from "constants/ThemeConstants"; import { ThemeProp } from "widgets/constants"; // Utility functions interface ButtonData { id?: string; type?: string; label?: string; iconName?: string; } // Extract props influencing to width change const getButtonData = ( groupButtons: Record, ): ButtonData[] => { const buttonData = Object.keys(groupButtons).reduce( (acc: ButtonData[], id) => { return [ ...acc, { id, type: groupButtons[id].buttonType, label: groupButtons[id].label, iconName: groupButtons[id].iconName, }, ]; }, [], ); return buttonData as ButtonData[]; }; interface WrapperStyleProps { isHorizontal: boolean; borderRadius?: string; boxShadow?: string; buttonVariant: ButtonVariant; } const ButtonGroupWrapper = styled.div` height: 100%; width: 100%; position: relative; display: flex; justify-content: stretch; align-items: stretch; overflow: hidden; cursor: not-allowed; gap: ${({ buttonVariant }) => `${buttonVariant === ButtonVariantTypes.PRIMARY ? "1px" : "0px"}`}; ${(props) => props.isHorizontal ? "flex-direction: row" : "flex-direction: column"}; box-shadow: ${({ boxShadow }) => boxShadow}; border-radius: ${({ borderRadius }) => borderRadius}; & > *:first-child, & > *:first-child button { border-radius: ${({ borderRadius, isHorizontal }) => isHorizontal ? `${borderRadius} 0px 0px ${borderRadius}` : `${borderRadius} ${borderRadius} 0px 0px`}; } & > *:last-child, & > *:last-child button { border-radius: ${({ borderRadius, isHorizontal }) => isHorizontal ? `0px ${borderRadius} ${borderRadius} 0` : `0px 0px ${borderRadius} ${borderRadius}`}; } `; const MenuButtonWrapper = styled.div<{ renderMode: RenderMode }>` flex: 1 1 auto; cursor: pointer; position: relative; ${({ renderMode }) => renderMode === RenderModes.CANVAS && `height: 100%`}; & > .${Classes.POPOVER2_TARGET} > button { width: 100%; height: 100%; } & > .${Classes.POPOVER2_TARGET} { height: 100%; } `; const PopoverStyles = createGlobalStyle<{ minPopoverWidth: number; popoverTargetWidth?: number; id: string; borderRadius?: string; }>` ${({ borderRadius, id, minPopoverWidth, popoverTargetWidth }) => ` .${id}.${Classes.POPOVER2} { background: none; box-shadow: 0 6px 20px 0px rgba(0, 0, 0, 0.15) !important; margin-top: 8px !important; margin-bottom: 8px !important; border-radius: ${ borderRadius === THEMEING_TEXT_SIZES.lg ? `0.375rem` : borderRadius }; box-shadow: none; overflow: hidden; ${popoverTargetWidth && `width: ${popoverTargetWidth}px`}; min-width: ${minPopoverWidth}px; } .button-group-menu-popover > .${Classes.POPOVER2_CONTENT} { background: none; } `} `; interface ButtonStyleProps { isHorizontal: boolean; borderRadius?: string; buttonVariant?: ButtonVariant; // solid | outline | ghost buttonColor?: string; iconAlign?: string; placement?: ButtonPlacement; isLabel: boolean; } /* Don't use buttonHoverActiveStyles in a nested function it won't work - const buttonHoverActiveStyles = css `` const Button = styled.button` // won't work ${({ buttonColor, theme }) => { &:hover, &:active { ${buttonHoverActiveStyles} } }} // will work &:hover, &:active { ${buttonHoverActiveStyles} }` */ const StyledButton = styled.button` flex: 1 1 auto; display: flex; justify-content: stretch; align-items: center; padding: 0px 10px; &:hover, &:active, &:focus { ${buttonHoverActiveStyles} } ${({ buttonColor, buttonVariant, iconAlign, isLabel, theme }) => ` & { background: ${ getCustomBackgroundColor(buttonVariant, buttonColor) !== "none" ? getCustomBackgroundColor(buttonVariant, buttonColor) : buttonVariant === ButtonVariantTypes.PRIMARY ? theme.colors.button.primary.primary.bgColor : "none" } !important; flex-direction : ${iconAlign === "right" ? "row-reverse" : "row"}; .bp3-icon { ${ isLabel ? iconAlign === "right" ? "margin-left: 10px" : "margin-right: 10px" : "" }; } } border: ${ getCustomBorderColor(buttonVariant, buttonColor) !== "none" ? `1px solid ${getCustomBorderColor(buttonVariant, buttonColor)}` : buttonVariant === ButtonVariantTypes.SECONDARY ? `1px solid ${theme.colors.button.primary.secondary.borderColor}` : "none" } ${buttonVariant === ButtonVariantTypes.PRIMARY ? "" : "!important"}; & span { color: ${ buttonVariant === ButtonVariantTypes.PRIMARY ? getComplementaryGrayscaleColor(buttonColor) : getCustomBackgroundColor(ButtonVariantTypes.PRIMARY, buttonColor) } !important; } &:disabled { cursor: not-allowed; border: ${buttonVariant === ButtonVariantTypes.SECONDARY && "1px solid var(--wds-color-border-disabled)"} !important; background: ${buttonVariant !== ButtonVariantTypes.TERTIARY && "var(--wds-color-bg-disabled)"} !important; span { color: var(--wds-color-text-disabled) !important; } } `} `; const StyledButtonContent = styled.div<{ iconAlign: string; placement?: ButtonPlacement; }>` flex: 1; display: flex; align-items: center; justify-content: ${({ placement }) => getCustomJustifyContent(placement)}; flex-direction: ${({ iconAlign }) => iconAlign === Alignment.RIGHT ? "row-reverse" : "row"}; `; export interface BaseStyleProps { backgroundColor?: string; borderRadius?: string; boxShadow?: string; buttonColor?: string; buttonStyle?: ButtonStyleType; buttonVariant?: ButtonVariant; textColor?: string; } const BaseMenuItem = styled(MenuItem)` padding: 8px 10px !important; border-radius: 0px; ${({ backgroundColor, theme }) => backgroundColor ? ` background-color: ${backgroundColor} !important; &:hover, &:focus { background-color: ${darkenHover(backgroundColor)} !important; } &:active { background-color: ${darkenActive(backgroundColor)} !important; } ` : ` background: none !important &:hover, &:focus { background-color: ${tinycolor( theme.colors.button.primary.primary.textColor, ) .darken() .toString()} !important; } &:active { background-color: ${tinycolor( theme.colors.button.primary.primary.textColor, ) .darken() .toString()} !important; } `} ${({ textColor }) => textColor && ` color: ${textColor} !important; `} `; const StyledMenu = styled(Menu)` padding: 0; min-width: 0px; `; interface PopoverContentProps { buttonId: string; menuItems: Record< string, { widgetId: string; id: string; index: number; isVisible?: boolean; isDisabled?: boolean; label?: string; backgroundColor?: string; textColor?: string; iconName?: IconName; iconColor?: string; iconAlign?: Alignment; onClick?: string; } >; onItemClicked: (onClick: string | undefined, buttonId: string) => void; } function PopoverContent(props: PopoverContentProps) { const { buttonId, menuItems, onItemClicked } = props; let items = Object.keys(menuItems) .map((itemKey) => menuItems[itemKey]) .filter((item) => item.isVisible === true); // sort btns by index items = sortBy(items, ["index"]); const listItems = items.map((menuItem) => { const { backgroundColor, iconAlign, iconColor, iconName, id, isDisabled, label, onClick, textColor, } = menuItem; return ( ) : null } key={id} labelElement={ iconAlign === Alignment.RIGHT && iconName ? ( ) : null } onClick={() => onItemClicked(onClick, buttonId)} text={label} textColor={textColor} /> ); }); return {listItems}; } class ButtonGroupComponent extends React.Component< ButtonGroupComponentProps, ButtonGroupComponentState > { private timer?: number; constructor(props: ButtonGroupComponentProps) { super(props); this.state = { itemRefs: {}, itemWidths: {}, loadedBtnId: "", }; } componentDidMount() { this.setState(() => { return { ...this.state, itemRefs: this.createMenuButtonRefs(), }; }); this.timer = setTimeout(() => { this.setState(() => { return { ...this.state, itemWidths: this.getMenuButtonWidths(), }; }); }, 0); } componentDidUpdate( prevProps: ButtonGroupComponentProps, prevState: ButtonGroupComponentState, ) { if ( this.state.itemRefs !== prevState.itemRefs || this.props.width !== prevProps.width || this.props.orientation !== prevProps.orientation ) { if (this.timer) { clearTimeout(this.timer); } this.timer = setTimeout(() => { this.setState(() => { return { ...this.state, itemWidths: this.getMenuButtonWidths(), }; }); }); } else { // Reset refs array if // * A button is added/removed or changed into a menu button // * A label is changed or icon is newly added or removed let isWidthChanged = false; const buttons = getButtonData(this.props.groupButtons); const menuButtons = buttons.filter((button) => button.type === "MENU"); const prevButtons = getButtonData(prevProps.groupButtons); const prevMenuButtons = prevButtons.filter( (button) => button.type === "MENU", ); if (buttons.length !== prevButtons.length) { isWidthChanged = true; } else if (menuButtons.length > prevMenuButtons.length) { isWidthChanged = true; } else { isWidthChanged = buttons.some((button) => { const prevButton = prevButtons.find((btn) => btn.id === button.id); return ( button.label !== prevButton?.label || (button.iconName && !prevButton?.iconName) || (!button.iconName && prevButton?.iconName) ); }); } if (isWidthChanged) { this.setState(() => { return { ...this.state, itemRefs: this.createMenuButtonRefs(), }; }); } } } componentWillUnmount() { if (this.timer) { clearTimeout(this.timer); } } // Get widths of menu buttons getMenuButtonWidths = () => Object.keys(this.props.groupButtons).reduce((acc, id) => { if (this.props.groupButtons[id].buttonType === "MENU") { return { ...acc, [id]: this.state.itemRefs[id].current?.getBoundingClientRect().width, }; } return acc; }, {}); // Create refs of menu buttons createMenuButtonRefs = () => Object.keys(this.props.groupButtons).reduce((acc, id) => { if (this.props.groupButtons[id].buttonType === "MENU") { return { ...acc, [id]: createRef(), }; } return acc; }, {}); // Start Loading handleActionStart = (id: string) => { this.setState({ loadedBtnId: id, }); }; // Stop Loading handleActionComplete = () => { this.setState({ loadedBtnId: "", }); }; onButtonClick = (onClick: string | undefined, buttonId: string) => { if (onClick) { this.handleActionStart(buttonId); this.props.buttonClickHandler(onClick, () => this.handleActionComplete()); } }; render = () => { const { buttonVariant, groupButtons, isDisabled, minPopoverWidth, orientation, widgetId, } = this.props; const { loadedBtnId } = this.state; const isHorizontal = orientation === "horizontal"; let items = Object.keys(groupButtons) .map((itemKey) => groupButtons[itemKey]) .filter((item) => item.isVisible === true); // sort btns by index items = sortBy(items, ["index"]); const popoverId = `button-group-${widgetId}`; return ( {items.map((button) => { const isLoading = button.id === loadedBtnId; const isButtonDisabled = button.isDisabled || isDisabled || !!loadedBtnId || isLoading; if (button.buttonType === "MENU" && !isButtonDisabled) { const { menuItems } = button; return ( } disabled={button.isDisabled} fill minimal placement="bottom-end" popoverClassName={popoverId} > {isLoading ? ( ) : ( <> {button.iconName && } {!!button.label && ( {button.label} )} )} ); } return ( { this.onButtonClick(button.onClick, button.id); }} renderMode={this.props.renderMode} style={{ flex: "1 1 auto" }} > this.onButtonClick(button.onClick, button.id)} > {isLoading ? ( ) : ( <> {button.iconName && } {!!button.label && ( {button.label} )} )} ); })} ); }; } interface GroupButtonProps { widgetId: string; id: string; index: number; isVisible?: boolean; isDisabled?: boolean; label?: string; buttonType?: string; buttonColor?: string; iconName?: IconName; iconAlign?: Alignment; placement?: ButtonPlacement; onClick?: string; menuItems: Record< string, { widgetId: string; id: string; index: number; isVisible?: boolean; isDisabled?: boolean; label?: string; backgroundColor?: string; textColor?: string; iconName?: IconName; iconColor?: string; iconAlign?: Alignment; onClick?: string; } >; } export interface ButtonGroupComponentProps { borderRadius?: string; boxShadow?: string; buttonVariant: ButtonVariant; buttonClickHandler: ( onClick: string | undefined, callback: () => void, ) => void; groupButtons: Record; isDisabled: boolean; orientation: string; renderMode: RenderMode; width: number; minPopoverWidth: number; widgetId: string; } export interface ButtonGroupComponentState { itemRefs: Record>; itemWidths: Record; loadedBtnId: string; } export default ButtonGroupComponent;