@rozie-ui/pagination-react 0.0.0-stage → 0.1.8
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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/dist/Pagination.css +59 -0
- package/dist/index.cjs +311 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.mjs +306 -0
- package/package.json +71 -3
- package/src/Pagination.css +59 -0
- package/src/Pagination.d.ts +63 -0
- package/src/Pagination.tsx +279 -0
- package/src/index.ts +6 -0
- package/src/internal/paginationItems.ts +137 -0
- package/src/themes/base.css +74 -0
- package/src/themes/bootstrap.css +46 -0
- package/src/themes/material.css +44 -0
- package/src/themes/shadcn.css +48 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as _$react from "react";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/Pagination.d.ts
|
|
4
|
+
/** The `change` event payload: the page that was just navigated to. */
|
|
5
|
+
interface PaginationChangePayload {
|
|
6
|
+
/** The new, clamped, 1-based current page. */
|
|
7
|
+
page: number;
|
|
8
|
+
}
|
|
9
|
+
interface PrevControlCtx {
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
goto: () => void;
|
|
12
|
+
page: number;
|
|
13
|
+
}
|
|
14
|
+
interface EllipsisCtx {
|
|
15
|
+
index: number;
|
|
16
|
+
}
|
|
17
|
+
interface ItemCtx {
|
|
18
|
+
page: number;
|
|
19
|
+
selected: boolean;
|
|
20
|
+
goto: () => void;
|
|
21
|
+
}
|
|
22
|
+
interface NextControlCtx {
|
|
23
|
+
disabled: boolean;
|
|
24
|
+
goto: () => void;
|
|
25
|
+
page: number;
|
|
26
|
+
}
|
|
27
|
+
interface PaginationProps extends Omit<_$react.ComponentPropsWithoutRef<'nav'>, 'modelValue' | 'defaultModelValue' | 'onModelValueChange' | 'totalPages' | 'total' | 'pageSize' | 'siblingCount' | 'boundaryCount' | 'disabled' | 'ariaLabel' | 'onChange' | 'renderPrevControl' | 'renderEllipsis' | 'renderItem' | 'renderNextControl' | 'slots' | 'children' | 'dangerouslySetInnerHTML'> {
|
|
28
|
+
/**
|
|
29
|
+
* The 1-based current page (two-way model). Clamped into `[1, totalPages]`. Bind it with `r-model:modelValue` / `v-model:modelValue` / `modelValue` + `onModelValueChange`; it is also the Angular ControlValueAccessor control value.
|
|
30
|
+
*/
|
|
31
|
+
modelValue?: number;
|
|
32
|
+
defaultModelValue?: number;
|
|
33
|
+
onModelValueChange?: (modelValue: number) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Explicit total page count. When provided (> 0) it takes precedence over `total` + `pageSize`. Use it when the backend already reports the page count.
|
|
36
|
+
*/
|
|
37
|
+
totalPages?: (number) | null;
|
|
38
|
+
/**
|
|
39
|
+
* Total item count. Combined with `pageSize` to derive the page count (`ceil(total / pageSize)`) when `totalPages` is not given.
|
|
40
|
+
*/
|
|
41
|
+
total?: (number) | null;
|
|
42
|
+
/**
|
|
43
|
+
* Items per page. Combined with `total` to derive the page count when `totalPages` is not given.
|
|
44
|
+
*/
|
|
45
|
+
pageSize?: (number) | null;
|
|
46
|
+
/**
|
|
47
|
+
* Number of page buttons shown on each side of the current page (the sibling window). Larger values show more context around the current page.
|
|
48
|
+
*/
|
|
49
|
+
siblingCount?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Number of page buttons always shown at each boundary (the first and last `boundaryCount` pages), regardless of the current page.
|
|
52
|
+
*/
|
|
53
|
+
boundaryCount?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Disable the entire control — every page button and the prev/next controls become non-interactive and are marked `aria-disabled`.
|
|
56
|
+
*/
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Accessible name for the surrounding `<nav>` landmark (its `aria-label`). Defaults to `"Pagination"`.
|
|
60
|
+
*/
|
|
61
|
+
ariaLabel?: string;
|
|
62
|
+
onChange?: (payload: PaginationChangePayload) => void;
|
|
63
|
+
renderPrevControl?: (ctx: PrevControlCtx) => ReactNode;
|
|
64
|
+
renderEllipsis?: (ctx: EllipsisCtx) => ReactNode;
|
|
65
|
+
renderItem?: (ctx: ItemCtx) => ReactNode;
|
|
66
|
+
renderNextControl?: (ctx: NextControlCtx) => ReactNode;
|
|
67
|
+
slots?: Record<string, () => _$react.ReactNode>;
|
|
68
|
+
}
|
|
69
|
+
interface PaginationHandle {
|
|
70
|
+
goto: (page: number) => void;
|
|
71
|
+
next: () => void;
|
|
72
|
+
prev: () => void;
|
|
73
|
+
first: () => void;
|
|
74
|
+
last: () => void;
|
|
75
|
+
}
|
|
76
|
+
declare const Pagination: _$react.ForwardRefExoticComponent<PaginationProps & _$react.RefAttributes<PaginationHandle>>;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { Pagination, Pagination as default, PaginationChangePayload, type PaginationHandle };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { Fragment, forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
|
2
|
+
import { flushSync } from "react-dom";
|
|
3
|
+
import { clsx, mergeListeners, pickListeners, rozieAttr, rozieDisplay, useControllableState } from "@rozie/runtime-react";
|
|
4
|
+
import "./Pagination.css";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/internal/paginationItems.ts
|
|
7
|
+
/** Inclusive integer range [start, end]; empty when start > end. */
|
|
8
|
+
function range(start, end) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (let i = start; i <= end; i++) out.push(i);
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the effective total page count from either an explicit `totalPages`
|
|
15
|
+
* or `ceil(total / pageSize)`. Always at least 1 (an empty data set still has a
|
|
16
|
+
* single page).
|
|
17
|
+
*/
|
|
18
|
+
function resolveTotalPages(input) {
|
|
19
|
+
const tp = input.totalPages;
|
|
20
|
+
if (typeof tp === "number" && tp > 0) return Math.max(1, Math.floor(tp));
|
|
21
|
+
const total = typeof input.total === "number" ? input.total : 0;
|
|
22
|
+
const size = typeof input.pageSize === "number" && input.pageSize > 0 ? input.pageSize : 0;
|
|
23
|
+
if (total > 0 && size > 0) return Math.max(1, Math.ceil(total / size));
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the windowed page-item model. Pure: no clamping side effects, no DOM.
|
|
28
|
+
*
|
|
29
|
+
* Algorithm (MUI usePagination semantics):
|
|
30
|
+
* - `startPages` = first `boundaryCount` pages; `endPages` = last
|
|
31
|
+
* `boundaryCount` pages.
|
|
32
|
+
* - a sibling window of `siblingCount` pages on each side of `page`, kept
|
|
33
|
+
* clear of the boundary blocks.
|
|
34
|
+
* - an `'ellipsis'` collapses a gap of 2+ pages; a gap of exactly one page is
|
|
35
|
+
* rendered as that single bridging page (never a 1-page ellipsis).
|
|
36
|
+
*/
|
|
37
|
+
function paginationItems(input) {
|
|
38
|
+
const totalPages = resolveTotalPages(input);
|
|
39
|
+
const rawPage = typeof input.page === "number" && Number.isFinite(input.page) ? Math.floor(input.page) : 1;
|
|
40
|
+
const page = Math.min(Math.max(rawPage, 1), totalPages);
|
|
41
|
+
const siblingCount = Math.max(0, Math.floor(input.siblingCount ?? 1));
|
|
42
|
+
const boundaryCount = Math.max(0, Math.floor(input.boundaryCount ?? 1));
|
|
43
|
+
const startPages = range(1, Math.min(boundaryCount, totalPages));
|
|
44
|
+
const endPages = range(Math.max(totalPages - boundaryCount + 1, boundaryCount + 1), totalPages);
|
|
45
|
+
const siblingsStart = Math.max(Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1), boundaryCount + 2);
|
|
46
|
+
const endPagesFirst = endPages[0];
|
|
47
|
+
const siblingsEnd = Math.min(Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2), endPagesFirst !== void 0 ? endPagesFirst - 2 : totalPages - 1);
|
|
48
|
+
const pages = [];
|
|
49
|
+
pages.push(...startPages);
|
|
50
|
+
if (siblingsStart > boundaryCount + 2) pages.push("ellipsis");
|
|
51
|
+
else if (boundaryCount + 1 < totalPages - boundaryCount) pages.push(boundaryCount + 1);
|
|
52
|
+
pages.push(...range(siblingsStart, siblingsEnd));
|
|
53
|
+
if (siblingsEnd < totalPages - boundaryCount - 1) pages.push("ellipsis");
|
|
54
|
+
else if (totalPages - boundaryCount > boundaryCount) pages.push(totalPages - boundaryCount);
|
|
55
|
+
pages.push(...endPages);
|
|
56
|
+
return {
|
|
57
|
+
totalPages,
|
|
58
|
+
page,
|
|
59
|
+
pages,
|
|
60
|
+
hasPrev: page > 1,
|
|
61
|
+
hasNext: page < totalPages
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/Pagination.tsx
|
|
66
|
+
const Pagination = forwardRef(function Pagination(_props, ref) {
|
|
67
|
+
const props = {
|
|
68
|
+
..._props,
|
|
69
|
+
totalPages: _props.totalPages ?? null,
|
|
70
|
+
total: _props.total ?? null,
|
|
71
|
+
pageSize: _props.pageSize ?? null,
|
|
72
|
+
siblingCount: _props.siblingCount ?? 1,
|
|
73
|
+
boundaryCount: _props.boundaryCount ?? 1,
|
|
74
|
+
disabled: _props.disabled ?? false,
|
|
75
|
+
ariaLabel: _props.ariaLabel ?? "Pagination"
|
|
76
|
+
};
|
|
77
|
+
const attrs = (() => {
|
|
78
|
+
const { modelValue, totalPages, total, pageSize, siblingCount, boundaryCount, disabled, ariaLabel, defaultValue, onModelValueChange, defaultModelValue, onChange, renderPrevControl, renderEllipsis, renderItem, renderNextControl, slots, ...rest } = _props;
|
|
79
|
+
return rest;
|
|
80
|
+
})();
|
|
81
|
+
const [modelValue, setModelValue] = useControllableState({
|
|
82
|
+
value: props.modelValue,
|
|
83
|
+
defaultValue: props.defaultModelValue ?? 1,
|
|
84
|
+
onValueChange: props.onModelValueChange
|
|
85
|
+
});
|
|
86
|
+
const nav = useRef(null);
|
|
87
|
+
function model() {
|
|
88
|
+
return paginationItems({
|
|
89
|
+
page: modelValue,
|
|
90
|
+
totalPages: props.totalPages,
|
|
91
|
+
total: props.total,
|
|
92
|
+
pageSize: props.pageSize,
|
|
93
|
+
siblingCount: props.siblingCount,
|
|
94
|
+
boundaryCount: props.boundaryCount
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function effectivePages() {
|
|
98
|
+
return model().totalPages;
|
|
99
|
+
}
|
|
100
|
+
function currentPage() {
|
|
101
|
+
return model().page;
|
|
102
|
+
}
|
|
103
|
+
function canPrev() {
|
|
104
|
+
return model().hasPrev;
|
|
105
|
+
}
|
|
106
|
+
function canNext() {
|
|
107
|
+
return model().hasNext;
|
|
108
|
+
}
|
|
109
|
+
function isActive(page) {
|
|
110
|
+
return page === currentPage();
|
|
111
|
+
}
|
|
112
|
+
function tabIndexFor(active) {
|
|
113
|
+
return active ? 0 : -1;
|
|
114
|
+
}
|
|
115
|
+
const { onChange: _rozieProp_onChange } = props;
|
|
116
|
+
const goToPage = useCallback((page) => {
|
|
117
|
+
if (props.disabled) return;
|
|
118
|
+
const tp = effectivePages();
|
|
119
|
+
let target = typeof page === "number" ? Math.floor(page) : 1;
|
|
120
|
+
if (target < 1) target = 1;
|
|
121
|
+
if (target > tp) target = tp;
|
|
122
|
+
if (target === currentPage()) return;
|
|
123
|
+
setModelValue(target);
|
|
124
|
+
_rozieProp_onChange && _rozieProp_onChange({ page: target });
|
|
125
|
+
}, [
|
|
126
|
+
_rozieProp_onChange,
|
|
127
|
+
currentPage,
|
|
128
|
+
effectivePages,
|
|
129
|
+
props.disabled,
|
|
130
|
+
setModelValue
|
|
131
|
+
]);
|
|
132
|
+
const goNext = useCallback(() => {
|
|
133
|
+
if (canNext()) goToPage(currentPage() + 1);
|
|
134
|
+
}, [
|
|
135
|
+
canNext,
|
|
136
|
+
currentPage,
|
|
137
|
+
goToPage
|
|
138
|
+
]);
|
|
139
|
+
const goPrev = useCallback(() => {
|
|
140
|
+
if (canPrev()) goToPage(currentPage() - 1);
|
|
141
|
+
}, [
|
|
142
|
+
canPrev,
|
|
143
|
+
currentPage,
|
|
144
|
+
goToPage
|
|
145
|
+
]);
|
|
146
|
+
function goFirst() {
|
|
147
|
+
return goToPage(1);
|
|
148
|
+
}
|
|
149
|
+
function goLast() {
|
|
150
|
+
return goToPage(effectivePages());
|
|
151
|
+
}
|
|
152
|
+
function controls() {
|
|
153
|
+
const nav$local = nav.current;
|
|
154
|
+
if (!nav$local) return [];
|
|
155
|
+
return Array.from(nav$local.querySelectorAll("[data-page-control]"));
|
|
156
|
+
}
|
|
157
|
+
function focusControlAt(idx) {
|
|
158
|
+
const els = controls();
|
|
159
|
+
if (els.length === 0) return;
|
|
160
|
+
let i = idx;
|
|
161
|
+
if (i < 0) i = 0;
|
|
162
|
+
if (i >= els.length) i = els.length - 1;
|
|
163
|
+
const el = els[i];
|
|
164
|
+
if (el && el.focus) el.focus();
|
|
165
|
+
}
|
|
166
|
+
function focusedIndex() {
|
|
167
|
+
const els = controls();
|
|
168
|
+
const nav$local = nav.current;
|
|
169
|
+
const root = nav$local ? nav$local.getRootNode() : null;
|
|
170
|
+
const active = root ? root.activeElement : null;
|
|
171
|
+
return els.indexOf(active);
|
|
172
|
+
}
|
|
173
|
+
const onControlKeydown = useCallback(($event) => {
|
|
174
|
+
if (props.disabled) return;
|
|
175
|
+
const key = $event.key;
|
|
176
|
+
const cur = focusedIndex();
|
|
177
|
+
if (key === "ArrowRight" || key === "ArrowDown") {
|
|
178
|
+
$event.preventDefault();
|
|
179
|
+
focusControlAt(cur + 1);
|
|
180
|
+
} else if (key === "ArrowLeft" || key === "ArrowUp") {
|
|
181
|
+
$event.preventDefault();
|
|
182
|
+
focusControlAt(cur - 1);
|
|
183
|
+
} else if (key === "Home") {
|
|
184
|
+
$event.preventDefault();
|
|
185
|
+
focusControlAt(0);
|
|
186
|
+
} else if (key === "End") {
|
|
187
|
+
$event.preventDefault();
|
|
188
|
+
focusControlAt(controls().length - 1);
|
|
189
|
+
}
|
|
190
|
+
}, [
|
|
191
|
+
controls,
|
|
192
|
+
focusControlAt,
|
|
193
|
+
focusedIndex,
|
|
194
|
+
props.disabled
|
|
195
|
+
]);
|
|
196
|
+
function goto(page) {
|
|
197
|
+
return goToPage(page);
|
|
198
|
+
}
|
|
199
|
+
function next() {
|
|
200
|
+
return goNext();
|
|
201
|
+
}
|
|
202
|
+
function prev() {
|
|
203
|
+
return goPrev();
|
|
204
|
+
}
|
|
205
|
+
function first() {
|
|
206
|
+
return goFirst();
|
|
207
|
+
}
|
|
208
|
+
function last() {
|
|
209
|
+
return goLast();
|
|
210
|
+
}
|
|
211
|
+
const _rozieExposeRef = useRef({
|
|
212
|
+
goto,
|
|
213
|
+
next,
|
|
214
|
+
prev,
|
|
215
|
+
first,
|
|
216
|
+
last
|
|
217
|
+
});
|
|
218
|
+
_rozieExposeRef.current = {
|
|
219
|
+
goto,
|
|
220
|
+
next,
|
|
221
|
+
prev,
|
|
222
|
+
first,
|
|
223
|
+
last
|
|
224
|
+
};
|
|
225
|
+
useImperativeHandle(ref, () => ({
|
|
226
|
+
goto: (...args) => flushSync(() => _rozieExposeRef.current.goto(...args)),
|
|
227
|
+
next: (...args) => flushSync(() => _rozieExposeRef.current.next(...args)),
|
|
228
|
+
prev: (...args) => flushSync(() => _rozieExposeRef.current.prev(...args)),
|
|
229
|
+
first: (...args) => flushSync(() => _rozieExposeRef.current.first(...args)),
|
|
230
|
+
last: (...args) => flushSync(() => _rozieExposeRef.current.last(...args))
|
|
231
|
+
}), []);
|
|
232
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsxs("nav", {
|
|
233
|
+
ref: nav,
|
|
234
|
+
"aria-label": props.ariaLabel,
|
|
235
|
+
...attrs,
|
|
236
|
+
className: clsx(clsx("rozie-pagination", { "rozie-pagination--disabled": props.disabled }), attrs.className),
|
|
237
|
+
...mergeListeners({ onKeyDown: ($event) => {
|
|
238
|
+
onControlKeydown($event);
|
|
239
|
+
} }, pickListeners(attrs)),
|
|
240
|
+
"data-rozie-s-de247ae2": "",
|
|
241
|
+
children: [
|
|
242
|
+
props.renderPrevControl ?? props.slots?.["prevControl"] ? (props.renderPrevControl ?? props.slots?.["prevControl"])({
|
|
243
|
+
disabled: !canPrev() || props.disabled,
|
|
244
|
+
goto: goPrev,
|
|
245
|
+
page: currentPage() - 1
|
|
246
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
247
|
+
type: "button",
|
|
248
|
+
className: "rozie-pagination-control rozie-pagination-prev",
|
|
249
|
+
"data-page-control": "",
|
|
250
|
+
tabIndex: tabIndexFor(true),
|
|
251
|
+
disabled: !canPrev() || props.disabled,
|
|
252
|
+
"aria-disabled": !!(!canPrev() || props.disabled),
|
|
253
|
+
"aria-label": "Previous page",
|
|
254
|
+
onClick: goPrev,
|
|
255
|
+
"data-rozie-s-de247ae2": "",
|
|
256
|
+
children: "‹"
|
|
257
|
+
}),
|
|
258
|
+
model().pages.map((item, index) => /* @__PURE__ */ jsxs(Fragment, { children: [!!(item === "ellipsis") && /* @__PURE__ */ jsx("span", {
|
|
259
|
+
className: "rozie-pagination-ellipsis",
|
|
260
|
+
"aria-hidden": "true",
|
|
261
|
+
"data-rozie-s-de247ae2": "",
|
|
262
|
+
children: props.renderEllipsis ?? props.slots?.["ellipsis"] ? (props.renderEllipsis ?? props.slots?.["ellipsis"])({ index }) : "…"
|
|
263
|
+
}), !!(item !== "ellipsis") && /* @__PURE__ */ jsx("span", {
|
|
264
|
+
className: "rozie-pagination-item",
|
|
265
|
+
"data-rozie-s-de247ae2": "",
|
|
266
|
+
children: props.renderItem ?? props.slots?.["item"] ? (props.renderItem ?? props.slots?.["item"])({
|
|
267
|
+
page: item,
|
|
268
|
+
selected: isActive(item),
|
|
269
|
+
goto: () => goToPage(item)
|
|
270
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
271
|
+
type: "button",
|
|
272
|
+
className: clsx("rozie-pagination-page", { "is-active": isActive(item) }),
|
|
273
|
+
"data-page-control": "",
|
|
274
|
+
tabIndex: tabIndexFor(isActive(item)),
|
|
275
|
+
disabled: !!props.disabled,
|
|
276
|
+
"aria-disabled": !!props.disabled,
|
|
277
|
+
"aria-current": rozieAttr(isActive(item) ? "page" : void 0),
|
|
278
|
+
"aria-label": rozieAttr("Go to page " + item),
|
|
279
|
+
onClick: ($event) => {
|
|
280
|
+
goToPage(item);
|
|
281
|
+
},
|
|
282
|
+
"data-rozie-s-de247ae2": "",
|
|
283
|
+
children: rozieDisplay(item)
|
|
284
|
+
})
|
|
285
|
+
})] }, item === "ellipsis" ? "ellipsis-" + index : item)),
|
|
286
|
+
props.renderNextControl ?? props.slots?.["nextControl"] ? (props.renderNextControl ?? props.slots?.["nextControl"])({
|
|
287
|
+
disabled: !canNext() || props.disabled,
|
|
288
|
+
goto: goNext,
|
|
289
|
+
page: currentPage() + 1
|
|
290
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
291
|
+
type: "button",
|
|
292
|
+
className: "rozie-pagination-control rozie-pagination-next",
|
|
293
|
+
"data-page-control": "",
|
|
294
|
+
tabIndex: tabIndexFor(true),
|
|
295
|
+
disabled: !canNext() || props.disabled,
|
|
296
|
+
"aria-disabled": !!(!canNext() || props.disabled),
|
|
297
|
+
"aria-label": "Next page",
|
|
298
|
+
onClick: goNext,
|
|
299
|
+
"data-rozie-s-de247ae2": "",
|
|
300
|
+
children: "›"
|
|
301
|
+
})
|
|
302
|
+
]
|
|
303
|
+
}) });
|
|
304
|
+
});
|
|
305
|
+
//#endregion
|
|
306
|
+
export { Pagination, Pagination as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozie-ui/pagination-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "Idiomatic React headless WAI-ARIA pagination control (page-item model with ellipses, sibling/boundary windowing, prev/next, roving keyboard nav) — one accessible Rozie source compiled to React.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"rozie",
|
|
10
|
+
"rozie-ui",
|
|
11
|
+
"pagination",
|
|
12
|
+
"pager",
|
|
13
|
+
"page-navigation",
|
|
14
|
+
"nav",
|
|
15
|
+
"headless",
|
|
16
|
+
"accessibility",
|
|
17
|
+
"aria",
|
|
18
|
+
"component",
|
|
19
|
+
"react"
|
|
20
|
+
],
|
|
21
|
+
"author": "One Learning Community (https://github.com/One-Learning-Community)",
|
|
22
|
+
"homepage": "https://github.com/One-Learning-Community/rozie.js#readme",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/One-Learning-Community/rozie.js.git",
|
|
26
|
+
"directory": "packages/ui/pagination/packages/react"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/One-Learning-Community/rozie.js/issues"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.cjs",
|
|
32
|
+
"module": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"import": "./dist/index.mjs",
|
|
38
|
+
"require": "./dist/index.cjs"
|
|
39
|
+
},
|
|
40
|
+
"./source": "./src/Pagination.tsx",
|
|
41
|
+
"./themes/*": "./src/themes/*"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"src",
|
|
46
|
+
"CHANGELOG.md"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@rozie/runtime-react": "^0.10.1"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^18.2 || ^19",
|
|
53
|
+
"react-dom": "^18.2 || ^19"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"react": {
|
|
57
|
+
"optional": false
|
|
58
|
+
},
|
|
59
|
+
"react-dom": {
|
|
60
|
+
"optional": false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/react": "^18",
|
|
65
|
+
"@types/react-dom": "^18"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsdown",
|
|
72
|
+
"typecheck": "tsc --noEmit"
|
|
73
|
+
}
|
|
6
74
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.rozie-pagination[data-rozie-s-de247ae2] {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--rozie-pagination-gap, var(--rpg-gap, 0.25rem));
|
|
5
|
+
font: var(--rozie-pagination-font, inherit);
|
|
6
|
+
}
|
|
7
|
+
.rozie-pagination-page[data-rozie-s-de247ae2],
|
|
8
|
+
.rozie-pagination-control[data-rozie-s-de247ae2] {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
min-width: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
|
|
11
|
+
height: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
|
|
12
|
+
padding: 0 var(--rozie-pagination-padding-x, var(--rpg-padding-x, 0.5rem));
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
font: inherit;
|
|
17
|
+
font-weight: var(--rozie-pagination-font-weight, var(--rpg-font-weight, 500));
|
|
18
|
+
color: var(--rozie-pagination-fg, var(--rpg-fg, #1a1a1a));
|
|
19
|
+
background: var(--rozie-pagination-bg, var(--rpg-bg, transparent));
|
|
20
|
+
border: var(--rozie-pagination-border-width, var(--rpg-border-width, 1px)) solid var(--rozie-pagination-border, var(--rpg-border, rgba(0, 0, 0, 0.18)));
|
|
21
|
+
border-radius: var(--rozie-pagination-radius, var(--rpg-radius, 6px));
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
user-select: none;
|
|
24
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
25
|
+
}
|
|
26
|
+
.rozie-pagination-page[data-rozie-s-de247ae2]:hover,
|
|
27
|
+
.rozie-pagination-control[data-rozie-s-de247ae2]:hover {
|
|
28
|
+
background: var(--rozie-pagination-hover-bg, var(--rpg-hover-bg, rgba(0, 0, 0, 0.05)));
|
|
29
|
+
border-color: var(--rozie-pagination-hover-border, var(--rpg-hover-border, rgba(0, 0, 0, 0.28)));
|
|
30
|
+
}
|
|
31
|
+
.rozie-pagination-page[data-rozie-s-de247ae2]:focus-visible,
|
|
32
|
+
.rozie-pagination-control[data-rozie-s-de247ae2]:focus-visible {
|
|
33
|
+
outline: var(--rozie-pagination-ring-width, var(--rpg-ring-width, 2px)) solid var(--rozie-pagination-ring, var(--rozie-pagination-accent, var(--rpg-ring, var(--rpg-accent, #0066cc))));
|
|
34
|
+
outline-offset: var(--rozie-pagination-ring-offset, var(--rpg-ring-offset, 1px));
|
|
35
|
+
}
|
|
36
|
+
.rozie-pagination-page.is-active[data-rozie-s-de247ae2] {
|
|
37
|
+
color: var(--rozie-pagination-active-fg, var(--rpg-active-fg, #fff));
|
|
38
|
+
background: var(--rozie-pagination-active-bg, var(--rozie-pagination-accent, var(--rpg-active-bg, var(--rpg-accent, #0066cc))));
|
|
39
|
+
border-color: var(--rozie-pagination-active-border, var(--rozie-pagination-accent, var(--rpg-active-border, var(--rpg-accent, #0066cc))));
|
|
40
|
+
}
|
|
41
|
+
.rozie-pagination-page[data-rozie-s-de247ae2]:disabled,
|
|
42
|
+
.rozie-pagination-control[data-rozie-s-de247ae2]:disabled {
|
|
43
|
+
cursor: not-allowed;
|
|
44
|
+
opacity: var(--rozie-pagination-disabled-opacity, var(--rpg-disabled-opacity, 0.5));
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
}
|
|
47
|
+
.rozie-pagination--disabled[data-rozie-s-de247ae2] {
|
|
48
|
+
opacity: var(--rozie-pagination-disabled-opacity, var(--rpg-disabled-opacity, 0.5));
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
}
|
|
51
|
+
.rozie-pagination-ellipsis[data-rozie-s-de247ae2] {
|
|
52
|
+
min-width: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
|
|
53
|
+
height: var(--rozie-pagination-size, var(--rpg-size, 2.25rem));
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
color: var(--rozie-pagination-ellipsis-fg, var(--rpg-ellipsis-fg, rgba(0, 0, 0, 0.5)));
|
|
58
|
+
user-select: none;
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ForwardRefExoticComponent, RefAttributes } from 'react';
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
/** The `change` event payload: the page that was just navigated to. */
|
|
6
|
+
export interface PaginationChangePayload {
|
|
7
|
+
/** The new, clamped, 1-based current page. */
|
|
8
|
+
page: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PaginationProps extends Omit<import('react').ComponentPropsWithoutRef<'nav'>, 'modelValue' | 'defaultModelValue' | 'onModelValueChange' | 'totalPages' | 'total' | 'pageSize' | 'siblingCount' | 'boundaryCount' | 'disabled' | 'ariaLabel' | 'onChange' | 'renderPrevControl' | 'renderEllipsis' | 'renderItem' | 'renderNextControl' | 'slots' | 'children' | 'dangerouslySetInnerHTML'> {
|
|
12
|
+
/**
|
|
13
|
+
* The 1-based current page (two-way model). Clamped into `[1, totalPages]`. Bind it with `r-model:modelValue` / `v-model:modelValue` / `modelValue` + `onModelValueChange`; it is also the Angular ControlValueAccessor control value.
|
|
14
|
+
*/
|
|
15
|
+
modelValue?: number;
|
|
16
|
+
defaultModelValue?: number;
|
|
17
|
+
onModelValueChange?: (next: number) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Explicit total page count. When provided (> 0) it takes precedence over `total` + `pageSize`. Use it when the backend already reports the page count.
|
|
20
|
+
*/
|
|
21
|
+
totalPages?: (number) | null;
|
|
22
|
+
/**
|
|
23
|
+
* Total item count. Combined with `pageSize` to derive the page count (`ceil(total / pageSize)`) when `totalPages` is not given.
|
|
24
|
+
*/
|
|
25
|
+
total?: (number) | null;
|
|
26
|
+
/**
|
|
27
|
+
* Items per page. Combined with `total` to derive the page count when `totalPages` is not given.
|
|
28
|
+
*/
|
|
29
|
+
pageSize?: (number) | null;
|
|
30
|
+
/**
|
|
31
|
+
* Number of page buttons shown on each side of the current page (the sibling window). Larger values show more context around the current page.
|
|
32
|
+
*/
|
|
33
|
+
siblingCount?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Number of page buttons always shown at each boundary (the first and last `boundaryCount` pages), regardless of the current page.
|
|
36
|
+
*/
|
|
37
|
+
boundaryCount?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Disable the entire control — every page button and the prev/next controls become non-interactive and are marked `aria-disabled`.
|
|
40
|
+
*/
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Accessible name for the surrounding `<nav>` landmark (its `aria-label`). Defaults to `"Pagination"`.
|
|
44
|
+
*/
|
|
45
|
+
ariaLabel?: string;
|
|
46
|
+
onChange?: (payload: PaginationChangePayload) => void;
|
|
47
|
+
renderPrevControl?: (params: { disabled: boolean; goto: () => void; page: number }) => ReactNode;
|
|
48
|
+
renderEllipsis?: (params: { index: number }) => ReactNode;
|
|
49
|
+
renderItem?: (params: { page: number; selected: boolean; goto: () => void }) => ReactNode;
|
|
50
|
+
renderNextControl?: (params: { disabled: boolean; goto: () => void; page: number }) => ReactNode;
|
|
51
|
+
slots?: Record<string, () => ReactNode>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PaginationHandle {
|
|
55
|
+
goto: (page: number) => void;
|
|
56
|
+
next: () => void;
|
|
57
|
+
prev: () => void;
|
|
58
|
+
first: () => void;
|
|
59
|
+
last: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare const Pagination: React.ForwardRefExoticComponent<PaginationProps & React.RefAttributes<PaginationHandle>>;
|
|
63
|
+
export default Pagination;
|