Skip to content

Commit e447cc5

Browse files
committed
fix(table): Sortable Column Alignment
Closes #1321
1 parent 2e7a87a commit e447cc5

6 files changed

Lines changed: 2784 additions & 20 deletions

File tree

packages/table/src/TableCell.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
214214
iconAfter={sortIconAfter}
215215
sortOrder={sortOrder}
216216
rotated={sortIconRotated}
217+
hAlign={hAlign}
217218
>
218219
{children}
219220
</TableCellContent>

packages/table/src/TableCellContent.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from "@react-md/icon";
99
import { bem } from "@react-md/utils";
1010

11+
import type { TableCellHorizontalAlignment } from "./config";
12+
1113
export type SortOrder = "ascending" | "descending" | "none" | "other";
1214

1315
export interface TableCellContentProps extends TextIconSpacingProps {
@@ -31,6 +33,12 @@ export interface TableCellContentProps extends TextIconSpacingProps {
3133
* Boolean if the icon should be rotated.
3234
*/
3335
rotated?: boolean;
36+
37+
/**
38+
* @remarks \@since 4.0.3
39+
* @see {@link TableCellHorizontalAlignment}
40+
*/
41+
hAlign?: TableCellHorizontalAlignment;
3442
}
3543

3644
const block = bem("rmd-table-cell");
@@ -53,6 +61,7 @@ export const TableCellContent = forwardRef<
5361
sortOrder,
5462
children,
5563
rotated: propRotated,
64+
hAlign = "left",
5665
...props
5766
},
5867
ref
@@ -73,7 +82,12 @@ export const TableCellContent = forwardRef<
7382
id={id}
7483
ref={ref}
7584
style={style}
76-
className={cn(block("child"), className)}
85+
className={cn(
86+
block("child", {
87+
[hAlign]: hAlign !== "left",
88+
}),
89+
className
90+
)}
7791
>
7892
<TextIconSpacing {...props} icon={icon}>
7993
{children}

packages/table/src/__tests__/Table.tsx

Lines changed: 252 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { ReactElement } from "react";
2-
import { render } from "@testing-library/react";
1+
import { ReactElement, useState } from "react";
2+
import { fireEvent, render } from "@testing-library/react";
3+
import { upperFirst } from "lodash";
34

45
import { Table, TableProps } from "../Table";
5-
import { TableHeader } from "../TableHeader";
6-
import { TableRow } from "../TableRow";
7-
import { TableCell } from "../TableCell";
86
import { TableBody } from "../TableBody";
7+
import { TableCell } from "../TableCell";
8+
import { SortOrder } from "../TableCellContent";
99
import { TableContainer } from "../TableContainer";
10+
import { TableHeader } from "../TableHeader";
11+
import { TableRow } from "../TableRow";
1012

1113
function Test(props: TableProps): ReactElement {
1214
return (
@@ -53,4 +55,249 @@ describe("Table", () => {
5355
);
5456
expect(container).toMatchSnapshot();
5557
});
58+
59+
it("should allow for sorting", () => {
60+
interface Dessert {
61+
name: string;
62+
calories: number;
63+
fat: number;
64+
carbs: number;
65+
protein: number;
66+
sodium: number;
67+
calcium: number;
68+
iron: number;
69+
type: "Ice cream" | "Pastry" | "Other";
70+
}
71+
type DessertKey = keyof Dessert;
72+
73+
const desserts: readonly Dessert[] = [
74+
{
75+
name: "Frozen yogurt",
76+
type: "Ice cream",
77+
calories: 159,
78+
fat: 6.0,
79+
carbs: 24,
80+
protein: 4.0,
81+
sodium: 87,
82+
calcium: 14,
83+
iron: 1,
84+
},
85+
{
86+
name: "Ice cream sandwhich",
87+
type: "Ice cream",
88+
calories: 237,
89+
fat: 9.0,
90+
carbs: 37,
91+
protein: 4.3,
92+
sodium: 129,
93+
calcium: 8,
94+
iron: 1,
95+
},
96+
{
97+
name: "Eclair",
98+
type: "Pastry",
99+
calories: 262,
100+
fat: 16.0,
101+
carbs: 37,
102+
protein: 6.0,
103+
sodium: 337,
104+
calcium: 6,
105+
iron: 7,
106+
},
107+
{
108+
name: "Cupcake",
109+
type: "Pastry",
110+
calories: 305,
111+
fat: 3.7,
112+
carbs: 67,
113+
protein: 4.3,
114+
sodium: 413,
115+
calcium: 3,
116+
iron: 8,
117+
},
118+
{
119+
name: "Gingerbread",
120+
type: "Pastry",
121+
calories: 356,
122+
fat: 16.0,
123+
carbs: 49,
124+
protein: 3.9,
125+
sodium: 327,
126+
calcium: 7,
127+
iron: 16,
128+
},
129+
{
130+
name: "Jelly bean",
131+
type: "Other",
132+
calories: 375,
133+
fat: 0.0,
134+
carbs: 94,
135+
protein: 0.0,
136+
sodium: 50,
137+
calcium: 0,
138+
iron: 0,
139+
},
140+
{
141+
name: "Lollipop",
142+
type: "Other",
143+
calories: 392,
144+
fat: 0.2,
145+
carbs: 98,
146+
protein: 0.0,
147+
sodium: 38,
148+
calcium: 0,
149+
iron: 2,
150+
},
151+
{
152+
name: "Honeycomb",
153+
type: "Other",
154+
calories: 408,
155+
fat: 3.2,
156+
carbs: 87,
157+
protein: 6.5,
158+
sodium: 562,
159+
calcium: 0,
160+
iron: 45,
161+
},
162+
{
163+
name: "Donut",
164+
type: "Pastry",
165+
calories: 52,
166+
fat: 25.0,
167+
carbs: 51,
168+
protein: 4.9,
169+
sodium: 326,
170+
calcium: 2,
171+
iron: 22,
172+
},
173+
{
174+
name: "KitKat",
175+
type: "Other",
176+
calories: 16,
177+
fat: 6.0,
178+
carbs: 65,
179+
protein: 7.0,
180+
sodium: 54,
181+
calcium: 12,
182+
iron: 6,
183+
},
184+
];
185+
186+
const sort = (key: DessertKey, ascending: boolean): readonly Dessert[] => {
187+
const sorted = desserts.slice();
188+
sorted.sort((a, b) => {
189+
const aValue = a[key];
190+
const bValue = b[key];
191+
192+
const value =
193+
typeof aValue === "number"
194+
? aValue - (bValue as number)
195+
: aValue.localeCompare(bValue as string);
196+
197+
return value * (ascending ? 1 : -1);
198+
});
199+
200+
return sorted;
201+
};
202+
203+
const columns = Object.keys(desserts[0]) as DessertKey[];
204+
205+
interface SortState {
206+
data: readonly Dessert[];
207+
sortKey: DessertKey;
208+
sortOrder: SortOrder;
209+
}
210+
211+
function Test(): ReactElement {
212+
const [state, setState] = useState<SortState>(() => ({
213+
data: sort("name", true),
214+
sortKey: "name",
215+
sortOrder: "ascending",
216+
}));
217+
218+
const update = (sortKey: DessertKey): void => {
219+
setState((prevState) => {
220+
const prevSortKey = prevState.sortKey;
221+
const prevSortOrder = prevState.sortOrder;
222+
223+
let sortOrder: SortOrder;
224+
if (sortKey === prevSortKey) {
225+
// it's the same column, so toggle the sort order
226+
sortOrder =
227+
prevSortOrder === "ascending" ? "descending" : "ascending";
228+
} else {
229+
// it's a new column to sort by, so default to ascending for the name column
230+
// but descending for all the rest.
231+
sortOrder = sortKey === "name" ? "ascending" : "descending";
232+
}
233+
234+
return {
235+
data: sort(sortKey, sortOrder === "ascending"),
236+
sortKey,
237+
sortOrder,
238+
};
239+
});
240+
};
241+
242+
const { data, sortKey, sortOrder } = state;
243+
return (
244+
<TableContainer>
245+
<Table fullWidth>
246+
<TableHeader>
247+
<TableRow>
248+
{columns.map((name) => (
249+
<TableCell
250+
key={name}
251+
aria-sort={name === sortKey ? sortOrder : "none"}
252+
onClick={() => update(name)}
253+
>
254+
{upperFirst(name)}
255+
</TableCell>
256+
))}
257+
</TableRow>
258+
</TableHeader>
259+
<TableBody>
260+
{data.map((dessert) => (
261+
<TableRow key={dessert.name}>
262+
{columns.map((key) => (
263+
<TableCell
264+
key={key}
265+
grow={key === "name"}
266+
hAlign={
267+
typeof dessert[key] === "number" ? "right" : undefined
268+
}
269+
>
270+
{dessert[key]}
271+
</TableCell>
272+
))}
273+
</TableRow>
274+
))}
275+
</TableBody>
276+
</Table>
277+
</TableContainer>
278+
);
279+
}
280+
const { container, getByRole } = render(<Test />);
281+
282+
const nameColumn = getByRole("columnheader", { name: "Name" });
283+
const typeColumn = getByRole("columnheader", { name: "Type" });
284+
expect(nameColumn).toHaveAttribute("aria-sort", "ascending");
285+
expect(typeColumn).not.toHaveAttribute("aria-sort");
286+
expect(container).toMatchSnapshot();
287+
288+
fireEvent.click(getByRole("button", { name: "Name" }));
289+
expect(nameColumn).toHaveAttribute("aria-sort", "descending");
290+
expect(typeColumn).not.toHaveAttribute("aria-sort");
291+
expect(container).toMatchSnapshot();
292+
293+
fireEvent.click(getByRole("button", { name: "Type" }));
294+
expect(nameColumn).not.toHaveAttribute("aria-sort");
295+
expect(typeColumn).toHaveAttribute("aria-sort", "descending");
296+
expect(container).toMatchSnapshot();
297+
298+
fireEvent.click(getByRole("button", { name: "Name" }));
299+
expect(nameColumn).toHaveAttribute("aria-sort", "ascending");
300+
expect(typeColumn).not.toHaveAttribute("aria-sort");
301+
expect(container).toMatchSnapshot();
302+
});
56303
});

0 commit comments

Comments
 (0)