@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,424 @@
1
+ import React from "react";
2
+ import { AnimationWrapper } from "./index";
3
+
4
+ import { render, screen } from "@testing-library/react";
5
+
6
+ import "@testing-library/jest-dom";
7
+
8
+ // Mock framer-motion
9
+ const mockUseReducedMotion = jest.fn(() => false);
10
+
11
+ jest.mock("framer-motion", () => {
12
+ const actual = jest.requireActual("framer-motion");
13
+ return {
14
+ ...actual,
15
+ useReducedMotion: () => mockUseReducedMotion(),
16
+ motion: new Proxy(
17
+ {},
18
+ {
19
+ get: (_target, prop: string) => {
20
+ const motionKeys = [
21
+ "whileHover",
22
+ "whileTap",
23
+ "transition",
24
+ "animate",
25
+ "initial",
26
+ "exit",
27
+ "variants",
28
+ ];
29
+ const MotionMock = React.forwardRef(
30
+ (props: Record<string, unknown>, ref: React.Ref<unknown>) => {
31
+ const rest: Record<string, unknown> = {};
32
+ Object.keys(props).forEach(key => {
33
+ if (!motionKeys.includes(key)) {
34
+ rest[key] = props[key];
35
+ }
36
+ });
37
+ return React.createElement(prop, {
38
+ ...rest,
39
+ ref,
40
+ "data-motion": "true",
41
+ "data-while-hover": JSON.stringify(props.whileHover),
42
+ "data-while-tap": JSON.stringify(props.whileTap),
43
+ "data-transition": JSON.stringify(props.transition),
44
+ });
45
+ }
46
+ );
47
+ MotionMock.displayName = `Motion(${prop})`;
48
+ return MotionMock;
49
+ },
50
+ }
51
+ ),
52
+ };
53
+ });
54
+
55
+ describe("AnimationWrapper", () => {
56
+ beforeEach(() => {
57
+ mockUseReducedMotion.mockReturnValue(false);
58
+ });
59
+
60
+ describe("when animation is disabled", () => {
61
+ it("returns the child directly when disableAnimation is true (default)", () => {
62
+ render(
63
+ <AnimationWrapper>
64
+ <button data-testid="child">Click</button>
65
+ </AnimationWrapper>
66
+ );
67
+ const btn = screen.getByTestId("child");
68
+ expect(btn).toBeInTheDocument();
69
+ expect(btn.tagName).toBe("BUTTON");
70
+ expect(btn).not.toHaveAttribute("data-motion");
71
+ });
72
+
73
+ it("returns the child directly when disableAnimation is explicitly true", () => {
74
+ render(
75
+ <AnimationWrapper disableAnimation={true}>
76
+ <span data-testid="child">Text</span>
77
+ </AnimationWrapper>
78
+ );
79
+ expect(screen.getByTestId("child")).not.toHaveAttribute("data-motion");
80
+ });
81
+
82
+ it("returns the child directly when user prefers reduced motion", () => {
83
+ mockUseReducedMotion.mockReturnValue(true);
84
+ render(
85
+ <AnimationWrapper disableAnimation={false} animationType="scale">
86
+ <div data-testid="child">Content</div>
87
+ </AnimationWrapper>
88
+ );
89
+ expect(screen.getByTestId("child")).not.toHaveAttribute("data-motion");
90
+ });
91
+ });
92
+
93
+ describe("when animation is enabled", () => {
94
+ describe("with intrinsic elements", () => {
95
+ it("wraps intrinsic element with motion component for scale animation", () => {
96
+ render(
97
+ <AnimationWrapper disableAnimation={false} animationType="scale">
98
+ <div data-testid="child">Animated</div>
99
+ </AnimationWrapper>
100
+ );
101
+ const el = screen.getByTestId("child");
102
+ expect(el).toHaveAttribute("data-motion", "true");
103
+ const whileHover = JSON.parse(
104
+ el.getAttribute("data-while-hover") || "{}"
105
+ );
106
+ expect(whileHover).toEqual({ scale: 1.02 });
107
+ const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
108
+ expect(whileTap).toEqual({ scale: 0.98 });
109
+ });
110
+
111
+ it("applies shadow animation preset", () => {
112
+ render(
113
+ <AnimationWrapper disableAnimation={false} animationType="shadow">
114
+ <div data-testid="child">Shadow</div>
115
+ </AnimationWrapper>
116
+ );
117
+ const el = screen.getByTestId("child");
118
+ const whileHover = JSON.parse(
119
+ el.getAttribute("data-while-hover") || "{}"
120
+ );
121
+ expect(whileHover).toEqual({
122
+ boxShadow: "0 10px 25px rgba(0,0,0,0.15)",
123
+ });
124
+ // shadow preset has no whileTap, so mergedWhileTap is undefined
125
+ const whileTap = el.getAttribute("data-while-tap");
126
+ expect(whileTap).toBeNull();
127
+ });
128
+
129
+ it("applies lift animation preset", () => {
130
+ render(
131
+ <AnimationWrapper disableAnimation={false} animationType="lift">
132
+ <div data-testid="child">Lift</div>
133
+ </AnimationWrapper>
134
+ );
135
+ const el = screen.getByTestId("child");
136
+ const whileHover = JSON.parse(
137
+ el.getAttribute("data-while-hover") || "{}"
138
+ );
139
+ expect(whileHover).toEqual({ y: -5 });
140
+ const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
141
+ expect(whileTap).toEqual({ y: 0 });
142
+ });
143
+
144
+ it("applies opacity animation preset", () => {
145
+ render(
146
+ <AnimationWrapper disableAnimation={false} animationType="opacity">
147
+ <div data-testid="child">Opacity</div>
148
+ </AnimationWrapper>
149
+ );
150
+ const el = screen.getByTestId("child");
151
+ const whileHover = JSON.parse(
152
+ el.getAttribute("data-while-hover") || "{}"
153
+ );
154
+ expect(whileHover).toEqual({ opacity: 0.8 });
155
+ const transition = JSON.parse(
156
+ el.getAttribute("data-transition") || "{}"
157
+ );
158
+ expect(transition).toEqual({ duration: 0.2 });
159
+ });
160
+
161
+ it("applies grow animation preset", () => {
162
+ render(
163
+ <AnimationWrapper disableAnimation={false} animationType="grow">
164
+ <div data-testid="child">Grow</div>
165
+ </AnimationWrapper>
166
+ );
167
+ const el = screen.getByTestId("child");
168
+ const whileHover = JSON.parse(
169
+ el.getAttribute("data-while-hover") || "{}"
170
+ );
171
+ expect(whileHover).toEqual({ scale: 1.1 });
172
+ const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
173
+ expect(whileTap).toEqual({ scale: 0.95 });
174
+ const transition = JSON.parse(
175
+ el.getAttribute("data-transition") || "{}"
176
+ );
177
+ expect(transition).toEqual({
178
+ type: "spring",
179
+ stiffness: 300,
180
+ damping: 20,
181
+ });
182
+ });
183
+
184
+ it("works with different intrinsic HTML elements (span)", () => {
185
+ render(
186
+ <AnimationWrapper disableAnimation={false} animationType="scale">
187
+ <span data-testid="child">Span</span>
188
+ </AnimationWrapper>
189
+ );
190
+ const el = screen.getByTestId("child");
191
+ expect(el.tagName).toBe("SPAN");
192
+ expect(el).toHaveAttribute("data-motion", "true");
193
+ });
194
+
195
+ it("works with anchor elements", () => {
196
+ render(
197
+ <AnimationWrapper disableAnimation={false} animationType="lift">
198
+ <a data-testid="child" href="/test">
199
+ Link
200
+ </a>
201
+ </AnimationWrapper>
202
+ );
203
+ const el = screen.getByTestId("child");
204
+ expect(el.tagName).toBe("A");
205
+ expect(el).toHaveAttribute("href", "/test");
206
+ expect(el).toHaveAttribute("data-motion", "true");
207
+ });
208
+ });
209
+
210
+ describe("with composite (non-intrinsic) elements", () => {
211
+ const CustomComponent = ({ label }: { label: string }) => (
212
+ <span data-testid="custom">{label}</span>
213
+ );
214
+
215
+ it("wraps composite component in motion.div", () => {
216
+ const { container } = render(
217
+ <AnimationWrapper disableAnimation={false} animationType="scale">
218
+ <CustomComponent label="Hello" />
219
+ </AnimationWrapper>
220
+ );
221
+ // The wrapper should be a div with motion attributes
222
+ const wrapper = container.firstElementChild;
223
+ expect(wrapper?.tagName).toBe("DIV");
224
+ expect(wrapper).toHaveAttribute("data-motion", "true");
225
+ // The child should be rendered inside
226
+ expect(screen.getByTestId("custom")).toBeInTheDocument();
227
+ expect(screen.getByText("Hello")).toBeInTheDocument();
228
+ });
229
+
230
+ it("applies animation presets to motion.div wrapper", () => {
231
+ const { container } = render(
232
+ <AnimationWrapper disableAnimation={false} animationType="grow">
233
+ <CustomComponent label="Test" />
234
+ </AnimationWrapper>
235
+ );
236
+ const wrapper = container.firstElementChild;
237
+ const whileHover = JSON.parse(
238
+ wrapper?.getAttribute("data-while-hover") || "{}"
239
+ );
240
+ expect(whileHover).toEqual({ scale: 1.1 });
241
+ });
242
+ });
243
+
244
+ describe("with multiple animation types", () => {
245
+ it("merges multiple animation presets", () => {
246
+ render(
247
+ <AnimationWrapper
248
+ disableAnimation={false}
249
+ animationType={["scale", "shadow"]}
250
+ >
251
+ <div data-testid="child">Multi</div>
252
+ </AnimationWrapper>
253
+ );
254
+ const el = screen.getByTestId("child");
255
+ const whileHover = JSON.parse(
256
+ el.getAttribute("data-while-hover") || "{}"
257
+ );
258
+ expect(whileHover).toEqual({
259
+ scale: 1.02,
260
+ boxShadow: "0 10px 25px rgba(0,0,0,0.15)",
261
+ });
262
+ });
263
+
264
+ it("merges whileTap from multiple presets", () => {
265
+ render(
266
+ <AnimationWrapper
267
+ disableAnimation={false}
268
+ animationType={["scale", "lift"]}
269
+ >
270
+ <div data-testid="child">Multi Tap</div>
271
+ </AnimationWrapper>
272
+ );
273
+ const el = screen.getByTestId("child");
274
+ const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
275
+ expect(whileTap).toEqual({ scale: 0.98, y: 0 });
276
+ });
277
+
278
+ it("merges transitions from multiple presets", () => {
279
+ render(
280
+ <AnimationWrapper
281
+ disableAnimation={false}
282
+ animationType={["opacity", "lift"]}
283
+ >
284
+ <div data-testid="child">Merged</div>
285
+ </AnimationWrapper>
286
+ );
287
+ const el = screen.getByTestId("child");
288
+ const transition = JSON.parse(
289
+ el.getAttribute("data-transition") || "{}"
290
+ );
291
+ expect(transition).toEqual({
292
+ duration: 0.2,
293
+ type: "spring",
294
+ stiffness: 300,
295
+ damping: 20,
296
+ });
297
+ });
298
+
299
+ it("handles empty array of animation types", () => {
300
+ render(
301
+ <AnimationWrapper disableAnimation={false} animationType={[]}>
302
+ <div data-testid="child">No Animation</div>
303
+ </AnimationWrapper>
304
+ );
305
+ const el = screen.getByTestId("child");
306
+ // With empty array, mergePresets returns undefined, so no preset is applied
307
+ expect(el).toHaveAttribute("data-motion", "true");
308
+ });
309
+ });
310
+
311
+ describe("custom overrides", () => {
312
+ it("overrides preset whileHover with custom prop", () => {
313
+ render(
314
+ <AnimationWrapper
315
+ disableAnimation={false}
316
+ animationType="scale"
317
+ whileHover={{ scale: 2 }}
318
+ >
319
+ <div data-testid="child">Override</div>
320
+ </AnimationWrapper>
321
+ );
322
+ const el = screen.getByTestId("child");
323
+ const whileHover = JSON.parse(
324
+ el.getAttribute("data-while-hover") || "{}"
325
+ );
326
+ // Custom override should merge with preset (custom wins)
327
+ expect(whileHover.scale).toBe(2);
328
+ });
329
+
330
+ it("overrides preset whileTap with custom prop", () => {
331
+ render(
332
+ <AnimationWrapper
333
+ disableAnimation={false}
334
+ animationType="scale"
335
+ whileTap={{ scale: 0.5 }}
336
+ >
337
+ <div data-testid="child">Override Tap</div>
338
+ </AnimationWrapper>
339
+ );
340
+ const el = screen.getByTestId("child");
341
+ const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
342
+ expect(whileTap.scale).toBe(0.5);
343
+ });
344
+
345
+ it("overrides preset transition with custom prop", () => {
346
+ render(
347
+ <AnimationWrapper
348
+ disableAnimation={false}
349
+ animationType="scale"
350
+ transition={{ duration: 1 }}
351
+ >
352
+ <div data-testid="child">Override Trans</div>
353
+ </AnimationWrapper>
354
+ );
355
+ const el = screen.getByTestId("child");
356
+ const transition = JSON.parse(
357
+ el.getAttribute("data-transition") || "{}"
358
+ );
359
+ expect(transition.duration).toBe(1);
360
+ });
361
+
362
+ it("uses custom whileHover even without animationType", () => {
363
+ render(
364
+ <AnimationWrapper
365
+ disableAnimation={false}
366
+ whileHover={{ rotate: 10 }}
367
+ >
368
+ <div data-testid="child">Custom Only</div>
369
+ </AnimationWrapper>
370
+ );
371
+ const el = screen.getByTestId("child");
372
+ const whileHover = JSON.parse(
373
+ el.getAttribute("data-while-hover") || "{}"
374
+ );
375
+ expect(whileHover).toEqual({ rotate: 10 });
376
+ });
377
+
378
+ it("uses custom whileTap without animationType", () => {
379
+ render(
380
+ <AnimationWrapper disableAnimation={false} whileTap={{ rotate: -5 }}>
381
+ <div data-testid="child">Tap</div>
382
+ </AnimationWrapper>
383
+ );
384
+ const el = screen.getByTestId("child");
385
+ const whileTap = JSON.parse(el.getAttribute("data-while-tap") || "{}");
386
+ expect(whileTap).toEqual({ rotate: -5 });
387
+ });
388
+
389
+ it("uses custom transition without animationType", () => {
390
+ render(
391
+ <AnimationWrapper
392
+ disableAnimation={false}
393
+ transition={{ delay: 0.5 }}
394
+ >
395
+ <div data-testid="child">Trans</div>
396
+ </AnimationWrapper>
397
+ );
398
+ const el = screen.getByTestId("child");
399
+ const transition = JSON.parse(
400
+ el.getAttribute("data-transition") || "{}"
401
+ );
402
+ expect(transition).toEqual({ delay: 0.5 });
403
+ });
404
+ });
405
+
406
+ describe("no animationType and no custom overrides", () => {
407
+ it("renders motion component with undefined animation props", () => {
408
+ render(
409
+ <AnimationWrapper disableAnimation={false}>
410
+ <div data-testid="child">Plain</div>
411
+ </AnimationWrapper>
412
+ );
413
+ const el = screen.getByTestId("child");
414
+ expect(el).toHaveAttribute("data-motion", "true");
415
+ });
416
+ });
417
+ });
418
+
419
+ describe("displayName", () => {
420
+ it("has correct displayName", () => {
421
+ expect(AnimationWrapper.displayName).toBe("AnimationWrapper");
422
+ });
423
+ });
424
+ });
@@ -1,129 +1,129 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { AnimationType, AnimationWrapperProps } from "./types";
5
- import { motion, useReducedMotion } from "framer-motion";
6
-
7
- interface AnimationPreset {
8
- whileHover: Record<string, unknown>;
9
- whileTap?: Record<string, unknown>;
10
- transition: Record<string, unknown>;
11
- }
12
-
13
- const ANIMATION_PRESETS: Record<AnimationType, AnimationPreset> = {
14
- scale: {
15
- whileHover: { scale: 1.02 },
16
- whileTap: { scale: 0.98 },
17
- transition: { duration: 0.3 },
18
- },
19
- shadow: {
20
- whileHover: { boxShadow: "0 10px 25px rgba(0,0,0,0.15)" },
21
- transition: { duration: 0.3 },
22
- },
23
- lift: {
24
- whileHover: { y: -5 },
25
- whileTap: { y: 0 },
26
- transition: { type: "spring", stiffness: 300, damping: 20 },
27
- },
28
- opacity: {
29
- whileHover: { opacity: 0.8 },
30
- transition: { duration: 0.2 },
31
- },
32
- grow: {
33
- whileHover: { scale: 1.1 },
34
- whileTap: { scale: 0.95 },
35
- transition: { type: "spring", stiffness: 300, damping: 20 },
36
- },
37
- };
38
-
39
- function mergePresets(animationType?: AnimationType | AnimationType[]) {
40
- if (!animationType) return undefined;
41
-
42
- const types = Array.isArray(animationType) ? animationType : [animationType];
43
- if (types.length === 0) return undefined;
44
-
45
- return types.reduce<AnimationPreset>(
46
- (merged, type) => {
47
- const preset = ANIMATION_PRESETS[type];
48
- return {
49
- whileHover: { ...merged.whileHover, ...preset.whileHover },
50
- ...(merged.whileTap || preset.whileTap
51
- ? {
52
- whileTap: { ...merged.whileTap, ...preset.whileTap },
53
- }
54
- : {}),
55
- transition: { ...merged.transition, ...preset.transition },
56
- };
57
- },
58
- { whileHover: {}, transition: {} }
59
- );
60
- }
61
-
62
- export function AnimationWrapper({
63
- children,
64
- animationType,
65
- // default to true for backward compatibility
66
- disableAnimation = true,
67
- whileHover,
68
- whileTap,
69
- transition,
70
- ...motionProps
71
- }: AnimationWrapperProps) {
72
- const prefersReducedMotion = useReducedMotion();
73
-
74
- const child = React.Children.only(children) as React.ReactElement<
75
- Record<string, unknown>
76
- >;
77
-
78
- if (disableAnimation || prefersReducedMotion) {
79
- return child;
80
- }
81
-
82
- const preset = mergePresets(animationType);
83
-
84
- const mergedWhileHover = preset
85
- ? { ...preset.whileHover, ...(whileHover as Record<string, unknown>) }
86
- : whileHover;
87
-
88
- const mergedWhileTap =
89
- preset?.whileTap || whileTap
90
- ? { ...preset?.whileTap, ...(whileTap as Record<string, unknown>) }
91
- : undefined;
92
-
93
- const mergedTransition = preset
94
- ? { ...preset.transition, ...(transition as Record<string, unknown>) }
95
- : transition;
96
-
97
- const isIntrinsicElement = typeof child.type === "string";
98
-
99
- if (isIntrinsicElement) {
100
- const MotionComponent = motion[
101
- child.type as keyof typeof motion
102
- ] as React.ComponentType<Record<string, unknown>>;
103
-
104
- return (
105
- <MotionComponent
106
- {...child.props}
107
- whileHover={mergedWhileHover}
108
- whileTap={mergedWhileTap}
109
- transition={mergedTransition}
110
- {...motionProps}
111
- />
112
- );
113
- }
114
-
115
- return (
116
- <motion.div
117
- whileHover={mergedWhileHover}
118
- whileTap={mergedWhileTap}
119
- transition={mergedTransition}
120
- {...motionProps}
121
- >
122
- {child}
123
- </motion.div>
124
- );
125
- }
126
-
127
- AnimationWrapper.displayName = "AnimationWrapper";
128
-
129
- export type { AnimationWrapperProps, AnimationType };
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { AnimationType, AnimationWrapperProps } from "./types";
5
+ import { motion, useReducedMotion } from "framer-motion";
6
+
7
+ interface AnimationPreset {
8
+ whileHover: Record<string, unknown>;
9
+ whileTap?: Record<string, unknown>;
10
+ transition: Record<string, unknown>;
11
+ }
12
+
13
+ const ANIMATION_PRESETS: Record<AnimationType, AnimationPreset> = {
14
+ scale: {
15
+ whileHover: { scale: 1.02 },
16
+ whileTap: { scale: 0.98 },
17
+ transition: { duration: 0.3 },
18
+ },
19
+ shadow: {
20
+ whileHover: { boxShadow: "0 10px 25px rgba(0,0,0,0.15)" },
21
+ transition: { duration: 0.3 },
22
+ },
23
+ lift: {
24
+ whileHover: { y: -5 },
25
+ whileTap: { y: 0 },
26
+ transition: { type: "spring", stiffness: 300, damping: 20 },
27
+ },
28
+ opacity: {
29
+ whileHover: { opacity: 0.8 },
30
+ transition: { duration: 0.2 },
31
+ },
32
+ grow: {
33
+ whileHover: { scale: 1.1 },
34
+ whileTap: { scale: 0.95 },
35
+ transition: { type: "spring", stiffness: 300, damping: 20 },
36
+ },
37
+ };
38
+
39
+ function mergePresets(animationType?: AnimationType | AnimationType[]) {
40
+ if (!animationType) return undefined;
41
+
42
+ const types = Array.isArray(animationType) ? animationType : [animationType];
43
+ if (types.length === 0) return undefined;
44
+
45
+ return types.reduce<AnimationPreset>(
46
+ (merged, type) => {
47
+ const preset = ANIMATION_PRESETS[type];
48
+ return {
49
+ whileHover: { ...merged.whileHover, ...preset.whileHover },
50
+ ...(merged.whileTap || preset.whileTap
51
+ ? {
52
+ whileTap: { ...merged.whileTap, ...preset.whileTap },
53
+ }
54
+ : {}),
55
+ transition: { ...merged.transition, ...preset.transition },
56
+ };
57
+ },
58
+ { whileHover: {}, transition: {} }
59
+ );
60
+ }
61
+
62
+ export function AnimationWrapper({
63
+ children,
64
+ animationType,
65
+ // default to true for backward compatibility
66
+ disableAnimation = true,
67
+ whileHover,
68
+ whileTap,
69
+ transition,
70
+ ...motionProps
71
+ }: AnimationWrapperProps) {
72
+ const prefersReducedMotion = useReducedMotion();
73
+
74
+ const child = React.Children.only(children) as React.ReactElement<
75
+ Record<string, unknown>
76
+ >;
77
+
78
+ if (disableAnimation || prefersReducedMotion) {
79
+ return child;
80
+ }
81
+
82
+ const preset = mergePresets(animationType);
83
+
84
+ const mergedWhileHover = preset
85
+ ? { ...preset.whileHover, ...(whileHover as Record<string, unknown>) }
86
+ : whileHover;
87
+
88
+ const mergedWhileTap =
89
+ preset?.whileTap || whileTap
90
+ ? { ...preset?.whileTap, ...(whileTap as Record<string, unknown>) }
91
+ : undefined;
92
+
93
+ const mergedTransition = preset
94
+ ? { ...preset.transition, ...(transition as Record<string, unknown>) }
95
+ : transition;
96
+
97
+ const isIntrinsicElement = typeof child.type === "string";
98
+
99
+ if (isIntrinsicElement) {
100
+ const MotionComponent = motion[
101
+ child.type as keyof typeof motion
102
+ ] as React.ComponentType<Record<string, unknown>>;
103
+
104
+ return (
105
+ <MotionComponent
106
+ {...child.props}
107
+ whileHover={mergedWhileHover}
108
+ whileTap={mergedWhileTap}
109
+ transition={mergedTransition}
110
+ {...motionProps}
111
+ />
112
+ );
113
+ }
114
+
115
+ return (
116
+ <motion.div
117
+ whileHover={mergedWhileHover}
118
+ whileTap={mergedWhileTap}
119
+ transition={mergedTransition}
120
+ {...motionProps}
121
+ >
122
+ {child}
123
+ </motion.div>
124
+ );
125
+ }
126
+
127
+ AnimationWrapper.displayName = "AnimationWrapper";
128
+
129
+ export type { AnimationWrapperProps, AnimationType };