@windstream/react-shared-components 0.1.92 → 0.1.94

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.
Files changed (293) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.d.ts +1 -0
  3. package/dist/contentful/index.esm.js +2 -2
  4. package/dist/contentful/index.esm.js.map +1 -1
  5. package/dist/contentful/index.js +3 -3
  6. package/dist/contentful/index.js.map +1 -1
  7. package/dist/core.d.ts +5 -5
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.esm.js +1 -1
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles.css +1 -1
  14. package/dist/utils/index.esm.js +1 -1
  15. package/dist/utils/index.js +1 -1
  16. package/package.json +191 -185
  17. package/src/components/accordion/Accordion.stories.tsx +230 -230
  18. package/src/components/accordion/index.test.tsx +270 -0
  19. package/src/components/accordion/index.tsx +70 -70
  20. package/src/components/accordion/types.ts +12 -12
  21. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  22. package/src/components/alert-card/index.test.tsx +152 -0
  23. package/src/components/alert-card/index.tsx +41 -41
  24. package/src/components/alert-card/types.ts +13 -13
  25. package/src/components/animation-wrapper/index.test.tsx +424 -0
  26. package/src/components/animation-wrapper/index.tsx +129 -129
  27. package/src/components/animation-wrapper/types.ts +11 -11
  28. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  29. package/src/components/brand-button/helpers.ts +35 -35
  30. package/src/components/brand-button/index.test.tsx +292 -0
  31. package/src/components/brand-button/index.tsx +120 -120
  32. package/src/components/brand-button/types.ts +38 -38
  33. package/src/components/button/Button.stories.tsx +108 -108
  34. package/src/components/button/index.test.tsx +91 -0
  35. package/src/components/button/index.tsx +27 -27
  36. package/src/components/button/types.ts +14 -14
  37. package/src/components/call-button/CallButton.stories.tsx +324 -324
  38. package/src/components/call-button/index.test.tsx +260 -0
  39. package/src/components/call-button/index.tsx +106 -106
  40. package/src/components/call-button/types.ts +16 -16
  41. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  42. package/src/components/checkbox/index.test.tsx +252 -0
  43. package/src/components/checkbox/index.tsx +197 -197
  44. package/src/components/checkbox/types.ts +27 -27
  45. package/src/components/checklist/Checklist.stories.tsx +150 -150
  46. package/src/components/checklist/index.test.tsx +231 -0
  47. package/src/components/checklist/index.tsx +96 -61
  48. package/src/components/checklist/types.ts +23 -17
  49. package/src/components/collapse/Collapse.stories.tsx +255 -255
  50. package/src/components/collapse/index.test.tsx +277 -0
  51. package/src/components/collapse/index.tsx +47 -46
  52. package/src/components/collapse/types.ts +6 -6
  53. package/src/components/divider/Divider.stories.tsx +205 -205
  54. package/src/components/divider/index.test.tsx +53 -0
  55. package/src/components/divider/index.tsx +22 -22
  56. package/src/components/divider/type.ts +3 -3
  57. package/src/components/image/Image.stories.tsx +113 -113
  58. package/src/components/image/index.test.tsx +174 -0
  59. package/src/components/image/index.tsx +25 -25
  60. package/src/components/image/types.ts +40 -40
  61. package/src/components/input/Input.stories.tsx +325 -325
  62. package/src/components/input/index.test.tsx +348 -0
  63. package/src/components/input/index.tsx +177 -177
  64. package/src/components/input/types.ts +37 -37
  65. package/src/components/link/Link.stories.tsx +163 -163
  66. package/src/components/link/index.test.tsx +199 -0
  67. package/src/components/link/index.tsx +116 -116
  68. package/src/components/link/types.ts +25 -25
  69. package/src/components/list/List.stories.tsx +272 -272
  70. package/src/components/list/index.test.tsx +166 -0
  71. package/src/components/list/index.tsx +88 -88
  72. package/src/components/list/list-item/index.tsx +38 -38
  73. package/src/components/list/list-item/types.ts +13 -13
  74. package/src/components/list/types.ts +29 -29
  75. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  76. package/src/components/material-icon/constants.ts +99 -99
  77. package/src/components/material-icon/index.test.tsx +130 -0
  78. package/src/components/material-icon/index.tsx +47 -47
  79. package/src/components/material-icon/types.ts +31 -31
  80. package/src/components/modal/Modal.stories.tsx +171 -171
  81. package/src/components/modal/index.test.tsx +310 -0
  82. package/src/components/modal/index.tsx +164 -164
  83. package/src/components/modal/types.ts +24 -24
  84. package/src/components/next-image/index.test.tsx +406 -0
  85. package/src/components/next-image/index.tsx +74 -74
  86. package/src/components/next-image/types.ts +1 -1
  87. package/src/components/pagination/index.test.tsx +521 -0
  88. package/src/components/pagination/index.tsx +91 -91
  89. package/src/components/pagination/types.ts +6 -6
  90. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  91. package/src/components/radio-button/index.test.tsx +151 -0
  92. package/src/components/radio-button/index.tsx +75 -75
  93. package/src/components/radio-button/types.ts +21 -21
  94. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  95. package/src/components/see-more/index.test.tsx +96 -0
  96. package/src/components/see-more/index.tsx +44 -44
  97. package/src/components/see-more/types.ts +4 -4
  98. package/src/components/select/Select.stories.tsx +411 -411
  99. package/src/components/select/index.test.tsx +256 -0
  100. package/src/components/select/index.tsx +155 -155
  101. package/src/components/select/types.ts +36 -36
  102. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  103. package/src/components/select-plan-button/index.test.tsx +173 -0
  104. package/src/components/select-plan-button/index.tsx +63 -63
  105. package/src/components/select-plan-button/types.ts +17 -17
  106. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  107. package/src/components/skeleton/index.test.tsx +74 -0
  108. package/src/components/skeleton/index.tsx +61 -61
  109. package/src/components/skeleton/types.ts +4 -4
  110. package/src/components/spinner/Spinner.stories.tsx +335 -335
  111. package/src/components/spinner/index.test.tsx +76 -0
  112. package/src/components/spinner/index.tsx +44 -44
  113. package/src/components/spinner/types.ts +5 -5
  114. package/src/components/text/Text.stories.tsx +321 -321
  115. package/src/components/text/index.test.tsx +65 -0
  116. package/src/components/text/index.tsx +25 -25
  117. package/src/components/text/types.ts +45 -45
  118. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  119. package/src/components/tooltip/index.test.tsx +50 -0
  120. package/src/components/tooltip/index.tsx +74 -74
  121. package/src/components/tooltip/types.ts +7 -7
  122. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  123. package/src/components/view-cart-button/index.test.tsx +57 -0
  124. package/src/components/view-cart-button/index.tsx +42 -42
  125. package/src/components/view-cart-button/types.ts +5 -5
  126. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  127. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  128. package/src/contentful/blocks/accordion/index.test.tsx +218 -0
  129. package/src/contentful/blocks/accordion/index.tsx +114 -112
  130. package/src/contentful/blocks/accordion/types.ts +34 -34
  131. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
  132. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  133. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  134. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
  135. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  136. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  137. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
  138. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +157 -156
  139. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
  140. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  141. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  142. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
  143. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  144. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  145. package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
  146. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
  147. package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
  148. package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
  149. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  150. package/src/contentful/blocks/button/index.test.tsx +339 -0
  151. package/src/contentful/blocks/button/index.tsx +131 -131
  152. package/src/contentful/blocks/button/types.ts +39 -39
  153. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  154. package/src/contentful/blocks/callout/index.test.tsx +539 -0
  155. package/src/contentful/blocks/callout/index.tsx +277 -277
  156. package/src/contentful/blocks/callout/types.ts +78 -78
  157. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  158. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
  159. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  160. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  161. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
  162. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  163. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  164. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
  165. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  166. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  167. package/src/contentful/blocks/cards/index.test.tsx +39 -0
  168. package/src/contentful/blocks/cards/index.tsx +13 -13
  169. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
  170. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  171. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  172. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
  173. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  174. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  175. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
  176. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  177. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  178. package/src/contentful/blocks/cards/types.ts +1 -1
  179. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  180. package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
  181. package/src/contentful/blocks/carousel/helper.tsx +494 -494
  182. package/src/contentful/blocks/carousel/index.test.tsx +308 -0
  183. package/src/contentful/blocks/carousel/index.tsx +87 -87
  184. package/src/contentful/blocks/carousel/types.test.ts +16 -0
  185. package/src/contentful/blocks/carousel/types.ts +145 -145
  186. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
  187. package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
  188. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  189. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
  190. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  191. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  192. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
  193. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  194. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  195. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  196. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
  197. package/src/contentful/blocks/cta-callout/index.tsx +73 -73
  198. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  199. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
  200. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  201. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  202. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
  203. package/src/contentful/blocks/email-input-block/index.tsx +121 -116
  204. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  205. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  206. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
  207. package/src/contentful/blocks/find-kinetic/index.tsx +138 -130
  208. package/src/contentful/blocks/find-kinetic/types.ts +20 -19
  209. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  210. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
  211. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  212. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  213. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  214. package/src/contentful/blocks/footer/index.test.tsx +302 -0
  215. package/src/contentful/blocks/footer/index.tsx +91 -91
  216. package/src/contentful/blocks/footer/types.ts +13 -13
  217. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  218. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
  219. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  220. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
  221. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  222. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  223. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
  224. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  225. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
  226. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  227. package/src/contentful/blocks/modal/constants.ts +53 -53
  228. package/src/contentful/blocks/modal/index.test.tsx +209 -0
  229. package/src/contentful/blocks/modal/index.tsx +108 -108
  230. package/src/contentful/blocks/modal/types.ts +12 -12
  231. package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -0
  232. package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -0
  233. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
  234. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -139
  235. package/src/contentful/blocks/navigation/index.test.tsx +924 -0
  236. package/src/contentful/blocks/navigation/index.tsx +569 -568
  237. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
  238. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  239. package/src/contentful/blocks/navigation/types.ts +71 -71
  240. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  241. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
  242. package/src/contentful/blocks/primary-hero/index.tsx +239 -236
  243. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  244. package/src/contentful/blocks/search-block/index.test.tsx +268 -0
  245. package/src/contentful/blocks/search-block/index.tsx +90 -90
  246. package/src/contentful/blocks/search-block/types.ts +15 -15
  247. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  248. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
  249. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  250. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  251. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  252. package/src/contentful/blocks/text/index.test.tsx +36 -0
  253. package/src/contentful/blocks/text/index.tsx +12 -12
  254. package/src/contentful/blocks/text/types.ts +1 -1
  255. package/src/contentful/index.test.ts +45 -0
  256. package/src/contentful/index.ts +105 -105
  257. package/src/global-mocks/contentful/to-document.ts +25 -0
  258. package/src/global-mocks/cookie.ts +48 -0
  259. package/src/global-mocks/cx.ts +37 -0
  260. package/src/global-mocks/index.ts +89 -0
  261. package/src/global-mocks/speed-card-bg.ts +27 -0
  262. package/src/global-mocks/utm.ts +49 -0
  263. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
  264. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  265. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
  266. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  267. package/src/hooks/use-body-scroll-lock.test.ts +134 -0
  268. package/src/hooks/use-body-scroll-lock.ts +34 -34
  269. package/src/hooks/use-carousel-swipe.test.ts +393 -0
  270. package/src/hooks/use-carousel-swipe.ts +264 -264
  271. package/src/hooks/use-outside-click.test.ts +142 -0
  272. package/src/hooks/use-outside-click.ts +17 -17
  273. package/src/index.ts +107 -107
  274. package/src/next/index.test.ts +7 -0
  275. package/src/next/index.ts +5 -5
  276. package/src/setupTests.ts +52 -46
  277. package/src/stories/DocsTemplate.tsx +24 -24
  278. package/src/styles/globals.css +343 -343
  279. package/src/types/global.d.ts +9 -9
  280. package/src/types/micro-components.ts +99 -99
  281. package/src/types/utm.ts +49 -49
  282. package/src/utils/contentful/to-document.test.ts +85 -0
  283. package/src/utils/contentful/to-document.ts +24 -24
  284. package/src/utils/cookie.test.ts +180 -0
  285. package/src/utils/cookie.ts +84 -84
  286. package/src/utils/cx.test.ts +90 -0
  287. package/src/utils/cx.ts +49 -49
  288. package/src/utils/index.test.ts +115 -0
  289. package/src/utils/index.ts +41 -41
  290. package/src/utils/speed-card-bg.test.ts +46 -0
  291. package/src/utils/speed-card-bg.ts +24 -24
  292. package/src/utils/utm.test.ts +359 -0
  293. package/src/utils/utm.ts +221 -221
@@ -0,0 +1,521 @@
1
+ import React from "react";
2
+ import { Pagination } from "./index";
3
+
4
+ import { fireEvent, render, screen } from "@testing-library/react";
5
+
6
+ // Mock dependencies
7
+ jest.mock("@shared/components/button", () => ({
8
+ Button: ({ children, onClick, disabled, className, ...rest }: any) => (
9
+ <button
10
+ onClick={onClick}
11
+ disabled={disabled}
12
+ className={className}
13
+ {...rest}
14
+ >
15
+ {children}
16
+ </button>
17
+ ),
18
+ }));
19
+
20
+ jest.mock("@shared/components/material-icon", () => ({
21
+ MaterialIcon: ({ name, ...rest }: any) => (
22
+ <span data-testid={`icon-${name}`} {...rest}>
23
+ {name}
24
+ </span>
25
+ ),
26
+ }));
27
+
28
+ describe("Pagination", () => {
29
+ const mockOnPageChange = jest.fn();
30
+
31
+ beforeEach(() => {
32
+ mockOnPageChange.mockClear();
33
+ });
34
+
35
+ describe("Rendering", () => {
36
+ it("renders the pagination nav with default aria-label", () => {
37
+ render(
38
+ <Pagination
39
+ currentPage={1}
40
+ totalPages={5}
41
+ onPageChange={mockOnPageChange}
42
+ />
43
+ );
44
+ expect(screen.getByRole("navigation")).toHaveAttribute(
45
+ "aria-label",
46
+ "Pagination"
47
+ );
48
+ });
49
+
50
+ it("renders with custom aria-label", () => {
51
+ render(
52
+ <Pagination
53
+ currentPage={1}
54
+ totalPages={5}
55
+ onPageChange={mockOnPageChange}
56
+ ariaLabel="Blog pages"
57
+ />
58
+ );
59
+ expect(screen.getByRole("navigation")).toHaveAttribute(
60
+ "aria-label",
61
+ "Blog pages"
62
+ );
63
+ });
64
+
65
+ it("renders previous and next navigation buttons", () => {
66
+ render(
67
+ <Pagination
68
+ currentPage={3}
69
+ totalPages={5}
70
+ onPageChange={mockOnPageChange}
71
+ />
72
+ );
73
+ expect(
74
+ screen.getByRole("button", { name: "Go to previous page" })
75
+ ).toBeInTheDocument();
76
+ expect(
77
+ screen.getByRole("button", { name: "Go to next page" })
78
+ ).toBeInTheDocument();
79
+ });
80
+
81
+ it("renders chevron icons in nav buttons", () => {
82
+ render(
83
+ <Pagination
84
+ currentPage={1}
85
+ totalPages={5}
86
+ onPageChange={mockOnPageChange}
87
+ />
88
+ );
89
+ expect(screen.getByTestId("icon-chevron_left")).toBeInTheDocument();
90
+ expect(screen.getByTestId("icon-chevron_right")).toBeInTheDocument();
91
+ });
92
+ });
93
+
94
+ describe("Button states", () => {
95
+ it("disables previous button on first page", () => {
96
+ render(
97
+ <Pagination
98
+ currentPage={1}
99
+ totalPages={5}
100
+ onPageChange={mockOnPageChange}
101
+ />
102
+ );
103
+ expect(
104
+ screen.getByRole("button", { name: "Go to previous page" })
105
+ ).toBeDisabled();
106
+ });
107
+
108
+ it("enables previous button when not on first page", () => {
109
+ render(
110
+ <Pagination
111
+ currentPage={3}
112
+ totalPages={5}
113
+ onPageChange={mockOnPageChange}
114
+ />
115
+ );
116
+ expect(
117
+ screen.getByRole("button", { name: "Go to previous page" })
118
+ ).not.toBeDisabled();
119
+ });
120
+
121
+ it("disables next button on last page", () => {
122
+ render(
123
+ <Pagination
124
+ currentPage={5}
125
+ totalPages={5}
126
+ onPageChange={mockOnPageChange}
127
+ />
128
+ );
129
+ expect(
130
+ screen.getByRole("button", { name: "Go to next page" })
131
+ ).toBeDisabled();
132
+ });
133
+
134
+ it("enables next button when not on last page", () => {
135
+ render(
136
+ <Pagination
137
+ currentPage={3}
138
+ totalPages={5}
139
+ onPageChange={mockOnPageChange}
140
+ />
141
+ );
142
+ expect(
143
+ screen.getByRole("button", { name: "Go to next page" })
144
+ ).not.toBeDisabled();
145
+ });
146
+
147
+ it("marks current page with aria-current='page'", () => {
148
+ render(
149
+ <Pagination
150
+ currentPage={3}
151
+ totalPages={5}
152
+ onPageChange={mockOnPageChange}
153
+ />
154
+ );
155
+ expect(
156
+ screen.getByRole("button", { name: "Go to page 3" })
157
+ ).toHaveAttribute("aria-current", "page");
158
+ });
159
+
160
+ it("does not set aria-current on non-current pages", () => {
161
+ render(
162
+ <Pagination
163
+ currentPage={3}
164
+ totalPages={5}
165
+ onPageChange={mockOnPageChange}
166
+ />
167
+ );
168
+ expect(
169
+ screen.getByRole("button", { name: "Go to page 1" })
170
+ ).not.toHaveAttribute("aria-current");
171
+ });
172
+
173
+ it("applies active class to current page button", () => {
174
+ render(
175
+ <Pagination
176
+ currentPage={2}
177
+ totalPages={5}
178
+ onPageChange={mockOnPageChange}
179
+ />
180
+ );
181
+ const activeBtn = screen.getByRole("button", { name: "Go to page 2" });
182
+ expect(activeBtn.className).toContain("bg-bg-surface-active");
183
+ expect(activeBtn.className).toContain("font-bold");
184
+ });
185
+
186
+ it("does not apply active class to non-current page buttons", () => {
187
+ render(
188
+ <Pagination
189
+ currentPage={2}
190
+ totalPages={5}
191
+ onPageChange={mockOnPageChange}
192
+ />
193
+ );
194
+ const inactiveBtn = screen.getByRole("button", {
195
+ name: "Go to page 1",
196
+ });
197
+ expect(inactiveBtn.className).not.toContain("bg-bg-surface-active");
198
+ });
199
+ });
200
+
201
+ describe("Page navigation clicks", () => {
202
+ it("calls onPageChange with previous page when previous button clicked", () => {
203
+ render(
204
+ <Pagination
205
+ currentPage={3}
206
+ totalPages={5}
207
+ onPageChange={mockOnPageChange}
208
+ />
209
+ );
210
+ fireEvent.click(
211
+ screen.getByRole("button", { name: "Go to previous page" })
212
+ );
213
+ expect(mockOnPageChange).toHaveBeenCalledWith(2);
214
+ });
215
+
216
+ it("calls onPageChange with next page when next button clicked", () => {
217
+ render(
218
+ <Pagination
219
+ currentPage={3}
220
+ totalPages={5}
221
+ onPageChange={mockOnPageChange}
222
+ />
223
+ );
224
+ fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
225
+ expect(mockOnPageChange).toHaveBeenCalledWith(4);
226
+ });
227
+
228
+ it("calls onPageChange with clicked page number", () => {
229
+ render(
230
+ <Pagination
231
+ currentPage={1}
232
+ totalPages={5}
233
+ onPageChange={mockOnPageChange}
234
+ />
235
+ );
236
+ fireEvent.click(screen.getByRole("button", { name: "Go to page 4" }));
237
+ expect(mockOnPageChange).toHaveBeenCalledWith(4);
238
+ });
239
+
240
+ it("does not call onPageChange when disabled previous button is clicked", () => {
241
+ render(
242
+ <Pagination
243
+ currentPage={1}
244
+ totalPages={5}
245
+ onPageChange={mockOnPageChange}
246
+ />
247
+ );
248
+ fireEvent.click(
249
+ screen.getByRole("button", { name: "Go to previous page" })
250
+ );
251
+ expect(mockOnPageChange).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it("does not call onPageChange when disabled next button is clicked", () => {
255
+ render(
256
+ <Pagination
257
+ currentPage={5}
258
+ totalPages={5}
259
+ onPageChange={mockOnPageChange}
260
+ />
261
+ );
262
+ fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
263
+ expect(mockOnPageChange).not.toHaveBeenCalled();
264
+ });
265
+ });
266
+
267
+ describe("buildPageItems algorithm — total pages <= 7", () => {
268
+ it("renders all pages for totalPages=1", () => {
269
+ render(
270
+ <Pagination
271
+ currentPage={1}
272
+ totalPages={1}
273
+ onPageChange={mockOnPageChange}
274
+ />
275
+ );
276
+ expect(
277
+ screen.getByRole("button", { name: "Go to page 1" })
278
+ ).toBeInTheDocument();
279
+ expect(screen.queryByText("…")).not.toBeInTheDocument();
280
+ });
281
+
282
+ it("renders all pages for totalPages=5", () => {
283
+ render(
284
+ <Pagination
285
+ currentPage={1}
286
+ totalPages={5}
287
+ onPageChange={mockOnPageChange}
288
+ />
289
+ );
290
+ for (let i = 1; i <= 5; i++) {
291
+ expect(
292
+ screen.getByRole("button", { name: `Go to page ${i}` })
293
+ ).toBeInTheDocument();
294
+ }
295
+ expect(screen.queryByText("…")).not.toBeInTheDocument();
296
+ });
297
+
298
+ it("renders all pages for totalPages=7", () => {
299
+ render(
300
+ <Pagination
301
+ currentPage={4}
302
+ totalPages={7}
303
+ onPageChange={mockOnPageChange}
304
+ />
305
+ );
306
+ for (let i = 1; i <= 7; i++) {
307
+ expect(
308
+ screen.getByRole("button", { name: `Go to page ${i}` })
309
+ ).toBeInTheDocument();
310
+ }
311
+ expect(screen.queryByText("…")).not.toBeInTheDocument();
312
+ });
313
+ });
314
+
315
+ describe("buildPageItems algorithm — current near start (current <= 4)", () => {
316
+ it("shows pages 1-5 and ellipsis then last page when on page 1", () => {
317
+ render(
318
+ <Pagination
319
+ currentPage={1}
320
+ totalPages={10}
321
+ onPageChange={mockOnPageChange}
322
+ />
323
+ );
324
+ for (let i = 1; i <= 5; i++) {
325
+ expect(
326
+ screen.getByRole("button", { name: `Go to page ${i}` })
327
+ ).toBeInTheDocument();
328
+ }
329
+ expect(screen.getByText("…")).toBeInTheDocument();
330
+ expect(
331
+ screen.getByRole("button", { name: "Go to page 10" })
332
+ ).toBeInTheDocument();
333
+ expect(
334
+ screen.queryByRole("button", { name: "Go to page 6" })
335
+ ).not.toBeInTheDocument();
336
+ });
337
+
338
+ it("shows pages 1-5, ellipsis, last for page 4 of 10", () => {
339
+ render(
340
+ <Pagination
341
+ currentPage={4}
342
+ totalPages={10}
343
+ onPageChange={mockOnPageChange}
344
+ />
345
+ );
346
+ for (let i = 1; i <= 5; i++) {
347
+ expect(
348
+ screen.getByRole("button", { name: `Go to page ${i}` })
349
+ ).toBeInTheDocument();
350
+ }
351
+ expect(screen.getByText("…")).toBeInTheDocument();
352
+ expect(
353
+ screen.getByRole("button", { name: "Go to page 10" })
354
+ ).toBeInTheDocument();
355
+ });
356
+ });
357
+
358
+ describe("buildPageItems algorithm — current near end (current >= total - 3)", () => {
359
+ it("shows first page, ellipsis, last 5 pages when on last page", () => {
360
+ render(
361
+ <Pagination
362
+ currentPage={10}
363
+ totalPages={10}
364
+ onPageChange={mockOnPageChange}
365
+ />
366
+ );
367
+ expect(
368
+ screen.getByRole("button", { name: "Go to page 1" })
369
+ ).toBeInTheDocument();
370
+ expect(screen.getByText("…")).toBeInTheDocument();
371
+ for (let i = 6; i <= 10; i++) {
372
+ expect(
373
+ screen.getByRole("button", { name: `Go to page ${i}` })
374
+ ).toBeInTheDocument();
375
+ }
376
+ });
377
+
378
+ it("shows first, ellipsis, last 5 for page 7 of 10", () => {
379
+ render(
380
+ <Pagination
381
+ currentPage={7}
382
+ totalPages={10}
383
+ onPageChange={mockOnPageChange}
384
+ />
385
+ );
386
+ expect(
387
+ screen.getByRole("button", { name: "Go to page 1" })
388
+ ).toBeInTheDocument();
389
+ expect(screen.getByText("…")).toBeInTheDocument();
390
+ for (let i = 6; i <= 10; i++) {
391
+ expect(
392
+ screen.getByRole("button", { name: `Go to page ${i}` })
393
+ ).toBeInTheDocument();
394
+ }
395
+ });
396
+ });
397
+
398
+ describe("buildPageItems algorithm — current in middle", () => {
399
+ it("shows first, ellipsis, current-1/current/current+1, ellipsis, last", () => {
400
+ render(
401
+ <Pagination
402
+ currentPage={5}
403
+ totalPages={10}
404
+ onPageChange={mockOnPageChange}
405
+ />
406
+ );
407
+ expect(
408
+ screen.getByRole("button", { name: "Go to page 1" })
409
+ ).toBeInTheDocument();
410
+ expect(
411
+ screen.getByRole("button", { name: "Go to page 4" })
412
+ ).toBeInTheDocument();
413
+ expect(
414
+ screen.getByRole("button", { name: "Go to page 5" })
415
+ ).toBeInTheDocument();
416
+ expect(
417
+ screen.getByRole("button", { name: "Go to page 6" })
418
+ ).toBeInTheDocument();
419
+ expect(
420
+ screen.getByRole("button", { name: "Go to page 10" })
421
+ ).toBeInTheDocument();
422
+ const ellipses = screen.getAllByText("…");
423
+ expect(ellipses).toHaveLength(2);
424
+ });
425
+
426
+ it("renders ellipsis as non-interactive with aria-hidden", () => {
427
+ render(
428
+ <Pagination
429
+ currentPage={5}
430
+ totalPages={10}
431
+ onPageChange={mockOnPageChange}
432
+ />
433
+ );
434
+ const ellipses = screen.getAllByText("…");
435
+ ellipses.forEach(el => {
436
+ expect(el).toHaveAttribute("aria-hidden", "true");
437
+ expect(el.tagName).toBe("SPAN");
438
+ });
439
+ });
440
+
441
+ it("shows correct pages for page 6 of 10", () => {
442
+ render(
443
+ <Pagination
444
+ currentPage={6}
445
+ totalPages={10}
446
+ onPageChange={mockOnPageChange}
447
+ />
448
+ );
449
+ expect(
450
+ screen.getByRole("button", { name: "Go to page 1" })
451
+ ).toBeInTheDocument();
452
+ expect(
453
+ screen.getByRole("button", { name: "Go to page 5" })
454
+ ).toBeInTheDocument();
455
+ expect(
456
+ screen.getByRole("button", { name: "Go to page 6" })
457
+ ).toBeInTheDocument();
458
+ expect(
459
+ screen.getByRole("button", { name: "Go to page 7" })
460
+ ).toBeInTheDocument();
461
+ expect(
462
+ screen.getByRole("button", { name: "Go to page 10" })
463
+ ).toBeInTheDocument();
464
+ expect(screen.getAllByText("…")).toHaveLength(2);
465
+ });
466
+ });
467
+
468
+ describe("Edge cases", () => {
469
+ it("handles single page (both nav buttons disabled)", () => {
470
+ render(
471
+ <Pagination
472
+ currentPage={1}
473
+ totalPages={1}
474
+ onPageChange={mockOnPageChange}
475
+ />
476
+ );
477
+ expect(
478
+ screen.getByRole("button", { name: "Go to previous page" })
479
+ ).toBeDisabled();
480
+ expect(
481
+ screen.getByRole("button", { name: "Go to next page" })
482
+ ).toBeDisabled();
483
+ });
484
+
485
+ it("handles totalPages=8 with current=4 (near-start boundary)", () => {
486
+ render(
487
+ <Pagination
488
+ currentPage={4}
489
+ totalPages={8}
490
+ onPageChange={mockOnPageChange}
491
+ />
492
+ );
493
+ for (let i = 1; i <= 5; i++) {
494
+ expect(
495
+ screen.getByRole("button", { name: `Go to page ${i}` })
496
+ ).toBeInTheDocument();
497
+ }
498
+ expect(
499
+ screen.getByRole("button", { name: "Go to page 8" })
500
+ ).toBeInTheDocument();
501
+ });
502
+
503
+ it("handles totalPages=8 with current=5 (near-end boundary)", () => {
504
+ render(
505
+ <Pagination
506
+ currentPage={5}
507
+ totalPages={8}
508
+ onPageChange={mockOnPageChange}
509
+ />
510
+ );
511
+ expect(
512
+ screen.getByRole("button", { name: "Go to page 1" })
513
+ ).toBeInTheDocument();
514
+ for (let i = 4; i <= 8; i++) {
515
+ expect(
516
+ screen.getByRole("button", { name: `Go to page ${i}` })
517
+ ).toBeInTheDocument();
518
+ }
519
+ });
520
+ });
521
+ });
@@ -1,91 +1,91 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { PaginationProps } from "./types";
5
-
6
- import { Button } from "@shared/components/button";
7
- import { MaterialIcon } from "@shared/components/material-icon";
8
-
9
- export const Pagination: React.FC<PaginationProps> = ({
10
- currentPage,
11
- totalPages,
12
- onPageChange,
13
- ariaLabel = "Pagination",
14
- }: PaginationProps) => {
15
- const pageItems = buildPageItems(currentPage, totalPages);
16
-
17
- const navBtnBase =
18
- "inline-flex items-center justify-center w-9 h-9 rounded-full bg-transparent cursor-pointer transition-colors duration-150 hover:border-text-brand hover:text-text-brand disabled:opacity-40 disabled:cursor-not-allowed";
19
-
20
- const pageBtnBase =
21
- "inline-flex items-center justify-center w-9 h-9 rounded-lg text-body2 text-gray-700 bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-100 hover:text-gray-900";
22
-
23
- const pageBtnActive = "bg-bg-surface-active font-bold hover:bg-bg-gray-300";
24
-
25
- return (
26
- <nav
27
- className="flex items-center justify-center gap-1 px-6 pb-12"
28
- aria-label={ariaLabel}
29
- >
30
- {/* Previous */}
31
- <Button
32
- className={navBtnBase}
33
- onClick={() => onPageChange(currentPage - 1)}
34
- disabled={currentPage === 1}
35
- aria-label="Go to previous page"
36
- >
37
- <MaterialIcon name="chevron_left" size={32} aria-hidden="true" />
38
- </Button>
39
-
40
- {/* Page numbers */}
41
- {pageItems.map((item, idx) =>
42
- item === "..." ? (
43
- <span
44
- key={`ellipsis-${idx}`}
45
- className="inline-flex h-9 w-9 cursor-default items-center justify-center text-body2 text-gray-500"
46
- aria-hidden="true"
47
- >
48
-
49
- </span>
50
- ) : (
51
- <Button
52
- key={item}
53
- className={`${pageBtnBase} ${item === currentPage ? pageBtnActive : ""}`}
54
- onClick={() => onPageChange(item)}
55
- aria-label={`Go to page ${item}`}
56
- aria-current={item === currentPage ? "page" : undefined}
57
- >
58
- {item}
59
- </Button>
60
- )
61
- )}
62
-
63
- {/* Next */}
64
- <Button
65
- className={navBtnBase}
66
- onClick={() => onPageChange(currentPage + 1)}
67
- disabled={currentPage === totalPages}
68
- aria-label="Go to next page"
69
- >
70
- <MaterialIcon name="chevron_right" size={32} aria-hidden="true" />
71
- </Button>
72
- </nav>
73
- );
74
- };
75
-
76
- /** Returns an array of page numbers and ellipsis to render. */
77
- function buildPageItems(current: number, total: number): (number | "...")[] {
78
- if (total <= 7) {
79
- return Array.from({ length: total }, (_, i) => i + 1);
80
- }
81
-
82
- if (current <= 4) {
83
- return [1, 2, 3, 4, 5, "...", total];
84
- }
85
-
86
- if (current >= total - 3) {
87
- return [1, "...", total - 4, total - 3, total - 2, total - 1, total];
88
- }
89
-
90
- return [1, "...", current - 1, current, current + 1, "...", total];
91
- }
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { PaginationProps } from "./types";
5
+
6
+ import { Button } from "@shared/components/button";
7
+ import { MaterialIcon } from "@shared/components/material-icon";
8
+
9
+ export const Pagination: React.FC<PaginationProps> = ({
10
+ currentPage,
11
+ totalPages,
12
+ onPageChange,
13
+ ariaLabel = "Pagination",
14
+ }: PaginationProps) => {
15
+ const pageItems = buildPageItems(currentPage, totalPages);
16
+
17
+ const navBtnBase =
18
+ "inline-flex items-center justify-center w-9 h-9 rounded-full bg-transparent cursor-pointer transition-colors duration-150 hover:border-text-brand hover:text-text-brand disabled:opacity-40 disabled:cursor-not-allowed";
19
+
20
+ const pageBtnBase =
21
+ "inline-flex items-center justify-center w-9 h-9 rounded-lg text-body2 text-gray-700 bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-100 hover:text-gray-900";
22
+
23
+ const pageBtnActive = "bg-bg-surface-active font-bold hover:bg-bg-gray-300";
24
+
25
+ return (
26
+ <nav
27
+ className="flex items-center justify-center gap-1 px-6 pb-12"
28
+ aria-label={ariaLabel}
29
+ >
30
+ {/* Previous */}
31
+ <Button
32
+ className={navBtnBase}
33
+ onClick={() => onPageChange(currentPage - 1)}
34
+ disabled={currentPage === 1}
35
+ aria-label="Go to previous page"
36
+ >
37
+ <MaterialIcon name="chevron_left" size={32} aria-hidden="true" />
38
+ </Button>
39
+
40
+ {/* Page numbers */}
41
+ {pageItems.map((item, idx) =>
42
+ item === "..." ? (
43
+ <span
44
+ key={`ellipsis-${idx}`}
45
+ className="inline-flex h-9 w-9 cursor-default items-center justify-center text-body2 text-gray-500"
46
+ aria-hidden="true"
47
+ >
48
+
49
+ </span>
50
+ ) : (
51
+ <Button
52
+ key={item}
53
+ className={`${pageBtnBase} ${item === currentPage ? pageBtnActive : ""}`}
54
+ onClick={() => onPageChange(item)}
55
+ aria-label={`Go to page ${item}`}
56
+ aria-current={item === currentPage ? "page" : undefined}
57
+ >
58
+ {item}
59
+ </Button>
60
+ )
61
+ )}
62
+
63
+ {/* Next */}
64
+ <Button
65
+ className={navBtnBase}
66
+ onClick={() => onPageChange(currentPage + 1)}
67
+ disabled={currentPage === totalPages}
68
+ aria-label="Go to next page"
69
+ >
70
+ <MaterialIcon name="chevron_right" size={32} aria-hidden="true" />
71
+ </Button>
72
+ </nav>
73
+ );
74
+ };
75
+
76
+ /** Returns an array of page numbers and ellipsis to render. */
77
+ function buildPageItems(current: number, total: number): (number | "...")[] {
78
+ if (total <= 7) {
79
+ return Array.from({ length: total }, (_, i) => i + 1);
80
+ }
81
+
82
+ if (current <= 4) {
83
+ return [1, 2, 3, 4, 5, "...", total];
84
+ }
85
+
86
+ if (current >= total - 3) {
87
+ return [1, "...", total - 4, total - 3, total - 2, total - 1, total];
88
+ }
89
+
90
+ return [1, "...", current - 1, current, current + 1, "...", total];
91
+ }