@indico-data/design-system 2.47.3 → 2.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/index.d.ts +1 -0
- package/lib/components/pagination/Pagination.d.ts +2 -0
- package/lib/components/pagination/Pagination.stories.d.ts +6 -0
- package/lib/components/pagination/__tests__/Pagination.test.d.ts +1 -0
- package/lib/components/pagination/index.d.ts +1 -0
- package/lib/components/pagination/types.d.ts +6 -0
- package/lib/components/table/__tests__/Table.test.d.ts +1 -0
- package/lib/components/table/components/TablePagination.d.ts +9 -0
- package/lib/components/table/components/__tests__/TablePagination.test.d.ts +1 -0
- package/lib/components/table/sampleData.d.ts +2 -0
- package/lib/components/table/types.d.ts +5 -4
- package/lib/index.css +50 -8
- package/lib/index.d.ts +5 -5
- package/lib/index.esm.css +50 -8
- package/lib/index.esm.js +69 -14
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +68 -13
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/pagination/Pagination.mdx +31 -0
- package/src/components/pagination/Pagination.stories.tsx +80 -0
- package/src/components/pagination/Pagination.tsx +117 -0
- package/src/components/pagination/__tests__/Pagination.test.tsx +91 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/styles/Pagination.scss +22 -0
- package/src/components/pagination/types.ts +6 -0
- package/src/components/table/Table.mdx +2 -0
- package/src/components/table/Table.stories.tsx +20 -28
- package/src/components/table/Table.tsx +9 -1
- package/src/components/table/__tests__/Table.test.tsx +10 -0
- package/src/components/table/components/TablePagination.tsx +44 -0
- package/src/components/table/components/__tests__/TablePagination.test.tsx +17 -0
- package/src/components/table/sampleData.ts +110 -0
- package/src/components/table/styles/Table.scss +40 -9
- package/src/components/table/styles/_variables.scss +1 -0
- package/src/components/table/types.ts +6 -6
- package/src/setup/setupIcons.ts +4 -0
- package/src/styles/index.scss +1 -0
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
|
|
2
|
+
import * as Pagination from './Pagination.stories';
|
|
3
|
+
import { Container, Row, Col } from '../grid';
|
|
4
|
+
import { fas } from '@fortawesome/free-solid-svg-icons';
|
|
5
|
+
import { registerFontAwesomeIcons } from '@/setup/setupIcons';
|
|
6
|
+
import { indiconDefinitions } from '@/components/icons/indicons';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<Meta title="Layout/Pagination" name="Pagination" of={Pagination} />
|
|
10
|
+
|
|
11
|
+
# Pagination
|
|
12
|
+
|
|
13
|
+
<Canvas of={Pagination.Default} />
|
|
14
|
+
|
|
15
|
+
### The following props are available for the Pagination component:
|
|
16
|
+
|
|
17
|
+
<Controls of={Pagination.Default} />
|
|
18
|
+
|
|
19
|
+
### Usage
|
|
20
|
+
|
|
21
|
+
The pagination component is used to navigate through a list of items. It is already baked into the Table component. To manage the pagination, there is an `onChange` callback that returns the next page, previous page, or the page entered in the input field. This will then be used by your implementation to update the page.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
|
|
25
|
+
<Pagination totalPages={10} currentPage={1} onChange={(page) => console.log(page)} />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
The input field allows the user to enter an invalid number but it will warn them by highlighting the input in red. When the user clicks out, it will then revert to its original value.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Pagination } from './Pagination';
|
|
3
|
+
import { Col, Container, Row } from '../grid';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Layout/Pagination',
|
|
8
|
+
component: Pagination,
|
|
9
|
+
argTypes: {
|
|
10
|
+
className: {
|
|
11
|
+
control: false,
|
|
12
|
+
description: 'The css class name for the pagination component',
|
|
13
|
+
table: {
|
|
14
|
+
category: 'Props',
|
|
15
|
+
type: {
|
|
16
|
+
summary: 'css class',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
totalPages: {
|
|
21
|
+
control: 'number',
|
|
22
|
+
description: 'The total number of pages to be displayed',
|
|
23
|
+
table: {
|
|
24
|
+
category: 'Props',
|
|
25
|
+
type: {
|
|
26
|
+
summary: 'number',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
currentPage: {
|
|
31
|
+
control: 'number',
|
|
32
|
+
description: 'The current page displayed in the input field.',
|
|
33
|
+
table: {
|
|
34
|
+
category: 'Props',
|
|
35
|
+
type: {
|
|
36
|
+
summary: 'number',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
onChange: {
|
|
41
|
+
action: 'change',
|
|
42
|
+
description: 'The callback function that is called when the page changes.',
|
|
43
|
+
table: {
|
|
44
|
+
category: 'Callbacks',
|
|
45
|
+
type: {
|
|
46
|
+
summary: '(page: number) => void',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
|
|
55
|
+
type Story = StoryObj<typeof Pagination>;
|
|
56
|
+
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
totalPages: 10,
|
|
60
|
+
currentPage: 1,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
render: (args) => {
|
|
64
|
+
const [currentPage, setCurrentPage] = useState(args.currentPage);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setCurrentPage(args.currentPage);
|
|
68
|
+
}, [args.currentPage]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Container>
|
|
72
|
+
<Row>
|
|
73
|
+
<Col sm={4}>
|
|
74
|
+
<Pagination {...args} currentPage={currentPage} onChange={setCurrentPage} />
|
|
75
|
+
</Col>
|
|
76
|
+
</Row>
|
|
77
|
+
</Container>
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { PaginationProps } from './types';
|
|
4
|
+
import { Container, Row, Col } from '../grid';
|
|
5
|
+
import { Input } from '../forms/input';
|
|
6
|
+
import { Button } from '../button';
|
|
7
|
+
|
|
8
|
+
export const Pagination = ({
|
|
9
|
+
totalPages,
|
|
10
|
+
currentPage = 1,
|
|
11
|
+
onChange,
|
|
12
|
+
className,
|
|
13
|
+
...rest
|
|
14
|
+
}: PaginationProps) => {
|
|
15
|
+
const [inputValue, setInputValue] = useState(currentPage.toString());
|
|
16
|
+
const totalPagesText = `of ${totalPages}`;
|
|
17
|
+
const classes = classNames('pagination', className);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setInputValue(currentPage.toString());
|
|
21
|
+
}, [currentPage]);
|
|
22
|
+
|
|
23
|
+
const handleNextPage = () => {
|
|
24
|
+
if (currentPage < totalPages) {
|
|
25
|
+
onChange?.(currentPage + 1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handlePreviousPage = () => {
|
|
30
|
+
if (currentPage > 1) {
|
|
31
|
+
onChange?.(currentPage - 1);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const validateAndUpdatePage = (value: string) => {
|
|
36
|
+
// If empty or invalid, reset to current page
|
|
37
|
+
if (!value) {
|
|
38
|
+
setInputValue(currentPage.toString());
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const page = Number(value);
|
|
43
|
+
if (!isNaN(page) && page > 0 && page <= totalPages) {
|
|
44
|
+
onChange?.(page);
|
|
45
|
+
} else {
|
|
46
|
+
setInputValue(currentPage.toString());
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isNextButtonDisabled = currentPage === totalPages;
|
|
51
|
+
const isPreviousButtonDisabled = currentPage === 1;
|
|
52
|
+
|
|
53
|
+
const hasError = Number(inputValue) > totalPages || Number(inputValue) < 1;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={classes} {...rest}>
|
|
57
|
+
<Container>
|
|
58
|
+
<Row gutterWidth={12} align="center">
|
|
59
|
+
<Col xs="content">
|
|
60
|
+
<div className="pagination__previous">
|
|
61
|
+
<Button
|
|
62
|
+
data-testid="pagination-previous-button"
|
|
63
|
+
ariaLabel="Previous Page"
|
|
64
|
+
variant="link"
|
|
65
|
+
onClick={handlePreviousPage}
|
|
66
|
+
iconLeft="chevron-left"
|
|
67
|
+
isDisabled={isPreviousButtonDisabled}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</Col>
|
|
71
|
+
<Col xs="content">
|
|
72
|
+
<div className="pagination__current-page">
|
|
73
|
+
<Input
|
|
74
|
+
data-testid="pagination-current-page-input"
|
|
75
|
+
className={classNames('pagination__current-page-input', {
|
|
76
|
+
'has-error': hasError,
|
|
77
|
+
})}
|
|
78
|
+
value={inputValue}
|
|
79
|
+
name="currentPage"
|
|
80
|
+
label="Current Page"
|
|
81
|
+
hasHiddenLabel
|
|
82
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
83
|
+
if (e.key === 'Enter') {
|
|
84
|
+
validateAndUpdatePage(e.currentTarget.value);
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
88
|
+
const value = e.currentTarget.value;
|
|
89
|
+
// Allow empty value or numbers
|
|
90
|
+
if (value === '' || /^\d*$/.test(value)) {
|
|
91
|
+
setInputValue(value);
|
|
92
|
+
}
|
|
93
|
+
}}
|
|
94
|
+
onBlur={(e) => validateAndUpdatePage(e.currentTarget.value)}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</Col>
|
|
98
|
+
<Col xs="content">
|
|
99
|
+
<p className="pagination__page-total">{totalPagesText}</p>
|
|
100
|
+
</Col>
|
|
101
|
+
<Col xs="content">
|
|
102
|
+
<div className="pagination__next">
|
|
103
|
+
<Button
|
|
104
|
+
data-testid="pagination-next-button"
|
|
105
|
+
ariaLabel="Next Page"
|
|
106
|
+
variant="link"
|
|
107
|
+
onClick={handleNextPage}
|
|
108
|
+
iconLeft="chevron-right"
|
|
109
|
+
isDisabled={isNextButtonDisabled}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</Col>
|
|
113
|
+
</Row>
|
|
114
|
+
</Container>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { Pagination } from '../Pagination';
|
|
3
|
+
|
|
4
|
+
describe('Pagination', () => {
|
|
5
|
+
it('fires the onChange callback when the user clicks the next button', () => {
|
|
6
|
+
const onChange = jest.fn();
|
|
7
|
+
render(<Pagination totalPages={10} currentPage={1} onChange={onChange} />);
|
|
8
|
+
fireEvent.click(screen.getByTestId('pagination-next-button'));
|
|
9
|
+
expect(onChange).toHaveBeenCalledWith(2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('fires the onChange callback when the user clicks the previous button', () => {
|
|
13
|
+
const onChange = jest.fn();
|
|
14
|
+
render(<Pagination totalPages={10} currentPage={2} onChange={onChange} />);
|
|
15
|
+
fireEvent.click(screen.getByTestId('pagination-previous-button'));
|
|
16
|
+
expect(onChange).toHaveBeenCalledWith(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('does not fire the onchange callback when the user clicks next and they are already on the last page', () => {
|
|
20
|
+
const onChange = jest.fn();
|
|
21
|
+
render(<Pagination totalPages={10} currentPage={10} onChange={onChange} />);
|
|
22
|
+
fireEvent.click(screen.getByTestId('pagination-next-button'));
|
|
23
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does not fire the onchange callback when the user clicks previous and they are already on the first page', () => {
|
|
27
|
+
const onChange = jest.fn();
|
|
28
|
+
render(<Pagination totalPages={10} currentPage={1} onChange={onChange} />);
|
|
29
|
+
fireEvent.click(screen.getByTestId('pagination-previous-button'));
|
|
30
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('fires the onchange callback when the user enters a value in the input field', () => {
|
|
34
|
+
const onChange = jest.fn();
|
|
35
|
+
render(<Pagination totalPages={10} currentPage={1} onChange={onChange} />);
|
|
36
|
+
fireEvent.change(screen.getByTestId('pagination-current-page-input'), {
|
|
37
|
+
target: { value: '2' },
|
|
38
|
+
});
|
|
39
|
+
fireEvent.keyDown(screen.getByTestId('pagination-current-page-input'), {
|
|
40
|
+
key: 'Enter',
|
|
41
|
+
code: 'Enter',
|
|
42
|
+
});
|
|
43
|
+
expect(onChange).toHaveBeenCalledWith(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not fire the onChange callback when the user enters an invalid value in the input field', () => {
|
|
47
|
+
const onChange = jest.fn();
|
|
48
|
+
render(<Pagination totalPages={10} currentPage={1} onChange={onChange} />);
|
|
49
|
+
|
|
50
|
+
// Test non-numeric input
|
|
51
|
+
fireEvent.change(screen.getByTestId('pagination-current-page-input'), {
|
|
52
|
+
target: { value: 'abc' },
|
|
53
|
+
});
|
|
54
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
55
|
+
|
|
56
|
+
// Test empty input
|
|
57
|
+
fireEvent.change(screen.getByTestId('pagination-current-page-input'), {
|
|
58
|
+
target: { value: '' },
|
|
59
|
+
});
|
|
60
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
61
|
+
|
|
62
|
+
// Test out of range input
|
|
63
|
+
fireEvent.change(screen.getByTestId('pagination-current-page-input'), {
|
|
64
|
+
target: { value: '0' },
|
|
65
|
+
});
|
|
66
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
67
|
+
|
|
68
|
+
fireEvent.change(screen.getByTestId('pagination-current-page-input'), {
|
|
69
|
+
target: { value: '11' },
|
|
70
|
+
});
|
|
71
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('disables the next button when the user is on the last page', () => {
|
|
75
|
+
render(<Pagination totalPages={10} currentPage={10} onChange={() => {}} />);
|
|
76
|
+
expect(screen.getByTestId('pagination-next-button')).toBeDisabled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('disables the previous button when the user is on the first page', () => {
|
|
80
|
+
render(<Pagination totalPages={10} currentPage={1} onChange={() => {}} />);
|
|
81
|
+
expect(screen.getByTestId('pagination-previous-button')).toBeDisabled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('adds the has-error class to the input field when the user enters an invalid value', () => {
|
|
85
|
+
render(<Pagination totalPages={10} currentPage={1} onChange={() => {}} />);
|
|
86
|
+
fireEvent.change(screen.getByTestId('pagination-current-page-input'), {
|
|
87
|
+
target: { value: '11' },
|
|
88
|
+
});
|
|
89
|
+
expect(screen.getByTestId('pagination-current-page-input')).toHaveClass('has-error');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Pagination } from './Pagination';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.pagination {
|
|
2
|
+
.form-control {
|
|
3
|
+
margin-bottom: 0;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.pagination__current-page {
|
|
8
|
+
max-width: 50px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.pagination__current-page-input {
|
|
12
|
+
text-align: center;
|
|
13
|
+
font-weight: var(--pf-font-weight-heavy);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.pagination {
|
|
17
|
+
.pagination__current-page-input {
|
|
18
|
+
&.has-error {
|
|
19
|
+
border-color: var(--pf-error-color);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import { Table } from './Table';
|
|
3
|
-
import {
|
|
3
|
+
import { columns, sampleData, SampleDataRow } from './sampleData';
|
|
4
|
+
import { registerFontAwesomeIcons } from '@/setup/setupIcons';
|
|
5
|
+
import { indiconDefinitions } from '@/components/icons/indicons';
|
|
6
|
+
|
|
7
|
+
registerFontAwesomeIcons(...Object.values(indiconDefinitions));
|
|
4
8
|
|
|
5
9
|
const meta: Meta = {
|
|
6
10
|
title: 'Layout/Table',
|
|
@@ -239,6 +243,14 @@ const meta: Meta = {
|
|
|
239
243
|
defaultValue: { summary: 'false' },
|
|
240
244
|
},
|
|
241
245
|
},
|
|
246
|
+
totalEntriesText: {
|
|
247
|
+
control: 'text',
|
|
248
|
+
description:
|
|
249
|
+
'The text to display in the total entries section. This is hidden if 1. No pagination exists, 2. No string is passed.',
|
|
250
|
+
table: {
|
|
251
|
+
category: 'Styling',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
242
254
|
// hidden props
|
|
243
255
|
onRowDoubleClicked: {
|
|
244
256
|
table: {
|
|
@@ -495,6 +507,11 @@ const meta: Meta = {
|
|
|
495
507
|
disable: true,
|
|
496
508
|
},
|
|
497
509
|
},
|
|
510
|
+
currentPage: {
|
|
511
|
+
table: {
|
|
512
|
+
disable: true,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
498
515
|
},
|
|
499
516
|
};
|
|
500
517
|
|
|
@@ -525,33 +542,8 @@ export const Default: Story = {
|
|
|
525
542
|
subHeaderComponent: null,
|
|
526
543
|
paginationPerPage: 10,
|
|
527
544
|
isFullHeight: false,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
name: 'Name',
|
|
531
|
-
selector: (row) => row.name,
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
name: 'Class',
|
|
535
|
-
selector: (row) => row.class,
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
name: 'Age',
|
|
539
|
-
selector: (row) => row.age,
|
|
540
|
-
sortable: true,
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
name: 'Weapon',
|
|
544
|
-
selector: (row) => row.weapon,
|
|
545
|
-
},
|
|
546
|
-
{
|
|
547
|
-
name: 'Backstory',
|
|
548
|
-
selector: (row) => row.backstory,
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
name: 'Favorite Meal',
|
|
552
|
-
selector: (row) => row.favoriteMeal,
|
|
553
|
-
},
|
|
554
|
-
],
|
|
545
|
+
totalEntriesText: 'Showing 12 of 12 entries.',
|
|
546
|
+
columns: columns,
|
|
555
547
|
data: sampleData,
|
|
556
548
|
},
|
|
557
549
|
render: ({ ...args }) => <Table {...args} />,
|
|
@@ -6,6 +6,7 @@ import DataTable, {
|
|
|
6
6
|
|
|
7
7
|
import { LoadingComponent } from './LoadingComponent';
|
|
8
8
|
import { TableProps } from './types';
|
|
9
|
+
import { TablePagination } from './components/TablePagination';
|
|
9
10
|
|
|
10
11
|
export const Table = <T,>(props: TableProps<T>) => {
|
|
11
12
|
const {
|
|
@@ -19,6 +20,8 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
19
20
|
isFullHeight = false,
|
|
20
21
|
subHeaderAlign = 'left',
|
|
21
22
|
className,
|
|
23
|
+
paginationTotalRows,
|
|
24
|
+
totalEntriesText,
|
|
22
25
|
...rest
|
|
23
26
|
} = props;
|
|
24
27
|
|
|
@@ -32,7 +35,7 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
return (
|
|
35
|
-
<div className={tableWrapperClassName}>
|
|
38
|
+
<div className={tableWrapperClassName} data-testid="table">
|
|
36
39
|
<DataTable
|
|
37
40
|
responsive={responsive}
|
|
38
41
|
direction={direction as RDTDirection}
|
|
@@ -44,6 +47,11 @@ export const Table = <T,>(props: TableProps<T>) => {
|
|
|
44
47
|
noDataComponent={noDataComponent}
|
|
45
48
|
progressPending={isLoading}
|
|
46
49
|
progressComponent={<LoadingComponent />}
|
|
50
|
+
pagination
|
|
51
|
+
paginationComponent={(props) => (
|
|
52
|
+
<TablePagination {...props} totalEntriesText={totalEntriesText} />
|
|
53
|
+
)}
|
|
54
|
+
paginationTotalRows={paginationTotalRows}
|
|
47
55
|
{...rest}
|
|
48
56
|
/>
|
|
49
57
|
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Table } from '../Table';
|
|
3
|
+
import { sampleData, columns } from '../sampleData';
|
|
4
|
+
|
|
5
|
+
describe('Table', () => {
|
|
6
|
+
it('renders the total entries text', () => {
|
|
7
|
+
render(<Table columns={columns} data={sampleData} totalEntriesText="100 entries" />);
|
|
8
|
+
expect(screen.getByTestId('table-pagination-total-entries')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Pagination as PaginationComponent } from '../../pagination';
|
|
2
|
+
import { Row, Col } from '../../grid';
|
|
3
|
+
interface TablePaginationProps {
|
|
4
|
+
rowsPerPage: number;
|
|
5
|
+
rowCount: number;
|
|
6
|
+
onChangePage: (page: number, perPage: number) => void;
|
|
7
|
+
currentPage: number;
|
|
8
|
+
totalEntriesText?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TablePagination = ({
|
|
12
|
+
rowsPerPage,
|
|
13
|
+
rowCount,
|
|
14
|
+
onChangePage,
|
|
15
|
+
currentPage,
|
|
16
|
+
totalEntriesText,
|
|
17
|
+
}: TablePaginationProps) => {
|
|
18
|
+
const totalPages = Math.ceil(rowCount / rowsPerPage);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="table__pagination">
|
|
22
|
+
<Row align="center" justify="between">
|
|
23
|
+
<Col xs="content">
|
|
24
|
+
{totalEntriesText && (
|
|
25
|
+
<span
|
|
26
|
+
data-testid="table-pagination-total-entries"
|
|
27
|
+
className="table__pagination-total-entries"
|
|
28
|
+
>
|
|
29
|
+
{totalEntriesText}
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
</Col>
|
|
33
|
+
<Col xs="content">
|
|
34
|
+
<PaginationComponent
|
|
35
|
+
data-testid="table-pagination-component"
|
|
36
|
+
totalPages={totalPages}
|
|
37
|
+
currentPage={currentPage}
|
|
38
|
+
onChange={(page) => onChangePage(page, rowsPerPage)}
|
|
39
|
+
/>
|
|
40
|
+
</Col>
|
|
41
|
+
</Row>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { TablePagination } from '../TablePagination';
|
|
3
|
+
|
|
4
|
+
describe('TablePagination', () => {
|
|
5
|
+
it('renders total entries text', () => {
|
|
6
|
+
render(
|
|
7
|
+
<TablePagination
|
|
8
|
+
rowsPerPage={10}
|
|
9
|
+
rowCount={100}
|
|
10
|
+
onChangePage={() => {}}
|
|
11
|
+
currentPage={1}
|
|
12
|
+
totalEntriesText="100 entries"
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByTestId('table-pagination-total-entries')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TableColumn } from './types';
|
|
2
|
+
|
|
1
3
|
export interface SampleDataRow {
|
|
2
4
|
name: string;
|
|
3
5
|
class: string;
|
|
@@ -168,4 +170,112 @@ export const sampleData: SampleDataRow[] = [
|
|
|
168
170
|
backstory: 'A thief with a quick hand.',
|
|
169
171
|
favoriteMeal: 'Fish and chips',
|
|
170
172
|
},
|
|
173
|
+
{
|
|
174
|
+
name: 'Thalia',
|
|
175
|
+
class: 'Wizard',
|
|
176
|
+
age: 150,
|
|
177
|
+
weapon: 'Crystal Staff',
|
|
178
|
+
backstory: 'An archivist of forbidden knowledge.',
|
|
179
|
+
favoriteMeal: 'Spiced wine and cheese',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'Ragnar',
|
|
183
|
+
class: 'Fighter',
|
|
184
|
+
age: 45,
|
|
185
|
+
weapon: 'War Hammer',
|
|
186
|
+
backstory: 'A dwarven smith turned adventurer.',
|
|
187
|
+
favoriteMeal: 'Mead and roasted mutton',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'Sylvana',
|
|
191
|
+
class: 'Ranger',
|
|
192
|
+
age: 95,
|
|
193
|
+
weapon: 'Elven Bow',
|
|
194
|
+
backstory: 'Guardian of the enchanted forest.',
|
|
195
|
+
favoriteMeal: 'Elvish waybread',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'Magnus',
|
|
199
|
+
class: 'Warlock',
|
|
200
|
+
age: 38,
|
|
201
|
+
weapon: 'Cursed Dagger',
|
|
202
|
+
backstory: 'Made a deal with a demon for revenge.',
|
|
203
|
+
favoriteMeal: 'Blood oranges',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'Echo',
|
|
207
|
+
class: 'Bard',
|
|
208
|
+
age: 23,
|
|
209
|
+
weapon: 'Enchanted Violin',
|
|
210
|
+
backstory: 'Can mimic any sound perfectly.',
|
|
211
|
+
favoriteMeal: 'Sweet rolls',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'Korg',
|
|
215
|
+
class: 'Barbarian',
|
|
216
|
+
age: 32,
|
|
217
|
+
weapon: 'Stone Maul',
|
|
218
|
+
backstory: 'Last survivor of a petrified tribe.',
|
|
219
|
+
favoriteMeal: 'Raw meat',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Celeste',
|
|
223
|
+
class: 'Sorcerer',
|
|
224
|
+
age: 19,
|
|
225
|
+
weapon: 'Starlight Wand',
|
|
226
|
+
backstory: 'Born during a celestial convergence.',
|
|
227
|
+
favoriteMeal: 'Moon cakes',
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'Raven',
|
|
231
|
+
class: 'Rogue',
|
|
232
|
+
age: 27,
|
|
233
|
+
weapon: 'Shadow Blade',
|
|
234
|
+
backstory: 'Professional assassin seeking redemption.',
|
|
235
|
+
favoriteMeal: 'Whatever their mark is having',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'Terra',
|
|
239
|
+
class: 'Druid',
|
|
240
|
+
age: 156,
|
|
241
|
+
weapon: 'Living Wood Staff',
|
|
242
|
+
backstory: 'Speaks with ancient trees.',
|
|
243
|
+
favoriteMeal: 'Wild mushrooms',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'Ash',
|
|
247
|
+
class: 'Monk',
|
|
248
|
+
age: 29,
|
|
249
|
+
weapon: 'Flame Fists',
|
|
250
|
+
backstory: 'Raised by phoenix monks in a volcano.',
|
|
251
|
+
favoriteMeal: 'Spicy noodles',
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
export const columns: TableColumn<SampleDataRow>[] = [
|
|
256
|
+
{
|
|
257
|
+
name: 'Name',
|
|
258
|
+
selector: (row: SampleDataRow) => row.name,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'Class',
|
|
262
|
+
selector: (row) => row.class,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'Age',
|
|
266
|
+
selector: (row) => row.age,
|
|
267
|
+
sortable: true,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'Weapon',
|
|
271
|
+
selector: (row) => row.weapon,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'Backstory',
|
|
275
|
+
selector: (row) => row.backstory,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'Favorite Meal',
|
|
279
|
+
selector: (row) => row.favoriteMeal,
|
|
280
|
+
},
|
|
171
281
|
];
|