@windstream/react-shared-components 0.2.39 → 0.2.42

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 (40) hide show
  1. package/dist/contentful/index.d.ts +1 -1
  2. package/dist/contentful/index.esm.js +2 -2
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +2 -2
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +1 -1
  7. package/dist/index.esm.js +2 -2
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/next/index.esm.js +1 -1
  12. package/dist/next/index.esm.js.map +1 -1
  13. package/dist/next/index.js +1 -1
  14. package/dist/next/index.js.map +1 -1
  15. package/dist/styles.css +1 -1
  16. package/dist/utils/index.esm.js +1 -1
  17. package/dist/utils/index.esm.js.map +1 -1
  18. package/dist/utils/index.js +1 -1
  19. package/dist/utils/index.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/brand-button/index.tsx +4 -4
  22. package/src/components/link/index.test.tsx +2 -2
  23. package/src/components/link/index.tsx +1 -1
  24. package/src/contentful/blocks/button/Button.stories.tsx +0 -1
  25. package/src/contentful/blocks/button/index.test.tsx +109 -8
  26. package/src/contentful/blocks/button/index.tsx +64 -18
  27. package/src/contentful/blocks/button/types.ts +1 -1
  28. package/src/contentful/blocks/cards/blog-card/index.tsx +3 -5
  29. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +69 -2
  30. package/src/contentful/blocks/cards/floating-image-card/index.tsx +7 -4
  31. package/src/contentful/blocks/carousel/helper.test.tsx +33 -10
  32. package/src/contentful/blocks/carousel/index.test.tsx +6 -8
  33. package/src/contentful/blocks/carousel/types.test.ts +10 -0
  34. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +0 -1
  35. package/src/contentful/blocks/dynamic-tabs/ordered-tab-views.tsx +1 -1
  36. package/src/contentful/blocks/ordered-list/index.tsx +2 -2
  37. package/src/contentful/blocks/primary-hero/index.tsx +1 -1
  38. package/src/hooks/contentful/use-contentful-rich-text.tsx +69 -9
  39. package/src/hooks/use-carousel-swipe.test.ts +26 -6
  40. package/src/types/micro-components.ts +2 -2
@@ -152,13 +152,12 @@ describe("Carousel", () => {
152
152
  expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
153
153
  });
154
154
 
155
- it("does not render ProductCardCarousel when showSwitch is false", () => {
155
+ it("renders ProductCardCarousel when hasProductCards is true even if showSwitch is false", () => {
156
156
  render(
157
157
  <Carousel {...defaultProps} hasProductCards={true} showSwitch={false} />
158
158
  );
159
- expect(
160
- screen.queryByTestId("product-card-carousel")
161
- ).not.toBeInTheDocument();
159
+ expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
160
+ expect(screen.queryByTestId("tab-switch")).not.toBeInTheDocument();
162
161
  });
163
162
 
164
163
  it("renders TabSwitch when showSwitch and hasProductCards with multiple tabs", () => {
@@ -279,7 +278,7 @@ describe("Carousel", () => {
279
278
  expect(screen.getByTestId("testimonial-carousel")).toBeInTheDocument();
280
279
  });
281
280
 
282
- it("does not render product carousel when showSwitch is false even with hasProductCards", () => {
281
+ it("renders product carousel and hides tab switch when showSwitch is false with hasProductCards", () => {
283
282
  render(
284
283
  <Carousel
285
284
  {...defaultProps}
@@ -288,9 +287,8 @@ describe("Carousel", () => {
288
287
  hasTestimonialCards={false}
289
288
  />
290
289
  );
291
- expect(
292
- screen.queryByTestId("product-card-carousel")
293
- ).not.toBeInTheDocument();
290
+ expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
291
+ expect(screen.queryByTestId("tab-switch")).not.toBeInTheDocument();
294
292
  });
295
293
 
296
294
  it("renders with empty tabs array and no TabSwitch", () => {
@@ -10,6 +10,16 @@ describe("carousel types exports", () => {
10
10
  expect(backgroundColorMap.white).toBe("bg-white");
11
11
  });
12
12
 
13
+ it("exports backgroundColorMap with only the supported color keys", () => {
14
+ expect(Object.keys(backgroundColorMap)).toEqual([
15
+ "blue",
16
+ "green",
17
+ "orange",
18
+ "purple",
19
+ "white",
20
+ ]);
21
+ });
22
+
13
23
  it("exports DEFAULT_TABS", () => {
14
24
  expect(DEFAULT_TABS).toEqual(["Business Ready", "Internet Only"]);
15
25
  });
@@ -29,7 +29,6 @@ const meta: Meta<typeof CtaCallout> = {
29
29
  buttonPrefix: "",
30
30
  badge: "",
31
31
  badgeIcon: "",
32
- buttonIcon: "",
33
32
  buttonIconPosition: "left",
34
33
  href: "",
35
34
  target: "_self",
@@ -175,7 +175,7 @@ export function OrderedSidebarView({
175
175
  return (
176
176
  <div className="flex gap-10 px-5 py-20">
177
177
  <div className="flex-shrink-0">
178
- <div className="flex min-w-[320px] gap-4 rounded-[20px] border-2 bg-bg-surface-active px-6 py-7 shadow-lg">
178
+ <div className="flex min-w-[320px] gap-4 rounded-[20px] border-2 px-6 py-7 shadow-lg">
179
179
  {[...new Array(Math.min(2, items.length))].map((_, colIdx) => {
180
180
  const perColumn = Math.ceil(items.length / 2);
181
181
 
@@ -30,7 +30,7 @@ export const OrderedList = ({
30
30
  return (
31
31
  <Text
32
32
  key={`ordered-item-${index}`}
33
- className="body2 relative text-left leading-7"
33
+ className="body1 relative text-left leading-7"
34
34
  >
35
35
  {formatItemName(item.itemName)}
36
36
  </Text>
@@ -44,7 +44,7 @@ export const OrderedList = ({
44
44
  title={item.itemName.toUpperCase()}
45
45
  className="flex items-center underline underline-offset-4"
46
46
  >
47
- <Text className="body2 relative cursor-pointer text-left leading-7">
47
+ <Text className="body1 relative cursor-pointer text-left leading-7">
48
48
  {formatItemName(item.itemName)}
49
49
  </Text>
50
50
  </Link>
@@ -328,7 +328,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
328
328
  ) : null}
329
329
 
330
330
  {bottomLink && (bottomLinkLabel || bottomLink?.href) && (
331
- <div className="lg:hidden">
331
+ <div className="flex lg:hidden">
332
332
  <Button
333
333
  {...bottomLink}
334
334
  onModalButtonClick={onModalButtonClick}
@@ -20,10 +20,66 @@ const isEmptyParagraph = (node: any): boolean =>
20
20
  Array.isArray(node?.content) &&
21
21
  node.content.every(
22
22
  (child: any) =>
23
- child?.nodeType === "text" &&
24
- (!child.value || child.value.trim() === "")
23
+ child?.nodeType === "text" && (!child.value || child.value.trim() === "")
25
24
  );
26
25
 
26
+ // A link is "external" only when it belongs to a different app than the one
27
+ // currently rendering this shared component (business vs. main/www are separate
28
+ // apps). Matching is environment-agnostic so a prod link like
29
+ // business.gokinetic.com is still "internal" while running on business-dev.
30
+ const hostFromUrl = (value?: string): string => {
31
+ if (!value) return "";
32
+ try {
33
+ const withProtocol = /^https?:\/\//i.test(value)
34
+ ? value
35
+ : `https://${value}`;
36
+ return new URL(withProtocol).hostname.toLowerCase();
37
+ } catch {
38
+ return "";
39
+ }
40
+ };
41
+
42
+ // e.g. "business-dev.gokinetic.com" -> "gokinetic.com|business",
43
+ // "kinetic-dev.gokinetic.com" / "www.gokinetic.com" -> "gokinetic.com|main"
44
+ const appIdentity = (host: string): string => {
45
+ const clean = host.toLowerCase().replace(/\.$/, "");
46
+ const base = clean.split(".").slice(-2).join(".");
47
+ const subdomain = clean
48
+ .slice(0, clean.length - base.length)
49
+ .replace(/\.$/, "");
50
+ const firstLabel = (subdomain.split(".")[0] || "").replace(
51
+ /-(dev|development|staging|stage|qa|test|uat|prod|production)$/,
52
+ ""
53
+ );
54
+ const app =
55
+ firstLabel === "" || firstLabel === "www" || firstLabel === "kinetic"
56
+ ? "main"
57
+ : firstLabel;
58
+ return `${base}|${app}`;
59
+ };
60
+
61
+ const getCurrentHost = (): string => {
62
+ const envHost =
63
+ hostFromUrl(process.env.NEXT_PUBLIC_SITE_URL) ||
64
+ hostFromUrl(process.env.NEXT_PUBLIC_MAIN_SITE_URL);
65
+ if (envHost) return envHost;
66
+ if (typeof window !== "undefined")
67
+ return window.location.hostname.toLowerCase();
68
+ return "";
69
+ };
70
+
71
+ const isExternalUrl = (url: string): boolean => {
72
+ if (!/^https?:\/\//i.test(url)) return false; // relative links are always internal
73
+ try {
74
+ const linkHost = new URL(url).hostname.toLowerCase();
75
+ const currentHost = getCurrentHost();
76
+ if (!currentHost) return true; // host unknown (SSR without env) → treat as external
77
+ return appIdentity(linkHost) !== appIdentity(currentHost);
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
82
+
27
83
  const defaultOptions: Options = {
28
84
  renderMark: {
29
85
  [MARKS.BOLD]: text => <strong className="font-black">{text}</strong>,
@@ -80,12 +136,12 @@ const defaultOptions: Options = {
80
136
 
81
137
  [INLINES.HYPERLINK]: (node, children) => {
82
138
  const url = (node as any)?.data?.uri as string;
83
- const external = /^https?:\/\//.test(url);
139
+ const external = isExternalUrl(url);
84
140
  return (
85
141
  <a
86
142
  href={url}
87
143
  target={external ? "_blank" : undefined}
88
- rel={external ? "noopener noreferrer" : undefined}
144
+ rel={external ? "noopener" : undefined}
89
145
  >
90
146
  {children}
91
147
  </a>
@@ -167,22 +223,26 @@ export function renderContentfulRichText(
167
223
  ...options?.renderNode,
168
224
  // Logic for links based on the isTargetBlank flag
169
225
  [BLOCKS.PARAGRAPH]: (node, children) => (
170
- <div className={[className, "whitespace-pre-line"].filter(Boolean).join(" ")}>
226
+ <div
227
+ className={[className, "whitespace-pre-line"]
228
+ .filter(Boolean)
229
+ .join(" ")}
230
+ >
171
231
  {isEmptyParagraph(node) ? "\n" : children}
172
232
  </div>
173
233
  ),
174
234
  [INLINES.HYPERLINK]: (node, children) => {
175
235
  const url = (node as any)?.data?.uri as string;
176
- const external = /^https?:\/\//.test(url);
236
+ const external = isExternalUrl(url);
177
237
 
178
- // Priority: 1. Manual flag from Contentful, 2. Regex check for external URLs
238
+ // Priority: 1. Manual flag from Contentful, 2. Same-domain URLs are internal
179
239
  const shouldOpenBlank = target ?? external;
180
240
 
181
241
  return (
182
242
  <Link
183
243
  href={url}
184
- target={shouldOpenBlank ? "_blank" : "_self"}
185
- rel={shouldOpenBlank ? "noopener noreferrer" : undefined}
244
+ target={shouldOpenBlank ? "_blank" : undefined}
245
+ rel={shouldOpenBlank ? "noopener" : undefined}
186
246
  variant="default"
187
247
  className={linkClassName}
188
248
  >
@@ -1,3 +1,4 @@
1
+ import { TextDecoder, TextEncoder } from "util";
1
2
  import React from "react";
2
3
  import { CarouselSwipeConfig, useCarouselSwipe } from "./use-carousel-swipe";
3
4
 
@@ -368,6 +369,28 @@ describe("useCarouselSwipe", () => {
368
369
  expect(result.current.containerWidth).toBe(800);
369
370
  });
370
371
 
372
+ it("uses the attached containerRef width when present", () => {
373
+ Object.defineProperty(window, "innerWidth", {
374
+ writable: true,
375
+ value: 1024,
376
+ });
377
+
378
+ const { result } = renderHook(() => useCarouselSwipe(defaultConfig));
379
+ const container = document.createElement("div");
380
+
381
+ Object.defineProperty(container, "offsetWidth", {
382
+ configurable: true,
383
+ value: 640,
384
+ });
385
+
386
+ act(() => {
387
+ result.current.containerRef.current = container as HTMLDivElement;
388
+ window.dispatchEvent(new Event("resize"));
389
+ });
390
+
391
+ expect(result.current.containerWidth).toBe(640);
392
+ });
393
+
371
394
  it("uses custom mobileBreakpoint", () => {
372
395
  Object.defineProperty(window, "innerWidth", {
373
396
  writable: true,
@@ -392,13 +415,10 @@ describe("useCarouselSwipe", () => {
392
415
  });
393
416
 
394
417
  describe("SSR safety (window undefined)", () => {
395
- it("can be created without throwing when window is undefined and uses fallback containerWidth", () => {
418
+ it("can be created without throwing when window is undefined and uses fallback containerWidth", async () => {
396
419
  const origWindow = globalThis.window;
397
420
  // @ts-expect-error - intentionally deleting window to simulate SSR
398
421
  delete globalThis.window;
399
-
400
- // Polyfill TextEncoder/TextDecoder required by react-dom/server in jest
401
- const { TextEncoder, TextDecoder } = require("util");
402
422
  const origTE = globalThis.TextEncoder;
403
423
  const origTD = globalThis.TextDecoder;
404
424
  globalThis.TextEncoder = TextEncoder;
@@ -412,8 +432,8 @@ describe("useCarouselSwipe", () => {
412
432
  }
413
433
 
414
434
  try {
415
- // eslint-disable-next-line @typescript-eslint/no-var-requires
416
- const { renderToString } = require("react-dom/server");
435
+ const { renderToString } = await import("react-dom/server");
436
+
417
437
  // renderToString simulates real SSR: no window, no effects run
418
438
  renderToString(React.createElement(TestComponent));
419
439
 
@@ -24,8 +24,8 @@ export interface Button {
24
24
  preserveQueryParameters: boolean; // non-null in your model
25
25
  badgeIcon?: Asset | null;
26
26
  buttonVariant?: "primary_brand" | "primary_inverse" | "secondary" | null;
27
- buttonIcon?: Asset | null;
28
- buttonIconPosition?: boolean | null; // matches EntryFieldTypes.Boolean in your model
27
+ buttonIconName?: string | null;
28
+ buttonIconPosition?: "left" | "right" | null;
29
29
  clickToOpen?: "accordion" | "modal" | "tab" | null;
30
30
  tabmodalNameToOpen?: string | null;
31
31
  preDefinedFunctionExecution?: "check availability" | null;