Skip to content

Commit 6e052f2

Browse files
authored
feat(DataTable): add support for alphanumeric, datatable, and custom sort functions (#3001)
* chore: check-in work * chore: clean-up work * Update generated/components.json * docs: add custom sorting story --------- Co-authored-by: Josh Black <joshblack@users.noreply.github.com>
1 parent ae5cd7f commit 6e052f2

File tree

7 files changed

+363
-10
lines changed

7 files changed

+363
-10
lines changed

generated/components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1866,7 +1866,7 @@
18661866
},
18671867
{
18681868
"id": "components-datatable-features--with-sorting",
1869-
"code": "() => {\n const rows = Array.from(data).sort((a, b) => {\n return b.updatedAt - a.updatedAt\n })\n return (\n <Table.Container>\n <Table.Title as=\"h2\" id=\"repositories\">\n Repositories\n </Table.Title>\n <Table.Subtitle as=\"p\" id=\"repositories-subtitle\">\n A subtitle could appear here to give extra context to the data.\n </Table.Subtitle>\n <DataTable\n aria-labelledby=\"repositories\"\n aria-describedby=\"repositories-subtitle\"\n data={rows}\n columns={[\n {\n header: 'Repository',\n field: 'name',\n rowHeader: true,\n sortBy: true,\n },\n {\n header: 'Type',\n field: 'type',\n renderCell: (row) => {\n return <Label>{uppercase(row.type)}</Label>\n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n sortBy: true,\n renderCell: (row) => {\n return <RelativeTime date={new Date(row.updatedAt)} />\n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n <LabelGroup>\n {row.securityFeatures.dependabot.map((feature) => {\n return <Label key={feature}>{uppercase(feature)}</Label>\n })}\n </LabelGroup>\n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n <LabelGroup>\n {row.securityFeatures.codeScanning.map((feature) => {\n return <Label key={feature}>{uppercase(feature)}</Label>\n })}\n </LabelGroup>\n ) : null\n },\n },\n ]}\n initialSortColumn=\"updatedAt\"\n initialSortDirection=\"DESC\"\n />\n </Table.Container>\n )\n}"
1869+
"code": "() => {\n const rows = Array.from(data).sort((a, b) => {\n return b.updatedAt - a.updatedAt\n })\n return (\n <Table.Container>\n <Table.Title as=\"h2\" id=\"repositories\">\n Repositories\n </Table.Title>\n <Table.Subtitle as=\"p\" id=\"repositories-subtitle\">\n A subtitle could appear here to give extra context to the data.\n </Table.Subtitle>\n <DataTable\n aria-labelledby=\"repositories\"\n aria-describedby=\"repositories-subtitle\"\n data={rows}\n columns={[\n {\n header: 'Repository',\n field: 'name',\n rowHeader: true,\n sortBy: 'alphanumeric',\n },\n {\n header: 'Type',\n field: 'type',\n renderCell: (row) => {\n return <Label>{uppercase(row.type)}</Label>\n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n sortBy: 'datetime',\n renderCell: (row) => {\n return <RelativeTime date={new Date(row.updatedAt)} />\n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n <LabelGroup>\n {row.securityFeatures.dependabot.map((feature) => {\n return <Label key={feature}>{uppercase(feature)}</Label>\n })}\n </LabelGroup>\n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n <LabelGroup>\n {row.securityFeatures.codeScanning.map((feature) => {\n return <Label key={feature}>{uppercase(feature)}</Label>\n })}\n </LabelGroup>\n ) : null\n },\n },\n ]}\n initialSortColumn=\"updatedAt\"\n initialSortDirection=\"DESC\"\n />\n </Table.Container>\n )\n}"
18701870
},
18711871
{
18721872
"id": "components-datatable-features--with-actions",

src/DataTable/DataTable.features.stories.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export const WithSorting = () => {
267267
header: 'Repository',
268268
field: 'name',
269269
rowHeader: true,
270-
sortBy: true,
270+
sortBy: 'alphanumeric',
271271
},
272272
{
273273
header: 'Type',
@@ -279,7 +279,7 @@ export const WithSorting = () => {
279279
{
280280
header: 'Updated',
281281
field: 'updatedAt',
282-
sortBy: true,
282+
sortBy: 'datetime',
283283
renderCell: row => {
284284
return <RelativeTime date={new Date(row.updatedAt)} />
285285
},
@@ -318,6 +318,87 @@ export const WithSorting = () => {
318318
)
319319
}
320320

321+
export const WithCustomSorting = () => {
322+
const rows = Array.from(data).sort((a, b) => {
323+
return b.updatedAt - a.updatedAt
324+
})
325+
const sortByDependabotFeatures = (a: Repo, b: Repo): number => {
326+
if (a.securityFeatures.dependabot.length > b.securityFeatures.dependabot.length) {
327+
return -1
328+
} else if (b.securityFeatures.dependabot.length < a.securityFeatures.dependabot.length) {
329+
return 1
330+
}
331+
return 0
332+
}
333+
return (
334+
<Table.Container>
335+
<Table.Title as="h2" id="repositories">
336+
Repositories
337+
</Table.Title>
338+
<Table.Subtitle as="p" id="repositories-subtitle">
339+
A subtitle could appear here to give extra context to the data.
340+
</Table.Subtitle>
341+
<DataTable
342+
aria-labelledby="repositories"
343+
aria-describedby="repositories-subtitle"
344+
data={rows}
345+
columns={[
346+
{
347+
header: 'Repository',
348+
field: 'name',
349+
rowHeader: true,
350+
sortBy: 'alphanumeric',
351+
},
352+
{
353+
header: 'Type',
354+
field: 'type',
355+
renderCell: row => {
356+
return <Label>{uppercase(row.type)}</Label>
357+
},
358+
},
359+
{
360+
header: 'Updated',
361+
field: 'updatedAt',
362+
sortBy: 'datetime',
363+
renderCell: row => {
364+
return <RelativeTime date={new Date(row.updatedAt)} />
365+
},
366+
},
367+
{
368+
header: 'Dependabot',
369+
field: 'securityFeatures.dependabot',
370+
renderCell: row => {
371+
return row.securityFeatures.dependabot.length > 0 ? (
372+
<LabelGroup>
373+
{row.securityFeatures.dependabot.map(feature => {
374+
return <Label key={feature}>{uppercase(feature)}</Label>
375+
})}
376+
</LabelGroup>
377+
) : null
378+
},
379+
sortBy: sortByDependabotFeatures,
380+
},
381+
{
382+
header: 'Code scanning',
383+
field: 'securityFeatures.codeScanning',
384+
renderCell: row => {
385+
return row.securityFeatures.codeScanning.length > 0 ? (
386+
<LabelGroup>
387+
{row.securityFeatures.codeScanning.map(feature => {
388+
return <Label key={feature}>{uppercase(feature)}</Label>
389+
})}
390+
</LabelGroup>
391+
) : null
392+
},
393+
},
394+
]}
395+
initialSortColumn="updatedAt"
396+
initialSortDirection="DESC"
397+
/>
398+
</Table.Container>
399+
)
400+
}
401+
321402
export const WithAction = () => (
322403
<Table.Container>
323404
<Table.Title as="h2" id="repositories">

src/DataTable/__tests__/DataTable.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,5 +757,56 @@ describe('DataTable', () => {
757757
[1, 3],
758758
])
759759
})
760+
761+
it('should support a custom sort function', async () => {
762+
const user = userEvent.setup()
763+
const customSortFn = jest.fn().mockImplementation((a, b) => {
764+
return a.value - b.value
765+
})
766+
767+
render(
768+
<DataTable
769+
data={[
770+
{
771+
id: 1,
772+
value: 1,
773+
},
774+
{
775+
id: 2,
776+
value: 2,
777+
},
778+
{
779+
id: 3,
780+
value: 3,
781+
},
782+
]}
783+
columns={[
784+
{
785+
header: 'Value',
786+
field: 'value',
787+
sortBy: customSortFn,
788+
},
789+
]}
790+
initialSortColumn="value"
791+
initialSortDirection="ASC"
792+
/>,
793+
)
794+
795+
function getRowOrder() {
796+
return screen
797+
.getAllByRole('row')
798+
.filter(row => {
799+
return queryByRole(row, 'cell')
800+
})
801+
.map(row => {
802+
const cell = getByRole(row, 'cell')
803+
return cell.textContent
804+
})
805+
}
806+
807+
await user.click(screen.getByText('Value'))
808+
expect(customSortFn).toHaveBeenCalled()
809+
expect(getRowOrder()).toEqual(['3', '2', '1'])
810+
})
760811
})
761812
})
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {alphanumeric, basic, datetime} from '../sorting'
2+
3+
const Second = 1000
4+
const Minute = 60 * Second
5+
const Hour = 60 * Minute
6+
const Day = 24 * Hour
7+
const today = Date.now()
8+
const yesterday = today - Day
9+
10+
describe('sorting', () => {
11+
describe('alphanumeric', () => {
12+
test.each([
13+
[
14+
'characters only',
15+
{
16+
input: ['c', 'b', 'a'],
17+
sorted: ['a', 'b', 'c'],
18+
},
19+
],
20+
[
21+
'numbers only',
22+
{
23+
input: ['3', '2', '1'],
24+
sorted: ['1', '2', '3'],
25+
},
26+
],
27+
[
28+
'text with numbers',
29+
{
30+
input: ['test456', 'test789', 'test123'],
31+
sorted: ['test123', 'test456', 'test789'],
32+
},
33+
],
34+
[
35+
'text and numbers',
36+
{
37+
input: ['test-c', '1', 'test-b', '2', 'test-a', '3'],
38+
sorted: ['1', '2', '3', 'test-a', 'test-b', 'test-c'],
39+
},
40+
],
41+
[
42+
'text with same base',
43+
{
44+
input: ['test', 'test-123', 'test-123-test-456'],
45+
sorted: ['test', 'test-123', 'test-123-test-456'],
46+
},
47+
],
48+
[
49+
'text case sensitive',
50+
{
51+
input: ['test456', 'Test456', 'test123'],
52+
sorted: ['test123', 'test456', 'Test456'],
53+
},
54+
],
55+
])('%s', (_name, options) => {
56+
expect(options.input.sort(alphanumeric)).toEqual(options.sorted)
57+
})
58+
})
59+
60+
describe('basic', () => {
61+
test.each([
62+
[
63+
'text',
64+
{
65+
input: ['c', 'b', 'a'],
66+
sorted: ['a', 'b', 'c'],
67+
},
68+
],
69+
[
70+
'numbers',
71+
{
72+
input: [3, 2, 1],
73+
sorted: [1, 2, 3],
74+
},
75+
],
76+
])('%s', (_name, options) => {
77+
expect(options.input.sort(basic)).toEqual(options.sorted)
78+
})
79+
})
80+
81+
describe('datetime', () => {
82+
test.each([
83+
[
84+
'only Date objects',
85+
{
86+
input: [new Date(today), new Date(yesterday)],
87+
sorted: [new Date(yesterday), new Date(today)],
88+
},
89+
],
90+
[
91+
'Date and Date.now()',
92+
{
93+
input: [new Date(today), yesterday],
94+
sorted: [yesterday, new Date(today)],
95+
},
96+
],
97+
[
98+
'only Date.now()',
99+
{
100+
input: [today, yesterday],
101+
sorted: [yesterday, today],
102+
},
103+
],
104+
])('%s', (_name, options) => {
105+
expect(options.input.sort(datetime)).toEqual(options.sorted)
106+
})
107+
})
108+
})

src/DataTable/column.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ObjectPaths} from './utils'
22
import {UniqueRow} from './row'
3-
import {SortStrategies} from './sorting'
3+
import {SortStrategy, CustomSortStrategy} from './sorting'
44

55
export interface Column<Data extends UniqueRow> {
66
id?: string
@@ -14,7 +14,7 @@ export interface Column<Data extends UniqueRow> {
1414
/**
1515
* Optionally provide a field to render for this column. This may be the key
1616
* of the object or a string that accesses nested objects through `.`. For
17-
* exmaple: `field: a.b.c`
17+
* example: `field: a.b.c`
1818
*
1919
* Alternatively, you may provide a `renderCell` for this column to render the
2020
* field in a row
@@ -37,7 +37,7 @@ export interface Column<Data extends UniqueRow> {
3737
* Specify if the table should sort by this column and, if applicable, a
3838
* specific sort strategy or custom sort strategy
3939
*/
40-
sortBy?: boolean | SortStrategies
40+
sortBy?: boolean | SortStrategy | CustomSortStrategy<Data>
4141
}
4242

4343
export function createColumnHelper<T extends UniqueRow>() {

0 commit comments

Comments
 (0)