uikit-react-public 0.37.0 → 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.
@@ -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,2 @@
1
+ export { default as PaginationCompactControls } from './PaginationCompactControls';
2
+ export { default as PaginationStandardControls } from './PaginationStandardControls';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.37.0",
5
+ "version": "0.37.2",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -55,11 +55,10 @@
55
55
  "@eslint/compat": "^1.2.7",
56
56
  "@eslint/eslintrc": "^3.3.0",
57
57
  "@eslint/js": "^9.22.0",
58
+ "@storybook/addon-docs": "10.4.6",
58
59
  "@storybook/addon-links": "10.4.6",
59
60
  "@storybook/addon-onboarding": "10.4.6",
60
61
  "@storybook/react-vite": "10.4.6",
61
- "@storybook/addon-docs": "10.4.6",
62
- "eslint-plugin-storybook": "10.4.6",
63
62
  "@testing-library/jest-dom": "^6.6.3",
64
63
  "@testing-library/react": "^16.2.0",
65
64
  "@testing-library/user-event": "^14.6.1",
@@ -77,9 +76,10 @@
77
76
  "eslint-plugin-jest": "^28.11.0",
78
77
  "eslint-plugin-no-for-of-loops": "^1.0.1",
79
78
  "eslint-plugin-no-function-declare-after-return": "^1.1.0",
80
- "eslint-plugin-react": "^7.37.4",
79
+ "eslint-plugin-react": "^7.37.5",
81
80
  "eslint-plugin-react-hooks": "^5.2.0",
82
81
  "eslint-plugin-react-refresh": "^0.4.19",
82
+ "eslint-plugin-storybook": "10.4.6",
83
83
  "gh-pages": "^6.3.0",
84
84
  "globals": "^16.0.0",
85
85
  "husky": "^9.1.7",
@@ -88,7 +88,7 @@
88
88
  "prettier": "^3.5.3",
89
89
  "react": "^19.1.0",
90
90
  "react-dom": "^19.1.0",
91
- "react-router": "^7.13.1",
91
+ "react-router": "^8.3.0",
92
92
  "storybook": "10.4.6",
93
93
  "typescript": "^5.8.2",
94
94
  "typescript-eslint": "^8.26.1",