@sproutsocial/seeds-react-pagination 0.1.28 → 0.1.31

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,164 @@
1
+ import {
2
+ render,
3
+ screen,
4
+ fireEvent,
5
+ } from "@sproutsocial/seeds-react-testing-library";
6
+ import { Pagination } from "../Pagination";
7
+ import { PAGE_SIZE_OPTIONS } from "../PaginationTypes";
8
+ import type { TypePaginationConfig } from "../PaginationTypes";
9
+
10
+ const tableCountFunction: TypePaginationConfig["tableCountFunction"] = ({
11
+ startNum,
12
+ endNum,
13
+ totalRowCount,
14
+ }) => `${startNum}-${endNum} of ${totalRowCount}`;
15
+
16
+ const baseConfig: TypePaginationConfig = {
17
+ initialPageSize: 10,
18
+ tableCountFunction,
19
+ };
20
+
21
+ describe("Pagination", () => {
22
+ it("is a named component definition (not a bare re-export) so react-docgen can extract its props", () => {
23
+ // The public entry must contain an actual component definition rather than a
24
+ // pure `export { ... } from "./PaginationHybrid"` re-export. The Publish
25
+ // pipeline runs `react-docgen --failOnWarning` over Pagination.tsx, which
26
+ // errors with "No suitable component definition found" when the file only
27
+ // re-exports the hybrid. A self-named component with a displayName gives
28
+ // react-docgen a definition to analyze.
29
+ expect(typeof Pagination).toBe("function");
30
+ expect(Pagination.displayName).toBe("Pagination");
31
+ });
32
+
33
+ it("renders nothing when there is a single page and no page size options", () => {
34
+ const { container } = render(
35
+ <Pagination
36
+ paginationConfig={baseConfig}
37
+ currentPageIndex={0}
38
+ pageSize={10}
39
+ totalRows={0}
40
+ onPageChange={jest.fn()}
41
+ onPageSizeChange={jest.fn()}
42
+ />
43
+ );
44
+ expect(container.firstChild).toBeNull();
45
+ });
46
+
47
+ it("renders the count text and page-number controls for a multi-page config", () => {
48
+ render(
49
+ <Pagination
50
+ paginationConfig={baseConfig}
51
+ currentPageIndex={0}
52
+ pageSize={10}
53
+ totalRows={100}
54
+ onPageChange={jest.fn()}
55
+ onPageSizeChange={jest.fn()}
56
+ />
57
+ );
58
+ expect(screen.getByText("1-10 of 100")).toBeInTheDocument();
59
+ expect(screen.getByLabelText("Page 2")).toBeInTheDocument();
60
+ expect(screen.getByLabelText("Previous page")).toBeInTheDocument();
61
+ expect(screen.getByLabelText("Next page")).toBeInTheDocument();
62
+ });
63
+
64
+ it("calls onPageChange with the 0-based index when a page button is clicked", () => {
65
+ const onPageChange = jest.fn();
66
+ render(
67
+ <Pagination
68
+ paginationConfig={baseConfig}
69
+ currentPageIndex={0}
70
+ pageSize={10}
71
+ totalRows={100}
72
+ onPageChange={onPageChange}
73
+ onPageSizeChange={jest.fn()}
74
+ />
75
+ );
76
+ fireEvent.click(screen.getByLabelText("Page 2"));
77
+ expect(onPageChange).toHaveBeenCalledWith(1);
78
+ });
79
+
80
+ it("marks the current page button as disabled with aria-current='page'", () => {
81
+ render(
82
+ <Pagination
83
+ paginationConfig={baseConfig}
84
+ currentPageIndex={0}
85
+ pageSize={10}
86
+ totalRows={100}
87
+ onPageChange={jest.fn()}
88
+ onPageSizeChange={jest.fn()}
89
+ />
90
+ );
91
+ const currentPage = screen.getByLabelText("Page 1");
92
+ expect(currentPage).toBeDisabled();
93
+ expect(currentPage).toHaveAttribute("aria-current", "page");
94
+ });
95
+
96
+ it("disables Previous on the first page and Next on the last page", () => {
97
+ const { rerender } = render(
98
+ <Pagination
99
+ paginationConfig={baseConfig}
100
+ currentPageIndex={0}
101
+ pageSize={10}
102
+ totalRows={100}
103
+ onPageChange={jest.fn()}
104
+ onPageSizeChange={jest.fn()}
105
+ />
106
+ );
107
+ expect(screen.getByLabelText("Previous page")).toBeDisabled();
108
+ expect(screen.getByLabelText("Next page")).not.toBeDisabled();
109
+
110
+ rerender(
111
+ <Pagination
112
+ paginationConfig={baseConfig}
113
+ currentPageIndex={9}
114
+ pageSize={10}
115
+ totalRows={100}
116
+ onPageChange={jest.fn()}
117
+ onPageSizeChange={jest.fn()}
118
+ />
119
+ );
120
+ expect(screen.getByLabelText("Previous page")).not.toBeDisabled();
121
+ expect(screen.getByLabelText("Next page")).toBeDisabled();
122
+ });
123
+
124
+ it("forwards an optional className onto the root container so consumers can apply utility overrides", () => {
125
+ const { container } = render(
126
+ <Pagination
127
+ paginationConfig={baseConfig}
128
+ currentPageIndex={0}
129
+ pageSize={10}
130
+ totalRows={100}
131
+ onPageChange={jest.fn()}
132
+ onPageSizeChange={jest.fn()}
133
+ className="test-utility"
134
+ />
135
+ );
136
+ expect(container.firstChild).toHaveClass("test-utility");
137
+ // The base component class must remain so the override composes rather than replaces.
138
+ expect(container.firstChild).toHaveClass("seeds-pagination-container");
139
+ });
140
+
141
+ it("calls onPageSizeChange and resets to the first page when the page size changes", () => {
142
+ const onPageChange = jest.fn();
143
+ const onPageSizeChange = jest.fn();
144
+ render(
145
+ <Pagination
146
+ paginationConfig={{
147
+ ...baseConfig,
148
+ displayType: "dropdown",
149
+ pageSizeOptions: PAGE_SIZE_OPTIONS.STANDARD,
150
+ }}
151
+ currentPageIndex={2}
152
+ pageSize={10}
153
+ totalRows={100}
154
+ onPageChange={onPageChange}
155
+ onPageSizeChange={onPageSizeChange}
156
+ />
157
+ );
158
+ fireEvent.change(screen.getByLabelText("Rows per page"), {
159
+ target: { value: "25" },
160
+ });
161
+ expect(onPageSizeChange).toHaveBeenCalledWith(25);
162
+ expect(onPageChange).toHaveBeenCalledWith(0);
163
+ });
164
+ });
@@ -0,0 +1,91 @@
1
+ import type * as React from "react";
2
+ import { usePaginationModel } from "../usePaginationModel";
3
+ import type { PaginationProps, TypePaginationConfig } from "../PaginationTypes";
4
+
5
+ const tableCountFunction: TypePaginationConfig["tableCountFunction"] = ({
6
+ startNum,
7
+ endNum,
8
+ totalRowCount,
9
+ }) => `${startNum}-${endNum} of ${totalRowCount}`;
10
+
11
+ const baseConfig: TypePaginationConfig = {
12
+ initialPageSize: 10,
13
+ tableCountFunction,
14
+ };
15
+
16
+ const buildProps = (
17
+ overrides: Partial<PaginationProps> = {}
18
+ ): PaginationProps => ({
19
+ paginationConfig: baseConfig,
20
+ currentPageIndex: 0,
21
+ pageSize: 10,
22
+ totalRows: 100,
23
+ onPageChange: jest.fn(),
24
+ onPageSizeChange: jest.fn(),
25
+ ...overrides,
26
+ });
27
+
28
+ describe("usePaginationModel", () => {
29
+ it("derives the page math (totalPages / currentPage / row range) for a multi-page config", () => {
30
+ const model = usePaginationModel(buildProps());
31
+ expect(model.totalPages).toBe(10);
32
+ expect(model.currentPage).toBe(1); // 0-based index -> 1-based display
33
+ expect(model.startRow).toBe(1);
34
+ expect(model.endRow).toBe(10);
35
+ expect(model.canGoPrevious).toBe(false);
36
+ expect(model.canGoNext).toBe(true);
37
+ });
38
+
39
+ it("reports startRow as 0 when there are no rows", () => {
40
+ const model = usePaginationModel(buildProps({ totalRows: 0 }));
41
+ expect(model.totalPages).toBe(0);
42
+ expect(model.startRow).toBe(0);
43
+ expect(model.endRow).toBe(0);
44
+ });
45
+
46
+ it("collapses the page-number list with ellipses for large page counts", () => {
47
+ // 20 pages, on the first page: first page, a window, ellipsis, last page.
48
+ const model = usePaginationModel(
49
+ buildProps({ totalRows: 200, currentPageIndex: 0 })
50
+ );
51
+ expect(model.pageNumbers).toEqual([1, 2, 3, 4, 5, 6, "...", 20]);
52
+ });
53
+
54
+ it("shows leading and trailing ellipses when the current page is in the middle", () => {
55
+ const model = usePaginationModel(
56
+ buildProps({ totalRows: 200, currentPageIndex: 9 }) // page 10 of 20
57
+ );
58
+ expect(model.pageNumbers).toEqual([1, "...", 9, 10, 11, 12, "...", 20]);
59
+ });
60
+
61
+ it("lists every page without ellipsis when the count is small", () => {
62
+ const model = usePaginationModel(
63
+ buildProps({ totalRows: 30, currentPageIndex: 0 }) // 3 pages
64
+ );
65
+ expect(model.pageNumbers).toEqual([1, 2, 3]);
66
+ });
67
+
68
+ it("handlePrevious / handleNext respect the boundary guards", () => {
69
+ const onPageChange = jest.fn();
70
+ const first = usePaginationModel(
71
+ buildProps({ currentPageIndex: 0, onPageChange })
72
+ );
73
+ first.handlePrevious(); // already on first page -> no-op
74
+ expect(onPageChange).not.toHaveBeenCalled();
75
+ first.handleNext();
76
+ expect(onPageChange).toHaveBeenCalledWith(1);
77
+ });
78
+
79
+ it("handlePageSizeChange forwards the new size and resets to the first page", () => {
80
+ const onPageChange = jest.fn();
81
+ const onPageSizeChange = jest.fn();
82
+ const model = usePaginationModel(
83
+ buildProps({ onPageChange, onPageSizeChange })
84
+ );
85
+ model.handlePageSizeChange({
86
+ currentTarget: { value: "25" },
87
+ } as React.SyntheticEvent<HTMLSelectElement>);
88
+ expect(onPageSizeChange).toHaveBeenCalledWith(25);
89
+ expect(onPageChange).toHaveBeenCalledWith(0);
90
+ });
91
+ });
package/src/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Seeds Pagination component classes
3
+ * Use these instead of writing out individual Tailwind utility classes.
4
+ *
5
+ * Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
6
+ * CSS variable definitions and dark mode support.
7
+ *
8
+ * Rules are wrapped in @layer components so consumer Tailwind utilities (which
9
+ * live in @layer utilities) win the cascade. Without this layer the unlayered
10
+ * component CSS would beat utilities like mx-200 / p-300 / inline-flex and they
11
+ * could not override component styling. Keep every rule inside this single block
12
+ * in source order — the -selected modifier and the :disabled overrides resolve
13
+ * by source order within the same layer.
14
+ */
15
+
16
+ @layer components {
17
+ /* StyledPaginationContainer */
18
+ .seeds-pagination-container {
19
+ font-size: var(--font-size-200);
20
+ line-height: var(--line-height-200);
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--space-300);
24
+ margin-top: var(--space-400);
25
+ flex-wrap: wrap;
26
+ }
27
+
28
+ /* StyledPaginationInfo (styled(Text)) */
29
+ .seeds-pagination-info {
30
+ white-space: nowrap;
31
+ }
32
+
33
+ /* StyledPageSizeSelector */
34
+ .seeds-pagination-page-size-selector {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: var(--space-300);
38
+ }
39
+
40
+ /* StyledPaginationControls */
41
+ .seeds-pagination-controls {
42
+ display: flex;
43
+ align-items: center;
44
+ margin-left: auto;
45
+ }
46
+
47
+ /* StyledPageButtons */
48
+ .seeds-pagination-page-buttons {
49
+ display: inline-flex;
50
+ flex-flow: row wrap;
51
+ background-clip: padding-box;
52
+ border: 1px solid var(--color-button-secondary-border-base);
53
+ border-radius: var(--radius-outer);
54
+ padding: var(--space-100);
55
+ background: transparent;
56
+ }
57
+
58
+ /* StyledPaginationButton — base */
59
+ .seeds-pagination-button {
60
+ min-width: calc(var(--space-500) + 4px);
61
+ min-height: calc(var(--space-500) + 2px);
62
+ padding: calc(var(--space-350) - 6px);
63
+ line-height: var(--space-400);
64
+ font-weight: var(--font-weight-semibold);
65
+ background-color: transparent;
66
+ color: var(--color-text-body);
67
+ border: none;
68
+ border-radius: var(--radius-inner);
69
+ cursor: pointer;
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ }
74
+
75
+ /* selected modifier MUST follow base (equal specificity — source order wins) */
76
+ .seeds-pagination-button-selected {
77
+ background-color: var(--color-button-secondary-bg-active);
78
+ color: var(--color-text-inverse);
79
+ }
80
+
81
+ .seeds-pagination-button:hover:not(:disabled) {
82
+ background-color: var(--color-listItem-bg-hover);
83
+ }
84
+
85
+ .seeds-pagination-button-selected:hover:not(:disabled) {
86
+ background-color: var(--color-button-secondary-bg-active);
87
+ color: var(--color-text-inverse);
88
+ }
89
+
90
+ .seeds-pagination-button:disabled {
91
+ cursor: default;
92
+ opacity: 0.5;
93
+ }
94
+
95
+ /* current page renders disabled + selected and must stay fully opaque */
96
+ .seeds-pagination-button-selected:disabled {
97
+ opacity: 1;
98
+ }
99
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Presentation-agnostic Pagination model: page math, the page-number token
3
+ * list, and the event handlers consumed identically by PaginationTailwind and
4
+ * PaginationStyled. Contains NO rendering — the JSX and the `return null` guard
5
+ * stay in each component — so the two implementations cannot drift apart. Call
6
+ * it unconditionally at the top of each component, before any early return.
7
+ */
8
+
9
+ import * as React from "react";
10
+ import type { PaginationProps } from "./PaginationTypes";
11
+
12
+ export const usePaginationModel = ({
13
+ paginationConfig,
14
+ currentPageIndex,
15
+ pageSize,
16
+ totalRows,
17
+ onPageChange,
18
+ onPageSizeChange,
19
+ }: PaginationProps) => {
20
+ const { displayType, pageSizeOptions, tableCountFunction } = paginationConfig;
21
+
22
+ // Calculate pagination values
23
+ const totalPages = Math.ceil(totalRows / pageSize);
24
+ const currentPage = currentPageIndex + 1; // Convert 0-based to 1-based for display
25
+ const startRow = totalRows === 0 ? 0 : currentPageIndex * pageSize + 1;
26
+ const endRow = Math.min((currentPageIndex + 1) * pageSize, totalRows);
27
+
28
+ const canGoPrevious = currentPageIndex > 0;
29
+ const canGoNext = currentPageIndex < totalPages - 1;
30
+
31
+ const handlePrevious = () => {
32
+ if (canGoPrevious) {
33
+ onPageChange(currentPageIndex - 1);
34
+ }
35
+ };
36
+
37
+ const handleNext = () => {
38
+ if (canGoNext) {
39
+ onPageChange(currentPageIndex + 1);
40
+ }
41
+ };
42
+
43
+ const handlePageSizeChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
44
+ const newPageSize = Number(e.currentTarget.value);
45
+ onPageSizeChange(newPageSize);
46
+ // Reset to first page when page size changes
47
+ onPageChange(0);
48
+ };
49
+
50
+ // Generate page number buttons using web-app-core's algorithm
51
+ const getPageNumbers = () => {
52
+ const pages: (number | string)[] = [];
53
+ const size = paginationConfig.size || "default";
54
+
55
+ // Configuration for how many pages to show around current page
56
+ const NUM_PAGE_LOWER = size === "mini" ? 0 : 1;
57
+ const NUM_PAGE_UPPER = size === "mini" ? 0 : 2;
58
+
59
+ // If number of pages is small enough, show all the page tokens without ellipsis
60
+ if (totalPages <= 5 + NUM_PAGE_LOWER + NUM_PAGE_UPPER) {
61
+ for (let i = 1; i <= totalPages; i++) {
62
+ pages.push(i);
63
+ }
64
+ } else {
65
+ // Add first page
66
+ pages.push(1);
67
+
68
+ // Add ellipsis at the beginning if needed
69
+ if (currentPage > 3 + NUM_PAGE_LOWER) {
70
+ pages.push("...");
71
+ }
72
+
73
+ // Add pages in the middle
74
+ let start: number;
75
+ let end: number; // inclusive index
76
+
77
+ // If near beginning - Show number of pages before ending ellipsis
78
+ if (currentPage <= 3 + NUM_PAGE_LOWER) {
79
+ start = 2;
80
+ end = 3 + NUM_PAGE_LOWER + NUM_PAGE_UPPER;
81
+ } else if (currentPage > totalPages - (3 + NUM_PAGE_UPPER)) {
82
+ // If near end - Show number of pages after starting ellipsis
83
+ start = totalPages - (2 + NUM_PAGE_LOWER + NUM_PAGE_UPPER);
84
+ end = totalPages - 1;
85
+ } else {
86
+ // If in the middle - Show number of pages between two ellipses
87
+ start = currentPage - NUM_PAGE_LOWER;
88
+ end = currentPage + NUM_PAGE_UPPER;
89
+ }
90
+
91
+ for (let i = start; i <= end; i++) {
92
+ pages.push(i);
93
+ }
94
+
95
+ // Add a final ellipsis if necessary
96
+ if (currentPage < totalPages - (2 + NUM_PAGE_UPPER)) {
97
+ pages.push("...");
98
+ }
99
+
100
+ // Add the last page number
101
+ pages.push(totalPages);
102
+ }
103
+
104
+ return pages;
105
+ };
106
+
107
+ const pageNumbers = getPageNumbers();
108
+
109
+ return {
110
+ displayType,
111
+ pageSizeOptions,
112
+ tableCountFunction,
113
+ totalPages,
114
+ currentPage,
115
+ startRow,
116
+ endRow,
117
+ canGoPrevious,
118
+ canGoNext,
119
+ handlePrevious,
120
+ handleNext,
121
+ handlePageSizeChange,
122
+ pageNumbers,
123
+ };
124
+ };