@windstream/react-shared-components 0.1.75 → 0.1.77

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 (204) 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/index.esm.js +5 -13
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +5 -13
  9. package/dist/index.js.map +1 -1
  10. package/dist/next/index.esm.js +2 -2
  11. package/dist/next/index.esm.js.map +1 -1
  12. package/dist/next/index.js +2 -2
  13. package/dist/next/index.js.map +1 -1
  14. package/dist/styles.css +1 -1
  15. package/dist/utils/index.esm.js +1 -1
  16. package/dist/utils/index.esm.js.map +1 -1
  17. package/dist/utils/index.js +1 -1
  18. package/dist/utils/index.js.map +1 -1
  19. package/package.json +185 -185
  20. package/src/components/accordion/Accordion.stories.tsx +230 -230
  21. package/src/components/accordion/index.tsx +70 -70
  22. package/src/components/accordion/types.ts +12 -12
  23. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  24. package/src/components/alert-card/index.tsx +41 -41
  25. package/src/components/alert-card/types.ts +13 -13
  26. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  27. package/src/components/brand-button/helpers.ts +35 -35
  28. package/src/components/brand-button/index.tsx +120 -120
  29. package/src/components/brand-button/types.ts +38 -38
  30. package/src/components/button/Button.stories.tsx +108 -108
  31. package/src/components/button/index.tsx +27 -27
  32. package/src/components/button/types.ts +14 -14
  33. package/src/components/call-button/CallButton.stories.tsx +324 -324
  34. package/src/components/call-button/index.tsx +106 -106
  35. package/src/components/call-button/types.ts +16 -16
  36. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  37. package/src/components/checkbox/index.tsx +197 -197
  38. package/src/components/checkbox/types.ts +27 -27
  39. package/src/components/checklist/Checklist.stories.tsx +150 -150
  40. package/src/components/checklist/index.tsx +61 -61
  41. package/src/components/checklist/types.ts +17 -17
  42. package/src/components/collapse/Collapse.stories.tsx +255 -255
  43. package/src/components/collapse/index.tsx +46 -46
  44. package/src/components/collapse/types.ts +6 -6
  45. package/src/components/divider/Divider.stories.tsx +205 -205
  46. package/src/components/divider/index.tsx +22 -22
  47. package/src/components/divider/type.ts +3 -3
  48. package/src/components/image/Image.stories.tsx +113 -113
  49. package/src/components/image/index.tsx +25 -25
  50. package/src/components/image/types.ts +40 -40
  51. package/src/components/input/Input.stories.tsx +325 -325
  52. package/src/components/input/index.tsx +177 -177
  53. package/src/components/input/types.ts +37 -37
  54. package/src/components/link/Link.stories.tsx +163 -163
  55. package/src/components/link/index.tsx +116 -116
  56. package/src/components/link/types.ts +25 -25
  57. package/src/components/list/List.stories.tsx +272 -272
  58. package/src/components/list/index.tsx +88 -88
  59. package/src/components/list/list-item/index.tsx +38 -38
  60. package/src/components/list/list-item/types.ts +13 -13
  61. package/src/components/list/types.ts +29 -29
  62. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  63. package/src/components/material-icon/constants.ts +99 -99
  64. package/src/components/material-icon/index.tsx +47 -47
  65. package/src/components/material-icon/types.ts +31 -31
  66. package/src/components/modal/Modal.stories.tsx +171 -171
  67. package/src/components/modal/index.tsx +164 -164
  68. package/src/components/modal/types.ts +24 -24
  69. package/src/components/next-image/index.tsx +74 -74
  70. package/src/components/next-image/types.ts +1 -1
  71. package/src/components/pagination/index.tsx +91 -91
  72. package/src/components/pagination/types.ts +6 -6
  73. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  74. package/src/components/radio-button/index.tsx +75 -75
  75. package/src/components/radio-button/types.ts +21 -21
  76. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  77. package/src/components/see-more/index.tsx +44 -44
  78. package/src/components/see-more/types.ts +4 -4
  79. package/src/components/select/Select.stories.tsx +411 -411
  80. package/src/components/select/index.tsx +155 -155
  81. package/src/components/select/types.ts +36 -36
  82. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  83. package/src/components/select-plan-button/index.tsx +63 -63
  84. package/src/components/select-plan-button/types.ts +17 -17
  85. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  86. package/src/components/skeleton/index.tsx +61 -61
  87. package/src/components/skeleton/types.ts +4 -4
  88. package/src/components/spinner/Spinner.stories.tsx +335 -335
  89. package/src/components/spinner/index.tsx +44 -44
  90. package/src/components/spinner/types.ts +5 -5
  91. package/src/components/text/Text.stories.tsx +321 -321
  92. package/src/components/text/index.tsx +25 -25
  93. package/src/components/text/types.ts +45 -45
  94. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  95. package/src/components/tooltip/index.tsx +74 -74
  96. package/src/components/tooltip/types.ts +7 -7
  97. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  98. package/src/components/view-cart-button/index.tsx +42 -42
  99. package/src/components/view-cart-button/types.ts +5 -5
  100. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  101. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  102. package/src/contentful/blocks/accordion/index.tsx +112 -112
  103. package/src/contentful/blocks/accordion/types.ts +34 -34
  104. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  105. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  106. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  107. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  108. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  109. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  110. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  111. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  112. package/src/contentful/blocks/breadcrumbs/index.tsx +81 -81
  113. package/src/contentful/blocks/breadcrumbs/types.ts +6 -6
  114. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  115. package/src/contentful/blocks/button/index.tsx +131 -131
  116. package/src/contentful/blocks/button/types.ts +39 -39
  117. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  118. package/src/contentful/blocks/callout/index.tsx +252 -252
  119. package/src/contentful/blocks/callout/types.ts +68 -68
  120. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  121. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  122. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  123. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  124. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  125. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  126. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  127. package/src/contentful/blocks/cards/index.tsx +13 -13
  128. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  129. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  130. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  131. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  132. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  133. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  134. package/src/contentful/blocks/cards/types.ts +1 -1
  135. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  136. package/src/contentful/blocks/carousel/helper.tsx +440 -440
  137. package/src/contentful/blocks/carousel/index.tsx +85 -85
  138. package/src/contentful/blocks/carousel/types.ts +144 -144
  139. package/src/contentful/blocks/cart-retention-banner/index.tsx +105 -105
  140. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  141. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  142. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  143. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  144. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  145. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  146. package/src/contentful/blocks/cta-callout/index.tsx +71 -71
  147. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  148. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  149. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  150. package/src/contentful/blocks/email-input-block/index.tsx +116 -116
  151. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  152. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  153. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  154. package/src/contentful/blocks/find-kinetic/types.ts +19 -19
  155. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  156. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  157. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  158. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  159. package/src/contentful/blocks/footer/index.tsx +91 -91
  160. package/src/contentful/blocks/footer/types.ts +13 -13
  161. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  162. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  163. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  164. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  165. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  166. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  167. package/src/contentful/blocks/modal/constants.ts +53 -53
  168. package/src/contentful/blocks/modal/index.tsx +107 -107
  169. package/src/contentful/blocks/modal/types.ts +12 -12
  170. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +139 -139
  171. package/src/contentful/blocks/navigation/index.tsx +567 -512
  172. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  173. package/src/contentful/blocks/navigation/types.ts +71 -71
  174. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  175. package/src/contentful/blocks/primary-hero/index.tsx +236 -236
  176. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  177. package/src/contentful/blocks/search-block/index.tsx +90 -90
  178. package/src/contentful/blocks/search-block/types.ts +15 -15
  179. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  180. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  181. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  182. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  183. package/src/contentful/blocks/text/index.tsx +12 -12
  184. package/src/contentful/blocks/text/types.ts +1 -1
  185. package/src/contentful/index.ts +105 -105
  186. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  187. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  188. package/src/hooks/use-body-scroll-lock.ts +34 -34
  189. package/src/hooks/use-carousel-swipe.ts +264 -264
  190. package/src/hooks/use-outside-click.ts +17 -17
  191. package/src/index.ts +101 -101
  192. package/src/next/index.ts +5 -5
  193. package/src/setupTests.ts +46 -46
  194. package/src/stories/DocsTemplate.tsx +24 -24
  195. package/src/styles/globals.css +343 -343
  196. package/src/types/global.d.ts +9 -9
  197. package/src/types/micro-components.ts +99 -99
  198. package/src/types/utm.ts +49 -49
  199. package/src/utils/contentful/to-document.ts +24 -24
  200. package/src/utils/cookie.ts +84 -84
  201. package/src/utils/cx.ts +49 -49
  202. package/src/utils/index.ts +41 -41
  203. package/src/utils/speed-card-bg.ts +24 -24
  204. package/src/utils/utm.ts +221 -221
@@ -1,512 +1,567 @@
1
- import React, { FormEvent } from "react";
2
- import { DesktopLinkGroups } from "./desktop-link-groups.tsx";
3
- import { MobileLinkGroups } from "./mobile-link-groups.tsx";
4
- import { NavigationProps } from "./types";
5
-
6
- import { CallButton } from "@shared/components/call-button";
7
- import { Input } from "@shared/components/input";
8
- import { Link } from "@shared/components/link";
9
- import { MaterialIcon } from "@shared/components/material-icon";
10
- import { NextImage } from "@shared/components/next-image";
11
- import { Text } from "@shared/components/text";
12
- import { Button as ContentfulButton } from "@shared/contentful/blocks/button";
13
- import { cx } from "@shared/utils";
14
-
15
- export const Navigation: React.FC<NavigationProps> = props => {
16
- const {
17
- primaryNavigationLinks,
18
- utilityNavigationLinks,
19
- checkPlansJSX,
20
- primaryNavigationLogo,
21
- accountNavigationLinks,
22
- supportNavigationLinks,
23
- searchBarIcon,
24
- invocaPhoneNumberLink,
25
- invocaPhoneNumberDisplayText,
26
- callNowCtaText,
27
- displayCartIcon,
28
- cartHref,
29
- cartHasRetention,
30
- cartIconAriaLabel = "Cart",
31
- onCallClickDesktop,
32
- onCallClickMobile,
33
- onCartClick,
34
- onSearch = () => {},
35
- utilityNavActiveIndex,
36
- primaryNavigationLogoWidth = 76.5,
37
- primaryNavigationLogoHeight = 24,
38
- hideMobileCallButton = false,
39
- displayUtilityNavigation = true,
40
- displaySearchBar = true,
41
- displayCallNowCta = true,
42
- showCallButton = true,
43
- navigationBackgroundColor,
44
- navigationLinkColor,
45
- } = props;
46
-
47
- const logoUrl =
48
- typeof primaryNavigationLogo === "string"
49
- ? primaryNavigationLogo
50
- : primaryNavigationLogo?.url || "";
51
-
52
- // Background token convention matches the legacy ResidentialNavbar:
53
- // when CMS provides a token like "navy500" we map it to
54
- // `bg-secondary-navy500`. Raw Tailwind classes (containing a `-`)
55
- // are passed through untouched. Defaults to `bg-secondary-navy500`
56
- // to preserve legacy behavior when CMS leaves the field empty.
57
- const mainNavBgClass = navigationBackgroundColor
58
- ? navigationBackgroundColor.includes("-") ||
59
- navigationBackgroundColor.startsWith("bg-")
60
- ? navigationBackgroundColor
61
- : `bg-secondary-${navigationBackgroundColor}`
62
- : "bg-secondary-navy500";
63
-
64
- const hasMobileMenuItems =
65
- (primaryNavigationLinks?.length ?? 0) > 0 ||
66
- (supportNavigationLinks?.length ?? 0) > 0 ||
67
- (accountNavigationLinks?.length ?? 0) > 0;
68
-
69
- return (
70
- <div className="component-container">
71
- <nav className={`menu-container z-[1000]`}>
72
- {displayUtilityNavigation ? (
73
- <div className="utility-container hidden lg:block lg:border-b lg:px-2">
74
- <div className="mx-auto flex max-w-120 justify-between">
75
- <ul className="flex gap-5" aria-label="Utility Navigation">
76
- {utilityNavigationLinks?.map((links, index) => {
77
- return (
78
- <li key={`main-menu-items-${index}`}>
79
- <ContentfulButton
80
- linkClassName={cx(
81
- "footnote flex items-center w-full h-11 text-text",
82
- // If utilityNavActiveIndex is not provided, default to making the second link active (for existing business app). If utilityNavActiveIndex is provided, use it to determine which link is active.
83
- typeof utilityNavActiveIndex !== "number"
84
- ? index === 1 && "label4"
85
- : utilityNavActiveIndex === index && "label4"
86
- )}
87
- linkVariant="unstyled"
88
- {...(Object.fromEntries(
89
- Object.entries(links).filter(([_, v]) => v !== null)
90
- ) as any)}
91
- />
92
- </li>
93
- );
94
- })}
95
- </ul>
96
- <div className="flex items-center gap-10">
97
- {displayCallNowCta ? (
98
- <CallButton
99
- className="border-none"
100
- href={invocaPhoneNumberLink}
101
- prefix={callNowCtaText}
102
- onClick={onCallClickDesktop}
103
- >
104
- <Text className="body3 text-text">
105
- {invocaPhoneNumberDisplayText}
106
- </Text>
107
- </CallButton>
108
- ) : null}
109
- {displayCartIcon ? (
110
- <Link
111
- href={cartHref || "#"}
112
- className="relative inline-flex cursor-pointer"
113
- aria-label={cartIconAriaLabel}
114
- onClick={onCartClick}
115
- >
116
- <MaterialIcon name="shopping_cart" />
117
- {cartHasRetention ? (
118
- <span className="absolute -right-2 -top-1 h-2.5 w-2.5 rounded-full bg-icon-brand" />
119
- ) : null}
120
- </Link>
121
- ) : null}
122
- {accountNavigationLinks?.map((links, index) => {
123
- return (
124
- <DesktopLinkGroups
125
- key={`my-account-${index}`}
126
- anchorName={`my-account-${index}`}
127
- link={links}
128
- />
129
- );
130
- })}
131
- </div>
132
- </div>
133
- </div>
134
- ) : null}
135
- <div
136
- className={cx("main-nav-container", mainNavBgClass, navigationLinkColor)}
137
- aria-label="Main Navigation"
138
- >
139
- <div className="mobile-nav-section flex h-14 items-center justify-between border border-b px-5 py-[10px] lg:hidden">
140
- <div>
141
- {logoUrl ? (
142
- <Link href="/" className="flex">
143
- <NextImage
144
- src={logoUrl}
145
- alt="Kinetic business logo"
146
- width={primaryNavigationLogoWidth}
147
- height={primaryNavigationLogoHeight}
148
- />
149
- </Link>
150
- ) : null}
151
- </div>
152
- <div className="flex items-center gap-6">
153
- {showCallButton && !hideMobileCallButton ? (
154
- <CallButton
155
- href={invocaPhoneNumberLink}
156
- onClick={onCallClickMobile}
157
- >
158
- <Text as="span" className="footnote">
159
- {invocaPhoneNumberDisplayText}
160
- </Text>
161
- </CallButton>
162
- ) : null}
163
- {displayCartIcon ? (
164
- <Link
165
- href={cartHref || "#"}
166
- className="relative inline-flex cursor-pointer"
167
- aria-label={cartIconAriaLabel}
168
- onClick={onCartClick}
169
- >
170
- <MaterialIcon name="shopping_cart" />
171
- {cartHasRetention ? (
172
- <span className="absolute -right-2 -top-1 h-2.5 w-2.5 rounded-full bg-icon-brand" />
173
- ) : null}
174
- </Link>
175
- ) : null}
176
- {hasMobileMenuItems ? <MobileMenu {...props} /> : null}
177
- </div>
178
- </div>
179
-
180
- <div className="desktop-nav-section hidden lg:block lg:border-b lg:px-2">
181
- <div className="mx-auto flex h-14 max-w-120 items-center justify-between">
182
- <div className="flex h-full">
183
- {logoUrl ? (
184
- <Link href="/" className="flex">
185
- <NextImage
186
- src={logoUrl}
187
- alt="Kinetic business logo"
188
- width={primaryNavigationLogoWidth}
189
- height={primaryNavigationLogoHeight}
190
- className="mr-[64px]"
191
- />
192
- </Link>
193
- ) : null}
194
-
195
- <div className="flex h-full items-center gap-5">
196
- {primaryNavigationLinks?.map((links, index) => {
197
- return (
198
- <DesktopLinkGroups
199
- key={`main-menu-${index}`}
200
- anchorName={`main-menu-${index}`}
201
- link={links}
202
- linkColorClassName={navigationLinkColor}
203
- />
204
- );
205
- })}
206
- </div>
207
- </div>
208
- <div className="flex h-full items-center gap-10">
209
- {displaySearchBar ? (
210
- <DesktopSearchInput
211
- searchBarIconURL={
212
- typeof searchBarIcon === "string"
213
- ? searchBarIcon
214
- : searchBarIcon?.url || ""
215
- }
216
- onSearch={onSearch}
217
- />
218
- ) : null}
219
- {supportNavigationLinks?.map((links, index) => {
220
- return (
221
- <DesktopLinkGroups
222
- key={`support-menu-${index}`}
223
- anchorName={`support-menu-${index}`}
224
- link={links}
225
- linkColorClassName={navigationLinkColor}
226
- />
227
- );
228
- })}
229
- </div>
230
- </div>
231
- </div>
232
- </div>
233
- </nav>
234
-
235
- {checkPlansJSX && <div className="md:hidden">{checkPlansJSX}</div>}
236
- </div>
237
- );
238
- };
239
-
240
- const MobileMenu = (props: NavigationProps) => {
241
- const {
242
- primaryNavigationLinks,
243
- utilityNavigationLinks,
244
- supportNavigationLinks,
245
- accountNavigationLinks,
246
- showCallButton,
247
- displayCallNowCta,
248
- displaySearchBar,
249
- } = props;
250
- const [isOpen, setIsOpen] = React.useState(false);
251
-
252
- React.useEffect(() => {
253
- if (typeof window === "undefined") return;
254
-
255
- if (isOpen) document.body.style.overflowY = "hidden";
256
- else document.body.style.overflowY = "unset";
257
-
258
- const element = document.getElementById("drawer-items");
259
- if (!element) return;
260
-
261
- const focusableEls = element.querySelectorAll(".focus-item");
262
- const firstFocusableEl = focusableEls[0];
263
- const lastFocusableEl = focusableEls[focusableEls.length - 1];
264
-
265
- const handleKeyDown = (e: {
266
- key: string;
267
- keyCode: number;
268
- shiftKey: any;
269
- preventDefault: () => void;
270
- }) => {
271
- const isTabPressed = e.key === "Tab" || e.keyCode === 9;
272
-
273
- if (!isTabPressed) return;
274
-
275
- if (e.shiftKey) {
276
- if (document.activeElement === firstFocusableEl) {
277
- (lastFocusableEl as HTMLButtonElement).focus?.();
278
- e.preventDefault();
279
- }
280
- } else {
281
- if (document.activeElement === lastFocusableEl) {
282
- (firstFocusableEl as HTMLButtonElement).focus?.();
283
- e.preventDefault();
284
- }
285
- }
286
- };
287
-
288
- window.addEventListener("keydown", handleKeyDown);
289
-
290
- return () => {
291
- document.body.style.overflowY = "unset";
292
- window.removeEventListener("keydown", handleKeyDown);
293
- };
294
- }, [isOpen]);
295
-
296
- const closeMenu = () => {
297
- setIsOpen(false);
298
- };
299
-
300
- return (
301
- <div>
302
- <ContentfulButton
303
- showButtonAs="unstyled"
304
- buttonClassName="flex"
305
- onClick={() => setIsOpen(true)}
306
- >
307
- <MaterialIcon name="menu" />
308
- </ContentfulButton>
309
- {isOpen ? (
310
- <div className="fixed bottom-0 left-0 right-0 top-0 z-[90] h-full w-full bg-scrim-bg-modal"></div>
311
- ) : null}
312
-
313
- <div
314
- className={cx(
315
- "fixed bottom-0 right-0 top-0",
316
- "z-[100] h-full bg-bg px-0 py-4 text-text",
317
- "transition-all duration-300 ease-in-out",
318
- "block",
319
- isOpen ? "right-0" : "-right-96"
320
- )}
321
- id="mobile-menu-overlay"
322
- >
323
- <div id="drawer-items" className="flex h-full flex-col gap-3">
324
- <div
325
- className={cx(
326
- "flex items-center px-4",
327
- showCallButton !== false || displayCallNowCta !== false
328
- ? "justify-between"
329
- : "justify-end"
330
- )}
331
- >
332
- {showCallButton !== false || displayCallNowCta !== false ? (
333
- <div>
334
- <CallButton
335
- className="border-none"
336
- href={props.invocaPhoneNumberLink}
337
- onClick={props.onCallClickMobile}
338
- >
339
- {props.invocaPhoneNumberDisplayText}
340
- </CallButton>
341
- </div>
342
- ) : null}
343
- <div>
344
- <ContentfulButton
345
- showButtonAs="unstyled"
346
- buttonClassName="focus-item flex"
347
- onClick={closeMenu}
348
- >
349
- <MaterialIcon name="close" />
350
- </ContentfulButton>
351
- </div>
352
- </div>
353
- {displaySearchBar !== false ? (
354
- <MobileSearchInput
355
- closeMenu={closeMenu}
356
- isMenuOpen={isOpen}
357
- searchBarIconURL={
358
- typeof props.searchBarIcon === "string"
359
- ? props.searchBarIcon
360
- : props.searchBarIcon?.url || ""
361
- }
362
- onSearch={props.onSearch || (() => {})}
363
- />
364
- ) : null}
365
- <div
366
- className="flex-grow overflow-y-auto"
367
- onClick={event => {
368
- // Close the drawer when a link (anchor) is clicked.
369
- // Group-toggle buttons render as <button> and won't match,
370
- // so they continue to expand/collapse normally.
371
- // Required for Pages Router where MobileMenu persists across
372
- // client-side navigations and its local `isOpen` state would
373
- // otherwise stay true on the new route.
374
- const target = event.target as HTMLElement | null;
375
- if (target?.closest("a")) {
376
- closeMenu();
377
- }
378
- }}
379
- >
380
- <ul className="mt-2 flex flex-col gap-2">
381
- {[
382
- ...(primaryNavigationLinks || []),
383
- ...(supportNavigationLinks || []),
384
- ...(accountNavigationLinks || []),
385
- ].map((links, index) => (
386
- <li key={`main-menu-items-${index}`}>
387
- <MobileLinkGroups link={links} />
388
- </li>
389
- ))}
390
- </ul>
391
-
392
- <ul className="mt-2 flex gap-5 bg-bg-fill-info px-4">
393
- {utilityNavigationLinks?.map((link, index) => {
394
- return (
395
- <li key={`utility-menu-items-${index}`}>
396
- <ContentfulButton
397
- key={`utility-submenu-link-btn-${link.anchorId}`}
398
- {...(Object.fromEntries(
399
- Object.entries(link).filter(([_, v]) => v !== null)
400
- ) as any)}
401
- linkClassName={cx(
402
- "footnote flex items-center w-full h-11 text-text-link",
403
- typeof props.utilityNavActiveIndex !== "number"
404
- ? index === 1 && "label4"
405
- : props.utilityNavActiveIndex === index && "label4"
406
- )}
407
- linkVariant="unstyled"
408
- />
409
- </li>
410
- );
411
- })}
412
- </ul>
413
- </div>
414
- </div>
415
- </div>
416
- </div>
417
- );
418
- };
419
-
420
- const MobileSearchInput = (props: {
421
- closeMenu: () => void;
422
- onSearch: (query: string) => void;
423
- isMenuOpen: boolean;
424
- searchBarIconURL: string;
425
- }) => {
426
- const { closeMenu, onSearch, isMenuOpen, searchBarIconURL } = props;
427
- const [searchValue, setSearchValue] = React.useState("");
428
- const searchInputRef = React.useRef<HTMLInputElement>(null);
429
-
430
- const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
431
- closeMenu();
432
- e.preventDefault();
433
- onSearch(searchValue);
434
- };
435
-
436
- React.useEffect(() => {
437
- if (!isMenuOpen) {
438
- setSearchValue("");
439
- }
440
- }, [isMenuOpen]);
441
-
442
- return (
443
- <form
444
- name="searchForm"
445
- className="flex border-b border-t transition-colors focus-within:border-border-focus"
446
- onSubmit={redirectToSearchResults}
447
- >
448
- <NextImage
449
- src={searchBarIconURL}
450
- width={32}
451
- height={32}
452
- alt="Search icon"
453
- role="button"
454
- className="ml-2"
455
- onClick={redirectToSearchResults}
456
- />
457
- <div className="flex-grow">
458
- <Input
459
- ref={searchInputRef}
460
- className={"body3 h-[34px] rounded-none px-3 text-text"}
461
- name="search"
462
- placeholder="Search..."
463
- value={searchValue}
464
- onChange={e => setSearchValue(e.target.value)}
465
- autoComplete="off"
466
- containerClassName="h-[46px] px-4 pl-0 rounded-none flex-grow border-none"
467
- />
468
- </div>
469
- </form>
470
- );
471
- };
472
-
473
- const DesktopSearchInput = (props: {
474
- searchBarIconURL: string;
475
- onSearch: (query: string) => void;
476
- }) => {
477
- const { searchBarIconURL, onSearch } = props;
478
- const [searchValue, setSearchValue] = React.useState("");
479
- const searchInputRef = React.useRef<HTMLInputElement>(null);
480
-
481
- const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
482
- e.preventDefault();
483
- onSearch(searchValue);
484
- };
485
-
486
- return (
487
- <form
488
- name="searchForm"
489
- className="flex h-9 w-60 rounded-input-xl border px-1 transition-colors focus-within:border-border-focus"
490
- onSubmit={redirectToSearchResults}
491
- >
492
- <NextImage
493
- src={searchBarIconURL}
494
- width={32}
495
- height={32}
496
- alt="Search icon"
497
- role="button"
498
- onClick={redirectToSearchResults}
499
- />
500
- <Input
501
- ref={searchInputRef}
502
- className={"body3 rounded-full px-3 text-text"}
503
- name="search"
504
- placeholder="Search..."
505
- value={searchValue}
506
- onChange={e => setSearchValue(e.target.value)}
507
- autoComplete="off"
508
- containerClassName="px-0 h-full border-none rounded-full"
509
- />
510
- </form>
511
- );
512
- };
1
+ import React, { FormEvent } from "react";
2
+ import { DesktopLinkGroups } from "./desktop-link-groups.tsx";
3
+ import { MobileLinkGroups } from "./mobile-link-groups.tsx";
4
+ import { NavigationProps } from "./types";
5
+
6
+ import { CallButton } from "@shared/components/call-button";
7
+ import { Input } from "@shared/components/input";
8
+ import { Link } from "@shared/components/link";
9
+ import { MaterialIcon } from "@shared/components/material-icon";
10
+ import { NextImage } from "@shared/components/next-image";
11
+ import { Text } from "@shared/components/text";
12
+ import { Button as ContentfulButton } from "@shared/contentful/blocks/button";
13
+ import { cx } from "@shared/utils";
14
+
15
+ export const Navigation: React.FC<NavigationProps> = props => {
16
+ const {
17
+ primaryNavigationLinks,
18
+ utilityNavigationLinks,
19
+ checkPlansJSX,
20
+ primaryNavigationLogo,
21
+ accountNavigationLinks,
22
+ supportNavigationLinks,
23
+ searchBarIcon,
24
+ invocaPhoneNumberLink,
25
+ invocaPhoneNumberDisplayText,
26
+ callNowCtaText,
27
+ displayCartIcon,
28
+ cartHref,
29
+ cartHasRetention,
30
+ cartIconAriaLabel = "Cart",
31
+ onCallClickDesktop,
32
+ onCallClickMobile,
33
+ onCartClick,
34
+ onSearch = () => {},
35
+ utilityNavActiveIndex,
36
+ primaryNavigationLogoWidth = 76.5,
37
+ primaryNavigationLogoHeight = 24,
38
+ hideMobileCallButton = false,
39
+ displayUtilityNavigation = true,
40
+ displaySearchBar = true,
41
+ displayCallNowCta = true,
42
+ showCallButton = true,
43
+ showCallNowCtaInMainNav = false,
44
+ showMobileSliderMenu = true,
45
+ callNowCtaIcon,
46
+ navigationBackgroundColor,
47
+ navigationLinkColor,
48
+ } = props;
49
+
50
+ const logoUrl =
51
+ typeof primaryNavigationLogo === "string"
52
+ ? primaryNavigationLogo
53
+ : primaryNavigationLogo?.url || "";
54
+
55
+ // Background token convention matches the legacy ResidentialNavbar:
56
+ // when CMS provides a token like "navy500" we map it to
57
+ // `bg-secondary-navy500`. Raw Tailwind classes (containing a `-`)
58
+ // are passed through untouched. Defaults to `bg-secondary-navy500`
59
+ // to preserve legacy behavior when CMS leaves the field empty.
60
+ const mainNavBgClass = navigationBackgroundColor
61
+ ? navigationBackgroundColor.includes("-") ||
62
+ navigationBackgroundColor.startsWith("bg-")
63
+ ? navigationBackgroundColor
64
+ : `bg-secondary-${navigationBackgroundColor}`
65
+ : "bg-secondary-navy500";
66
+
67
+ const callNowCtaIconUrl =
68
+ typeof callNowCtaIcon === "string"
69
+ ? callNowCtaIcon
70
+ : callNowCtaIcon?.url || "";
71
+
72
+ // Styled "Call Now" pill used in the main nav rows when CMS opts in via
73
+ // `showCallNowCtaInMainNav` (desktop) or when the mobile slider menu is
74
+ // disabled (`showMobileSliderMenu: false`). Mirrors the legacy
75
+ // ResidentialNavbar styling so existing CMS entries render identically.
76
+ const renderMainNavCallCta = (
77
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void
78
+ ) => (
79
+ <Link
80
+ href={invocaPhoneNumberLink || "#"}
81
+ onClick={onClick}
82
+ className="bg-secondary-gray100 text-secondary-navy500 inline-flex h-8 items-center rounded-full pl-1 pr-2 no-underline shadow-none"
83
+ aria-label={
84
+ invocaPhoneNumberDisplayText
85
+ ? `Call ${invocaPhoneNumberDisplayText}`
86
+ : "Call now"
87
+ }
88
+ >
89
+ {callNowCtaIconUrl ? (
90
+ <NextImage
91
+ src={callNowCtaIconUrl}
92
+ alt="Call us"
93
+ width={24}
94
+ height={24}
95
+ className="mr-2 self-center rounded-full"
96
+ />
97
+ ) : null}
98
+ <Text as="span" className="text-secondary-navy500 footnote">
99
+ {invocaPhoneNumberDisplayText}
100
+ </Text>
101
+ </Link>
102
+ );
103
+
104
+ const hasMobileMenuItems =
105
+ (primaryNavigationLinks?.length ?? 0) > 0 ||
106
+ (supportNavigationLinks?.length ?? 0) > 0 ||
107
+ (accountNavigationLinks?.length ?? 0) > 0;
108
+
109
+ return (
110
+ <div className="component-container">
111
+ <nav className={`menu-container z-[1000]`}>
112
+ {displayUtilityNavigation ? (
113
+ <div className="utility-container hidden lg:block lg:border-b lg:px-2">
114
+ <div className="mx-auto flex max-w-120 justify-between">
115
+ <ul className="flex gap-5" aria-label="Utility Navigation">
116
+ {utilityNavigationLinks?.map((links, index) => {
117
+ return (
118
+ <li key={`main-menu-items-${index}`}>
119
+ <ContentfulButton
120
+ linkClassName={cx(
121
+ "footnote flex items-center w-full h-11 text-text",
122
+ // If utilityNavActiveIndex is not provided, default to making the second link active (for existing business app). If utilityNavActiveIndex is provided, use it to determine which link is active.
123
+ typeof utilityNavActiveIndex !== "number"
124
+ ? index === 1 && "label4"
125
+ : utilityNavActiveIndex === index && "label4"
126
+ )}
127
+ linkVariant="unstyled"
128
+ {...(Object.fromEntries(
129
+ Object.entries(links).filter(([_, v]) => v !== null)
130
+ ) as any)}
131
+ />
132
+ </li>
133
+ );
134
+ })}
135
+ </ul>
136
+ <div className="flex items-center gap-10">
137
+ {displayCallNowCta ? (
138
+ <CallButton
139
+ className="border-none"
140
+ href={invocaPhoneNumberLink}
141
+ prefix={callNowCtaText}
142
+ onClick={onCallClickDesktop}
143
+ >
144
+ <Text className="body3 text-text">
145
+ {invocaPhoneNumberDisplayText}
146
+ </Text>
147
+ </CallButton>
148
+ ) : null}
149
+ {displayCartIcon ? (
150
+ <Link
151
+ href={cartHref || "#"}
152
+ className="relative inline-flex cursor-pointer"
153
+ aria-label={cartIconAriaLabel}
154
+ onClick={onCartClick}
155
+ >
156
+ <MaterialIcon name="shopping_cart" />
157
+ {cartHasRetention ? (
158
+ <span className="absolute -right-2 -top-1 h-2.5 w-2.5 rounded-full bg-icon-brand" />
159
+ ) : null}
160
+ </Link>
161
+ ) : null}
162
+ {accountNavigationLinks?.map((links, index) => {
163
+ return (
164
+ <DesktopLinkGroups
165
+ key={`my-account-${index}`}
166
+ anchorName={`my-account-${index}`}
167
+ link={links}
168
+ />
169
+ );
170
+ })}
171
+ </div>
172
+ </div>
173
+ </div>
174
+ ) : null}
175
+ <div
176
+ className={cx(
177
+ "main-nav-container",
178
+ mainNavBgClass,
179
+ navigationLinkColor
180
+ )}
181
+ aria-label="Main Navigation"
182
+ >
183
+ <div className="mobile-nav-section flex h-14 items-center justify-between border border-b px-5 py-[10px] lg:hidden">
184
+ <div>
185
+ {logoUrl ? (
186
+ <Link href="/" className="flex">
187
+ <NextImage
188
+ src={logoUrl}
189
+ alt="Kinetic business logo"
190
+ width={primaryNavigationLogoWidth}
191
+ height={primaryNavigationLogoHeight}
192
+ />
193
+ </Link>
194
+ ) : null}
195
+ </div>
196
+ <div className="flex items-center gap-6">
197
+ {!showMobileSliderMenu ? (
198
+ showCallButton ? (
199
+ renderMainNavCallCta(onCallClickMobile)
200
+ ) : null
201
+ ) : (
202
+ <>
203
+ {showCallButton && !hideMobileCallButton ? (
204
+ <CallButton
205
+ href={invocaPhoneNumberLink}
206
+ onClick={onCallClickMobile}
207
+ >
208
+ <Text as="span" className="footnote">
209
+ {invocaPhoneNumberDisplayText}
210
+ </Text>
211
+ </CallButton>
212
+ ) : null}
213
+ {displayCartIcon ? (
214
+ <Link
215
+ href={cartHref || "#"}
216
+ className="relative inline-flex cursor-pointer"
217
+ aria-label={cartIconAriaLabel}
218
+ onClick={onCartClick}
219
+ >
220
+ <MaterialIcon name="shopping_cart" />
221
+ {cartHasRetention ? (
222
+ <span className="absolute -right-2 -top-1 h-2.5 w-2.5 rounded-full bg-icon-brand" />
223
+ ) : null}
224
+ </Link>
225
+ ) : null}
226
+ {hasMobileMenuItems ? <MobileMenu {...props} /> : null}
227
+ </>
228
+ )}
229
+ </div>
230
+ </div>
231
+
232
+ <div className="desktop-nav-section hidden lg:block lg:border-b lg:px-2">
233
+ <div className="mx-auto flex h-14 max-w-120 items-center justify-between">
234
+ <div className="flex h-full">
235
+ {logoUrl ? (
236
+ <Link href="/" className="flex">
237
+ <NextImage
238
+ src={logoUrl}
239
+ alt="Kinetic business logo"
240
+ width={primaryNavigationLogoWidth}
241
+ height={primaryNavigationLogoHeight}
242
+ className="mr-[64px]"
243
+ />
244
+ </Link>
245
+ ) : null}
246
+
247
+ <div className="flex h-full items-center gap-5">
248
+ {primaryNavigationLinks?.map((links, index) => {
249
+ return (
250
+ <DesktopLinkGroups
251
+ key={`main-menu-${index}`}
252
+ anchorName={`main-menu-${index}`}
253
+ link={links}
254
+ linkColorClassName={navigationLinkColor}
255
+ />
256
+ );
257
+ })}
258
+ </div>
259
+ </div>
260
+ <div className="flex h-full items-center gap-10">
261
+ {showCallNowCtaInMainNav
262
+ ? renderMainNavCallCta(onCallClickDesktop)
263
+ : null}
264
+ {displaySearchBar ? (
265
+ <DesktopSearchInput
266
+ searchBarIconURL={
267
+ typeof searchBarIcon === "string"
268
+ ? searchBarIcon
269
+ : searchBarIcon?.url || ""
270
+ }
271
+ onSearch={onSearch}
272
+ />
273
+ ) : null}
274
+ {supportNavigationLinks?.map((links, index) => {
275
+ return (
276
+ <DesktopLinkGroups
277
+ key={`support-menu-${index}`}
278
+ anchorName={`support-menu-${index}`}
279
+ link={links}
280
+ linkColorClassName={navigationLinkColor}
281
+ />
282
+ );
283
+ })}
284
+ </div>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </nav>
289
+
290
+ {checkPlansJSX && <div className="md:hidden">{checkPlansJSX}</div>}
291
+ </div>
292
+ );
293
+ };
294
+
295
+ const MobileMenu = (props: NavigationProps) => {
296
+ const {
297
+ primaryNavigationLinks,
298
+ utilityNavigationLinks,
299
+ supportNavigationLinks,
300
+ accountNavigationLinks,
301
+ showCallButton,
302
+ displayCallNowCta,
303
+ displaySearchBar,
304
+ } = props;
305
+ const [isOpen, setIsOpen] = React.useState(false);
306
+
307
+ React.useEffect(() => {
308
+ if (typeof window === "undefined") return;
309
+
310
+ if (isOpen) document.body.style.overflowY = "hidden";
311
+ else document.body.style.overflowY = "unset";
312
+
313
+ const element = document.getElementById("drawer-items");
314
+ if (!element) return;
315
+
316
+ const focusableEls = element.querySelectorAll(".focus-item");
317
+ const firstFocusableEl = focusableEls[0];
318
+ const lastFocusableEl = focusableEls[focusableEls.length - 1];
319
+
320
+ const handleKeyDown = (e: {
321
+ key: string;
322
+ keyCode: number;
323
+ shiftKey: any;
324
+ preventDefault: () => void;
325
+ }) => {
326
+ const isTabPressed = e.key === "Tab" || e.keyCode === 9;
327
+
328
+ if (!isTabPressed) return;
329
+
330
+ if (e.shiftKey) {
331
+ if (document.activeElement === firstFocusableEl) {
332
+ (lastFocusableEl as HTMLButtonElement).focus?.();
333
+ e.preventDefault();
334
+ }
335
+ } else {
336
+ if (document.activeElement === lastFocusableEl) {
337
+ (firstFocusableEl as HTMLButtonElement).focus?.();
338
+ e.preventDefault();
339
+ }
340
+ }
341
+ };
342
+
343
+ window.addEventListener("keydown", handleKeyDown);
344
+
345
+ return () => {
346
+ document.body.style.overflowY = "unset";
347
+ window.removeEventListener("keydown", handleKeyDown);
348
+ };
349
+ }, [isOpen]);
350
+
351
+ const closeMenu = () => {
352
+ setIsOpen(false);
353
+ };
354
+
355
+ return (
356
+ <div>
357
+ <ContentfulButton
358
+ showButtonAs="unstyled"
359
+ buttonClassName="flex"
360
+ onClick={() => setIsOpen(true)}
361
+ >
362
+ <MaterialIcon name="menu" />
363
+ </ContentfulButton>
364
+ {isOpen ? (
365
+ <div className="fixed bottom-0 left-0 right-0 top-0 z-[90] h-full w-full bg-scrim-bg-modal"></div>
366
+ ) : null}
367
+
368
+ <div
369
+ className={cx(
370
+ "fixed bottom-0 right-0 top-0",
371
+ "z-[100] h-full bg-bg px-0 py-4 text-text",
372
+ "transition-all duration-300 ease-in-out",
373
+ "block",
374
+ isOpen ? "right-0" : "-right-96"
375
+ )}
376
+ id="mobile-menu-overlay"
377
+ >
378
+ <div id="drawer-items" className="flex h-full flex-col gap-3">
379
+ <div
380
+ className={cx(
381
+ "flex items-center px-4",
382
+ showCallButton !== false || displayCallNowCta !== false
383
+ ? "justify-between"
384
+ : "justify-end"
385
+ )}
386
+ >
387
+ {showCallButton !== false || displayCallNowCta !== false ? (
388
+ <div>
389
+ <CallButton
390
+ className="border-none"
391
+ href={props.invocaPhoneNumberLink}
392
+ onClick={props.onCallClickMobile}
393
+ >
394
+ {props.invocaPhoneNumberDisplayText}
395
+ </CallButton>
396
+ </div>
397
+ ) : null}
398
+ <div>
399
+ <ContentfulButton
400
+ showButtonAs="unstyled"
401
+ buttonClassName="focus-item flex"
402
+ onClick={closeMenu}
403
+ >
404
+ <MaterialIcon name="close" />
405
+ </ContentfulButton>
406
+ </div>
407
+ </div>
408
+ {displaySearchBar !== false ? (
409
+ <MobileSearchInput
410
+ closeMenu={closeMenu}
411
+ isMenuOpen={isOpen}
412
+ searchBarIconURL={
413
+ typeof props.searchBarIcon === "string"
414
+ ? props.searchBarIcon
415
+ : props.searchBarIcon?.url || ""
416
+ }
417
+ onSearch={props.onSearch || (() => {})}
418
+ />
419
+ ) : null}
420
+ <div
421
+ className="flex-grow overflow-y-auto"
422
+ onClick={event => {
423
+ // Close the drawer when a link (anchor) is clicked.
424
+ // Group-toggle buttons render as <button> and won't match,
425
+ // so they continue to expand/collapse normally.
426
+ // Required for Pages Router where MobileMenu persists across
427
+ // client-side navigations and its local `isOpen` state would
428
+ // otherwise stay true on the new route.
429
+ const target = event.target as HTMLElement | null;
430
+ if (target?.closest("a")) {
431
+ closeMenu();
432
+ }
433
+ }}
434
+ >
435
+ <ul className="mt-2 flex flex-col gap-2">
436
+ {[
437
+ ...(primaryNavigationLinks || []),
438
+ ...(supportNavigationLinks || []),
439
+ ...(accountNavigationLinks || []),
440
+ ].map((links, index) => (
441
+ <li key={`main-menu-items-${index}`}>
442
+ <MobileLinkGroups link={links} />
443
+ </li>
444
+ ))}
445
+ </ul>
446
+
447
+ <ul className="mt-2 flex gap-5 bg-bg-fill-info px-4">
448
+ {utilityNavigationLinks?.map((link, index) => {
449
+ return (
450
+ <li key={`utility-menu-items-${index}`}>
451
+ <ContentfulButton
452
+ key={`utility-submenu-link-btn-${link.anchorId}`}
453
+ {...(Object.fromEntries(
454
+ Object.entries(link).filter(([_, v]) => v !== null)
455
+ ) as any)}
456
+ linkClassName={cx(
457
+ "footnote flex items-center w-full h-11 text-text-link",
458
+ typeof props.utilityNavActiveIndex !== "number"
459
+ ? index === 1 && "label4"
460
+ : props.utilityNavActiveIndex === index && "label4"
461
+ )}
462
+ linkVariant="unstyled"
463
+ />
464
+ </li>
465
+ );
466
+ })}
467
+ </ul>
468
+ </div>
469
+ </div>
470
+ </div>
471
+ </div>
472
+ );
473
+ };
474
+
475
+ const MobileSearchInput = (props: {
476
+ closeMenu: () => void;
477
+ onSearch: (query: string) => void;
478
+ isMenuOpen: boolean;
479
+ searchBarIconURL: string;
480
+ }) => {
481
+ const { closeMenu, onSearch, isMenuOpen, searchBarIconURL } = props;
482
+ const [searchValue, setSearchValue] = React.useState("");
483
+ const searchInputRef = React.useRef<HTMLInputElement>(null);
484
+
485
+ const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
486
+ closeMenu();
487
+ e.preventDefault();
488
+ onSearch(searchValue);
489
+ };
490
+
491
+ React.useEffect(() => {
492
+ if (!isMenuOpen) {
493
+ setSearchValue("");
494
+ }
495
+ }, [isMenuOpen]);
496
+
497
+ return (
498
+ <form
499
+ name="searchForm"
500
+ className="flex border-b border-t transition-colors focus-within:border-border-focus"
501
+ onSubmit={redirectToSearchResults}
502
+ >
503
+ <NextImage
504
+ src={searchBarIconURL}
505
+ width={32}
506
+ height={32}
507
+ alt="Search icon"
508
+ role="button"
509
+ className="ml-2"
510
+ onClick={redirectToSearchResults}
511
+ />
512
+ <div className="flex-grow">
513
+ <Input
514
+ ref={searchInputRef}
515
+ className={"body3 h-[34px] rounded-none px-3 text-text"}
516
+ name="search"
517
+ placeholder="Search..."
518
+ value={searchValue}
519
+ onChange={e => setSearchValue(e.target.value)}
520
+ autoComplete="off"
521
+ containerClassName="h-[46px] px-4 pl-0 rounded-none flex-grow border-none"
522
+ />
523
+ </div>
524
+ </form>
525
+ );
526
+ };
527
+
528
+ const DesktopSearchInput = (props: {
529
+ searchBarIconURL: string;
530
+ onSearch: (query: string) => void;
531
+ }) => {
532
+ const { searchBarIconURL, onSearch } = props;
533
+ const [searchValue, setSearchValue] = React.useState("");
534
+ const searchInputRef = React.useRef<HTMLInputElement>(null);
535
+
536
+ const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
537
+ e.preventDefault();
538
+ onSearch(searchValue);
539
+ };
540
+
541
+ return (
542
+ <form
543
+ name="searchForm"
544
+ className="flex h-9 w-60 rounded-input-xl border px-1 transition-colors focus-within:border-border-focus"
545
+ onSubmit={redirectToSearchResults}
546
+ >
547
+ <NextImage
548
+ src={searchBarIconURL}
549
+ width={32}
550
+ height={32}
551
+ alt="Search icon"
552
+ role="button"
553
+ onClick={redirectToSearchResults}
554
+ />
555
+ <Input
556
+ ref={searchInputRef}
557
+ className={"body3 rounded-full px-3 text-text"}
558
+ name="search"
559
+ placeholder="Search..."
560
+ value={searchValue}
561
+ onChange={e => setSearchValue(e.target.value)}
562
+ autoComplete="off"
563
+ containerClassName="px-0 h-full border-none rounded-full"
564
+ />
565
+ </form>
566
+ );
567
+ };