diff --git a/src/components/mui/__tests__/ellipsis-tooltip.test.js b/src/components/mui/__tests__/ellipsis-tooltip.test.js
new file mode 100644
index 00000000..1b7d9a55
--- /dev/null
+++ b/src/components/mui/__tests__/ellipsis-tooltip.test.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import EllipsisTooltip from "../ellipsis-tooltip";
+
+describe("EllipsisTooltip", () => {
+ let scrollWidthSpy;
+ let offsetWidthSpy;
+
+ beforeEach(() => {
+ scrollWidthSpy = jest.spyOn(Element.prototype, "scrollWidth", "get").mockReturnValue(0);
+ offsetWidthSpy = jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(0);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ test("renders children", () => {
+ render(content);
+ expect(screen.getByText("content")).toBeInTheDocument();
+ });
+
+ test("wrapper span has truncation styles", () => {
+ render(content);
+ const wrapper = screen.getByText("content").parentElement;
+ expect(wrapper).toHaveStyle({
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap"
+ });
+ });
+
+ test("shows tooltip on hover when content overflows", async () => {
+ scrollWidthSpy.mockReturnValue(200);
+ offsetWidthSpy.mockReturnValue(100);
+
+ render(long content);
+ await userEvent.hover(screen.getByText("long content").parentElement);
+
+ expect(await screen.findByRole("tooltip")).toHaveTextContent("overflow tip");
+ });
+
+ test("does not show tooltip when content fits", async () => {
+ // scrollWidth (0) <= offsetWidth (0) → not overflowing
+ render(short);
+ await userEvent.hover(screen.getByText("short").parentElement);
+
+ expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/mui/__tests__/mui-table.test.js b/src/components/mui/__tests__/mui-table.test.js
index 55435d0a..ece03d3e 100644
--- a/src/components/mui/__tests__/mui-table.test.js
+++ b/src/components/mui/__tests__/mui-table.test.js
@@ -265,4 +265,35 @@ describe("MuiTable", () => {
// MUI CheckIcon renders an SVG; just ensure no error
expect(screen.getByRole("cell", { hidden: true })).toBeInTheDocument();
});
+
+ describe("ellipsis column prop", () => {
+ beforeEach(() => {
+ jest.spyOn(Element.prototype, "scrollWidth", "get").mockReturnValue(200);
+ jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(100);
+ });
+ afterEach(() => jest.restoreAllMocks());
+
+ test("wraps cell in truncating span", () => {
+ setup({ columns: [{ columnKey: "name", header: "Name", ellipsis: true }] });
+ expect(screen.getByText("Alice").parentElement).toHaveStyle({
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap"
+ });
+ });
+
+ test("tooltip shows raw value when col.render is absent", async () => {
+ setup({ columns: [{ columnKey: "name", header: "Name", ellipsis: true }] });
+ await userEvent.hover(screen.getByText("Alice").parentElement);
+ expect(await screen.findByRole("tooltip")).toHaveTextContent("Alice");
+ });
+
+ test("tooltip matches rendered output when col.render transforms data", async () => {
+ const cols = [{ columnKey: "name", header: "Name", ellipsis: true, render: (row) => Formatted: {row.name} }];
+ setup({ columns: cols });
+ const cell = screen.getByText("Formatted: Alice");
+ await userEvent.hover(cell.parentElement);
+ expect(await screen.findByRole("tooltip")).toHaveTextContent("Formatted: Alice");
+ });
+ });
});
diff --git a/src/components/mui/editable-table/mui-table-editable.js b/src/components/mui/editable-table/mui-table-editable.js
index 3a9e75c9..7fd8deee 100644
--- a/src/components/mui/editable-table/mui-table-editable.js
+++ b/src/components/mui/editable-table/mui-table-editable.js
@@ -36,6 +36,7 @@ import {
TWENTY_PER_PAGE
} from "../../../utils/constants";
import showConfirmDialog from "../showConfirmDialog";
+import EllipsisTooltip from "../ellipsis-tooltip";
const ARCHIVED_CELL_SX = {
backgroundColor: "background.light",
@@ -289,7 +290,11 @@ const MuiTableEditable = ({
{data.map((row) => (
- {columns.map((col) => (
+ {columns.map((col) => {
+ const cellContent = col.render
+ ? col.render(row)
+ : {row[col.columnKey]};
+ return (
handleCellClick(row, col.columnKey)}
@@ -317,15 +322,16 @@ const MuiTableEditable = ({
}
validation={col.validation}
/>
- ) : col.render ? (
- col.render(row)
+ ) : col.ellipsis ? (
+
+ {cellContent}
+
) : (
-
- {row[col.columnKey]}
-
+ cellContent
)}
- ))}
+ );
+ })}
{onEdit && (
{
+ const ref = React.useRef(null);
+ const [isOverflowing, setIsOverflowing] = React.useState(false);
+
+ return (
+
+ {
+ if (ref.current)
+ setIsOverflowing(ref.current.scrollWidth > ref.current.offsetWidth);
+ }}
+ style={{ display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
+ >
+ {children}
+
+
+ );
+};
+
+export default EllipsisTooltip;
diff --git a/src/components/mui/sortable-table/mui-table-sortable.js b/src/components/mui/sortable-table/mui-table-sortable.js
index ade4f26d..a4c44ec1 100644
--- a/src/components/mui/sortable-table/mui-table-sortable.js
+++ b/src/components/mui/sortable-table/mui-table-sortable.js
@@ -38,6 +38,7 @@ import {
TWENTY_PER_PAGE
} from "../../../utils/constants";
import showConfirmDialog from "../showConfirmDialog";
+import EllipsisTooltip from "../ellipsis-tooltip";
const MuiTableSortable = ({
columns = [],
@@ -209,7 +210,9 @@ const MuiTableSortable = ({
}}
>
{/* Main content columns */}
- {columns.map((col) => (
+ {columns.map((col) => {
+ const cellContent = col.render?.(row) || {row[col.columnKey]};
+ return (
- {col.render?.(row) || row[col.columnKey]}
+ {col.ellipsis ? (
+
+ {cellContent}
+
+ ) : (
+ cellContent
+ )}
- ))}
+ );
+ })}
{/* Edit column */}
{onEdit && (
item.name,
- onEdit,
- onArchive,
- onDelete,
- onSelect,
- canDelete = () => true,
- deleteDialogTitle = null,
- deleteDialogBody = null,
- deleteDialogConfirmText = null,
- confirmButtonColor = null
- }) => {
+ columns = [],
+ data = [],
+ children,
+ totalRows,
+ perPage,
+ currentPage,
+ onPageChange,
+ onPerPageChange,
+ onSort,
+ options = { sortCol: "", sortDir: 1, disableProp: null }, // disableProp is the prop that will disable the row
+ getName = (item) => item.name,
+ onEdit,
+ onArchive,
+ onDelete,
+ onSelect,
+ canDelete = () => true,
+ deleteDialogTitle = null,
+ deleteDialogBody = null,
+ deleteDialogConfirmText = null,
+ confirmButtonColor = null
+}) => {
const totalColumnsCount =
columns.length + (onEdit ? 1 : 0) + (onDelete ? 1 : 0) + (onArchive ? 1 : 0) + (onSelect ? 1 : 0);
@@ -103,7 +104,7 @@ const MuiTable = ({
customPerPageOptions = [initialPerPage.current];
}
- const {sortCol, sortDir} = options;
+ const { sortCol, sortDir } = options;
const getDisabledSx = (row) =>
options.disableProp && row[options.disableProp] ? ARCHIVED_CELL_SX : {};
@@ -154,31 +155,39 @@ const MuiTable = ({
};
const renderCell = (row, col) => {
- if (col.render) {
- return col.render(row);
- }
-
if (isBoolean(row[col.columnKey])) {
return row[col.columnKey] ? (
-
+
) : (
-
+
+ );
+ }
+
+ const content = col.render
+ ? col.render(row)
+ : {row[col.columnKey]};
+
+ if (col.ellipsis) {
+ return (
+
+ {content}
+
);
}
- return {row[col.columnKey]};
+ return content;
};
return (
-
-
+
+
-
+
{/* TABLE HEADER */}
-
+
{columns.map((col) => (
))}
- {onEdit && }
- {onArchive && }
- {onDelete && }
- {onSelect && }
+ {onEdit && }
+ {onArchive && }
+ {onDelete && }
+ {onSelect && }
@@ -242,18 +251,18 @@ const MuiTable = ({
onEdit(row)}
- sx={{padding: 0}}
+ sx={{ padding: 0 }}
data-testid="action-edit"
disabled={options.disableProp && row[options.disableProp]}
>
-
+
)}
{onArchive && (
@@ -309,10 +318,10 @@ const MuiTable = ({
size="medium"
onClick={() => onSelect(row)}
data-testid="action-select"
- sx={{padding: 0}}
+ sx={{ padding: 0 }}
disabled={options.disableProp && row[options.disableProp]}
>
-
+
)}