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**  **After issue resolved**  <!-- 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:
parent
764d8f3cb5
commit
c6cf919beb
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user