From 35b1546f781cdff8cb91bfe5793c5aa31d86cbb1 Mon Sep 17 00:00:00 2001 From: Paul Li <82799722+wmdev0808@users.noreply.github.com> Date: Mon, 28 Mar 2022 15:14:40 +0800 Subject: [PATCH] fix: Button group widget's pop-over/drop-down enlarged along the width of the parent (#11804) * fix: Button group widget's pop-over/drop-down enlarged along the width -- Implement dynamic width calculation * fix: Button group widget's pop-over/drop-down enlarged along the width -- Add corresponding Cypress test cases * fix: Button group widget's pop-over/drop-down enlarged along the width -- Add min-width CSS property for popover * fix: Button group widget's pop-over/drop-down enlarged along the width -- Add a comment for minPopoverWidth * fix: Button group widget's pop-over/drop-down enlarged along the width -- Remove the comment for minPopoverWidth * feat: Button group widget's pop-over/drop-down enlarged along the width -- update the state variable, itemWidths inside setTimeout to access the updated DOM * fix: Button gruop widget's pop-over/drop-down enlarged along the width -- Refine update logic * fix: Button group widget's pop-over/drop-down enlarged along the width -- Make code DRY by creating createMenuButtonRefs and getMenuButtonWidths methods * fix: Button group widget's popover/dropdown enlarged along the width -- Make every popover class name unique * fix: Button group widget's pop-over/drop-down enlarged along the width -- Rewrite Cypress test * fix: Button group widget's pop-over/drop-down enlarged along the width -- Eliminate unnecessary test case from ButtonGroup_spec --- .../ButtonGroup_MenuButton_Width_dsl.json | 326 ++++++++++++++++++ .../ButtonGroup_MenuButton_Width_spec.js | 148 ++++++++ .../FormWidgets/ButtonGroup_spec.js | 2 + .../ButtonGroupWidget/component/index.tsx | 205 +++++++++-- .../ButtonGroupWidget/widget/index.tsx | 5 +- 5 files changed, 655 insertions(+), 31 deletions(-) create mode 100644 app/client/cypress/fixtures/ButtonGroup_MenuButton_Width_dsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_MenuButton_Width_spec.js diff --git a/app/client/cypress/fixtures/ButtonGroup_MenuButton_Width_dsl.json b/app/client/cypress/fixtures/ButtonGroup_MenuButton_Width_dsl.json new file mode 100644 index 0000000000..3168a243d3 --- /dev/null +++ b/app/client/cypress/fixtures/ButtonGroup_MenuButton_Width_dsl.json @@ -0,0 +1,326 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1160, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 680, + "containerStyle": "none", + "snapRows": 125, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 54, + "minHeight": 690, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "ButtonGroup1", + "orientation": "horizontal", + "rightColumn": 50, + "isCanvas": false, + "displayName": "Button Group", + "iconSVG": "/static/media/icon.d6773218.svg", + "widgetId": "t5l24fccio", + "topRow": 15, + "bottomRow": 19, + "parentRowSpace": 10, + "isVisible": true, + "groupButtons": { + "groupButton1": { + "label": "Favorite", + "iconName": "heart", + "id": "groupButton1", + "widgetId": "", + "buttonColor": "#03B365", + "buttonType": "SIMPLE", + "placement": "CENTER", + "isVisible": true, + "isDisabled": false, + "index": 0, + "menuItems": {} + }, + "groupButton2": { + "label": "Add", + "iconName": "add", + "id": "groupButton2", + "buttonColor": "#03B365", + "buttonType": "SIMPLE", + "placement": "CENTER", + "widgetId": "", + "isVisible": true, + "isDisabled": false, + "index": 1, + "menuItems": {} + }, + "groupButton3": { + "label": "More", + "iconName": "more", + "id": "groupButton3", + "buttonType": "MENU", + "placement": "CENTER", + "buttonColor": "#03B365", + "widgetId": "", + "isVisible": true, + "isDisabled": false, + "index": 2, + "menuItems": { + "menuItem1": { + "label": "First Option", + "backgroundColor": "#FFFFFF", + "id": "menuItem1", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 0 + }, + "menuItem2": { + "label": "Second Option", + "backgroundColor": "#FFFFFF", + "id": "menuItem2", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 1 + }, + "menuItem3": { + "label": "Delete", + "iconName": "trash", + "iconColor": "#FFFFFF", + "iconAlign": "right", + "textColor": "#FFFFFF", + "backgroundColor": "#DD4B34", + "id": "menuItem3", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 2 + } + } + } + }, + "type": "BUTTON_GROUP_WIDGET", + "version": 1, + "hideCard": false, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "animateLoading": true, + "parentColumnSpace": 17.9375, + "leftColumn": 1, + "buttonVariant": "PRIMARY", + "key": "qxtmv7r8yb" + }, + { + "widgetName": "ButtonGroup2", + "orientation": "horizontal", + "rightColumn": 25, + "isCanvas": false, + "displayName": "Button Group", + "iconSVG": "/static/media/icon.d6773218.svg", + "widgetId": "yxjq5sck7d", + "topRow": 4, + "bottomRow": 8, + "parentRowSpace": 10, + "isVisible": true, + "groupButtons": { + "groupButton1": { + "label": "Favorite", + "iconName": "heart", + "id": "groupButton1", + "widgetId": "", + "buttonColor": "#03B365", + "buttonType": "SIMPLE", + "placement": "CENTER", + "isVisible": true, + "isDisabled": false, + "index": 0, + "menuItems": {} + }, + "groupButton2": { + "label": "Add", + "iconName": "add", + "id": "groupButton2", + "buttonColor": "#03B365", + "buttonType": "SIMPLE", + "placement": "CENTER", + "widgetId": "", + "isVisible": true, + "isDisabled": false, + "index": 1, + "menuItems": {} + }, + "groupButton3": { + "label": "More", + "iconName": "more", + "id": "groupButton3", + "buttonType": "MENU", + "placement": "CENTER", + "buttonColor": "#03B365", + "widgetId": "", + "isVisible": true, + "isDisabled": false, + "index": 2, + "menuItems": { + "menuItem1": { + "label": "First Option", + "backgroundColor": "#FFFFFF", + "id": "menuItem1", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 0 + }, + "menuItem2": { + "label": "Second Option", + "backgroundColor": "#FFFFFF", + "id": "menuItem2", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 1 + }, + "menuItem3": { + "label": "Delete", + "iconName": "trash", + "iconColor": "#FFFFFF", + "iconAlign": "right", + "textColor": "#FFFFFF", + "backgroundColor": "#DD4B34", + "id": "menuItem3", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 2 + } + } + } + }, + "type": "BUTTON_GROUP_WIDGET", + "version": 1, + "hideCard": false, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "animateLoading": true, + "parentColumnSpace": 17.9375, + "leftColumn": 1, + "buttonVariant": "PRIMARY", + "key": "qxtmv7r8yb" + }, + { + "widgetName": "ButtonGroup3", + "isCanvas": false, + "displayName": "Button Group", + "iconSVG": "/static/media/icon.d6773218.svg", + "topRow": 29, + "bottomRow": 55, + "parentRowSpace": 10, + "groupButtons": { + "groupButton1": { + "label": "Favorite", + "iconName": "heart", + "id": "groupButton1", + "widgetId": "", + "buttonColor": "#03B365", + "buttonType": "SIMPLE", + "placement": "CENTER", + "isVisible": true, + "isDisabled": false, + "index": 0, + "menuItems": {} + }, + "groupButton2": { + "label": "Add", + "iconName": "add", + "id": "groupButton2", + "buttonColor": "#03B365", + "buttonType": "SIMPLE", + "placement": "CENTER", + "widgetId": "", + "isVisible": true, + "isDisabled": false, + "index": 1, + "menuItems": {} + }, + "groupButton3": { + "label": "More", + "iconName": "more", + "id": "groupButton3", + "buttonType": "MENU", + "placement": "CENTER", + "buttonColor": "#03B365", + "widgetId": "", + "isVisible": true, + "isDisabled": false, + "index": 2, + "menuItems": { + "menuItem1": { + "label": "First Option", + "backgroundColor": "#FFFFFF", + "id": "menuItem1", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 0 + }, + "menuItem2": { + "label": "Second Option", + "backgroundColor": "#FFFFFF", + "id": "menuItem2", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 1 + }, + "menuItem3": { + "label": "Delete", + "iconName": "trash", + "iconColor": "#FFFFFF", + "iconAlign": "right", + "textColor": "#FFFFFF", + "backgroundColor": "#DD4B34", + "id": "menuItem3", + "widgetId": "", + "onClick": "", + "isVisible": true, + "isDisabled": false, + "index": 2 + } + } + } + }, + "type": "BUTTON_GROUP_WIDGET", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 17.9375, + "dynamicTriggerPathList": [], + "leftColumn": 1, + "dynamicBindingPathList": [], + "key": "qxtmv7r8yb", + "orientation": "horizontal", + "rightColumn": 50, + "widgetId": "mr048y04aq", + "isVisible": true, + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "buttonVariant": "PRIMARY" + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_MenuButton_Width_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_MenuButton_Width_spec.js new file mode 100644 index 0000000000..372f34f163 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_MenuButton_Width_spec.js @@ -0,0 +1,148 @@ +const dsl = require("../../../../fixtures/ButtonGroup_MenuButton_Width_dsl.json"); + +const widgetName = "buttongroupwidget"; + +describe("In a button group widget, menu button width", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("If target width is smaller than min-width, The menu button popover width should be set to minimum width", () => { + const minWidth = 12 * 11.9375; + const widgetId = "yxjq5sck7d"; + const menuButtonId = "groupButton3"; + // Get the default menu button + cy.get(`.appsmith_widget_${widgetId} div.t--buttongroup-widget`) + .children() + .last() + .as("target"); + // Open popover + cy.get("@target").click(); + // Get the target width + cy.get("@target") + .invoke("outerWidth") + .then((targetWidth) => { + expect(targetWidth).to.be.lessThan(minWidth); + // Check if popover width is set to its target width + cy.get( + `.bp3-popover2.menu-button-width-${widgetId}-${menuButtonId}`, + ).should("have.css", "width", `${minWidth}px`); + }); + }); + + it("If target width is bigger than min width, The menu button popover width should always be the same as the target width", () => { + const minWidth = 12 * 11.9375; + const widgetId = "t5l24fccio"; + const menuButtonId = "groupButton3"; + + // Get the default menu button + cy.get(`.appsmith_widget_${widgetId} div.t--buttongroup-widget`) + .children() + .last() + .as("target"); + // Open popover + cy.get("@target").click(); + // Get the target width + cy.get("@target") + .invoke("outerWidth") + .then((targetWidth) => { + expect(targetWidth).to.be.greaterThan(minWidth); + // Check if popover width is set to its target width + cy.get( + `.bp3-popover2.menu-button-width-${widgetId}-${menuButtonId}`, + ).should("have.css", "width", `${targetWidth}px`); + }); + }); + + it("After converting a simple button to a menu button, The menu button popover width should always be the same as the target width", () => { + const minWidth = 12 * 11.9375; + const widgetId = "t5l24fccio"; + const menuButtonId = "groupButton1"; + // Change the first button type to menu + cy.editColumn(menuButtonId); + cy.selectDropdownValue(".t--property-control-buttontype", "Menu"); + cy.get(".t--add-menu-item-btn").click(); + // Get the newly converted menu button + cy.get(`.appsmith_widget_${widgetId} div.t--buttongroup-widget`) + .children() + .first() + .as("target"); + // Open popover + cy.get("@target").click(); + // Get the target width + cy.get("@target") + .invoke("outerWidth") + .then((targetWidth) => { + expect(targetWidth).to.be.greaterThan(minWidth); + // Check if popover width is set to its target width + cy.get( + `.bp3-popover2.menu-button-width-${widgetId}-${menuButtonId}`, + ).should("have.css", "width", `${targetWidth}px`); + }); + }); + + it("If an existing menu button width changes, its popover width should always be the same as the changed target width", () => { + const minWidth = 12 * 11.9375; + const widgetId = "t5l24fccio"; + const menuButtonId = "groupButton1"; + cy.get(".t--property-pane-back-btn").click(); + // Change the first button text + cy.get(".t--property-pane-section-buttons input") + .first() + .type("increase width"); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + // Get the menu button with its width changed + cy.get(`.appsmith_widget_${widgetId} div.t--buttongroup-widget`) + .children() + .first() + .as("target"); + // Open popover + cy.get("@target").click(); + // Get the target width + cy.get("@target") + .invoke("outerWidth") + .then((targetWidth) => { + expect(targetWidth).to.be.greaterThan(minWidth); + // Check if popover width is set to its target width + cy.get( + `.bp3-popover2.menu-button-width-${widgetId}-${menuButtonId}`, + ).should("have.css", "width", `${targetWidth}px`); + }); + }); + + it("After changing the orientation to vertical , The menu button popover width should always be the same as the target width", () => { + const widgetId = "mr048y04aq"; + const menuButtonId = "groupButton3"; + // Open property pane of ButtonGroup3 + cy.get(`.appsmith_widget_${widgetId} div.t--buttongroup-widget`) + .children() + .first() + .click(); + // Change its orientation to vetical + cy.selectDropdownValue(".t--property-control-orientation", "Vertical"); + // Get the default menu button + cy.get(`.appsmith_widget_${widgetId} div.t--buttongroup-widget`) + .children() + .last() + .as("target"); + // Open popover + cy.get("@target").click(); + // Get the target width + cy.get("@target") + .invoke("outerWidth") + .then((targetWidth) => { + // Check if popover width is set to its target width + cy.get( + `.bp3-popover2.menu-button-width-${widgetId}-${menuButtonId}`, + ).should("have.css", "width", `${targetWidth}px`); + }); + }); + + after(() => { + // clean up after done + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_spec.js index 24aa1f1122..6c05582653 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/ButtonGroup_spec.js @@ -1,5 +1,7 @@ const explorer = require("../../../../locators/explorerlocators.json"); +const widgetName = "buttongroupwidget"; + describe("Button Group Widget Functionality", function() { before(() => { // no dsl required diff --git a/app/client/src/widgets/ButtonGroupWidget/component/index.tsx b/app/client/src/widgets/ButtonGroupWidget/component/index.tsx index dc077216a7..0440c90919 100644 --- a/app/client/src/widgets/ButtonGroupWidget/component/index.tsx +++ b/app/client/src/widgets/ButtonGroupWidget/component/index.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { sortBy, uniqueId } from "lodash"; +import React, { RefObject, createRef } from "react"; +import { sortBy } from "lodash"; import { Alignment, Icon, @@ -29,12 +29,40 @@ import { getCustomBorderColor, getCustomTextColor, getCustomJustifyContent, - WidgetContainerDiff, } from "widgets/WidgetUtils"; import { RenderMode, RenderModes } from "constants/WidgetConstants"; import { DragContainer } from "widgets/ButtonWidget/component/DragContainer"; import { buttonHoverActiveStyles } from "../../ButtonWidget/component/utils"; +// 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?: ButtonBorderRadius; @@ -96,26 +124,19 @@ const MenuButtonWrapper = styled.div<{ renderMode: RenderMode }>` `; const PopoverStyles = createGlobalStyle<{ - parentWidth: number; - menuDropDownWidth: number; + minPopoverWidth: number; + popoverTargetWidth?: number; id: string; }>` .menu-button-popover > .${Classes.POPOVER2_CONTENT} { background: none; } - ${({ id, menuDropDownWidth, parentWidth }) => ` - .menu-button-width-${id} { - - max-width: ${ - menuDropDownWidth > parentWidth - ? `${menuDropDownWidth}px` - : `${parentWidth}px` - } !important; - min-width: ${ - parentWidth > menuDropDownWidth ? parentWidth : menuDropDownWidth - }px !important; - } -`} + ${({ id, minPopoverWidth, popoverTargetWidth }) => ` + .menu-button-width-${id} { + ${popoverTargetWidth && `width: ${popoverTargetWidth}px`}; + min-width: ${minPopoverWidth}px; + } + `} `; interface ButtonStyleProps { @@ -398,8 +419,128 @@ function PopoverContent(props: PopoverContentProps) { return {listItems}; } -class ButtonGroupComponent extends React.Component { - onButtonClick = (onClick?: string) => { +class ButtonGroupComponent extends React.Component< + ButtonGroupComponentProps, + ButtonGroupComponentState +> { + private timer?: number; + + constructor(props: ButtonGroupComponentProps) { + super(props); + this.state = { + itemRefs: {}, + itemWidths: {}, + }; + } + + 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; + }, {}); + + onButtonClick = (onClick: string | undefined) => () => { this.props.buttonClickHandler(onClick); }; @@ -408,9 +549,9 @@ class ButtonGroupComponent extends React.Component { buttonVariant, groupButtons, isDisabled, - menuDropDownWidth, + minPopoverWidth, orientation, - width, + widgetId, } = this.props; const isHorizontal = orientation === "horizontal"; @@ -435,17 +576,16 @@ class ButtonGroupComponent extends React.Component { if (button.buttonType === "MENU" && !isButtonDisabled) { const { menuItems } = button; - const id = uniqueId(); - + const popoverId = `${widgetId}-${button.id}`; return ( { fill minimal placement="bottom-end" - popoverClassName={`menu-button-popover menu-button-width-${id}`} + popoverClassName={`menu-button-popover menu-button-width-${popoverId}`} > { isHorizontal={isHorizontal} isLabel={!!button.label} key={button.id} + ref={this.state.itemRefs[button.id]} > void; groupButtons: Record; isDisabled: boolean; - menuDropDownWidth: number; orientation: string; renderMode: RenderMode; width: number; + minPopoverWidth: number; + widgetId: string; +} + +export interface ButtonGroupComponentState { + itemRefs: Record>; + itemWidths: Record; } export default ButtonGroupComponent; diff --git a/app/client/src/widgets/ButtonGroupWidget/widget/index.tsx b/app/client/src/widgets/ButtonGroupWidget/widget/index.tsx index 69955a43e0..753888dcd0 100644 --- a/app/client/src/widgets/ButtonGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/ButtonGroupWidget/widget/index.tsx @@ -536,7 +536,7 @@ class ButtonGroupWidget extends BaseWidget< getPageView() { const { componentWidth } = this.getComponentDimensions(); - const menuDropDownWidth = MinimumPopupRows * this.props.parentColumnSpace; + const minPopoverWidth = MinimumPopupRows * this.props.parentColumnSpace; return ( );