From c6cf919beb8b6444411cd9b2b332465dc197b3af Mon Sep 17 00:00:00 2001 From: skjameela <83003444+skjameela@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:29:59 +0530 Subject: [PATCH] fix:- added elipsis and tooltip to the button content in table widget component (#36865) **Description** [ Bug issue ](https://github.com/appsmithorg/appsmith/issues/10278) I have raised the pr inorder to adding a elipsis and tooltip for the button content in the table widget component to prevent the data hiding in overflow **Screenshot** **Before issue resolved** ![Screenshot from 2024-09-26 16-09-21](https://github.com/user-attachments/assets/a5fff31e-27ca-4ee7-b5b8-9d5a02178adc) **After issue resolved** ![Screenshot from 2024-10-07 11-46-04](https://github.com/user-attachments/assets/effc40ee-df92-4cef-bdc4-8b9a316daffa) ## Summary by CodeRabbit - **New Features** - Enhanced table widget styling for improved responsiveness and interactivity. - Introduced tooltip functionality for buttons, displaying tooltips when text is truncated. - Added new styled components for better structure and visual appeal. - **Bug Fixes** - Improved hover effects and conditional styling for table cells. - **Tests** - Added unit tests for the `AutoToolTipComponent`, covering various tooltip scenarios and behavior. --- .../component/TableStyledWrappers.tsx | 1 + .../cellComponents/AutoToolTipComponent.tsx | 98 ++++++++---- .../AutoTooltipComponent.test.tsx | 141 ++++++++++++++++++ .../component/cellComponents/Button.tsx | 50 ++++--- 4 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 app/client/src/widgets/TableWidgetV2/component/cellComponents/AutoTooltipComponent.test.tsx diff --git a/app/client/src/widgets/TableWidgetV2/component/TableStyledWrappers.tsx b/app/client/src/widgets/TableWidgetV2/component/TableStyledWrappers.tsx index 76cc4267ef..d212ac7915 100644 --- a/app/client/src/widgets/TableWidgetV2/component/TableStyledWrappers.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/TableStyledWrappers.tsx @@ -473,6 +473,7 @@ export const MenuColumnWrapper = styled.div<{ selected: boolean }>` export const ActionWrapper = styled.div<{ disabled: boolean }>` margin: 0 5px 0 0; + max-width: 100%; ${(props) => (props.disabled ? "cursor: not-allowed;" : null)} &&&&&& { .bp3-button { diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/AutoToolTipComponent.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/AutoToolTipComponent.tsx index 7d264fdb9a..86b3c41cae 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/AutoToolTipComponent.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/AutoToolTipComponent.tsx @@ -36,45 +36,71 @@ const MAX_WIDTH = 500; const TOOLTIP_OPEN_DELAY = 500; const MAX_CHARS_ALLOWED_IN_TOOLTIP = 200; -function useToolTip(children: React.ReactNode, title?: string) { +export function isButtonTextTruncated(element: HTMLElement): boolean { + const spanElement = element.querySelector("span"); + + if (!spanElement) { + return false; + } + + const offsetWidth = spanElement.offsetWidth; + const scrollWidth = spanElement.scrollWidth; + + return scrollWidth > offsetWidth; +} + +function useToolTip( + children: React.ReactNode, + title?: string, + isButton?: boolean, +) { const ref = createRef(); const [requiresTooltip, setRequiresTooltip] = useState(false); - useEffect(() => { - let timeout: ReturnType; + useEffect( + function setupMouseHandlers() { + let timeout: ReturnType; + const currentRef = ref.current; - const mouseEnterHandler = () => { - const element = ref.current?.querySelector("div") as HTMLDivElement; + if (!currentRef) return; - /* - * Using setTimeout to simulate hoverOpenDelay of the tooltip - * during initial render - */ - timeout = setTimeout(() => { - if (element && element.offsetWidth < element.scrollWidth) { - setRequiresTooltip(true); - } else { - setRequiresTooltip(false); - } + const mouseEnterHandler = () => { + timeout = setTimeout(() => { + const element = currentRef?.querySelector("div") as HTMLDivElement; - ref.current?.removeEventListener("mouseenter", mouseEnterHandler); - ref.current?.removeEventListener("mouseleave", mouseLeaveHandler); - }, TOOLTIP_OPEN_DELAY); - }; + /* + * Using setTimeout to simulate hoverOpenDelay of the tooltip + * during initial render + */ + if (element && element.offsetWidth < element.scrollWidth) { + setRequiresTooltip(true); + } else if (isButton && element && isButtonTextTruncated(element)) { + setRequiresTooltip(true); + } else { + setRequiresTooltip(false); + } - const mouseLeaveHandler = () => { - clearTimeout(timeout); - }; + currentRef?.removeEventListener("mouseenter", mouseEnterHandler); + currentRef?.removeEventListener("mouseleave", mouseLeaveHandler); + }, TOOLTIP_OPEN_DELAY); + }; - ref.current?.addEventListener("mouseenter", mouseEnterHandler); - ref.current?.addEventListener("mouseleave", mouseLeaveHandler); + const mouseLeaveHandler = () => { + setRequiresTooltip(false); + clearTimeout(timeout); + }; - return () => { - ref.current?.removeEventListener("mouseenter", mouseEnterHandler); - ref.current?.removeEventListener("mouseleave", mouseLeaveHandler); - clearTimeout(timeout); - }; - }, [children]); + currentRef?.addEventListener("mouseenter", mouseEnterHandler); + currentRef?.addEventListener("mouseleave", mouseLeaveHandler); + + return () => { + currentRef?.removeEventListener("mouseenter", mouseEnterHandler); + currentRef?.removeEventListener("mouseleave", mouseLeaveHandler); + clearTimeout(timeout); + }; + }, + [children, isButton, ref], + ); return requiresTooltip && children ? ( ; } + if (props.columnType === ColumnTypes.BUTTON && props.title) { + return content; + } + return ( { + const actualReact = jest.requireActual("react"); + + return { + ...actualReact, + useState: jest.fn((initial) => [initial, jest.fn()]), + }; +}); + +test.each([ + ["truncated text", "This is a long text that will be truncated"], + [ + "truncated button text", + "This is a long text that will be truncated in the button", + ], +])("shows tooltip for %s", (_, longText) => { + const { getByText } = render( + + + {longText} + + , + ); + + fireEvent.mouseEnter(getByText(longText)); + expect(getByText(longText)).toBeInTheDocument(); +}); + +test("does not show tooltip for non-button types", () => { + const { getByText } = render( + + Not a button + , + ); + + expect(getByText("Not a button")).toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); +}); + +test("handles empty tooltip", () => { + const { getByText } = render( + + + , + ); + + expect(getByText("Empty button")).toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); +}); + +test("renders content without tooltip for normal text", () => { + const { getByText } = render( + + Normal Text + , + ); + + expect(getByText("Normal Text")).toBeInTheDocument(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); +}); + +test("does not show tooltip for non-truncated text", () => { + const shortText = "Short text"; + const { getByText } = render( + + {shortText} + , + ); + + fireEvent.mouseEnter(getByText(shortText)); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); +}); + +test("opens a new tab for URL column type when clicked", () => { + const openSpy = jest.spyOn(window, "open").mockImplementation(() => null); + + render( + + Go to Google + , + ); + + fireEvent.click(screen.getByText("Go to Google")); + expect(openSpy).toHaveBeenCalledWith("https://www.google.com", "_blank"); + + openSpy.mockRestore(); +}); + +describe("isButtonTextTruncated", () => { + function mockElementWidths( + offsetWidth: number, + scrollWidth: number, + ): HTMLElement { + const spanElement = document.createElement("span"); + + Object.defineProperty(spanElement, "offsetWidth", { value: offsetWidth }); + Object.defineProperty(spanElement, "scrollWidth", { value: scrollWidth }); + const container = document.createElement("div"); + + container.appendChild(spanElement); + + return container; + } + + test("returns true when text is truncated (scrollWidth > offsetWidth)", () => { + const element = mockElementWidths(100, 150); + + expect(isButtonTextTruncated(element)).toBe(true); + }); + + test("returns false when text is not truncated (scrollWidth <= offsetWidth)", () => { + const element = mockElementWidths(150, 150); + + expect(isButtonTextTruncated(element)).toBe(false); + }); + + test("returns false when no span element is found", () => { + const element = document.createElement("div"); + + expect(isButtonTextTruncated(element)).toBe(false); + }); +}); diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/Button.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/Button.tsx index 35ccc27e22..3eca9af7bd 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/Button.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/Button.tsx @@ -2,8 +2,12 @@ import React, { useState } from "react"; import { ActionWrapper } from "../TableStyledWrappers"; import { BaseButton } from "widgets/ButtonWidget/component"; -import type { ButtonColumnActions } from "widgets/TableWidgetV2/constants"; +import { + ColumnTypes, + type ButtonColumnActions, +} from "widgets/TableWidgetV2/constants"; import styled from "styled-components"; +import AutoToolTipComponent from "widgets/TableWidgetV2/component/cellComponents/AutoToolTipComponent"; const StyledButton = styled(BaseButton)<{ compactMode?: string; @@ -37,27 +41,31 @@ export function Button(props: ButtonProps) { props.onCommandClick(props.action.dynamicTrigger, onComplete); }; + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + return ( - { - e.stopPropagation(); - }} - > - {props.isCellVisible && props.action.isVisible ? ( - + + {props.isCellVisible && props.action.isVisible && props.action.label ? ( + + + ) : null} );