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)




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
skjameela 2024-10-30 17:29:59 +05:30 committed by GitHub
parent 764d8f3cb5
commit c6cf919beb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 237 additions and 53 deletions

View File

@ -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 {

View File

@ -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<HTMLDivElement>();
const [requiresTooltip, setRequiresTooltip] = useState(false);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
useEffect(
function setupMouseHandlers() {
let timeout: ReturnType<typeof setTimeout>;
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 ? (
<Tooltip
@ -158,13 +184,21 @@ function LinkWrapper(props: Props) {
);
}
function AutoToolTipComponent(props: Props) {
const content = useToolTip(props.children, props.title);
export function AutoToolTipComponent(props: Props) {
const content = useToolTip(
props.children,
props.title,
props.columnType === ColumnTypes.BUTTON,
);
if (props.columnType === ColumnTypes.URL && props.title) {
return <LinkWrapper {...props} />;
}
if (props.columnType === ColumnTypes.BUTTON && props.title) {
return content;
}
return (
<ColumnWrapper className={props.className} textColor={props.textColor}>
<CellWrapper

View File

@ -0,0 +1,141 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import AutoToolTipComponent from "./AutoToolTipComponent";
import { ColumnTypes } from "widgets/TableWidgetV2/constants";
import "@testing-library/jest-dom";
import { isButtonTextTruncated } from "./AutoToolTipComponent";
jest.mock("react", () => {
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(
<AutoToolTipComponent columnType={ColumnTypes.BUTTON} title={longText}>
<span
style={{
width: "50px",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{longText}
</span>
</AutoToolTipComponent>,
);
fireEvent.mouseEnter(getByText(longText));
expect(getByText(longText)).toBeInTheDocument();
});
test("does not show tooltip for non-button types", () => {
const { getByText } = render(
<AutoToolTipComponent columnType={ColumnTypes.URL} title="Not a button">
<a href="#">Not a button</a>
</AutoToolTipComponent>,
);
expect(getByText("Not a button")).toBeInTheDocument();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
test("handles empty tooltip", () => {
const { getByText } = render(
<AutoToolTipComponent columnType={ColumnTypes.BUTTON} title="">
<button>Empty button</button>
</AutoToolTipComponent>,
);
expect(getByText("Empty button")).toBeInTheDocument();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
test("renders content without tooltip for normal text", () => {
const { getByText } = render(
<AutoToolTipComponent title="Normal Text">
<span>Normal Text</span>
</AutoToolTipComponent>,
);
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(
<AutoToolTipComponent columnType={ColumnTypes.BUTTON} title={shortText}>
<span>{shortText}</span>
</AutoToolTipComponent>,
);
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(
<AutoToolTipComponent
columnType={ColumnTypes.URL}
title="Go to Google"
url="https://www.google.com"
>
<span>Go to Google</span>
</AutoToolTipComponent>,
);
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);
});
});

View File

@ -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<HTMLDivElement>) => {
e.stopPropagation();
};
return (
<ActionWrapper
disabled={!!props.isDisabled}
onClick={(e) => {
e.stopPropagation();
}}
>
{props.isCellVisible && props.action.isVisible ? (
<StyledButton
borderRadius={props.action.borderRadius}
boxShadow={props.action.boxShadow}
buttonColor={props.action.backgroundColor}
buttonVariant={props.action.variant}
compactMode={props.compactMode}
disabled={props.isDisabled}
iconAlign={props.action.iconAlign}
iconName={props.action.iconName}
loading={loading}
onClick={handleClick}
text={props.action.label}
/>
<ActionWrapper disabled={!!props.isDisabled} onClick={stopPropagation}>
{props.isCellVisible && props.action.isVisible && props.action.label ? (
<AutoToolTipComponent
columnType={ColumnTypes.BUTTON}
title={props.action.label}
>
<StyledButton
borderRadius={props.action.borderRadius}
boxShadow={props.action.boxShadow}
buttonColor={props.action.backgroundColor}
buttonVariant={props.action.variant}
compactMode={props.compactMode}
disabled={props.isDisabled}
iconAlign={props.action.iconAlign}
iconName={props.action.iconName}
loading={loading}
onClick={handleClick}
text={props.action.label}
/>
</AutoToolTipComponent>
) : null}
</ActionWrapper>
);