From 2c8abfc8b45861a7f2547042a6d013abbc109db7 Mon Sep 17 00:00:00 2001
From: Frank Niessink <frank@niessink.com>
Date: Tue, 21 Jan 2025 22:35:17 +0100
Subject: [PATCH] Migration to Material UI fixes: "Some buttons do not show
 their cursor state".

Fixes #6443.
---
 .../frontend/src/source/SourceEntities.js     | 116 +++++++-----------
 .../src/source/SourceEntities.test.js         |  88 ++++---------
 .../src/subject/SubjectTableHeader.js         |   2 +-
 .../frontend/src/widgets/TableHeaderCell.js   |  41 +++++--
 .../src/widgets/TableHeaderCell.test.js       |   4 +-
 docs/src/changelog.md                         |   2 +-
 6 files changed, 98 insertions(+), 155 deletions(-)

diff --git a/components/frontend/src/source/SourceEntities.js b/components/frontend/src/source/SourceEntities.js
index a6fb76373f..31281f37b8 100644
--- a/components/frontend/src/source/SourceEntities.js
+++ b/components/frontend/src/source/SourceEntities.js
@@ -10,7 +10,6 @@ import {
     TableContainer,
     TableHead,
     TableRow,
-    TableSortLabel,
     Tooltip,
 } from "@mui/material"
 import { bool, func, object, string } from "prop-types"
@@ -19,7 +18,6 @@ import { useContext, useState } from "react"
 import { DataModel } from "../context/DataModel"
 import {
     alignmentPropType,
-    childrenPropType,
     entityAttributePropType,
     entityAttributesPropType,
     entityAttributeTypePropType,
@@ -34,6 +32,7 @@ import {
 import { capitalize } from "../utils"
 import { IgnoreIcon, ShowIcon } from "../widgets/icons"
 import { LoadingPlaceHolder } from "../widgets/Placeholder"
+import { SortableTableHeaderCell } from "../widgets/TableHeaderCell"
 import { FailedToLoadMeasurementsWarningMessage, InfoMessage } from "../widgets/WarningMessage"
 import { SourceEntity } from "./SourceEntity"
 
@@ -90,70 +89,19 @@ sorted.propTypes = {
     sortDirection: sortDirectionPropType,
 }
 
-function sort(column, columnType, setColumnType, setSortColumn, setSortDirection, sortColumn, sortDirection) {
-    setColumnType(columnType)
-    if (column === sortColumn) {
-        setSortDirection(sortDirection === "ascending" ? "descending" : "ascending")
-    } else {
-        setSortColumn(column)
-    }
-}
-sort.propTypes = {
-    column: string,
-    columnType: entityAttributeTypePropType,
-    setColumnType: func,
-    setSortColumn: func,
-    setSortDirection: func,
-    sortColumn: string,
-    sortDirection: sortDirectionPropType,
-}
-
-function MuiSortDirection(sortDirection) {
-    return sortDirection === "ascending" ? "asc" : "desc"
-}
-
-function SortableHeaderCell({
-    children,
-    column,
-    columnType,
-    setColumnType,
-    setSortColumn,
-    setSortDirection,
-    sortColumn,
-    sortDirection,
-    textAlign,
-}) {
-    return (
-        <TableCell align={textAlign} direction={sorted(column, sortColumn, sortDirection)}>
-            <TableSortLabel
-                active={column === sortColumn}
-                direction={column === sortColumn ? MuiSortDirection(sortDirection) : "asc"}
-                onClick={() =>
-                    sort(column, columnType, setColumnType, setSortColumn, setSortDirection, sortColumn, sortDirection)
-                }
-            >
-                {children}
-            </TableSortLabel>
-        </TableCell>
-    )
-}
-SortableHeaderCell.propTypes = {
-    children: childrenPropType,
-    column: string,
-    columnType: entityAttributeTypePropType,
-    setColumnType: func,
-    setSortColumn: func,
-    setSortDirection: func,
-    sortColumn: string,
-    sortDirection: sortDirectionPropType,
-    textAlign: alignmentPropType,
-}
-
 function EntityAttributeHeaderCell({ entityAttribute, ...sortProps }) {
+    function handleSort(column) {
+        sortProps.setColumnType(entityAttribute.type || "text")
+        if (column === sortProps.sortColumn) {
+            sortProps.setSortDirection(sortProps.sortDirection === "ascending" ? "descending" : "ascending")
+        } else {
+            sortProps.setSortColumn(column)
+        }
+    }
     return (
-        <SortableHeaderCell
+        <SortableTableHeaderCell
             column={entityAttribute.key}
-            columnType={entityAttribute.type || "text"}
+            handleSort={handleSort}
             textAlign={alignment(entityAttribute.type, entityAttribute.alignment)}
             {...sortProps}
         >
@@ -166,7 +114,7 @@ function EntityAttributeHeaderCell({ entityAttribute, ...sortProps }) {
                     </span>
                 </Tooltip>
             ) : null}
-        </SortableHeaderCell>
+        </SortableTableHeaderCell>
     )
 }
 EntityAttributeHeaderCell.propTypes = {
@@ -188,6 +136,14 @@ function sourceEntitiesHeaders(
     const entityName = metricEntities.name
     const entityNamePlural = metricEntities.name_plural
     const hideIgnoredEntitiesLabel = `${hideIgnoredEntities ? "Show" : "Hide"} ignored ${entityNamePlural}`
+    function handleSort(column, columnType) {
+        sortProps.setColumnType(columnType)
+        if (column === sortProps.sortColumn) {
+            sortProps.setSortDirection(sortProps.sortDirection === "ascending" ? "descending" : "ascending")
+        } else {
+            sortProps.setSortColumn(column)
+        }
+    }
     return (
         <TableRow>
             <TableCell align="center">
@@ -200,18 +156,34 @@ function sourceEntitiesHeaders(
                     </IconButton>
                 </Tooltip>
             </TableCell>
-            <SortableHeaderCell column="entity_status" columnType="text" {...sortProps}>
+            <SortableTableHeaderCell
+                column="entity_status"
+                handleSort={(column) => handleSort(column, "text")}
+                {...sortProps}
+            >
                 {`${capitalize(entityName)} status`}
-            </SortableHeaderCell>
-            <SortableHeaderCell column="status_end_date" columnType="date" {...sortProps}>
+            </SortableTableHeaderCell>
+            <SortableTableHeaderCell
+                column="status_end_date"
+                handleSort={(column) => handleSort(column, "date")}
+                {...sortProps}
+            >
                 Status end date
-            </SortableHeaderCell>
-            <SortableHeaderCell column="rationale" columnType="text" {...sortProps}>
+            </SortableTableHeaderCell>
+            <SortableTableHeaderCell
+                column="rationale"
+                handleSort={(column) => handleSort(column, "text")}
+                {...sortProps}
+            >
                 Status rationale
-            </SortableHeaderCell>
-            <SortableHeaderCell column="first_seen" columnType="datetime" {...sortProps}>
+            </SortableTableHeaderCell>
+            <SortableTableHeaderCell
+                column="first_seen"
+                handleSort={(column) => handleSort(column, "datetime")}
+                {...sortProps}
+            >
                 {capitalize(entityName)} first seen
-            </SortableHeaderCell>
+            </SortableTableHeaderCell>
             {entityAttributes.map((entityAttribute) => (
                 <EntityAttributeHeaderCell entityAttribute={entityAttribute} key={entityAttribute.key} {...sortProps} />
             ))}
diff --git a/components/frontend/src/source/SourceEntities.test.js b/components/frontend/src/source/SourceEntities.test.js
index 844d0b14fb..10f1e91e18 100644
--- a/components/frontend/src/source/SourceEntities.test.js
+++ b/components/frontend/src/source/SourceEntities.test.js
@@ -180,103 +180,59 @@ it("shows the show ignored entities button", async () => {
     expect(hideEntitiesButton).toHaveAttribute("aria-label", "Show ignored entities")
 })
 
-it("sorts the entities by status", async () => {
+async function expectColumnIsSortedCorrectly(header, ascending) {
     renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/Entity name status/))
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/Entity name status/))
-    expectOrder(["A", "B", "C"])
+    expectOrder(["C", "B", "A"]) // Initial order
+    await userEvent.click(screen.getByText(header))
+    expectOrder(ascending)
+    await userEvent.click(screen.getByText(header))
+    expectOrder(ascending.toReversed())
+    await userEvent.click(screen.getByText(header))
+    expectOrder(ascending)
+}
+
+it("sorts the entities by status", async () => {
+    await expectColumnIsSortedCorrectly(/Entity name status/, ["C", "B", "A"])
 })
 
 it("sorts the entities by status end date", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/Status end date/))
-    expectOrder(["A", "C", "B"])
-    await userEvent.click(screen.getByText(/Status end date/))
-    expectOrder(["B", "C", "A"])
+    await expectColumnIsSortedCorrectly(/Status end date/, ["A", "C", "B"])
 })
 
 it("sorts the entities by status rationale", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/Status rationale/))
-    expectOrder(["A", "C", "B"])
-    await userEvent.click(screen.getByText(/Status rationale/))
-    expectOrder(["B", "C", "A"])
+    await expectColumnIsSortedCorrectly(/Status rationale/, ["A", "C", "B"])
 })
 
 it("sorts the entities by first seen date", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/first seen/))
-    expectOrder(["C", "A", "B"])
-    await userEvent.click(screen.getByText(/first seen/))
-    expectOrder(["B", "A", "C"])
+    await expectColumnIsSortedCorrectly(/first seen/, ["C", "A", "B"])
 })
 
 it("sorts the entities by integer", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/integer/))
-    expectOrder(["C", "A", "B"])
-    await userEvent.click(screen.getByText(/integer/))
-    expectOrder(["B", "A", "C"])
+    await expectColumnIsSortedCorrectly(/integer/, ["C", "A", "B"])
 })
 
 it("sorts the entities by integer percentage", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/int percentage/))
-    expectOrder(["C", "A", "B"])
-    await userEvent.click(screen.getByText(/int percentage/))
-    expectOrder(["B", "A", "C"])
+    await expectColumnIsSortedCorrectly(/int percentage/, ["C", "A", "B"])
 })
 
 it("sorts the entities by float", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/float/))
-    expectOrder(["A", "B", "C"])
-    await userEvent.click(screen.getByText(/float/))
-    expectOrder(["C", "B", "A"])
+    await expectColumnIsSortedCorrectly(/float/, ["A", "B", "C"])
 })
 
 it("sorts the entities by text", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/text/))
-    expectOrder(["A", "B", "C"])
-    await userEvent.click(screen.getByText(/text/))
-    expectOrder(["C", "B", "A"])
+    await expectColumnIsSortedCorrectly(/text/, ["A", "B", "C"])
 })
 
 it("sorts the entities by date", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/date only/))
-    expectOrder(["C", "A", "B"])
-    await userEvent.click(screen.getByText(/date only/))
-    expectOrder(["B", "A", "C"])
+    await expectColumnIsSortedCorrectly(/date only/, ["C", "A", "B"])
 })
 
 it("sorts the entities by datetime", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/datetime/))
-    expectOrder(["C", "A", "B"])
-    await userEvent.click(screen.getByText(/datetime/))
-    expectOrder(["B", "A", "C"])
+    await expectColumnIsSortedCorrectly(/datetime/, ["C", "A", "B"])
 })
 
 it("sorts the entities by minutes", async () => {
-    renderSourceEntities()
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/minutes/))
-    expectOrder(["C", "B", "A"])
-    await userEvent.click(screen.getByText(/minutes/))
-    expectOrder(["A", "B", "C"])
+    await expectColumnIsSortedCorrectly(/minutes/, ["C", "B", "A"])
 })
 
 it("shows help", async () => {
diff --git a/components/frontend/src/subject/SubjectTableHeader.js b/components/frontend/src/subject/SubjectTableHeader.js
index 8a53f9be4a..88e0799fd6 100644
--- a/components/frontend/src/subject/SubjectTableHeader.js
+++ b/components/frontend/src/subject/SubjectTableHeader.js
@@ -291,7 +291,7 @@ MeasurementHeaderCells.propTypes = {
 
 export function SubjectTableHeader({ columnDates, handleSort, settings }) {
     const sortProps = {
-        sortColumn: settings.sortColumn,
+        sortColumn: settings.sortColumn.value,
         sortDirection: settings.sortDirection,
         handleSort: handleSort,
     }
diff --git a/components/frontend/src/widgets/TableHeaderCell.js b/components/frontend/src/widgets/TableHeaderCell.js
index 566a7060bf..618824b0f7 100644
--- a/components/frontend/src/widgets/TableHeaderCell.js
+++ b/components/frontend/src/widgets/TableHeaderCell.js
@@ -1,12 +1,12 @@
-import { TableCell, TableSortLabel, Tooltip } from "@mui/material"
+import { ButtonBase, TableCell, TableSortLabel, Tooltip } from "@mui/material"
 import { func, string } from "prop-types"
 
 import {
     alignmentPropType,
+    childrenPropType,
     labelPropType,
     popupContentPropType,
-    sortDirectionURLSearchQueryPropType,
-    stringURLSearchQueryPropType,
+    sortDirectionPropType,
 } from "../sharedPropTypes"
 
 function TableHeaderCellContents({ help, label }) {
@@ -28,6 +28,7 @@ function MuiSortDirection(sortDirection) {
 }
 
 export function SortableTableHeaderCell({
+    children,
     colSpan,
     column,
     sortColumn,
@@ -37,27 +38,41 @@ export function SortableTableHeaderCell({
     textAlign,
     help,
 }) {
-    const sorted = sortColumn.value === column ? MuiSortDirection(sortDirection.value) : null
+    const sorted = sortColumn === column ? MuiSortDirection(sortDirection) : null
+    const align = textAlign || "left"
     return (
-        <TableCell align={textAlign || "left"} colSpan={colSpan} sortDirection={sorted}>
-            <TableSortLabel
-                active={column === sortColumn.value}
-                direction={column === sortColumn.value ? MuiSortDirection(sortDirection.value) : "asc"}
-                onClick={() => handleSort(column)}
+        <TableCell colSpan={colSpan} sortDirection={sorted}>
+            <ButtonBase
+                focusRipple
+                sx={{
+                    display: "block",
+                    fontSize: "inherit",
+                    padding: "inherit",
+                    textAlign: align,
+                    width: "100%",
+                }}
+                tabIndex={-1}
             >
-                <TableHeaderCellContents help={help} label={label} />
-            </TableSortLabel>
+                <TableSortLabel
+                    active={column === sortColumn}
+                    direction={column === sortColumn ? MuiSortDirection(sortDirection) : "asc"}
+                    onClick={() => handleSort(column)}
+                >
+                    {children || <TableHeaderCellContents help={help} label={label} />}
+                </TableSortLabel>
+            </ButtonBase>
         </TableCell>
     )
 }
 SortableTableHeaderCell.propTypes = {
+    children: childrenPropType,
     colSpan: string,
     column: string,
     handleSort: func,
     help: popupContentPropType,
     label: labelPropType,
-    sortColumn: stringURLSearchQueryPropType,
-    sortDirection: sortDirectionURLSearchQueryPropType,
+    sortColumn: string,
+    sortDirection: sortDirectionPropType,
     textAlign: alignmentPropType,
 }
 
diff --git a/components/frontend/src/widgets/TableHeaderCell.test.js b/components/frontend/src/widgets/TableHeaderCell.test.js
index 72e793885f..03cb2cfb7d 100644
--- a/components/frontend/src/widgets/TableHeaderCell.test.js
+++ b/components/frontend/src/widgets/TableHeaderCell.test.js
@@ -14,8 +14,8 @@ function renderSortableTableHeaderCell(help) {
                     <SortableTableHeaderCell
                         label="Header"
                         help={help}
-                        sortColumn={settings.sortColumn}
-                        sortDirection={settings.sortDirection}
+                        sortColumn={settings.sortColumn.value}
+                        sortDirection={settings.sortDirection.value}
                     />
                 </TableRow>
             </TableHead>
diff --git a/docs/src/changelog.md b/docs/src/changelog.md
index 77039ebf27..be781432bd 100644
--- a/docs/src/changelog.md
+++ b/docs/src/changelog.md
@@ -22,7 +22,7 @@ If your currently installed *Quality-time* version is not the latest version, pl
 
 ### Changed
 
-- Completed the replacement of Semantic UI React with Material UI as frontend component library. Fixes [#5180] (https://github.com/ICTU/quality-time/issues/5180), [#9904](https://github.com/ICTU/quality-time/issues/9904) and [#10159](https://github.com/ICTU/quality-time/issues/10159). Closes [#9796](https://github.com/ICTU/quality-time/issues/9796).
+- Completed the replacement of Semantic UI React with Material UI as frontend component library. Fixes [#5180](https://github.com/ICTU/quality-time/issues/5180), [#6443](https://github.com/ICTU/quality-time/issues/6443), [#9904](https://github.com/ICTU/quality-time/issues/9904) and [#10159](https://github.com/ICTU/quality-time/issues/10159). Closes [#9796](https://github.com/ICTU/quality-time/issues/9796).
 
 ## v5.22.0 - 2025-01-16