diff --git a/src/components/mui/__tests__/mui-table.test.js b/src/components/mui/__tests__/mui-table.test.js index 55435d0a..36565db5 100644 --- a/src/components/mui/__tests__/mui-table.test.js +++ b/src/components/mui/__tests__/mui-table.test.js @@ -265,4 +265,26 @@ 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 and shows raw value tooltip on hover", async () => { + setup({ columns: [{ columnKey: "name", header: "Name", truncateText: true }] }); + const wrapper = screen.getByText("Alice").parentElement; + + expect(wrapper).toHaveStyle({ + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap" + }); + + await userEvent.hover(wrapper); + expect(await screen.findByRole("tooltip")).toHaveTextContent("Alice"); + }); + }); }); diff --git a/src/components/mui/__tests__/truncate-text.test.js b/src/components/mui/__tests__/truncate-text.test.js new file mode 100644 index 00000000..d670eda5 --- /dev/null +++ b/src/components/mui/__tests__/truncate-text.test.js @@ -0,0 +1,76 @@ +/** + * 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 TruncateText from "../truncate-text"; + +describe("TruncateText", () => { + 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(); + }); + + describe("css mode (charLimit={true})", () => { + test("wrapper span has truncation styles and shows tooltip on hover when content overflows", async () => { + scrollWidthSpy.mockReturnValue(200); + offsetWidthSpy.mockReturnValue(100); + + render(really long content); + const wrapper = screen.getByText("really long content").parentElement; + + expect(wrapper).toHaveStyle({ + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap" + }); + + await userEvent.hover(wrapper); + expect(await screen.findByRole("tooltip")).toHaveTextContent("really long content"); + }); + + test("does not show tooltip when content fits", async () => { + render(short); + await userEvent.hover(screen.getByText("short").parentElement); + + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + }); + }); + + describe("character limit mode (charLimit={number})", () => { + test("truncates display and shows full content in tooltip when beyond charLimit", async () => { + render(Hello World); + expect(screen.getByText("Hello...")).toBeInTheDocument(); + + await userEvent.hover(screen.getByText("Hello...").parentElement); + expect(await screen.findByRole("tooltip")).toHaveTextContent("Hello World"); + }); + + test("renders content unchanged and shows no tooltip when within charLimit", async () => { + render(short text); + expect(screen.getByText("short text")).toBeInTheDocument(); + + await userEvent.hover(screen.getByText("short text").parentElement); + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/mui/editable-table/mui-table-editable.js b/src/components/mui/editable-table/mui-table-editable.js index 3a9e75c9..442e9b38 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 TableCellContent from "../table/table-content"; const ARCHIVED_CELL_SX = { backgroundColor: "background.light", @@ -295,8 +296,7 @@ const MuiTableEditable = ({ onClick={() => handleCellClick(row, col.columnKey)} sx={getCellSx(row, { cursor: isEditable(col, row) ? "pointer" : "default", - padding: isEditable(col, row) ? "8px 16px" : undefined, - fontWeight: "normal" + padding: isEditable(col, row) ? "8px 16px" : undefined })} > {isEditable(col, row) ? ( @@ -317,12 +317,8 @@ const MuiTableEditable = ({ } validation={col.validation} /> - ) : col.render ? ( - col.render(row) ) : ( - - {row[col.columnKey]} - + )} ))} diff --git a/src/components/mui/sortable-table/mui-table-sortable.js b/src/components/mui/sortable-table/mui-table-sortable.js index ade4f26d..ec090af2 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 TableCellContent from "../table/table-content"; const MuiTableSortable = ({ columns = [], @@ -216,9 +217,8 @@ const MuiTableSortable = ({ className={`${ col.dottedBorder && styles.dottedBorderLeft } ${col.className}`} - sx={{ fontWeight: "normal" }} > - {col.render?.(row) || row[col.columnKey]} + ))} {/* Edit column */} diff --git a/src/components/mui/table/mui-table.js b/src/components/mui/table/mui-table.js index 136bf35e..1dbe07e4 100644 --- a/src/components/mui/table/mui-table.js +++ b/src/components/mui/table/mui-table.js @@ -13,7 +13,6 @@ import * as React from "react"; import T from "i18n-react/dist/i18n-react"; -import isBoolean from "lodash/isBoolean"; import { Box, Button, @@ -30,14 +29,13 @@ import { } from "@mui/material"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; -import CheckIcon from "@mui/icons-material/Check"; -import CloseIcon from "@mui/icons-material/Close"; -import {visuallyHidden} from "@mui/utils"; -import {DEFAULT_PER_PAGE, FIFTY_PER_PAGE, TWENTY_PER_PAGE} from "../../../utils/constants"; +import { visuallyHidden } from "@mui/utils"; +import { DEFAULT_PER_PAGE, FIFTY_PER_PAGE, TWENTY_PER_PAGE } from "../../../utils/constants"; import showConfirmDialog from "../showConfirmDialog"; import styles from "./mui-table.module.less"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import PropTypes from "prop-types"; +import TableCellContent from "./table-content"; const ARCHIVED_CELL_SX = { backgroundColor: "background.light", @@ -54,27 +52,27 @@ const ACTION_CELL_SX = { }; const MuiTable = ({ - 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 - }) => { + 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 +101,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 : {}; @@ -118,7 +116,6 @@ const MuiTable = ({ }) const getCellSx = (row, col) => ({ - fontWeight: "normal", ...(col.width && { width: col.width, minWidth: col.width, @@ -153,32 +150,16 @@ const MuiTable = ({ } }; - const renderCell = (row, col) => { - if (col.render) { - return col.render(row); - } - - if (isBoolean(row[col.columnKey])) { - return row[col.columnKey] ? ( - - ) : ( - - ); - } - - return {row[col.columnKey]}; - }; - return ( - - + + - +
{/* TABLE HEADER */} - + {columns.map((col) => ( ))} - {onEdit && } - {onArchive && } - {onDelete && } - {onSelect && } + {onEdit && } + {onArchive && } + {onDelete && } + {onSelect && } @@ -229,7 +210,7 @@ const MuiTable = ({ className={`${col.dottedBorder && styles.dottedBorderLeft} ${col.className}`} sx={getCellSx(row, col)} > - {renderCell(row, col)} + ))} {/* Edit column */} @@ -242,18 +223,18 @@ const MuiTable = ({ onEdit(row)} - sx={{padding: 0}} + sx={{ padding: 0 }} data-testid="action-edit" disabled={options.disableProp && row[options.disableProp]} > - + )} {onArchive && (