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 }>`
|
export const ActionWrapper = styled.div<{ disabled: boolean }>`
|
||||||
margin: 0 5px 0 0;
|
margin: 0 5px 0 0;
|
||||||
|
max-width: 100%;
|
||||||
${(props) => (props.disabled ? "cursor: not-allowed;" : null)}
|
${(props) => (props.disabled ? "cursor: not-allowed;" : null)}
|
||||||
&&&&&& {
|
&&&&&& {
|
||||||
.bp3-button {
|
.bp3-button {
|
||||||
|
|
|
||||||
|
|
@ -36,45 +36,71 @@ const MAX_WIDTH = 500;
|
||||||
const TOOLTIP_OPEN_DELAY = 500;
|
const TOOLTIP_OPEN_DELAY = 500;
|
||||||
const MAX_CHARS_ALLOWED_IN_TOOLTIP = 200;
|
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 ref = createRef<HTMLDivElement>();
|
||||||
const [requiresTooltip, setRequiresTooltip] = useState(false);
|
const [requiresTooltip, setRequiresTooltip] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
function setupMouseHandlers() {
|
||||||
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
const currentRef = ref.current;
|
||||||
|
|
||||||
const mouseEnterHandler = () => {
|
if (!currentRef) return;
|
||||||
const element = ref.current?.querySelector("div") as HTMLDivElement;
|
|
||||||
|
|
||||||
/*
|
const mouseEnterHandler = () => {
|
||||||
* Using setTimeout to simulate hoverOpenDelay of the tooltip
|
timeout = setTimeout(() => {
|
||||||
* during initial render
|
const element = currentRef?.querySelector("div") as HTMLDivElement;
|
||||||
*/
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
if (element && element.offsetWidth < element.scrollWidth) {
|
|
||||||
setRequiresTooltip(true);
|
|
||||||
} else {
|
|
||||||
setRequiresTooltip(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.current?.removeEventListener("mouseenter", mouseEnterHandler);
|
/*
|
||||||
ref.current?.removeEventListener("mouseleave", mouseLeaveHandler);
|
* Using setTimeout to simulate hoverOpenDelay of the tooltip
|
||||||
}, TOOLTIP_OPEN_DELAY);
|
* during initial render
|
||||||
};
|
*/
|
||||||
|
if (element && element.offsetWidth < element.scrollWidth) {
|
||||||
|
setRequiresTooltip(true);
|
||||||
|
} else if (isButton && element && isButtonTextTruncated(element)) {
|
||||||
|
setRequiresTooltip(true);
|
||||||
|
} else {
|
||||||
|
setRequiresTooltip(false);
|
||||||
|
}
|
||||||
|
|
||||||
const mouseLeaveHandler = () => {
|
currentRef?.removeEventListener("mouseenter", mouseEnterHandler);
|
||||||
clearTimeout(timeout);
|
currentRef?.removeEventListener("mouseleave", mouseLeaveHandler);
|
||||||
};
|
}, TOOLTIP_OPEN_DELAY);
|
||||||
|
};
|
||||||
|
|
||||||
ref.current?.addEventListener("mouseenter", mouseEnterHandler);
|
const mouseLeaveHandler = () => {
|
||||||
ref.current?.addEventListener("mouseleave", mouseLeaveHandler);
|
setRequiresTooltip(false);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
currentRef?.addEventListener("mouseenter", mouseEnterHandler);
|
||||||
ref.current?.removeEventListener("mouseenter", mouseEnterHandler);
|
currentRef?.addEventListener("mouseleave", mouseLeaveHandler);
|
||||||
ref.current?.removeEventListener("mouseleave", mouseLeaveHandler);
|
|
||||||
clearTimeout(timeout);
|
return () => {
|
||||||
};
|
currentRef?.removeEventListener("mouseenter", mouseEnterHandler);
|
||||||
}, [children]);
|
currentRef?.removeEventListener("mouseleave", mouseLeaveHandler);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[children, isButton, ref],
|
||||||
|
);
|
||||||
|
|
||||||
return requiresTooltip && children ? (
|
return requiresTooltip && children ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|
@ -158,13 +184,21 @@ function LinkWrapper(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutoToolTipComponent(props: Props) {
|
export function AutoToolTipComponent(props: Props) {
|
||||||
const content = useToolTip(props.children, props.title);
|
const content = useToolTip(
|
||||||
|
props.children,
|
||||||
|
props.title,
|
||||||
|
props.columnType === ColumnTypes.BUTTON,
|
||||||
|
);
|
||||||
|
|
||||||
if (props.columnType === ColumnTypes.URL && props.title) {
|
if (props.columnType === ColumnTypes.URL && props.title) {
|
||||||
return <LinkWrapper {...props} />;
|
return <LinkWrapper {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.columnType === ColumnTypes.BUTTON && props.title) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnWrapper className={props.className} textColor={props.textColor}>
|
<ColumnWrapper className={props.className} textColor={props.textColor}>
|
||||||
<CellWrapper
|
<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 { ActionWrapper } from "../TableStyledWrappers";
|
||||||
import { BaseButton } from "widgets/ButtonWidget/component";
|
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 styled from "styled-components";
|
||||||
|
import AutoToolTipComponent from "widgets/TableWidgetV2/component/cellComponents/AutoToolTipComponent";
|
||||||
|
|
||||||
const StyledButton = styled(BaseButton)<{
|
const StyledButton = styled(BaseButton)<{
|
||||||
compactMode?: string;
|
compactMode?: string;
|
||||||
|
|
@ -37,27 +41,31 @@ export function Button(props: ButtonProps) {
|
||||||
props.onCommandClick(props.action.dynamicTrigger, onComplete);
|
props.onCommandClick(props.action.dynamicTrigger, onComplete);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopPropagation = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionWrapper
|
<ActionWrapper disabled={!!props.isDisabled} onClick={stopPropagation}>
|
||||||
disabled={!!props.isDisabled}
|
{props.isCellVisible && props.action.isVisible && props.action.label ? (
|
||||||
onClick={(e) => {
|
<AutoToolTipComponent
|
||||||
e.stopPropagation();
|
columnType={ColumnTypes.BUTTON}
|
||||||
}}
|
title={props.action.label}
|
||||||
>
|
>
|
||||||
{props.isCellVisible && props.action.isVisible ? (
|
<StyledButton
|
||||||
<StyledButton
|
borderRadius={props.action.borderRadius}
|
||||||
borderRadius={props.action.borderRadius}
|
boxShadow={props.action.boxShadow}
|
||||||
boxShadow={props.action.boxShadow}
|
buttonColor={props.action.backgroundColor}
|
||||||
buttonColor={props.action.backgroundColor}
|
buttonVariant={props.action.variant}
|
||||||
buttonVariant={props.action.variant}
|
compactMode={props.compactMode}
|
||||||
compactMode={props.compactMode}
|
disabled={props.isDisabled}
|
||||||
disabled={props.isDisabled}
|
iconAlign={props.action.iconAlign}
|
||||||
iconAlign={props.action.iconAlign}
|
iconName={props.action.iconName}
|
||||||
iconName={props.action.iconName}
|
loading={loading}
|
||||||
loading={loading}
|
onClick={handleClick}
|
||||||
onClick={handleClick}
|
text={props.action.label}
|
||||||
text={props.action.label}
|
/>
|
||||||
/>
|
</AutoToolTipComponent>
|
||||||
) : null}
|
) : null}
|
||||||
</ActionWrapper>
|
</ActionWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user