diff --git a/src/components/generic/Table/index.tsx b/src/components/generic/Table/index.tsx index 0a4c7adf..5e71c121 100755 --- a/src/components/generic/Table/index.tsx +++ b/src/components/generic/Table/index.tsx @@ -1,9 +1,10 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { useState } from "react"; +import React from "react"; export interface TableProps { headings: Heading[]; - rows: [][]; + rows: React.ReactNode[][]; } export interface Heading { @@ -12,18 +13,15 @@ export interface Heading { sortable: boolean; } -/** - * @param hopsAway String describing the number of hops away the node is from the current node - * @returns number of hopsAway or `0` if hopsAway is 'Direct' - */ -function numericHops(hopsAway: string): number { +function numericHops(hopsAway: string | unknown): number { + if (typeof hopsAway !== "string") { + return Number.MAX_SAFE_INTEGER; + } if (hopsAway.match(/direct/i)) { return 0; } - if (hopsAway.match(/\d+\s+hop/gi)) { - return Number(hopsAway.match(/(\d+)\s+hop/i)?.[1]); - } - return Number.MAX_SAFE_INTEGER; + const match = hopsAway.match(/(\d+)\s+hop/i); + return Number(match?.[1] ?? Number.MAX_SAFE_INTEGER); } export const Table = ({ headings, rows }: TableProps) => { @@ -39,49 +37,63 @@ export const Table = ({ headings, rows }: TableProps) => { } }; + const getElement = (cell: React.ReactNode): React.ReactElement | null => { + if (!React.isValidElement(cell)) { + return null; + } + if (cell.type === React.Fragment) { + const childrenArray = React.Children.toArray(cell.props.children); + const firstElement = childrenArray.find((child) => + React.isValidElement(child) + ); + return (firstElement as React.ReactElement) ?? null; + } + // If not a fragment, return the element itself + return cell; + }; + const sortedRows = rows.slice().sort((a, b) => { if (!sortColumn) return 0; const columnIndex = headings.findIndex((h) => h.title === sortColumn); - const aValue = a[columnIndex].props.children; - const bValue = b[columnIndex].props.children; + if (columnIndex === -1) return 0; + + const elementA = getElement(a[columnIndex]); + const elementB = getElement(b[columnIndex]); if (sortColumn === "Last Heard") { - const aTimestamp = aValue.props.timestamp ?? 0; - const bTimestamp = bValue.props.timestamp ?? 0; - - if (aTimestamp < bTimestamp) { - return sortOrder === "asc" ? -1 : 1; - } - if (aTimestamp > bTimestamp) { - return sortOrder === "asc" ? 1 : -1; - } + const aTimestamp = elementA?.props?.timestamp ?? 0; + const bTimestamp = elementB?.props?.timestamp ?? 0; + if (aTimestamp < bTimestamp) return sortOrder === "asc" ? -1 : 1; + if (aTimestamp > bTimestamp) return sortOrder === "asc" ? 1 : -1; return 0; } if (sortColumn === "Connection") { - const aNumHops = numericHops( - aValue instanceof Array ? aValue[0] : aValue, - ); - const bNumHops = numericHops( - bValue instanceof Array ? bValue[0] : bValue, - ); - - if (aNumHops < bNumHops) { - return sortOrder === "asc" ? -1 : 1; - } - if (aNumHops > bNumHops) { - return sortOrder === "asc" ? 1 : -1; - } + const aHopsStr = elementA?.props?.children; + const bHopsStr = elementB?.props?.children; + const aNumHops = numericHops(aHopsStr); + const bNumHops = numericHops(bHopsStr); + if (aNumHops < bNumHops) return sortOrder === "asc" ? -1 : 1; + if (aNumHops > bNumHops) return sortOrder === "asc" ? 1 : -1; return 0; } - if (aValue < bValue) { - return sortOrder === "asc" ? -1 : 1; - } - if (aValue > bValue) { - return sortOrder === "asc" ? 1 : -1; - } + const aValue = elementA?.props?.children; + const bValue = elementB?.props?.children; + const valA = aValue ?? ""; + const valB = bValue ?? ""; + + // Ensure consistent comparison for potentially different types + const compareA = typeof valA === "string" || typeof valA === "number" + ? valA + : String(valA); + const compareB = typeof valB === "string" || typeof valB === "number" + ? valB + : String(valB); + + if (compareA < compareB) return sortOrder === "asc" ? -1 : 1; + if (compareA > compareB) return sortOrder === "asc" ? 1 : -1; return 0; }); @@ -99,37 +111,52 @@ export const Table = ({ headings, rows }: TableProps) => { : "" }`} onClick={() => heading.sortable && headingSort(heading.title)} - onKeyUp={() => heading.sortable && headingSort(heading.title)} + onKeyUp={(e) => { + if (heading.sortable && (e.key === "Enter" || e.key === " ")) { + headingSort(heading.title); + } + }} + tabIndex={heading.sortable ? 0 : -1} + role="columnheader" + aria-sort={sortColumn === heading.title + ? sortOrder === "asc" ? "ascending" : "descending" + : "none"} > -