uikit-react-public 0.36.1 → 0.37.2
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/dist/components/Pagination/Pagination.stories.d.ts +1 -0
- package/dist/components/Pagination/PaginationControls.d.ts +2 -1
- package/dist/components/Pagination/__tests__/PaginationControls.test.d.ts +1 -0
- package/dist/components/Pagination/subcomponents/PaginationCompactControls.d.ts +7 -0
- package/dist/components/Pagination/subcomponents/PaginationStandardControls.d.ts +8 -0
- package/dist/components/Pagination/subcomponents/index.d.ts +2 -0
- package/dist/components/Table/Table.context.d.ts +6 -0
- package/dist/components/Table/Table.d.ts +4 -3
- package/dist/components/Table/Table.stories.d.ts +3 -3
- package/dist/components/Table/Table.types.d.ts +6 -0
- package/dist/components/Table/index.d.ts +1 -1
- package/dist/components/Table/subcomponents/Cell/Cell.d.ts +8 -1
- package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +5 -1
- package/dist/components/Table/subcomponents/Cell/CellContent.d.ts +3 -2
- package/dist/components/Table/subcomponents/HeadCell/HeadCell.d.ts +4 -2
- package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +3 -1
- package/dist/components/Table/subcomponents/Row.d.ts +8 -1
- package/dist/index.js +7219 -6668
- package/lib/components/Pagination/Pagination.stories.tsx +9 -0
- package/lib/components/Pagination/PaginationControls.tsx +17 -245
- package/lib/components/Pagination/__tests__/PaginationControls.test.tsx +213 -0
- package/lib/components/Pagination/subcomponents/PaginationCompactControls.tsx +266 -0
- package/lib/components/Pagination/subcomponents/PaginationStandardControls.tsx +246 -0
- package/lib/components/Pagination/subcomponents/index.ts +2 -0
- package/lib/components/Table/Table.context.ts +12 -0
- package/lib/components/Table/Table.tsx +91 -12
- package/lib/components/Table/Table.types.ts +21 -0
- package/lib/components/Table/__tests__/Table.test.tsx +95 -1
- package/lib/components/Table/__tests__/__snapshots__/Table.test.tsx.snap +54 -13
- package/lib/components/Table/index.ts +6 -1
- package/lib/components/Table/subcomponents/Body.tsx +16 -3
- package/lib/components/Table/subcomponents/Cell/Cell.tsx +193 -4
- package/lib/components/Table/subcomponents/Cell/CellContent.tsx +63 -3
- package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +15 -10
- package/lib/components/Table/subcomponents/Head.tsx +14 -3
- package/lib/components/Table/subcomponents/HeadCell/HeadCell.tsx +17 -4
- package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +3 -3
- package/lib/components/Table/subcomponents/Row.tsx +195 -6
- package/lib/components/Table/subcomponents/SortIcon.tsx +4 -4
- package/package.json +5 -5
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
HTMLAttributes,
|
|
4
|
+
KeyboardEvent,
|
|
5
|
+
memo,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { css, cx } from '@emotion/css';
|
|
12
|
+
import useTheme from '../../../theme/useTheme';
|
|
13
|
+
import { PaginationContext } from '../Pagination';
|
|
14
|
+
import Button from '../../Button';
|
|
15
|
+
import Icon from '../../Icon';
|
|
16
|
+
import Input from '../../Input';
|
|
17
|
+
import Text from '../../Text';
|
|
18
|
+
import announce from '../../../utils/announce';
|
|
19
|
+
|
|
20
|
+
export interface PaginationCompactControlsProps extends HTMLAttributes<HTMLDivElement> {
|
|
21
|
+
name: string;
|
|
22
|
+
testId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PaginationCompactControls = ({
|
|
26
|
+
name,
|
|
27
|
+
testId,
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: PaginationCompactControlsProps) => {
|
|
31
|
+
const [theme] = useTheme();
|
|
32
|
+
const contextValue = useContext(PaginationContext);
|
|
33
|
+
const [pageInputValue, setPageInputValue] = useState(
|
|
34
|
+
String(contextValue?.currentPage ?? 1)
|
|
35
|
+
);
|
|
36
|
+
const [pageInputError, setPageInputError] = useState('');
|
|
37
|
+
const pageInputDescriptionId = useId();
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setPageInputValue(String(contextValue?.currentPage ?? 1));
|
|
41
|
+
setPageInputError('');
|
|
42
|
+
}, [contextValue?.currentPage]);
|
|
43
|
+
|
|
44
|
+
if (!contextValue) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
mode,
|
|
50
|
+
limit,
|
|
51
|
+
total,
|
|
52
|
+
currentPage,
|
|
53
|
+
trackedMaxPage,
|
|
54
|
+
hasPreviousPage,
|
|
55
|
+
hasNextPage,
|
|
56
|
+
onPageChange,
|
|
57
|
+
onNextPage,
|
|
58
|
+
onPreviousPage,
|
|
59
|
+
} = contextValue;
|
|
60
|
+
|
|
61
|
+
const totalPages =
|
|
62
|
+
typeof total === 'number' ? Math.ceil(total / limit) : undefined;
|
|
63
|
+
const lastPageNumber = totalPages ?? trackedMaxPage;
|
|
64
|
+
const displayedCurrentPage = totalPages === 0 ? 0 : currentPage;
|
|
65
|
+
|
|
66
|
+
const handlePageChange = (newPage: number) => {
|
|
67
|
+
if (mode === 'tracked') {
|
|
68
|
+
if (newPage < 1 || newPage > trackedMaxPage || newPage === currentPage) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
onPageChange?.(newPage);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onPageChange?.((newPage - 1) * limit);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handlePreviousPage = () => {
|
|
79
|
+
if (mode === 'tracked' && onPreviousPage) {
|
|
80
|
+
onPreviousPage();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
handlePageChange(currentPage - 1);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleNextPage = () => {
|
|
87
|
+
if (mode === 'tracked') {
|
|
88
|
+
onNextPage?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
handlePageChange(currentPage + 1);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handlePageInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
95
|
+
setPageInputValue(event.target.value);
|
|
96
|
+
setPageInputError('');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const resetPageInput = () => {
|
|
100
|
+
setPageInputValue(String(currentPage));
|
|
101
|
+
setPageInputError('');
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handlePageInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
105
|
+
if (event.key === 'Escape') {
|
|
106
|
+
resetPageInput();
|
|
107
|
+
event.currentTarget.blur();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (event.key !== 'Enter') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
|
|
117
|
+
const isDigitsOnly = /^\d+$/.test(pageInputValue);
|
|
118
|
+
const newPageNumber = Number(pageInputValue);
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
!isDigitsOnly ||
|
|
122
|
+
!Number.isInteger(newPageNumber) ||
|
|
123
|
+
newPageNumber < 1 ||
|
|
124
|
+
newPageNumber > lastPageNumber
|
|
125
|
+
) {
|
|
126
|
+
const errorMessage = `Enter a page number from 1 to ${lastPageNumber}.`;
|
|
127
|
+
setPageInputError(errorMessage);
|
|
128
|
+
announce(errorMessage, true);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setPageInputError('');
|
|
133
|
+
handlePageChange(newPageNumber);
|
|
134
|
+
event.currentTarget.blur();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const controlsListStyle = css`
|
|
138
|
+
margin: 0;
|
|
139
|
+
padding: 0;
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
align-items: center;
|
|
143
|
+
list-style: none;
|
|
144
|
+
gap: ${theme.margin.m8};
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
const navigationButtonStyle = css`
|
|
148
|
+
gap: ${theme.margin.m8};
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const pageInputStyle = css`
|
|
152
|
+
height: 48px;
|
|
153
|
+
width: 48px;
|
|
154
|
+
box-sizing: border-box;
|
|
155
|
+
border: ${theme.thickness.t2} solid ${theme.colour.border.default};
|
|
156
|
+
border-radius: ${theme.radius.r4};
|
|
157
|
+
padding: 0;
|
|
158
|
+
text-align: center;
|
|
159
|
+
color: ${theme.colour.text.default};
|
|
160
|
+
font-family: ${theme.typography.body.md.fontFamily};
|
|
161
|
+
font-size: ${theme.typography.body.md.fontSize};
|
|
162
|
+
|
|
163
|
+
&[aria-invalid='true'] {
|
|
164
|
+
border-color: ${theme.colour.border.critical};
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const totalPagesLabelStyle = css`
|
|
169
|
+
color: ${theme.colour.text.secondary};
|
|
170
|
+
white-space: nowrap;
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
const visuallyHiddenStyle = css`
|
|
174
|
+
position: absolute;
|
|
175
|
+
width: 1px;
|
|
176
|
+
height: 1px;
|
|
177
|
+
padding: 0;
|
|
178
|
+
margin: -1px;
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
clip: rect(0, 0, 0, 0);
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
border: 0;
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
const pageInputDescription =
|
|
186
|
+
typeof totalPages === 'number'
|
|
187
|
+
? `Page ${displayedCurrentPage} of ${totalPages}.${
|
|
188
|
+
totalPages === 0 ? '' : ' Enter a page number and press Enter.'
|
|
189
|
+
}`
|
|
190
|
+
: `Page ${currentPage}. Enter a page number and press Enter.`;
|
|
191
|
+
|
|
192
|
+
const style = cx(name, className);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<nav
|
|
196
|
+
{...props}
|
|
197
|
+
className={style}
|
|
198
|
+
aria-label={
|
|
199
|
+
typeof totalPages === 'number'
|
|
200
|
+
? `Pagination, ${totalPages} pages total`
|
|
201
|
+
: 'Pagination'
|
|
202
|
+
}
|
|
203
|
+
data-testid={testId}
|
|
204
|
+
>
|
|
205
|
+
<ul className={controlsListStyle}>
|
|
206
|
+
<li>
|
|
207
|
+
<Button
|
|
208
|
+
className={navigationButtonStyle}
|
|
209
|
+
variant='tertiary'
|
|
210
|
+
icon={<Icon.ChevronLeft />}
|
|
211
|
+
aria-label='Go to previous page'
|
|
212
|
+
onClick={handlePreviousPage}
|
|
213
|
+
disabled={!hasPreviousPage}
|
|
214
|
+
>
|
|
215
|
+
Previous
|
|
216
|
+
</Button>
|
|
217
|
+
</li>
|
|
218
|
+
<li>
|
|
219
|
+
<Input
|
|
220
|
+
inputClassName={pageInputStyle}
|
|
221
|
+
type='text'
|
|
222
|
+
inputMode='numeric'
|
|
223
|
+
pattern='[0-9]*'
|
|
224
|
+
value={
|
|
225
|
+
totalPages === 0 ? String(displayedCurrentPage) : pageInputValue
|
|
226
|
+
}
|
|
227
|
+
aria-label='Page number'
|
|
228
|
+
aria-current={totalPages === 0 ? undefined : 'page'}
|
|
229
|
+
aria-describedby={pageInputDescriptionId}
|
|
230
|
+
aria-invalid={pageInputError ? true : undefined}
|
|
231
|
+
disabled={totalPages === 0}
|
|
232
|
+
onChange={handlePageInputChange}
|
|
233
|
+
onBlur={resetPageInput}
|
|
234
|
+
onKeyDown={handlePageInputKeyDown}
|
|
235
|
+
/>
|
|
236
|
+
<span
|
|
237
|
+
id={pageInputDescriptionId}
|
|
238
|
+
className={visuallyHiddenStyle}
|
|
239
|
+
>
|
|
240
|
+
{pageInputDescription}
|
|
241
|
+
</span>
|
|
242
|
+
</li>
|
|
243
|
+
{typeof totalPages === 'number' && (
|
|
244
|
+
<li>
|
|
245
|
+
<Text className={totalPagesLabelStyle}>of {totalPages}</Text>
|
|
246
|
+
</li>
|
|
247
|
+
)}
|
|
248
|
+
<li>
|
|
249
|
+
<Button
|
|
250
|
+
className={navigationButtonStyle}
|
|
251
|
+
variant='tertiary'
|
|
252
|
+
icon={<Icon.ChevronRight />}
|
|
253
|
+
iconPosition='right'
|
|
254
|
+
aria-label='Go to next page'
|
|
255
|
+
onClick={handleNextPage}
|
|
256
|
+
disabled={!hasNextPage}
|
|
257
|
+
>
|
|
258
|
+
Next
|
|
259
|
+
</Button>
|
|
260
|
+
</li>
|
|
261
|
+
</ul>
|
|
262
|
+
</nav>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export default memo(PaginationCompactControls);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { HTMLAttributes, memo, useContext } from 'react';
|
|
2
|
+
import { css, cx } from '@emotion/css';
|
|
3
|
+
import useTheme from '../../../theme/useTheme';
|
|
4
|
+
import { PaginationContext } from '../Pagination';
|
|
5
|
+
import Button from '../../Button';
|
|
6
|
+
import getPaginationButtons from '../getPaginationButtons';
|
|
7
|
+
import { useMediaQuery } from '../../../hooks';
|
|
8
|
+
|
|
9
|
+
export interface PaginationStandardControlsProps extends HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
maxNumberButtons: number;
|
|
11
|
+
name: string;
|
|
12
|
+
testId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PaginationStandardControls = ({
|
|
16
|
+
maxNumberButtons,
|
|
17
|
+
name,
|
|
18
|
+
testId,
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: PaginationStandardControlsProps) => {
|
|
22
|
+
const [theme] = useTheme();
|
|
23
|
+
|
|
24
|
+
const isTabletPlus = useMediaQuery(
|
|
25
|
+
`(min-width: ${theme.breakpoints.tablet}px)`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const contextValue = useContext(PaginationContext);
|
|
29
|
+
|
|
30
|
+
if (!contextValue) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
mode,
|
|
36
|
+
limit,
|
|
37
|
+
total,
|
|
38
|
+
currentPage,
|
|
39
|
+
trackedMaxPage,
|
|
40
|
+
hasPreviousPage,
|
|
41
|
+
hasNextPage,
|
|
42
|
+
onPageChange,
|
|
43
|
+
onNextPage,
|
|
44
|
+
onPreviousPage,
|
|
45
|
+
} = contextValue;
|
|
46
|
+
|
|
47
|
+
const handlePageChange = (newPage: number) => {
|
|
48
|
+
if (mode === 'tracked') {
|
|
49
|
+
if (newPage < 1 || newPage > trackedMaxPage || newPage === currentPage) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
onPageChange?.(newPage);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onPageChange?.((newPage - 1) * limit);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const totalPages =
|
|
60
|
+
typeof total === 'number' ? Math.ceil(total / limit) : undefined;
|
|
61
|
+
|
|
62
|
+
const paginationButtons =
|
|
63
|
+
mode === 'tracked'
|
|
64
|
+
? Array.from({ length: trackedMaxPage }, (_, i) => i + 1)
|
|
65
|
+
: getPaginationButtons(
|
|
66
|
+
currentPage,
|
|
67
|
+
totalPages ?? 0,
|
|
68
|
+
isTabletPlus ? maxNumberButtons : 5
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const style = cx(name, className);
|
|
72
|
+
|
|
73
|
+
const listStyle = css`
|
|
74
|
+
margin: 0;
|
|
75
|
+
padding: 0;
|
|
76
|
+
display: flex;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
align-items: center;
|
|
79
|
+
list-style: none;
|
|
80
|
+
gap: ${theme.margin.m4};
|
|
81
|
+
flex-wrap: nowrap;
|
|
82
|
+
|
|
83
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
84
|
+
gap: ${theme.margin.m8};
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const buttonBaseStyle = css`
|
|
89
|
+
@media (max-width: ${theme.breakpoints.tablet}px) {
|
|
90
|
+
gap: ${theme.margin.m4};
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
const previousButtonStyle = cx(
|
|
95
|
+
buttonBaseStyle,
|
|
96
|
+
css`
|
|
97
|
+
margin-right: ${theme.margin.m16};
|
|
98
|
+
display: none;
|
|
99
|
+
|
|
100
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
}
|
|
103
|
+
`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const nextButtonStyle = cx(
|
|
107
|
+
buttonBaseStyle,
|
|
108
|
+
css`
|
|
109
|
+
margin-left: ${theme.margin.m16};
|
|
110
|
+
display: none;
|
|
111
|
+
|
|
112
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
113
|
+
display: inline-block;
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const pageNumberButtonBaseStyle = cx(
|
|
119
|
+
buttonBaseStyle,
|
|
120
|
+
css`
|
|
121
|
+
height: 40px;
|
|
122
|
+
min-width: 40px;
|
|
123
|
+
padding: 0;
|
|
124
|
+
|
|
125
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
126
|
+
height: 48px;
|
|
127
|
+
min-width: 48px;
|
|
128
|
+
}
|
|
129
|
+
`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const pageNumberButtonStyle = cx(
|
|
133
|
+
pageNumberButtonBaseStyle,
|
|
134
|
+
css`
|
|
135
|
+
border: none;
|
|
136
|
+
color: ${theme.colour.text.secondary};
|
|
137
|
+
`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const currentPageNumberButtonStyle = cx(
|
|
141
|
+
pageNumberButtonBaseStyle,
|
|
142
|
+
css`
|
|
143
|
+
color: ${theme.colour.text.brand};
|
|
144
|
+
border-color: ${theme.colour.border.brand};
|
|
145
|
+
`
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const buttonNumberLabelStyle = css`
|
|
149
|
+
padding: 0 ${theme.padding.p8};
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<nav
|
|
154
|
+
{...props}
|
|
155
|
+
className={style}
|
|
156
|
+
aria-label={
|
|
157
|
+
typeof totalPages === 'number'
|
|
158
|
+
? `Pagination, ${totalPages} pages total`
|
|
159
|
+
: 'Pagination'
|
|
160
|
+
}
|
|
161
|
+
data-testid={testId}
|
|
162
|
+
>
|
|
163
|
+
<ul className={listStyle}>
|
|
164
|
+
<li>
|
|
165
|
+
<Button
|
|
166
|
+
className={previousButtonStyle}
|
|
167
|
+
variant='tertiary'
|
|
168
|
+
size={isTabletPlus ? 'default' : 'small'}
|
|
169
|
+
aria-label='Go to previous page'
|
|
170
|
+
onClick={() => {
|
|
171
|
+
if (mode === 'tracked') {
|
|
172
|
+
if (onPreviousPage) {
|
|
173
|
+
onPreviousPage();
|
|
174
|
+
} else {
|
|
175
|
+
handlePageChange(currentPage - 1);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
handlePageChange(currentPage - 1);
|
|
180
|
+
}}
|
|
181
|
+
disabled={!hasPreviousPage}
|
|
182
|
+
>
|
|
183
|
+
Previous
|
|
184
|
+
</Button>
|
|
185
|
+
</li>
|
|
186
|
+
|
|
187
|
+
{paginationButtons.map((page) => (
|
|
188
|
+
<li key={page}>
|
|
189
|
+
{page === '...1' || page === '...2' ? (
|
|
190
|
+
<span aria-hidden>...</span>
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
{page !== currentPage && (
|
|
194
|
+
<Button
|
|
195
|
+
className={pageNumberButtonStyle}
|
|
196
|
+
variant='secondary'
|
|
197
|
+
size={isTabletPlus ? 'default' : 'small'}
|
|
198
|
+
aria-label={`Page ${page}`}
|
|
199
|
+
onClick={() => handlePageChange(page)}
|
|
200
|
+
>
|
|
201
|
+
<span className={buttonNumberLabelStyle}>{page}</span>
|
|
202
|
+
</Button>
|
|
203
|
+
)}
|
|
204
|
+
{page === currentPage && (
|
|
205
|
+
<Button
|
|
206
|
+
className={currentPageNumberButtonStyle}
|
|
207
|
+
variant='secondary'
|
|
208
|
+
size={isTabletPlus ? 'default' : 'small'}
|
|
209
|
+
aria-label={`Page ${page}, current page`}
|
|
210
|
+
aria-current='page'
|
|
211
|
+
disabled
|
|
212
|
+
onClick={() => handlePageChange(page)}
|
|
213
|
+
>
|
|
214
|
+
<span className={buttonNumberLabelStyle}>{page}</span>
|
|
215
|
+
</Button>
|
|
216
|
+
)}
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
</li>
|
|
220
|
+
))}
|
|
221
|
+
|
|
222
|
+
<li>
|
|
223
|
+
<Button
|
|
224
|
+
className={nextButtonStyle}
|
|
225
|
+
variant='tertiary'
|
|
226
|
+
aria-label='Go to next page'
|
|
227
|
+
onClick={() => {
|
|
228
|
+
if (mode === 'tracked') {
|
|
229
|
+
if (onNextPage) {
|
|
230
|
+
onNextPage();
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
handlePageChange(currentPage + 1);
|
|
235
|
+
}}
|
|
236
|
+
disabled={!hasNextPage}
|
|
237
|
+
>
|
|
238
|
+
Next
|
|
239
|
+
</Button>
|
|
240
|
+
</li>
|
|
241
|
+
</ul>
|
|
242
|
+
</nav>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export default memo(PaginationStandardControls);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, use } from 'react';
|
|
2
|
+
import type { TableColumnMetadata } from './Table.types';
|
|
3
|
+
|
|
4
|
+
export type TableContextValue = {
|
|
5
|
+
columns: TableColumnMetadata[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const TableContext = createContext<TableContextValue>({ columns: [] });
|
|
9
|
+
|
|
10
|
+
export const TableContextProvider = TableContext.Provider;
|
|
11
|
+
|
|
12
|
+
export const useTableContext = () => use(TableContext);
|
|
@@ -1,10 +1,77 @@
|
|
|
1
|
+
import { Children, isValidElement } from 'react';
|
|
1
2
|
import { css, cx } from '@emotion/css';
|
|
2
3
|
import { Head, HeadCell, Body, Row, Cell } from './subcomponents';
|
|
3
4
|
import { useTheme } from '../../theme';
|
|
4
|
-
import
|
|
5
|
+
import { TableContextProvider } from './Table.context';
|
|
6
|
+
import type {
|
|
7
|
+
ColumnVariant,
|
|
8
|
+
NormalizedTableMobileRole,
|
|
9
|
+
TableColumnMetadata,
|
|
10
|
+
TableMobileRole,
|
|
11
|
+
TableProps,
|
|
12
|
+
} from './Table.types';
|
|
13
|
+
import type { TableHeadCellProps } from './subcomponents';
|
|
5
14
|
|
|
6
15
|
const NAME = 'ucl-uikit-table';
|
|
7
16
|
|
|
17
|
+
const getTextFromChildren = (children: React.ReactNode): string => {
|
|
18
|
+
if (typeof children === 'string' || typeof children === 'number')
|
|
19
|
+
return String(children);
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(children))
|
|
22
|
+
return children.map(getTextFromChildren).join('').trim();
|
|
23
|
+
|
|
24
|
+
if (isValidElement<{ children?: React.ReactNode }>(children))
|
|
25
|
+
return getTextFromChildren(children.props.children);
|
|
26
|
+
|
|
27
|
+
return '';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const normalizeMobileRole = (
|
|
31
|
+
mobileRole: TableMobileRole | undefined,
|
|
32
|
+
variant: ColumnVariant | undefined
|
|
33
|
+
): NormalizedTableMobileRole => {
|
|
34
|
+
if (mobileRole === 'primaryStatus') return 'status';
|
|
35
|
+
if (mobileRole) return mobileRole;
|
|
36
|
+
if (variant === 'checkbox') return 'selection';
|
|
37
|
+
return 'field';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getColumnsFromChildren = (
|
|
41
|
+
children: React.ReactNode
|
|
42
|
+
): TableColumnMetadata[] => {
|
|
43
|
+
const head = Children.toArray(children).find(
|
|
44
|
+
(child) => isValidElement(child) && child.type === Head
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!isValidElement<{ children?: React.ReactNode }>(head)) return [];
|
|
48
|
+
|
|
49
|
+
const columns = Children.toArray(head.props.children)
|
|
50
|
+
.filter(isValidElement<TableHeadCellProps>)
|
|
51
|
+
.map((cell) => ({
|
|
52
|
+
mobileRole: normalizeMobileRole(
|
|
53
|
+
cell.props.mobileRole,
|
|
54
|
+
cell.props.variant
|
|
55
|
+
),
|
|
56
|
+
mobileLabel:
|
|
57
|
+
cell.props.mobileLabel ?? getTextFromChildren(cell.props.children),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
if (columns.some((column) => column.mobileRole === 'primary')) return columns;
|
|
61
|
+
|
|
62
|
+
const fallbackPrimaryIndex = columns.findIndex(
|
|
63
|
+
(column) => column.mobileRole === 'field'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (fallbackPrimaryIndex === -1) return columns;
|
|
67
|
+
|
|
68
|
+
return columns.map((column, index) =>
|
|
69
|
+
index === fallbackPrimaryIndex
|
|
70
|
+
? { ...column, mobileRole: 'primary' }
|
|
71
|
+
: column
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
8
75
|
const Table = ({
|
|
9
76
|
testId = NAME,
|
|
10
77
|
className,
|
|
@@ -14,25 +81,37 @@ const Table = ({
|
|
|
14
81
|
...props
|
|
15
82
|
}: TableProps) => {
|
|
16
83
|
const [theme] = useTheme();
|
|
84
|
+
const columns = getColumnsFromChildren(children);
|
|
17
85
|
|
|
18
86
|
const baseStyle = css`
|
|
19
|
-
|
|
20
|
-
font-
|
|
87
|
+
width: 100%;
|
|
88
|
+
font-family: ${theme.typography.body.sm.fontFamily};
|
|
89
|
+
font-feature-settings: ${theme.typography.body.sm.fontSettings};
|
|
90
|
+
font-size: ${theme.typography.body.sm.fontSize}px;
|
|
91
|
+
font-weight: ${theme.typography.body.sm.fontWeight};
|
|
92
|
+
line-height: ${theme.typography.body.sm.lineHeight}%;
|
|
21
93
|
border-collapse: collapse;
|
|
94
|
+
|
|
95
|
+
@media screen and (max-width: ${theme.breakpoints.tablet - 1}px) {
|
|
96
|
+
display: block;
|
|
97
|
+
border-collapse: separate;
|
|
98
|
+
}
|
|
22
99
|
`;
|
|
23
100
|
|
|
24
101
|
const style = cx(baseStyle, className, NAME);
|
|
25
102
|
|
|
26
103
|
return (
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
104
|
+
<TableContextProvider value={{ columns }}>
|
|
105
|
+
<table
|
|
106
|
+
className={style}
|
|
107
|
+
data-testid={testId}
|
|
108
|
+
aria-label={ariaLabel}
|
|
109
|
+
ref={ref}
|
|
110
|
+
{...props}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</table>
|
|
114
|
+
</TableContextProvider>
|
|
36
115
|
);
|
|
37
116
|
};
|
|
38
117
|
|
|
@@ -2,6 +2,27 @@ export type ColumnVariant = 'default' | 'numeric' | 'checkbox' | 'button';
|
|
|
2
2
|
|
|
3
3
|
export type SortOrder = 'asc' | 'desc' | null;
|
|
4
4
|
|
|
5
|
+
export type TableMobileRole =
|
|
6
|
+
| 'selection'
|
|
7
|
+
| 'primaryMeta'
|
|
8
|
+
| 'primary'
|
|
9
|
+
| 'status'
|
|
10
|
+
| 'primaryStatus'
|
|
11
|
+
| 'field'
|
|
12
|
+
| 'primaryAction'
|
|
13
|
+
| 'contextMenu'
|
|
14
|
+
| 'hidden';
|
|
15
|
+
|
|
16
|
+
export type NormalizedTableMobileRole = Exclude<
|
|
17
|
+
TableMobileRole,
|
|
18
|
+
'primaryStatus'
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export type TableColumnMetadata = {
|
|
22
|
+
mobileRole: NormalizedTableMobileRole;
|
|
23
|
+
mobileLabel?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
5
26
|
export type SortPattern = {
|
|
6
27
|
accessor: string | null;
|
|
7
28
|
order: SortOrder;
|