@windstream/react-shared-components 0.1.31 → 0.1.33

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 (175) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.d.ts +14 -2
  3. package/dist/contentful/index.esm.js +3 -3
  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 +4 -4
  8. package/dist/index.d.ts +4 -4
  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.d.ts +5 -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 +182 -182
  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 +114 -115
  29. package/src/components/brand-button/types.ts +37 -37
  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 +86 -86
  35. package/src/components/call-button/types.ts +11 -11
  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 +63 -61
  41. package/src/components/checklist/types.ts +18 -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/types.ts +25 -25
  56. package/src/components/list/List.stories.tsx +272 -272
  57. package/src/components/list/index.tsx +88 -88
  58. package/src/components/list/list-item/index.tsx +38 -38
  59. package/src/components/list/list-item/types.ts +13 -13
  60. package/src/components/list/types.ts +29 -29
  61. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  62. package/src/components/material-icon/constants.ts +98 -98
  63. package/src/components/material-icon/index.tsx +47 -47
  64. package/src/components/material-icon/types.ts +31 -31
  65. package/src/components/modal/Modal.stories.tsx +171 -171
  66. package/src/components/modal/index.tsx +164 -164
  67. package/src/components/modal/types.ts +24 -24
  68. package/src/components/next-image/index.tsx +54 -54
  69. package/src/components/next-image/types.ts +1 -1
  70. package/src/components/pagination/index.tsx +100 -100
  71. package/src/components/pagination/types.ts +6 -6
  72. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  73. package/src/components/radio-button/index.tsx +75 -75
  74. package/src/components/radio-button/types.ts +21 -21
  75. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  76. package/src/components/see-more/index.tsx +44 -44
  77. package/src/components/see-more/types.ts +4 -4
  78. package/src/components/select/Select.stories.tsx +411 -411
  79. package/src/components/select/index.tsx +155 -155
  80. package/src/components/select/types.ts +36 -36
  81. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  82. package/src/components/select-plan-button/index.tsx +59 -59
  83. package/src/components/select-plan-button/types.ts +17 -17
  84. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  85. package/src/components/skeleton/index.tsx +61 -61
  86. package/src/components/skeleton/types.ts +4 -4
  87. package/src/components/spinner/Spinner.stories.tsx +335 -335
  88. package/src/components/spinner/index.tsx +44 -44
  89. package/src/components/spinner/types.ts +5 -5
  90. package/src/components/text/Text.stories.tsx +321 -321
  91. package/src/components/text/index.tsx +25 -25
  92. package/src/components/text/types.ts +45 -45
  93. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  94. package/src/components/tooltip/index.tsx +74 -74
  95. package/src/components/tooltip/types.ts +7 -7
  96. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  97. package/src/components/view-cart-button/index.tsx +42 -42
  98. package/src/components/view-cart-button/types.ts +5 -5
  99. package/src/contentful/blocks/accordion/index.tsx +62 -62
  100. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  101. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  102. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +65 -65
  103. package/src/contentful/blocks/anchored-bottom-banner/types.ts +9 -9
  104. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  105. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  106. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  107. package/src/contentful/blocks/button/index.tsx +129 -129
  108. package/src/contentful/blocks/button/types.ts +39 -39
  109. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  110. package/src/contentful/blocks/callout/index.tsx +88 -88
  111. package/src/contentful/blocks/callout/types.ts +15 -15
  112. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  113. package/src/contentful/blocks/cards/blog-card/index.tsx +110 -110
  114. package/src/contentful/blocks/cards/blog-card/types.ts +18 -18
  115. package/src/contentful/blocks/cards/index.tsx +13 -13
  116. package/src/contentful/blocks/cards/product-card/index.tsx +252 -252
  117. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  118. package/src/contentful/blocks/cards/simple-card/index.tsx +89 -89
  119. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  120. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  121. package/src/contentful/blocks/cards/types.ts +1 -1
  122. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  123. package/src/contentful/blocks/carousel/helper.tsx +440 -440
  124. package/src/contentful/blocks/carousel/index.tsx +85 -85
  125. package/src/contentful/blocks/carousel/types.ts +144 -144
  126. package/src/contentful/blocks/cookiebanner/index.tsx +146 -0
  127. package/src/contentful/blocks/cookiebanner/type.ts +7 -0
  128. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  129. package/src/contentful/blocks/cta-callout/index.tsx +60 -60
  130. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  131. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  132. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  133. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  134. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  135. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  136. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  137. package/src/contentful/blocks/footer/Footer.stories.tsx +30 -30
  138. package/src/contentful/blocks/footer/index.tsx +90 -90
  139. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  140. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  141. package/src/contentful/blocks/image-promo-bar/index.tsx +14 -12
  142. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  143. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  144. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  145. package/src/contentful/blocks/modal/constants.ts +53 -53
  146. package/src/contentful/blocks/modal/types.ts +12 -12
  147. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +113 -113
  148. package/src/contentful/blocks/navigation/index.tsx +394 -394
  149. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  150. package/src/contentful/blocks/navigation/types.ts +41 -41
  151. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  152. package/src/contentful/blocks/primary-hero/index.tsx +234 -234
  153. package/src/contentful/blocks/primary-hero/types.ts +35 -35
  154. package/src/contentful/blocks/search-block/index.tsx +90 -90
  155. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  156. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  157. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  158. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  159. package/src/contentful/blocks/text/index.tsx +12 -12
  160. package/src/contentful/blocks/text/types.ts +1 -1
  161. package/src/contentful/index.ts +81 -78
  162. package/src/hooks/use-body-scroll-lock.ts +34 -34
  163. package/src/hooks/use-carousel-swipe.ts +264 -264
  164. package/src/hooks/use-outside-click.ts +17 -17
  165. package/src/index.ts +101 -101
  166. package/src/next/index.ts +5 -5
  167. package/src/setupTests.ts +46 -46
  168. package/src/stories/DocsTemplate.tsx +24 -24
  169. package/src/styles/globals.css +343 -343
  170. package/src/types/global.d.ts +9 -9
  171. package/src/types/micro-components.ts +99 -99
  172. package/src/types/utm.ts +49 -49
  173. package/src/utils/cookie.ts +80 -58
  174. package/src/utils/index.ts +65 -65
  175. package/src/utils/utm.ts +221 -221
@@ -1,394 +1,394 @@
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
- onSearch = () => {},
27
- } = props;
28
- return (
29
- <div className="component-container">
30
- <nav className={`menu-container`}>
31
- <div className="utility-container hidden lg:block lg:border-b lg:px-2">
32
- <div className="mx-auto flex max-w-120 justify-between">
33
- <ul className="flex gap-5" aria-label="Utility Navigation">
34
- {utilityNavigationLinks?.map((links, index) => {
35
- return (
36
- <li key={`main-menu-items-${index}`}>
37
- <ContentfulButton
38
- linkClassName={cx(
39
- "footnote flex items-center w-full h-11 text-text",
40
- index === 1 && "label4"
41
- )}
42
- linkVariant="unstyled"
43
- {...(Object.fromEntries(
44
- Object.entries(links).filter(([_, v]) => v !== null)
45
- ) as any)}
46
- />
47
- </li>
48
- );
49
- })}
50
- </ul>
51
- <div className="flex items-center gap-10">
52
- <CallButton className="border-none" href={invocaPhoneNumberLink}>
53
- <Text className="body3 text-text">
54
- {invocaPhoneNumberDisplayText}
55
- </Text>
56
- </CallButton>
57
- {accountNavigationLinks?.map((links, index) => {
58
- return (
59
- <DesktopLinkGroups
60
- key={`my-account-${index}`}
61
- anchorName={`my-account-${index}`}
62
- link={links}
63
- />
64
- );
65
- })}
66
- </div>
67
- </div>
68
- </div>
69
- <div className="main-nav-container" aria-label="Main Navigation">
70
- <div className="mobile-nav-section flex h-14 items-center justify-between px-5 py-[10px] lg:hidden">
71
- <div>
72
- <Link href="/" className="flex">
73
- <NextImage
74
- src={
75
- typeof primaryNavigationLogo === "string"
76
- ? primaryNavigationLogo
77
- : primaryNavigationLogo?.url || ""
78
- }
79
- alt="Kinetic business logo"
80
- width={76.5}
81
- height={24}
82
- />
83
- </Link>
84
- </div>
85
- <div className="flex items-center gap-6">
86
- <CallButton href={invocaPhoneNumberLink}>
87
- <Text as="span" className="footnote">
88
- {invocaPhoneNumberDisplayText}
89
- </Text>
90
- </CallButton>
91
- <MobileMenu {...props} />
92
- </div>
93
- </div>
94
-
95
- <div className="desktop-nav-section hidden lg:block lg:border-b lg:px-2">
96
- <div className="mx-auto flex h-14 max-w-120 items-center justify-between">
97
- <div className="flex h-full">
98
- <Link href="/" className="flex">
99
- <NextImage
100
- src={
101
- typeof primaryNavigationLogo === "string"
102
- ? primaryNavigationLogo
103
- : primaryNavigationLogo?.url || ""
104
- }
105
- alt="Kinetic business logo"
106
- width={76.5}
107
- height={24}
108
- className="mr-[64px]"
109
- />
110
- </Link>
111
-
112
- <div className="flex h-full items-center gap-5">
113
- {primaryNavigationLinks?.map((links, index) => {
114
- return (
115
- <DesktopLinkGroups
116
- key={`main-menu-${index}`}
117
- anchorName={`main-menu-${index}`}
118
- link={links}
119
- />
120
- );
121
- })}
122
- </div>
123
- </div>
124
- <div className="flex h-full items-center gap-10">
125
- <DesktopSearchInput
126
- searchBarIconURL={
127
- typeof searchBarIcon === "string"
128
- ? searchBarIcon
129
- : searchBarIcon?.url || ""
130
- }
131
- onSearch={onSearch}
132
- />
133
- {supportNavigationLinks?.map((links, index) => {
134
- return (
135
- <DesktopLinkGroups
136
- key={`support-menu-${index}`}
137
- anchorName={`support-menu-${index}`}
138
- link={links}
139
- />
140
- );
141
- })}
142
- </div>
143
- </div>
144
- </div>
145
- </div>
146
- </nav>
147
-
148
- {checkPlansJSX && <div className="md:hidden">{checkPlansJSX}</div>}
149
- </div>
150
- );
151
- };
152
-
153
- const MobileMenu = (props: NavigationProps) => {
154
- const {
155
- primaryNavigationLinks,
156
- utilityNavigationLinks,
157
- supportNavigationLinks,
158
- accountNavigationLinks,
159
- } = props;
160
- const [isOpen, setIsOpen] = React.useState(false);
161
-
162
- React.useEffect(() => {
163
- if (typeof window === "undefined") return;
164
-
165
- if (isOpen) document.body.style.overflowY = "hidden";
166
- else document.body.style.overflowY = "unset";
167
-
168
- const element = document.getElementById("drawer-items");
169
- if (!element) return;
170
-
171
- const focusableEls = element.querySelectorAll(".focus-item");
172
- const firstFocusableEl = focusableEls[0];
173
- const lastFocusableEl = focusableEls[focusableEls.length - 1];
174
-
175
- const handleKeyDown = (e: {
176
- key: string;
177
- keyCode: number;
178
- shiftKey: any;
179
- preventDefault: () => void;
180
- }) => {
181
- const isTabPressed = e.key === "Tab" || e.keyCode === 9;
182
-
183
- if (!isTabPressed) return;
184
-
185
- if (e.shiftKey) {
186
- if (document.activeElement === firstFocusableEl) {
187
- (lastFocusableEl as HTMLButtonElement).focus?.();
188
- e.preventDefault();
189
- }
190
- } else {
191
- if (document.activeElement === lastFocusableEl) {
192
- (firstFocusableEl as HTMLButtonElement).focus?.();
193
- e.preventDefault();
194
- }
195
- }
196
- };
197
-
198
- window.addEventListener("keydown", handleKeyDown);
199
-
200
- return () => {
201
- document.body.style.overflowY = "unset";
202
- window.removeEventListener("keydown", handleKeyDown);
203
- };
204
- }, [isOpen]);
205
-
206
- const closeMenu = () => {
207
- setIsOpen(false);
208
- };
209
-
210
- return (
211
- <div>
212
- <ContentfulButton
213
- showButtonAs="unstyled"
214
- buttonClassName="flex"
215
- onClick={() => setIsOpen(true)}
216
- >
217
- <MaterialIcon name="menu" />
218
- </ContentfulButton>
219
- {isOpen ? (
220
- <div className="fixed bottom-0 left-0 right-0 top-0 z-[90] h-full w-full bg-scrim-bg-modal"></div>
221
- ) : null}
222
-
223
- <div
224
- className={cx(
225
- "fixed bottom-0 right-0 top-0",
226
- "z-[100] h-full bg-bg px-0 py-4",
227
- "transition-all duration-300 ease-in-out",
228
- "block",
229
- isOpen ? "right-0" : "-right-96"
230
- )}
231
- id="mobile-menu-overlay"
232
- >
233
- <div id="drawer-items" className="flex h-full flex-col gap-3">
234
- <div className="flex items-center justify-between px-4">
235
- <div>
236
- <CallButton
237
- className="border-none"
238
- href={props.invocaPhoneNumberLink}
239
- >
240
- {props.invocaPhoneNumberDisplayText}
241
- </CallButton>
242
- </div>
243
- <div>
244
- <ContentfulButton
245
- showButtonAs="unstyled"
246
- buttonClassName="focus-item flex"
247
- onClick={closeMenu}
248
- >
249
- <MaterialIcon name="close" />
250
- </ContentfulButton>
251
- </div>
252
- </div>
253
- <MobileSearchInput
254
- closeMenu={closeMenu}
255
- isMenuOpen={isOpen}
256
- searchBarIconURL={
257
- typeof props.searchBarIcon === "string"
258
- ? props.searchBarIcon
259
- : props.searchBarIcon?.url || ""
260
- }
261
- onSearch={props.onSearch || (() => {})}
262
- />
263
- <div className="flex-grow overflow-y-auto">
264
- <ul className="mt-2 flex flex-col gap-2">
265
- {[
266
- ...(primaryNavigationLinks || []),
267
- ...(supportNavigationLinks || []),
268
- ...(accountNavigationLinks || []),
269
- ].map((links, index) => (
270
- <li key={`main-menu-items-${index}`}>
271
- <MobileLinkGroups link={links} />
272
- </li>
273
- ))}
274
- </ul>
275
-
276
- <ul className="mt-2 flex gap-5 bg-bg-fill-info px-4">
277
- {utilityNavigationLinks?.map((link, index) => {
278
- return (
279
- <li key={`utility-menu-items-${index}`}>
280
- <ContentfulButton
281
- key={`utility-submenu-link-btn-${link.anchorId}`}
282
- {...(Object.fromEntries(
283
- Object.entries(link).filter(([_, v]) => v !== null)
284
- ) as any)}
285
- linkClassName={cx(
286
- "footnote flex items-center w-full h-11 text-text-link",
287
- index === 1 && "label4"
288
- )}
289
- linkVariant="unstyled"
290
- />
291
- </li>
292
- );
293
- })}
294
- </ul>
295
- </div>
296
- </div>
297
- </div>
298
- </div>
299
- );
300
- };
301
-
302
- const MobileSearchInput = (props: {
303
- closeMenu: () => void;
304
- onSearch: (query: string) => void;
305
- isMenuOpen: boolean;
306
- searchBarIconURL: string;
307
- }) => {
308
- const { closeMenu, onSearch, isMenuOpen, searchBarIconURL } = props;
309
- const [searchValue, setSearchValue] = React.useState("");
310
- const searchInputRef = React.useRef<HTMLInputElement>(null);
311
-
312
- const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
313
- closeMenu();
314
- e.preventDefault();
315
- onSearch(searchValue);
316
- };
317
-
318
- React.useEffect(() => {
319
- if (!isMenuOpen) {
320
- setSearchValue("");
321
- }
322
- }, [isMenuOpen]);
323
-
324
- return (
325
- <form
326
- name="searchForm"
327
- className="flex border-b border-t transition-colors focus-within:border-border-focus"
328
- onSubmit={redirectToSearchResults}
329
- >
330
- <NextImage
331
- src={searchBarIconURL}
332
- width={32}
333
- height={32}
334
- alt="Search icon"
335
- role="button"
336
- className="ml-2"
337
- onClick={redirectToSearchResults}
338
- />
339
- <div className="flex-grow">
340
- <Input
341
- ref={searchInputRef}
342
- className={"body3 h-[34px] rounded-none px-3 text-text"}
343
- name="search"
344
- placeholder="Search..."
345
- value={searchValue}
346
- onChange={e => setSearchValue(e.target.value)}
347
- autoComplete="off"
348
- containerClassName="h-[46px] px-4 pl-0 rounded-none flex-grow border-none"
349
- />
350
- </div>
351
- </form>
352
- );
353
- };
354
-
355
- const DesktopSearchInput = (props: {
356
- searchBarIconURL: string;
357
- onSearch: (query: string) => void;
358
- }) => {
359
- const { searchBarIconURL, onSearch } = props;
360
- const [searchValue, setSearchValue] = React.useState("");
361
- const searchInputRef = React.useRef<HTMLInputElement>(null);
362
-
363
- const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
364
- e.preventDefault();
365
- onSearch(searchValue);
366
- };
367
-
368
- return (
369
- <form
370
- name="searchForm"
371
- className="flex h-9 w-60 rounded-input-xl border px-1 transition-colors focus-within:border-border-focus"
372
- onSubmit={redirectToSearchResults}
373
- >
374
- <NextImage
375
- src={searchBarIconURL}
376
- width={32}
377
- height={32}
378
- alt="Search icon"
379
- role="button"
380
- onClick={redirectToSearchResults}
381
- />
382
- <Input
383
- ref={searchInputRef}
384
- className={"body3 rounded-full px-3 text-text"}
385
- name="search"
386
- placeholder="Search..."
387
- value={searchValue}
388
- onChange={e => setSearchValue(e.target.value)}
389
- autoComplete="off"
390
- containerClassName="px-0 h-full border-none rounded-full"
391
- />
392
- </form>
393
- );
394
- };
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
+ onSearch = () => {},
27
+ } = props;
28
+ return (
29
+ <div className="component-container">
30
+ <nav className={`menu-container z-[1000]`}>
31
+ <div className="utility-container hidden lg:block lg:border-b lg:px-2">
32
+ <div className="mx-auto flex max-w-120 justify-between">
33
+ <ul className="flex gap-5" aria-label="Utility Navigation">
34
+ {utilityNavigationLinks?.map((links, index) => {
35
+ return (
36
+ <li key={`main-menu-items-${index}`}>
37
+ <ContentfulButton
38
+ linkClassName={cx(
39
+ "footnote flex items-center w-full h-11 text-text",
40
+ index === 1 && "label4"
41
+ )}
42
+ linkVariant="unstyled"
43
+ {...(Object.fromEntries(
44
+ Object.entries(links).filter(([_, v]) => v !== null)
45
+ ) as any)}
46
+ />
47
+ </li>
48
+ );
49
+ })}
50
+ </ul>
51
+ <div className="flex items-center gap-10">
52
+ <CallButton className="border-none" href={invocaPhoneNumberLink}>
53
+ <Text className="body3 text-text">
54
+ {invocaPhoneNumberDisplayText}
55
+ </Text>
56
+ </CallButton>
57
+ {accountNavigationLinks?.map((links, index) => {
58
+ return (
59
+ <DesktopLinkGroups
60
+ key={`my-account-${index}`}
61
+ anchorName={`my-account-${index}`}
62
+ link={links}
63
+ />
64
+ );
65
+ })}
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <div className="main-nav-container" aria-label="Main Navigation">
70
+ <div className="mobile-nav-section flex h-14 items-center justify-between px-5 py-[10px] lg:hidden">
71
+ <div>
72
+ <Link href="/" className="flex">
73
+ <NextImage
74
+ src={
75
+ typeof primaryNavigationLogo === "string"
76
+ ? primaryNavigationLogo
77
+ : primaryNavigationLogo?.url || ""
78
+ }
79
+ alt="Kinetic business logo"
80
+ width={76.5}
81
+ height={24}
82
+ />
83
+ </Link>
84
+ </div>
85
+ <div className="flex items-center gap-6">
86
+ <CallButton href={invocaPhoneNumberLink}>
87
+ <Text as="span" className="footnote">
88
+ {invocaPhoneNumberDisplayText}
89
+ </Text>
90
+ </CallButton>
91
+ <MobileMenu {...props} />
92
+ </div>
93
+ </div>
94
+
95
+ <div className="desktop-nav-section hidden lg:block lg:border-b lg:px-2">
96
+ <div className="mx-auto flex h-14 max-w-120 items-center justify-between">
97
+ <div className="flex h-full">
98
+ <Link href="/" className="flex">
99
+ <NextImage
100
+ src={
101
+ typeof primaryNavigationLogo === "string"
102
+ ? primaryNavigationLogo
103
+ : primaryNavigationLogo?.url || ""
104
+ }
105
+ alt="Kinetic business logo"
106
+ width={76.5}
107
+ height={24}
108
+ className="mr-[64px]"
109
+ />
110
+ </Link>
111
+
112
+ <div className="flex h-full items-center gap-5">
113
+ {primaryNavigationLinks?.map((links, index) => {
114
+ return (
115
+ <DesktopLinkGroups
116
+ key={`main-menu-${index}`}
117
+ anchorName={`main-menu-${index}`}
118
+ link={links}
119
+ />
120
+ );
121
+ })}
122
+ </div>
123
+ </div>
124
+ <div className="flex h-full items-center gap-10">
125
+ <DesktopSearchInput
126
+ searchBarIconURL={
127
+ typeof searchBarIcon === "string"
128
+ ? searchBarIcon
129
+ : searchBarIcon?.url || ""
130
+ }
131
+ onSearch={onSearch}
132
+ />
133
+ {supportNavigationLinks?.map((links, index) => {
134
+ return (
135
+ <DesktopLinkGroups
136
+ key={`support-menu-${index}`}
137
+ anchorName={`support-menu-${index}`}
138
+ link={links}
139
+ />
140
+ );
141
+ })}
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </nav>
147
+
148
+ {checkPlansJSX && <div className="md:hidden">{checkPlansJSX}</div>}
149
+ </div>
150
+ );
151
+ };
152
+
153
+ const MobileMenu = (props: NavigationProps) => {
154
+ const {
155
+ primaryNavigationLinks,
156
+ utilityNavigationLinks,
157
+ supportNavigationLinks,
158
+ accountNavigationLinks,
159
+ } = props;
160
+ const [isOpen, setIsOpen] = React.useState(false);
161
+
162
+ React.useEffect(() => {
163
+ if (typeof window === "undefined") return;
164
+
165
+ if (isOpen) document.body.style.overflowY = "hidden";
166
+ else document.body.style.overflowY = "unset";
167
+
168
+ const element = document.getElementById("drawer-items");
169
+ if (!element) return;
170
+
171
+ const focusableEls = element.querySelectorAll(".focus-item");
172
+ const firstFocusableEl = focusableEls[0];
173
+ const lastFocusableEl = focusableEls[focusableEls.length - 1];
174
+
175
+ const handleKeyDown = (e: {
176
+ key: string;
177
+ keyCode: number;
178
+ shiftKey: any;
179
+ preventDefault: () => void;
180
+ }) => {
181
+ const isTabPressed = e.key === "Tab" || e.keyCode === 9;
182
+
183
+ if (!isTabPressed) return;
184
+
185
+ if (e.shiftKey) {
186
+ if (document.activeElement === firstFocusableEl) {
187
+ (lastFocusableEl as HTMLButtonElement).focus?.();
188
+ e.preventDefault();
189
+ }
190
+ } else {
191
+ if (document.activeElement === lastFocusableEl) {
192
+ (firstFocusableEl as HTMLButtonElement).focus?.();
193
+ e.preventDefault();
194
+ }
195
+ }
196
+ };
197
+
198
+ window.addEventListener("keydown", handleKeyDown);
199
+
200
+ return () => {
201
+ document.body.style.overflowY = "unset";
202
+ window.removeEventListener("keydown", handleKeyDown);
203
+ };
204
+ }, [isOpen]);
205
+
206
+ const closeMenu = () => {
207
+ setIsOpen(false);
208
+ };
209
+
210
+ return (
211
+ <div>
212
+ <ContentfulButton
213
+ showButtonAs="unstyled"
214
+ buttonClassName="flex"
215
+ onClick={() => setIsOpen(true)}
216
+ >
217
+ <MaterialIcon name="menu" />
218
+ </ContentfulButton>
219
+ {isOpen ? (
220
+ <div className="fixed bottom-0 left-0 right-0 top-0 z-[90] h-full w-full bg-scrim-bg-modal"></div>
221
+ ) : null}
222
+
223
+ <div
224
+ className={cx(
225
+ "fixed bottom-0 right-0 top-0",
226
+ "z-[100] h-full bg-bg px-0 py-4",
227
+ "transition-all duration-300 ease-in-out",
228
+ "block",
229
+ isOpen ? "right-0" : "-right-96"
230
+ )}
231
+ id="mobile-menu-overlay"
232
+ >
233
+ <div id="drawer-items" className="flex h-full flex-col gap-3">
234
+ <div className="flex items-center justify-between px-4">
235
+ <div>
236
+ <CallButton
237
+ className="border-none"
238
+ href={props.invocaPhoneNumberLink}
239
+ >
240
+ {props.invocaPhoneNumberDisplayText}
241
+ </CallButton>
242
+ </div>
243
+ <div>
244
+ <ContentfulButton
245
+ showButtonAs="unstyled"
246
+ buttonClassName="focus-item flex"
247
+ onClick={closeMenu}
248
+ >
249
+ <MaterialIcon name="close" />
250
+ </ContentfulButton>
251
+ </div>
252
+ </div>
253
+ <MobileSearchInput
254
+ closeMenu={closeMenu}
255
+ isMenuOpen={isOpen}
256
+ searchBarIconURL={
257
+ typeof props.searchBarIcon === "string"
258
+ ? props.searchBarIcon
259
+ : props.searchBarIcon?.url || ""
260
+ }
261
+ onSearch={props.onSearch || (() => {})}
262
+ />
263
+ <div className="flex-grow overflow-y-auto">
264
+ <ul className="mt-2 flex flex-col gap-2">
265
+ {[
266
+ ...(primaryNavigationLinks || []),
267
+ ...(supportNavigationLinks || []),
268
+ ...(accountNavigationLinks || []),
269
+ ].map((links, index) => (
270
+ <li key={`main-menu-items-${index}`}>
271
+ <MobileLinkGroups link={links} />
272
+ </li>
273
+ ))}
274
+ </ul>
275
+
276
+ <ul className="mt-2 flex gap-5 bg-bg-fill-info px-4">
277
+ {utilityNavigationLinks?.map((link, index) => {
278
+ return (
279
+ <li key={`utility-menu-items-${index}`}>
280
+ <ContentfulButton
281
+ key={`utility-submenu-link-btn-${link.anchorId}`}
282
+ {...(Object.fromEntries(
283
+ Object.entries(link).filter(([_, v]) => v !== null)
284
+ ) as any)}
285
+ linkClassName={cx(
286
+ "footnote flex items-center w-full h-11 text-text-link",
287
+ index === 1 && "label4"
288
+ )}
289
+ linkVariant="unstyled"
290
+ />
291
+ </li>
292
+ );
293
+ })}
294
+ </ul>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ };
301
+
302
+ const MobileSearchInput = (props: {
303
+ closeMenu: () => void;
304
+ onSearch: (query: string) => void;
305
+ isMenuOpen: boolean;
306
+ searchBarIconURL: string;
307
+ }) => {
308
+ const { closeMenu, onSearch, isMenuOpen, searchBarIconURL } = props;
309
+ const [searchValue, setSearchValue] = React.useState("");
310
+ const searchInputRef = React.useRef<HTMLInputElement>(null);
311
+
312
+ const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
313
+ closeMenu();
314
+ e.preventDefault();
315
+ onSearch(searchValue);
316
+ };
317
+
318
+ React.useEffect(() => {
319
+ if (!isMenuOpen) {
320
+ setSearchValue("");
321
+ }
322
+ }, [isMenuOpen]);
323
+
324
+ return (
325
+ <form
326
+ name="searchForm"
327
+ className="flex border-b border-t transition-colors focus-within:border-border-focus"
328
+ onSubmit={redirectToSearchResults}
329
+ >
330
+ <NextImage
331
+ src={searchBarIconURL}
332
+ width={32}
333
+ height={32}
334
+ alt="Search icon"
335
+ role="button"
336
+ className="ml-2"
337
+ onClick={redirectToSearchResults}
338
+ />
339
+ <div className="flex-grow">
340
+ <Input
341
+ ref={searchInputRef}
342
+ className={"body3 h-[34px] rounded-none px-3 text-text"}
343
+ name="search"
344
+ placeholder="Search..."
345
+ value={searchValue}
346
+ onChange={e => setSearchValue(e.target.value)}
347
+ autoComplete="off"
348
+ containerClassName="h-[46px] px-4 pl-0 rounded-none flex-grow border-none"
349
+ />
350
+ </div>
351
+ </form>
352
+ );
353
+ };
354
+
355
+ const DesktopSearchInput = (props: {
356
+ searchBarIconURL: string;
357
+ onSearch: (query: string) => void;
358
+ }) => {
359
+ const { searchBarIconURL, onSearch } = props;
360
+ const [searchValue, setSearchValue] = React.useState("");
361
+ const searchInputRef = React.useRef<HTMLInputElement>(null);
362
+
363
+ const redirectToSearchResults = (e: FormEvent<HTMLFormElement> | any) => {
364
+ e.preventDefault();
365
+ onSearch(searchValue);
366
+ };
367
+
368
+ return (
369
+ <form
370
+ name="searchForm"
371
+ className="flex h-9 w-60 rounded-input-xl border px-1 transition-colors focus-within:border-border-focus"
372
+ onSubmit={redirectToSearchResults}
373
+ >
374
+ <NextImage
375
+ src={searchBarIconURL}
376
+ width={32}
377
+ height={32}
378
+ alt="Search icon"
379
+ role="button"
380
+ onClick={redirectToSearchResults}
381
+ />
382
+ <Input
383
+ ref={searchInputRef}
384
+ className={"body3 rounded-full px-3 text-text"}
385
+ name="search"
386
+ placeholder="Search..."
387
+ value={searchValue}
388
+ onChange={e => setSearchValue(e.target.value)}
389
+ autoComplete="off"
390
+ containerClassName="px-0 h-full border-none rounded-full"
391
+ />
392
+ </form>
393
+ );
394
+ };