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
This commit is contained in:
Paul Li 2022-03-28 15:14:40 +08:00 committed by GitHub
parent 42bdb6c2ab
commit 35b1546f78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 655 additions and 31 deletions

View File

@ -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"
}
]
}
}

View File

@ -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
});
});

View File

@ -1,5 +1,7 @@
const explorer = require("../../../../locators/explorerlocators.json");
const widgetName = "buttongroupwidget";
describe("Button Group Widget Functionality", function() {
before(() => {
// no dsl required

View File

@ -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<string, GroupButtonProps>,
): 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 <StyledMenu>{listItems}</StyledMenu>;
}
class ButtonGroupComponent extends React.Component<ButtonGroupComponentProps> {
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<ButtonGroupComponentProps> {
buttonVariant,
groupButtons,
isDisabled,
menuDropDownWidth,
minPopoverWidth,
orientation,
width,
widgetId,
} = this.props;
const isHorizontal = orientation === "horizontal";
@ -435,17 +576,16 @@ class ButtonGroupComponent extends React.Component<ButtonGroupComponentProps> {
if (button.buttonType === "MENU" && !isButtonDisabled) {
const { menuItems } = button;
const id = uniqueId();
const popoverId = `${widgetId}-${button.id}`;
return (
<MenuButtonWrapper
key={button.id}
renderMode={this.props.renderMode}
>
<PopoverStyles
id={id}
menuDropDownWidth={menuDropDownWidth}
parentWidth={width - WidgetContainerDiff}
id={popoverId}
minPopoverWidth={minPopoverWidth}
popoverTargetWidth={this.state.itemWidths[button.id]}
/>
<Popover2
content={
@ -458,7 +598,7 @@ class ButtonGroupComponent extends React.Component<ButtonGroupComponentProps> {
fill
minimal
placement="bottom-end"
popoverClassName={`menu-button-popover menu-button-width-${id}`}
popoverClassName={`menu-button-popover menu-button-width-${popoverId}`}
>
<DragContainer
buttonColor={button.buttonColor}
@ -477,6 +617,7 @@ class ButtonGroupComponent extends React.Component<ButtonGroupComponentProps> {
isHorizontal={isHorizontal}
isLabel={!!button.label}
key={button.id}
ref={this.state.itemRefs[button.id]}
>
<StyledButtonContent
iconAlign={button.iconAlign || "left"}
@ -579,10 +720,16 @@ export interface ButtonGroupComponentProps {
buttonClickHandler: (onClick: string | undefined) => void;
groupButtons: Record<string, GroupButtonProps>;
isDisabled: boolean;
menuDropDownWidth: number;
orientation: string;
renderMode: RenderMode;
width: number;
minPopoverWidth: number;
widgetId: string;
}
export interface ButtonGroupComponentState {
itemRefs: Record<string, RefObject<HTMLButtonElement>>;
itemWidths: Record<string, number>;
}
export default ButtonGroupComponent;

View File

@ -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 (
<ButtonGroupComponent
@ -547,9 +547,10 @@ class ButtonGroupWidget extends BaseWidget<
buttonVariant={this.props.buttonVariant}
groupButtons={this.props.groupButtons}
isDisabled={this.props.isDisabled}
menuDropDownWidth={menuDropDownWidth}
minPopoverWidth={minPopoverWidth}
orientation={this.props.orientation}
renderMode={this.props.renderMode}
widgetId={this.props.widgetId}
width={componentWidth}
/>
);