diff --git a/app/client/src/widgets/TableWidgetV2/component/TableStyledWrappers.tsx b/app/client/src/widgets/TableWidgetV2/component/TableStyledWrappers.tsx index 76cc4267ef2e..d212ac791530 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 7d264fdb9aaf..86b3c41caec3 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; - - const mouseEnterHandler = () => { - const element = ref.current?.querySelector("div") as HTMLDivElement; - - /* - * Using setTimeout to simulate hoverOpenDelay of the tooltip - * during initial render - */ - timeout = setTimeout(() => { - if (element && element.offsetWidth < element.scrollWidth) { - setRequiresTooltip(true); - } else { - setRequiresTooltip(false); - } - - ref.current?.removeEventListener("mouseenter", mouseEnterHandler); - ref.current?.removeEventListener("mouseleave", mouseLeaveHandler); - }, TOOLTIP_OPEN_DELAY); - }; - - const mouseLeaveHandler = () => { - clearTimeout(timeout); - }; - - ref.current?.addEventListener("mouseenter", mouseEnterHandler); - ref.current?.addEventListener("mouseleave", mouseLeaveHandler); - - return () => { - ref.current?.removeEventListener("mouseenter", mouseEnterHandler); - ref.current?.removeEventListener("mouseleave", mouseLeaveHandler); - clearTimeout(timeout); - }; - }, [children]); + useEffect( + function setupMouseHandlers() { + let timeout: ReturnType; + const currentRef = ref.current; + + if (!currentRef) return; + + const mouseEnterHandler = () => { + timeout = setTimeout(() => { + const element = currentRef?.querySelector("div") as HTMLDivElement; + + /* + * 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); + } + + currentRef?.removeEventListener("mouseenter", mouseEnterHandler); + currentRef?.removeEventListener("mouseleave", mouseLeaveHandler); + }, TOOLTIP_OPEN_DELAY); + }; + + const mouseLeaveHandler = () => { + setRequiresTooltip(false); + clearTimeout(timeout); + }; + + 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 35ccc27e2216..3eca9af7bd3c 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} );