@windstream/react-shared-components 0.1.20 → 0.1.21

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 (160) 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 +1 -1
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.esm.js +6 -6
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +6 -6
  11. package/dist/index.js.map +1 -1
  12. package/package.json +182 -182
  13. package/src/components/accordion/Accordion.stories.tsx +230 -230
  14. package/src/components/accordion/types.ts +11 -11
  15. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  16. package/src/components/alert-card/index.tsx +41 -41
  17. package/src/components/alert-card/types.ts +13 -13
  18. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  19. package/src/components/brand-button/helpers.ts +35 -35
  20. package/src/components/brand-button/index.tsx +115 -115
  21. package/src/components/brand-button/types.ts +37 -37
  22. package/src/components/button/Button.stories.tsx +108 -108
  23. package/src/components/button/index.tsx +27 -27
  24. package/src/components/button/types.ts +14 -14
  25. package/src/components/call-button/CallButton.stories.tsx +324 -324
  26. package/src/components/call-button/index.tsx +86 -86
  27. package/src/components/call-button/types.ts +11 -11
  28. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  29. package/src/components/checkbox/index.tsx +197 -197
  30. package/src/components/checkbox/types.ts +27 -27
  31. package/src/components/checklist/Checklist.stories.tsx +150 -150
  32. package/src/components/checklist/index.tsx +59 -59
  33. package/src/components/checklist/types.ts +16 -16
  34. package/src/components/collapse/Collapse.stories.tsx +255 -255
  35. package/src/components/collapse/index.tsx +46 -46
  36. package/src/components/collapse/types.ts +6 -6
  37. package/src/components/divider/Divider.stories.tsx +205 -205
  38. package/src/components/divider/index.tsx +22 -22
  39. package/src/components/divider/type.ts +3 -3
  40. package/src/components/image/Image.stories.tsx +113 -113
  41. package/src/components/image/index.tsx +25 -25
  42. package/src/components/image/types.ts +40 -40
  43. package/src/components/input/Input.stories.tsx +325 -325
  44. package/src/components/input/index.tsx +177 -177
  45. package/src/components/input/types.ts +37 -37
  46. package/src/components/link/Link.stories.tsx +163 -163
  47. package/src/components/link/index.tsx +14 -2
  48. package/src/components/link/types.ts +25 -25
  49. package/src/components/list/List.stories.tsx +272 -272
  50. package/src/components/list/index.tsx +88 -88
  51. package/src/components/list/list-item/index.tsx +38 -38
  52. package/src/components/list/list-item/types.ts +13 -13
  53. package/src/components/list/types.ts +29 -29
  54. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  55. package/src/components/material-icon/constants.ts +98 -98
  56. package/src/components/material-icon/index.tsx +47 -47
  57. package/src/components/material-icon/types.ts +31 -31
  58. package/src/components/modal/Modal.stories.tsx +171 -171
  59. package/src/components/modal/index.tsx +164 -164
  60. package/src/components/modal/types.ts +24 -24
  61. package/src/components/next-image/index.tsx +54 -54
  62. package/src/components/next-image/types.ts +1 -1
  63. package/src/components/pagination/index.tsx +100 -100
  64. package/src/components/pagination/types.ts +6 -6
  65. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  66. package/src/components/radio-button/index.tsx +75 -75
  67. package/src/components/radio-button/types.ts +21 -21
  68. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  69. package/src/components/see-more/index.tsx +44 -44
  70. package/src/components/see-more/types.ts +4 -4
  71. package/src/components/select/Select.stories.tsx +411 -411
  72. package/src/components/select/index.tsx +155 -155
  73. package/src/components/select/types.ts +36 -36
  74. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  75. package/src/components/select-plan-button/index.tsx +57 -57
  76. package/src/components/select-plan-button/types.ts +14 -14
  77. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  78. package/src/components/skeleton/index.tsx +61 -61
  79. package/src/components/skeleton/types.ts +4 -4
  80. package/src/components/spinner/Spinner.stories.tsx +335 -335
  81. package/src/components/spinner/index.tsx +44 -44
  82. package/src/components/spinner/types.ts +5 -5
  83. package/src/components/text/Text.stories.tsx +321 -321
  84. package/src/components/text/index.tsx +25 -25
  85. package/src/components/text/types.ts +45 -45
  86. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  87. package/src/components/tooltip/index.tsx +74 -74
  88. package/src/components/tooltip/types.ts +7 -7
  89. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  90. package/src/components/view-cart-button/index.tsx +42 -42
  91. package/src/components/view-cart-button/types.ts +5 -5
  92. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  93. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  94. package/src/contentful/blocks/blogs-grid/index.tsx +129 -129
  95. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  96. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  97. package/src/contentful/blocks/button/index.tsx +121 -121
  98. package/src/contentful/blocks/button/types.ts +36 -36
  99. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  100. package/src/contentful/blocks/callout/index.tsx +86 -86
  101. package/src/contentful/blocks/callout/types.ts +15 -15
  102. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  103. package/src/contentful/blocks/cards/blog-card/index.tsx +104 -104
  104. package/src/contentful/blocks/cards/blog-card/types.ts +15 -15
  105. package/src/contentful/blocks/cards/index.tsx +13 -13
  106. package/src/contentful/blocks/cards/product-card/index.tsx +208 -208
  107. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  108. package/src/contentful/blocks/cards/testimonial-card/index.tsx +88 -88
  109. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  110. package/src/contentful/blocks/cards/types.ts +1 -1
  111. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  112. package/src/contentful/blocks/carousel/helper.tsx +356 -356
  113. package/src/contentful/blocks/carousel/index.tsx +73 -73
  114. package/src/contentful/blocks/carousel/types.ts +143 -143
  115. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  116. package/src/contentful/blocks/cta-callout/index.tsx +60 -60
  117. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  118. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  119. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  120. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  121. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  122. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  123. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  124. package/src/contentful/blocks/footer/Footer.stories.tsx +30 -30
  125. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  126. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  127. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  128. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  129. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  130. package/src/contentful/blocks/modal/constants.ts +53 -53
  131. package/src/contentful/blocks/modal/index.tsx +91 -91
  132. package/src/contentful/blocks/modal/types.ts +12 -12
  133. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +113 -113
  134. package/src/contentful/blocks/navigation/index.tsx +394 -394
  135. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  136. package/src/contentful/blocks/navigation/types.ts +41 -41
  137. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  138. package/src/contentful/blocks/primary-hero/index.tsx +234 -234
  139. package/src/contentful/blocks/primary-hero/types.ts +35 -35
  140. package/src/contentful/blocks/search-block/index.tsx +90 -90
  141. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  142. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  143. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  144. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  145. package/src/contentful/blocks/text/index.tsx +12 -12
  146. package/src/contentful/blocks/text/types.ts +1 -1
  147. package/src/contentful/index.ts +75 -75
  148. package/src/hooks/use-body-scroll-lock.ts +34 -34
  149. package/src/hooks/use-outside-click.ts +17 -17
  150. package/src/index.ts +96 -96
  151. package/src/next/index.ts +5 -5
  152. package/src/setupTests.ts +46 -46
  153. package/src/stories/DocsTemplate.tsx +24 -24
  154. package/src/styles/globals.css +343 -343
  155. package/src/types/global.d.ts +9 -9
  156. package/src/types/micro-components.ts +99 -99
  157. package/src/types/utm.ts +49 -49
  158. package/src/utils/cookie.ts +58 -58
  159. package/src/utils/index.ts +65 -65
  160. 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`}>
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
+ };