@windstream/react-shared-components 0.1.93 → 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 (298) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.esm.js +3 -3
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +3 -3
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +6 -6
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.esm.js +13 -5
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +13 -5
  11. package/dist/index.js.map +1 -1
  12. package/dist/next/index.esm.js +2 -2
  13. package/dist/next/index.esm.js.map +1 -1
  14. package/dist/next/index.js +2 -2
  15. package/dist/next/index.js.map +1 -1
  16. package/dist/styles.css +1 -1
  17. package/dist/utils/index.esm.js +1 -1
  18. package/dist/utils/index.esm.js.map +1 -1
  19. package/dist/utils/index.js +1 -1
  20. package/dist/utils/index.js.map +1 -1
  21. package/package.json +191 -185
  22. package/src/components/accordion/Accordion.stories.tsx +230 -230
  23. package/src/components/accordion/index.test.tsx +270 -0
  24. package/src/components/accordion/index.tsx +70 -70
  25. package/src/components/accordion/types.ts +12 -12
  26. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  27. package/src/components/alert-card/index.test.tsx +152 -0
  28. package/src/components/alert-card/index.tsx +41 -41
  29. package/src/components/alert-card/types.ts +13 -13
  30. package/src/components/animation-wrapper/index.test.tsx +424 -0
  31. package/src/components/animation-wrapper/index.tsx +129 -129
  32. package/src/components/animation-wrapper/types.ts +11 -11
  33. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  34. package/src/components/brand-button/helpers.ts +35 -35
  35. package/src/components/brand-button/index.test.tsx +292 -0
  36. package/src/components/brand-button/index.tsx +120 -120
  37. package/src/components/brand-button/types.ts +38 -38
  38. package/src/components/button/Button.stories.tsx +108 -108
  39. package/src/components/button/index.test.tsx +91 -0
  40. package/src/components/button/index.tsx +27 -27
  41. package/src/components/button/types.ts +14 -14
  42. package/src/components/call-button/CallButton.stories.tsx +324 -324
  43. package/src/components/call-button/index.test.tsx +260 -0
  44. package/src/components/call-button/index.tsx +106 -106
  45. package/src/components/call-button/types.ts +16 -16
  46. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  47. package/src/components/checkbox/index.test.tsx +252 -0
  48. package/src/components/checkbox/index.tsx +197 -197
  49. package/src/components/checkbox/types.ts +27 -27
  50. package/src/components/checklist/Checklist.stories.tsx +150 -150
  51. package/src/components/checklist/index.test.tsx +231 -0
  52. package/src/components/checklist/index.tsx +96 -61
  53. package/src/components/checklist/types.ts +23 -17
  54. package/src/components/collapse/Collapse.stories.tsx +255 -255
  55. package/src/components/collapse/index.test.tsx +277 -0
  56. package/src/components/collapse/index.tsx +47 -46
  57. package/src/components/collapse/types.ts +6 -6
  58. package/src/components/divider/Divider.stories.tsx +205 -205
  59. package/src/components/divider/index.test.tsx +53 -0
  60. package/src/components/divider/index.tsx +22 -22
  61. package/src/components/divider/type.ts +3 -3
  62. package/src/components/image/Image.stories.tsx +113 -113
  63. package/src/components/image/index.test.tsx +174 -0
  64. package/src/components/image/index.tsx +25 -25
  65. package/src/components/image/types.ts +40 -40
  66. package/src/components/input/Input.stories.tsx +325 -325
  67. package/src/components/input/index.test.tsx +348 -0
  68. package/src/components/input/index.tsx +177 -177
  69. package/src/components/input/types.ts +37 -37
  70. package/src/components/link/Link.stories.tsx +163 -163
  71. package/src/components/link/index.test.tsx +199 -0
  72. package/src/components/link/index.tsx +116 -116
  73. package/src/components/link/types.ts +25 -25
  74. package/src/components/list/List.stories.tsx +272 -272
  75. package/src/components/list/index.test.tsx +166 -0
  76. package/src/components/list/index.tsx +88 -88
  77. package/src/components/list/list-item/index.tsx +38 -38
  78. package/src/components/list/list-item/types.ts +13 -13
  79. package/src/components/list/types.ts +29 -29
  80. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  81. package/src/components/material-icon/constants.ts +99 -99
  82. package/src/components/material-icon/index.test.tsx +130 -0
  83. package/src/components/material-icon/index.tsx +47 -47
  84. package/src/components/material-icon/types.ts +31 -31
  85. package/src/components/modal/Modal.stories.tsx +171 -171
  86. package/src/components/modal/index.test.tsx +310 -0
  87. package/src/components/modal/index.tsx +164 -164
  88. package/src/components/modal/types.ts +24 -24
  89. package/src/components/next-image/index.test.tsx +406 -0
  90. package/src/components/next-image/index.tsx +74 -74
  91. package/src/components/next-image/types.ts +1 -1
  92. package/src/components/pagination/index.test.tsx +521 -0
  93. package/src/components/pagination/index.tsx +91 -91
  94. package/src/components/pagination/types.ts +6 -6
  95. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  96. package/src/components/radio-button/index.test.tsx +151 -0
  97. package/src/components/radio-button/index.tsx +75 -75
  98. package/src/components/radio-button/types.ts +21 -21
  99. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  100. package/src/components/see-more/index.test.tsx +96 -0
  101. package/src/components/see-more/index.tsx +44 -44
  102. package/src/components/see-more/types.ts +4 -4
  103. package/src/components/select/Select.stories.tsx +411 -411
  104. package/src/components/select/index.test.tsx +256 -0
  105. package/src/components/select/index.tsx +155 -155
  106. package/src/components/select/types.ts +36 -36
  107. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  108. package/src/components/select-plan-button/index.test.tsx +173 -0
  109. package/src/components/select-plan-button/index.tsx +63 -63
  110. package/src/components/select-plan-button/types.ts +17 -17
  111. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  112. package/src/components/skeleton/index.test.tsx +74 -0
  113. package/src/components/skeleton/index.tsx +61 -61
  114. package/src/components/skeleton/types.ts +4 -4
  115. package/src/components/spinner/Spinner.stories.tsx +335 -335
  116. package/src/components/spinner/index.test.tsx +76 -0
  117. package/src/components/spinner/index.tsx +44 -44
  118. package/src/components/spinner/types.ts +5 -5
  119. package/src/components/text/Text.stories.tsx +321 -321
  120. package/src/components/text/index.test.tsx +65 -0
  121. package/src/components/text/index.tsx +25 -25
  122. package/src/components/text/types.ts +45 -45
  123. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  124. package/src/components/tooltip/index.test.tsx +50 -0
  125. package/src/components/tooltip/index.tsx +74 -74
  126. package/src/components/tooltip/types.ts +7 -7
  127. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  128. package/src/components/view-cart-button/index.test.tsx +57 -0
  129. package/src/components/view-cart-button/index.tsx +42 -42
  130. package/src/components/view-cart-button/types.ts +5 -5
  131. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  132. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  133. package/src/contentful/blocks/accordion/index.test.tsx +218 -0
  134. package/src/contentful/blocks/accordion/index.tsx +114 -112
  135. package/src/contentful/blocks/accordion/types.ts +34 -34
  136. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
  137. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  138. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  139. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
  140. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  141. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  142. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
  143. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +157 -156
  144. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
  145. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  146. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  147. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
  148. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  149. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  150. package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
  151. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
  152. package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
  153. package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
  154. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  155. package/src/contentful/blocks/button/index.test.tsx +339 -0
  156. package/src/contentful/blocks/button/index.tsx +131 -131
  157. package/src/contentful/blocks/button/types.ts +39 -39
  158. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  159. package/src/contentful/blocks/callout/index.test.tsx +539 -0
  160. package/src/contentful/blocks/callout/index.tsx +277 -277
  161. package/src/contentful/blocks/callout/types.ts +78 -78
  162. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  163. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
  164. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  165. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  166. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
  167. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  168. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  169. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
  170. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  171. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  172. package/src/contentful/blocks/cards/index.test.tsx +39 -0
  173. package/src/contentful/blocks/cards/index.tsx +13 -13
  174. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
  175. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  176. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  177. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
  178. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  179. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  180. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
  181. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  182. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  183. package/src/contentful/blocks/cards/types.ts +1 -1
  184. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  185. package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
  186. package/src/contentful/blocks/carousel/helper.tsx +494 -494
  187. package/src/contentful/blocks/carousel/index.test.tsx +308 -0
  188. package/src/contentful/blocks/carousel/index.tsx +87 -87
  189. package/src/contentful/blocks/carousel/types.test.ts +16 -0
  190. package/src/contentful/blocks/carousel/types.ts +145 -145
  191. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
  192. package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
  193. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  194. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
  195. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  196. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  197. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
  198. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  199. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  200. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  201. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
  202. package/src/contentful/blocks/cta-callout/index.tsx +73 -73
  203. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  204. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
  205. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  206. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  207. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
  208. package/src/contentful/blocks/email-input-block/index.tsx +121 -116
  209. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  210. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  211. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
  212. package/src/contentful/blocks/find-kinetic/index.tsx +138 -138
  213. package/src/contentful/blocks/find-kinetic/types.ts +20 -20
  214. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  215. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
  216. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  217. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  218. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  219. package/src/contentful/blocks/footer/index.test.tsx +302 -0
  220. package/src/contentful/blocks/footer/index.tsx +91 -91
  221. package/src/contentful/blocks/footer/types.ts +13 -13
  222. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  223. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
  224. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  225. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
  226. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  227. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  228. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
  229. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  230. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
  231. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  232. package/src/contentful/blocks/modal/constants.ts +53 -53
  233. package/src/contentful/blocks/modal/index.test.tsx +209 -0
  234. package/src/contentful/blocks/modal/index.tsx +108 -108
  235. package/src/contentful/blocks/modal/types.ts +12 -12
  236. package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -78
  237. package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -138
  238. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
  239. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -141
  240. package/src/contentful/blocks/navigation/index.test.tsx +924 -0
  241. package/src/contentful/blocks/navigation/index.tsx +569 -569
  242. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
  243. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  244. package/src/contentful/blocks/navigation/types.ts +71 -71
  245. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  246. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
  247. package/src/contentful/blocks/primary-hero/index.tsx +239 -236
  248. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  249. package/src/contentful/blocks/search-block/index.test.tsx +268 -0
  250. package/src/contentful/blocks/search-block/index.tsx +90 -90
  251. package/src/contentful/blocks/search-block/types.ts +15 -15
  252. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  253. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
  254. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  255. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  256. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  257. package/src/contentful/blocks/text/index.test.tsx +36 -0
  258. package/src/contentful/blocks/text/index.tsx +12 -12
  259. package/src/contentful/blocks/text/types.ts +1 -1
  260. package/src/contentful/index.test.ts +45 -0
  261. package/src/contentful/index.ts +105 -105
  262. package/src/global-mocks/contentful/to-document.ts +25 -0
  263. package/src/global-mocks/cookie.ts +48 -0
  264. package/src/global-mocks/cx.ts +37 -0
  265. package/src/global-mocks/index.ts +89 -0
  266. package/src/global-mocks/speed-card-bg.ts +27 -0
  267. package/src/global-mocks/utm.ts +49 -0
  268. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
  269. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  270. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
  271. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  272. package/src/hooks/use-body-scroll-lock.test.ts +134 -0
  273. package/src/hooks/use-body-scroll-lock.ts +34 -34
  274. package/src/hooks/use-carousel-swipe.test.ts +393 -0
  275. package/src/hooks/use-carousel-swipe.ts +264 -264
  276. package/src/hooks/use-outside-click.test.ts +142 -0
  277. package/src/hooks/use-outside-click.ts +17 -17
  278. package/src/index.ts +107 -107
  279. package/src/next/index.test.ts +7 -0
  280. package/src/next/index.ts +5 -5
  281. package/src/setupTests.ts +52 -46
  282. package/src/stories/DocsTemplate.tsx +24 -24
  283. package/src/styles/globals.css +343 -343
  284. package/src/types/global.d.ts +9 -9
  285. package/src/types/micro-components.ts +99 -99
  286. package/src/types/utm.ts +49 -49
  287. package/src/utils/contentful/to-document.test.ts +85 -0
  288. package/src/utils/contentful/to-document.ts +24 -24
  289. package/src/utils/cookie.test.ts +180 -0
  290. package/src/utils/cookie.ts +84 -84
  291. package/src/utils/cx.test.ts +90 -0
  292. package/src/utils/cx.ts +49 -49
  293. package/src/utils/index.test.ts +115 -0
  294. package/src/utils/index.ts +41 -41
  295. package/src/utils/speed-card-bg.test.ts +46 -0
  296. package/src/utils/speed-card-bg.ts +24 -24
  297. package/src/utils/utm.test.ts +359 -0
  298. 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
+ }