Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion generated/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -1866,7 +1866,7 @@
},
{
"id": "components-datatable-features--with-sorting",
"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}"
"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}"
},
{
"id": "components-datatable-features--with-actions",
Expand Down
85 changes: 83 additions & 2 deletions src/DataTable/DataTable.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export const WithSorting = () => {
header: 'Repository',
field: 'name',
rowHeader: true,
sortBy: true,
sortBy: 'alphanumeric',
},
{
header: 'Type',
Expand All @@ -279,7 +279,7 @@ export const WithSorting = () => {
{
header: 'Updated',
field: 'updatedAt',
sortBy: true,
sortBy: 'datetime',
renderCell: row => {
return <RelativeTime date={new Date(row.updatedAt)} />
},
Expand Down Expand Up @@ -318,6 +318,87 @@ export const WithSorting = () => {
)
}

export const WithCustomSorting = () => {
const rows = Array.from(data).sort((a, b) => {
return b.updatedAt - a.updatedAt
})
const sortByDependabotFeatures = (a: Repo, b: Repo): number => {
if (a.securityFeatures.dependabot.length > b.securityFeatures.dependabot.length) {
return -1
} else if (b.securityFeatures.dependabot.length < a.securityFeatures.dependabot.length) {
return 1
}
return 0
}
return (
<Table.Container>
<Table.Title as="h2" id="repositories">
Repositories
</Table.Title>
<Table.Subtitle as="p" id="repositories-subtitle">
A subtitle could appear here to give extra context to the data.
</Table.Subtitle>
<DataTable
aria-labelledby="repositories"
aria-describedby="repositories-subtitle"
data={rows}
columns={[
{
header: 'Repository',
field: 'name',
rowHeader: true,
sortBy: 'alphanumeric',
},
{
header: 'Type',
field: 'type',
renderCell: row => {
return <Label>{uppercase(row.type)}</Label>
},
},
{
header: 'Updated',
field: 'updatedAt',
sortBy: 'datetime',
renderCell: row => {
return <RelativeTime date={new Date(row.updatedAt)} />
},
},
{
header: 'Dependabot',
field: 'securityFeatures.dependabot',
renderCell: row => {
return row.securityFeatures.dependabot.length > 0 ? (
<LabelGroup>
{row.securityFeatures.dependabot.map(feature => {
return <Label key={feature}>{uppercase(feature)}</Label>
})}
</LabelGroup>
) : null
},
sortBy: sortByDependabotFeatures,
},
{
header: 'Code scanning',
field: 'securityFeatures.codeScanning',
renderCell: row => {
return row.securityFeatures.codeScanning.length > 0 ? (
<LabelGroup>
{row.securityFeatures.codeScanning.map(feature => {
return <Label key={feature}>{uppercase(feature)}</Label>
})}
</LabelGroup>
) : null
},
},
]}
initialSortColumn="updatedAt"
initialSortDirection="DESC"
/>
</Table.Container>
)
}

export const WithAction = () => (
<Table.Container>
<Table.Title as="h2" id="repositories">
Expand Down
51 changes: 51 additions & 0 deletions src/DataTable/__tests__/DataTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -757,5 +757,56 @@ describe('DataTable', () => {
[1, 3],
])
})

it('should support a custom sort function', async () => {
const user = userEvent.setup()
const customSortFn = jest.fn().mockImplementation((a, b) => {
return a.value - b.value
})

render(
<DataTable
data={[
{
id: 1,
value: 1,
},
{
id: 2,
value: 2,
},
{
id: 3,
value: 3,
},
]}
columns={[
{
header: 'Value',
field: 'value',
sortBy: customSortFn,
},
]}
initialSortColumn="value"
initialSortDirection="ASC"
/>,
)

function getRowOrder() {
return screen
.getAllByRole('row')
.filter(row => {
return queryByRole(row, 'cell')
})
.map(row => {
const cell = getByRole(row, 'cell')
return cell.textContent
})
}

await user.click(screen.getByText('Value'))
expect(customSortFn).toHaveBeenCalled()
expect(getRowOrder()).toEqual(['3', '2', '1'])
})
})
})
108 changes: 108 additions & 0 deletions src/DataTable/__tests__/sorting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {alphanumeric, basic, datetime} from '../sorting'

const Second = 1000
const Minute = 60 * Second
const Hour = 60 * Minute
const Day = 24 * Hour
const today = Date.now()
const yesterday = today - Day

describe('sorting', () => {
describe('alphanumeric', () => {
test.each([
[
'characters only',
{
input: ['c', 'b', 'a'],
sorted: ['a', 'b', 'c'],
},
],
[
'numbers only',
{
input: ['3', '2', '1'],
sorted: ['1', '2', '3'],
},
],
[
'text with numbers',
{
input: ['test456', 'test789', 'test123'],
sorted: ['test123', 'test456', 'test789'],
},
],
[
'text and numbers',
{
input: ['test-c', '1', 'test-b', '2', 'test-a', '3'],
sorted: ['1', '2', '3', 'test-a', 'test-b', 'test-c'],
},
],
[
'text with same base',
{
input: ['test', 'test-123', 'test-123-test-456'],
sorted: ['test', 'test-123', 'test-123-test-456'],
},
],
[
'text case sensitive',
{
input: ['test456', 'Test456', 'test123'],
sorted: ['test123', 'test456', 'Test456'],
},
],
])('%s', (_name, options) => {
expect(options.input.sort(alphanumeric)).toEqual(options.sorted)
})
})

describe('basic', () => {
test.each([
[
'text',
{
input: ['c', 'b', 'a'],
sorted: ['a', 'b', 'c'],
},
],
[
'numbers',
{
input: [3, 2, 1],
sorted: [1, 2, 3],
},
],
])('%s', (_name, options) => {
expect(options.input.sort(basic)).toEqual(options.sorted)
})
})

describe('datetime', () => {
test.each([
[
'only Date objects',
{
input: [new Date(today), new Date(yesterday)],
sorted: [new Date(yesterday), new Date(today)],
},
],
[
'Date and Date.now()',
{
input: [new Date(today), yesterday],
sorted: [yesterday, new Date(today)],
},
],
[
'only Date.now()',
{
input: [today, yesterday],
sorted: [yesterday, today],
},
],
])('%s', (_name, options) => {
expect(options.input.sort(datetime)).toEqual(options.sorted)
})
})
})
6 changes: 3 additions & 3 deletions src/DataTable/column.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ObjectPaths} from './utils'
import {UniqueRow} from './row'
import {SortStrategies} from './sorting'
import {SortStrategy, CustomSortStrategy} from './sorting'

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

export function createColumnHelper<T extends UniqueRow>() {
Expand Down
Loading