Skip to content

Commit da849f6

Browse files
Add Strata icons to TableSortLabel (#1562)
Co-authored-by: Mayank <9084735+mayank99@users.noreply.github.com>
1 parent b3787ff commit da849f6

8 files changed

Lines changed: 188 additions & 4 deletions

File tree

.changeset/long-spies-bet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stratakit/mui": patch
3+
---
4+
5+
Use StrataKit sorting icons for TableSortLabel

apps/test-app/app/mui/Table.showcase.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
33
* See LICENSE.md in the project root for license terms and full copyright notice.
44
*--------------------------------------------------------------------------------------------*/
5+
56
import TableDefault from "examples/mui/Table.default.tsx";
67
import TableFooter from "examples/mui/Table.footer.tsx";
78
import TableSelect from "examples/mui/Table.select.tsx";
89
import TableSmall from "examples/mui/Table.small.tsx";
10+
import TableSort from "examples/mui/Table.sort.tsx";
911

1012
export default function TableExamples() {
1113
return (
1214
<>
1315
<TableDefault />
1416
<TableSmall />
1517
<TableSelect />
18+
<TableSort />
1619
<TableFooter />
1720
</>
1821
);

apps/website/src/content/docs/components/mui/Table.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ Use a [**Checkbox**](/components/checkbox) within the **Table**'s first column t
3434
:::caution
3535
Placing a "select all" checkbox within the column header is not recommended.
3636
:::
37+
38+
### Sorting
39+
40+
The [`TableSortLabel`](https://mui.com/material-ui/api/table-sort-label) component renders an interactive sort button and displays the current sort direction. A hidden description (`"change sort"`) can be associated with each sort button using `aria-describedby` to provide additional context for assistive technologies without affecting the column header.
41+
42+
The [`sortDirection`](https://mui.com/material-ui/api/table-cell/#table-cell-prop-sortDirection) prop on `TableCell` sets the appropriate [`aria-sort`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-sort) value on the column header so assistive technologies can announce the current sort state.
43+
44+
This example is adapted from the [MUI Table Sorting example](https://mui.com/material-ui/react-table/#sorting-selecting) and follows the accessibility guidance described in [Adrian Roselli's article on sortable table columns](https://adrianroselli.com/2021/04/sortable-table-columns.html).
45+
46+
::example{src="mui/Table.sort"}

examples/mui/Table.sort.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3+
* See LICENSE.md in the project root for license terms and full copyright notice.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import React from "react";
7+
import Paper from "@mui/material/Paper";
8+
import Table from "@mui/material/Table";
9+
import TableBody from "@mui/material/TableBody";
10+
import TableCell from "@mui/material/TableCell";
11+
import TableContainer from "@mui/material/TableContainer";
12+
import TableHead from "@mui/material/TableHead";
13+
import TableRow from "@mui/material/TableRow";
14+
import TableSortLabel from "@mui/material/TableSortLabel";
15+
16+
const rows = [
17+
{ name: "Cupcake", calories: 305, fat: 3.7, carbs: 67, protein: 4.3 },
18+
{ name: "Eclair", calories: 262, fat: 16.0, carbs: 24, protein: 6.0 },
19+
{
20+
name: "Frozen yoghurt",
21+
calories: 159,
22+
fat: 6.0,
23+
carbs: 24,
24+
protein: 4.0,
25+
},
26+
{ name: "Gingerbread", calories: 356, fat: 16.0, carbs: 49, protein: 3.9 },
27+
{
28+
name: "Ice cream sandwich",
29+
calories: 237,
30+
fat: 9.0,
31+
carbs: 37,
32+
protein: 4.3,
33+
},
34+
];
35+
36+
export default () => {
37+
const sortButtonDescriptionId = React.useId();
38+
39+
const [direction, setDirection] =
40+
React.useState<React.ComponentProps<typeof TableSortLabel>["direction"]>(
41+
"asc",
42+
);
43+
const [sortedColumn, setSortedColumn] = React.useState<"calories" | "fat">(
44+
"calories",
45+
);
46+
47+
const updateSort = (column: "calories" | "fat") => {
48+
if (column === sortedColumn) {
49+
setDirection((current) => (current === "asc" ? "desc" : "asc"));
50+
} else {
51+
setSortedColumn(column);
52+
setDirection("asc");
53+
}
54+
};
55+
56+
const sortedRows = React.useMemo(
57+
() =>
58+
Array.from(rows).sort((a, b) =>
59+
direction === "asc"
60+
? a[sortedColumn] - b[sortedColumn]
61+
: b[sortedColumn] - a[sortedColumn],
62+
),
63+
[direction, sortedColumn],
64+
);
65+
66+
return (
67+
<TableContainer render={<Paper />}>
68+
<span hidden id={sortButtonDescriptionId}>
69+
change sort
70+
</span>
71+
<Table>
72+
<caption>Dessert nutrition</caption>
73+
<TableHead>
74+
<TableRow>
75+
<TableCell>Dessert (100g serving)</TableCell>
76+
<TableCell
77+
align="right"
78+
sortDirection={sortedColumn === "calories" && direction}
79+
>
80+
<TableSortLabel
81+
direction={sortedColumn === "calories" ? direction : "asc"}
82+
active={sortedColumn === "calories"}
83+
onClick={() => updateSort("calories")}
84+
aria-describedby={sortButtonDescriptionId}
85+
>
86+
Calories
87+
</TableSortLabel>
88+
</TableCell>
89+
<TableCell
90+
align="right"
91+
sortDirection={sortedColumn === "fat" && direction}
92+
>
93+
<TableSortLabel
94+
direction={sortedColumn === "fat" ? direction : "asc"}
95+
active={sortedColumn === "fat"}
96+
onClick={() => updateSort("fat")}
97+
aria-describedby={sortButtonDescriptionId}
98+
>
99+
Fat&nbsp;(g)
100+
</TableSortLabel>
101+
</TableCell>
102+
</TableRow>
103+
</TableHead>
104+
<TableBody>
105+
{sortedRows.map((row) => (
106+
<TableRow key={row.name}>
107+
<TableCell render={<th />} scope="row">
108+
{row.name}
109+
</TableCell>
110+
<TableCell align="right">{row.calories}</TableCell>
111+
<TableCell align="right">{row.fat}</TableCell>
112+
</TableRow>
113+
))}
114+
</TableBody>
115+
</Table>
116+
</TableContainer>
117+
);
118+
};

packages/mui/src/Icon.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import svgDismiss from "@stratakit/icons/dismiss.svg";
1818
import svgDismissCircle from "@stratakit/icons/dismiss-circle.svg";
1919
import svgError from "@stratakit/icons/error.svg";
2020
import svgInfo from "@stratakit/icons/info.svg";
21+
import svgSortAscending from "@stratakit/icons/sort-ascending.svg";
22+
import svgSortDescending from "@stratakit/icons/sort-descending.svg";
2123
import svgStatusSuccess from "@stratakit/icons/status-success.svg";
2224
import svgWarning from "@stratakit/icons/warning.svg";
2325

@@ -53,6 +55,9 @@ const InfoIcon = createIconComponent(svgInfo);
5355
const SuccessIcon = createIconComponent(svgStatusSuccess);
5456
const WarningIcon = createIconComponent(svgWarning);
5557

58+
const SortAscendingIcon = createIconComponent(svgSortAscending);
59+
const SortDescendingIcon = createIconComponent(svgSortDescending);
60+
5661
// ----------------------------------------------------------------------------
5762

5863
export {
@@ -69,6 +74,8 @@ export {
6974
ErrorIcon,
7075
Icon,
7176
InfoIcon,
77+
SortAscendingIcon,
78+
SortDescendingIcon,
7279
SuccessIcon,
7380
WarningIcon,
7481
};

packages/mui/src/~components/MuiTable.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,19 @@
7777
}
7878
}
7979
}
80+
81+
.MuiTableSortLabel-icon {
82+
transform: none; /* MUI uses single icon and changes transform, but Stratakit uses two icons so don't transform */
83+
margin-inline-end: var(--stratakit-space-x1);
84+
opacity: 0; /* MUI sets opacity:0 to hide the icon when it isn't active and then overrides it with 1 when it should be shown */
85+
86+
:where(.MuiTableSortLabel-root.Mui-active) & {
87+
opacity: 1;
88+
}
89+
90+
@media (any-hover: hover) {
91+
:where(.MuiTableSortLabel-root:hover) & {
92+
opacity: 1;
93+
}
94+
}
95+
}

packages/mui/src/~components/MuiTable.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import * as React from "react";
77
import { Role } from "@ariakit/react/role";
88
import { ThemeProvider } from "@mui/material/styles";
99
import { forwardRef } from "@stratakit/foundations/secret-internals";
10+
import { SortAscendingIcon, SortDescendingIcon } from "../Icon.js";
1011

1112
import type { Theme } from "@mui/material/styles";
13+
import type { TableSortLabelOwnerState } from "@mui/material/TableSortLabel";
1214
import type { BaseProps } from "@stratakit/foundations/secret-internals";
1315

1416
// ----------------------------------------------------------------------------
@@ -68,4 +70,26 @@ DEV: MuiTableCell.displayName = "MuiTableCell";
6870

6971
// ----------------------------------------------------------------------------
7072

71-
export { MuiTableBody, MuiTableCell, MuiTableHead };
73+
interface MuiTableSortLcableIconSlotProps extends BaseProps<"svg"> {
74+
ownerState?: TableSortLabelOwnerState;
75+
as?: React.ElementType;
76+
}
77+
const MuiTableSortLabelIconSlot = forwardRef<
78+
"svg",
79+
MuiTableSortLcableIconSlotProps
80+
>((props, forwardRef) => {
81+
const { ownerState, as: _, ...rest } = props;
82+
switch (ownerState?.direction) {
83+
case "asc":
84+
return <SortAscendingIcon {...rest} ref={forwardRef} />;
85+
case "desc":
86+
return <SortDescendingIcon {...rest} ref={forwardRef} />;
87+
default:
88+
return null;
89+
}
90+
});
91+
DEV: MuiTableSortLabelIconSlot.displayName = "MuiTableSortLabelIconSlot";
92+
93+
// ----------------------------------------------------------------------------
94+
95+
export { MuiTableBody, MuiTableCell, MuiTableHead, MuiTableSortLabelIconSlot };

packages/mui/src/~createTheme.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ import {
4646
MuiTableBody,
4747
MuiTableCell,
4848
MuiTableHead,
49+
MuiTableSortLabelIconSlot,
4950
} from "./~components/MuiTable.js";
5051
import { MuiTab, MuiTabs } from "./~components/MuiTabs.js";
5152
import { MuiToggleButton } from "./~components/MuiToggleButton.js";
5253
import { MuiTypography, variantMapping } from "./~components/MuiTypography.js";
5354
import {
54-
ArrowDownIcon,
5555
CaretsUpDownIcon,
5656
ChevronDownIcon,
5757
ChevronLeftDoubleIcon,
@@ -542,8 +542,9 @@ function createTheme(args: CreateThemeArgs) {
542542
MuiTableSortLabel: {
543543
defaultProps: {
544544
component: Role.span,
545-
// TODO: This should use sort-ascending and sort-descending icons, but that requires disabling MUI's built-in icon rotation.
546-
IconComponent: ArrowDownIcon,
545+
slots: {
546+
icon: MuiTableSortLabelIconSlot,
547+
},
547548
},
548549
},
549550
MuiTablePaginationActions: {

0 commit comments

Comments
 (0)