@windstream/react-shared-components 0.2.71 → 0.2.72
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.
- package/dist/contentful/index.d.ts +11 -1
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/select-plan-button/index.tsx +2 -2
- package/src/components/select-plan-button/types.ts +1 -1
- package/src/components/video-link/vimeo-embed.test.tsx +14 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +48 -9
- package/src/contentful/blocks/cards/product-card/index.tsx +3 -1
- package/src/contentful/blocks/cards/product-card/types.ts +2 -1
- package/src/contentful/blocks/carousel/helper.test.tsx +43 -2
- package/src/contentful/blocks/carousel/helper.tsx +26 -3
- package/src/contentful/blocks/carousel/index.test.tsx +72 -2
- package/src/contentful/blocks/carousel/index.tsx +10 -1
- package/src/contentful/blocks/carousel/types.ts +12 -0
package/package.json
CHANGED
|
@@ -31,9 +31,9 @@ export const SelectPlanButton: React.FC<SelectPlanButtonProps> = ({
|
|
|
31
31
|
},
|
|
32
32
|
cta: cta,
|
|
33
33
|
});
|
|
34
|
-
const handleSelect = () => {
|
|
34
|
+
const handleSelect = (event: React.MouseEvent<HTMLElement>) => {
|
|
35
35
|
setOpen(true);
|
|
36
|
-
onSelect();
|
|
36
|
+
onSelect(event);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
return (
|
|
@@ -5,7 +5,7 @@ import { ButtonProps } from "@shared/contentful/blocks/button/types";
|
|
|
5
5
|
import { CheckPlansProps } from "@shared/types/micro-components";
|
|
6
6
|
|
|
7
7
|
export type SelectPlanButtonProps = {
|
|
8
|
-
onSelect: () => void;
|
|
8
|
+
onSelect: (event: React.MouseEvent<HTMLElement>) => void;
|
|
9
9
|
speed: string;
|
|
10
10
|
techType?: string;
|
|
11
11
|
isMax?: boolean;
|
|
@@ -7,6 +7,20 @@ jest.mock("@shared/utils", () => ({
|
|
|
7
7
|
cx: (...args: any[]) => args.filter(Boolean).join(" "),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
|
+
// Mock the heavy real @vimeo/player. These tests only assert on the rendered
|
|
11
|
+
// iframe, and instantiating the real Player leaves async postMessage work and
|
|
12
|
+
// timers running after the test, which can leak into sibling test files in the
|
|
13
|
+
// same Jest worker and make them flaky. A lightweight mock keeps the suite
|
|
14
|
+
// isolated and deterministic.
|
|
15
|
+
jest.mock("@vimeo/player", () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
default: jest.fn().mockImplementation(() => ({
|
|
18
|
+
on: jest.fn(),
|
|
19
|
+
off: jest.fn(),
|
|
20
|
+
destroy: jest.fn(() => Promise.resolve()),
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
23
|
+
|
|
10
24
|
describe("VimeoEmbed", () => {
|
|
11
25
|
describe("Rendering", () => {
|
|
12
26
|
it("renders iframe with correct src for standard vimeo link", () => {
|
|
@@ -86,16 +86,16 @@ describe("ProductCard", () => {
|
|
|
86
86
|
|
|
87
87
|
it("renders plan name", () => {
|
|
88
88
|
render(<ProductCard {...defaultProps} />);
|
|
89
|
-
expect(
|
|
90
|
-
|
|
91
|
-
)
|
|
89
|
+
expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
|
|
90
|
+
"Kinetic 500"
|
|
91
|
+
);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
it("renders plan subtext", () => {
|
|
95
95
|
render(<ProductCard {...defaultProps} />);
|
|
96
|
-
expect(
|
|
97
|
-
|
|
98
|
-
)
|
|
96
|
+
expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
|
|
97
|
+
"Up to 500 Mbps"
|
|
98
|
+
);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
it("renders price dollar sign", () => {
|
|
@@ -126,6 +126,45 @@ describe("ProductCard", () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
describe("Business theme", () => {
|
|
130
|
+
it("uses the semantic brand token for the plan name and lowercases it by default", () => {
|
|
131
|
+
render(<ProductCard {...defaultProps} />);
|
|
132
|
+
const heading = screen.getByRole("heading", { level: 3 });
|
|
133
|
+
// Color is driven by the themeable token, not a hardcoded hex.
|
|
134
|
+
expect(heading.className).toContain("text-text-brand");
|
|
135
|
+
expect(heading.className).toContain("lowercase");
|
|
136
|
+
expect(heading.className).not.toContain("[#0393BA]");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("keeps the authored casing in business mode (color stays theme-driven)", () => {
|
|
140
|
+
render(<ProductCard {...defaultProps} enableBusinessSettings={true} />);
|
|
141
|
+
const heading = screen.getByRole("heading", { level: 3 });
|
|
142
|
+
// Still the semantic brand token — the business teal comes from the
|
|
143
|
+
// data-theme scope, not a card-level class.
|
|
144
|
+
expect(heading.className).toContain("text-text-brand");
|
|
145
|
+
expect(heading.className).not.toContain("lowercase");
|
|
146
|
+
expect(heading.className).not.toContain("[#0393BA]");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("never hardcodes a hex accent on the plan name", () => {
|
|
150
|
+
render(<ProductCard {...defaultProps} enableBusinessSettings={true} />);
|
|
151
|
+
const heading = screen.getByRole("heading", { level: 3 });
|
|
152
|
+
expect(heading.className).not.toMatch(/#[0-9a-fA-F]{6}/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("keeps the dark (highlighted) accent regardless of business mode", () => {
|
|
156
|
+
render(
|
|
157
|
+
<ProductCard
|
|
158
|
+
{...defaultProps}
|
|
159
|
+
theme="dark"
|
|
160
|
+
enableBusinessSettings={true}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
const heading = screen.getByRole("heading", { level: 3 });
|
|
164
|
+
expect(heading.className).toContain("text-text-accent-on-bg-inverse");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
129
168
|
describe("Price Suffix", () => {
|
|
130
169
|
it("renders priceSuffixBottom when provided", () => {
|
|
131
170
|
render(
|
|
@@ -250,9 +289,9 @@ describe("ProductCard", () => {
|
|
|
250
289
|
const dollar = screen.getByText("$100");
|
|
251
290
|
expect(dollar).toHaveClass("label3");
|
|
252
291
|
// The surrounding text keeps the parent body3 weight (not label3).
|
|
253
|
-
expect(
|
|
254
|
-
|
|
255
|
-
)
|
|
292
|
+
expect(screen.getByText(/prepaid Mastercard®/)).not.toHaveClass(
|
|
293
|
+
"label3"
|
|
294
|
+
);
|
|
256
295
|
});
|
|
257
296
|
});
|
|
258
297
|
|
|
@@ -54,6 +54,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
54
54
|
priceSuffixBottom,
|
|
55
55
|
hasAnyBadge = false,
|
|
56
56
|
twoColumn = false,
|
|
57
|
+
enableBusinessSettings = false,
|
|
57
58
|
}) => {
|
|
58
59
|
const [internalExpanded, setInternalExpanded] = useState(false);
|
|
59
60
|
// Use parent state if provided, otherwise use internal state
|
|
@@ -116,7 +117,8 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
116
117
|
as="h3"
|
|
117
118
|
className={cx(
|
|
118
119
|
planNameClass,
|
|
119
|
-
"whitespace-normal
|
|
120
|
+
"whitespace-normal",
|
|
121
|
+
!enableBusinessSettings && "lowercase",
|
|
120
122
|
isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"
|
|
121
123
|
)}
|
|
122
124
|
>
|
|
@@ -26,7 +26,7 @@ export interface ProductCardProps {
|
|
|
26
26
|
onToggleExpand?: () => void;
|
|
27
27
|
ctaText?: string;
|
|
28
28
|
cta?: ButtonProps;
|
|
29
|
-
onCtaClick: () => void;
|
|
29
|
+
onCtaClick: (event: React.MouseEvent<HTMLElement>) => void;
|
|
30
30
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
31
31
|
/** True when any card in the set has a best-value badge, so non-badge cards
|
|
32
32
|
* reserve the banner height and stay vertically aligned. */
|
|
@@ -35,4 +35,5 @@ export interface ProductCardProps {
|
|
|
35
35
|
* compact mobile typography below `md`. One-column stacks keep the standard
|
|
36
36
|
* sizes at every breakpoint. */
|
|
37
37
|
twoColumn?: boolean;
|
|
38
|
+
enableBusinessSettings?: boolean;
|
|
38
39
|
}
|
|
@@ -31,12 +31,14 @@ jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
|
|
|
31
31
|
onCtaClick,
|
|
32
32
|
onToggleExpand,
|
|
33
33
|
isExpanded,
|
|
34
|
+
enableBusinessSettings,
|
|
34
35
|
}: any) => (
|
|
35
36
|
<div
|
|
36
37
|
data-testid="product-card"
|
|
37
38
|
data-plan={planName}
|
|
38
39
|
data-speed={speed}
|
|
39
40
|
data-expanded={isExpanded}
|
|
41
|
+
data-business={enableBusinessSettings ? "true" : "false"}
|
|
40
42
|
>
|
|
41
43
|
{planName}
|
|
42
44
|
<button onClick={onCtaClick} data-testid="product-cta">
|
|
@@ -329,15 +331,54 @@ describe("ProductCardCarousel", () => {
|
|
|
329
331
|
expect(toggleButtons[0]).toBeInTheDocument();
|
|
330
332
|
});
|
|
331
333
|
|
|
332
|
-
it("
|
|
334
|
+
it("fires onProductCardCtaClick with the product item and click event", () => {
|
|
335
|
+
const onProductCardCtaClick = jest.fn();
|
|
336
|
+
const fields = createProductFields([sampleProduct]);
|
|
337
|
+
render(
|
|
338
|
+
<ProductCardCarousel
|
|
339
|
+
fields={fields}
|
|
340
|
+
onProductCardCtaClick={onProductCardCtaClick}
|
|
341
|
+
/>
|
|
342
|
+
);
|
|
343
|
+
const ctaButton = screen.getByTestId("product-cta");
|
|
344
|
+
fireEvent.click(ctaButton);
|
|
345
|
+
expect(onProductCardCtaClick).toHaveBeenCalledTimes(1);
|
|
346
|
+
expect(onProductCardCtaClick.mock.calls[0][0]).toEqual(
|
|
347
|
+
expect.objectContaining({ sys: { id: "prod-1" } })
|
|
348
|
+
);
|
|
349
|
+
// Second argument is the originating click event.
|
|
350
|
+
expect(onProductCardCtaClick.mock.calls[0][1]).toBeDefined();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("handles CTA click when no onProductCardCtaClick is provided", () => {
|
|
333
354
|
const fields = createProductFields([sampleProduct]);
|
|
334
355
|
render(<ProductCardCarousel fields={fields} />);
|
|
335
356
|
const ctaButton = screen.getByTestId("product-cta");
|
|
336
357
|
fireEvent.click(ctaButton);
|
|
337
|
-
//
|
|
358
|
+
// No handler wired; click is a no-op and must not throw.
|
|
338
359
|
expect(ctaButton).toBeInTheDocument();
|
|
339
360
|
});
|
|
340
361
|
|
|
362
|
+
it("defaults product cards to the non-business theme", () => {
|
|
363
|
+
const fields = createProductFields([sampleProduct]);
|
|
364
|
+
render(<ProductCardCarousel fields={fields} />);
|
|
365
|
+
expect(screen.getAllByTestId("product-card")[0]).toHaveAttribute(
|
|
366
|
+
"data-business",
|
|
367
|
+
"false"
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("forwards enableBusinessSettings to the product cards", () => {
|
|
372
|
+
const fields = createProductFields([sampleProduct]);
|
|
373
|
+
render(
|
|
374
|
+
<ProductCardCarousel fields={fields} enableBusinessSettings={true} />
|
|
375
|
+
);
|
|
376
|
+
expect(screen.getAllByTestId("product-card")[0]).toHaveAttribute(
|
|
377
|
+
"data-business",
|
|
378
|
+
"true"
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
341
382
|
it("handles toggle expand for mobile cards in carousel mode", () => {
|
|
342
383
|
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
343
384
|
...sampleProduct,
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
CarouselWithProductCards,
|
|
4
4
|
CarouselWithTestimonialCards,
|
|
5
|
+
ProductCardFields,
|
|
5
6
|
TabSwitchProps,
|
|
6
7
|
} from "./types";
|
|
7
8
|
|
|
@@ -17,11 +18,18 @@ function ProductCardPanel({
|
|
|
17
18
|
fields,
|
|
18
19
|
renderCheckPlans,
|
|
19
20
|
isVisible = true,
|
|
21
|
+
onProductCardCtaClick,
|
|
22
|
+
enableBusinessSettings = false,
|
|
20
23
|
}: {
|
|
21
24
|
fields: CarouselWithProductCards;
|
|
22
25
|
onModalButtonClick?: (id?: string) => void;
|
|
23
26
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
24
27
|
isVisible?: boolean;
|
|
28
|
+
onProductCardCtaClick?: (
|
|
29
|
+
item: ProductCardFields,
|
|
30
|
+
event: React.MouseEvent<HTMLElement>
|
|
31
|
+
) => void;
|
|
32
|
+
enableBusinessSettings?: boolean;
|
|
25
33
|
}) {
|
|
26
34
|
const itemsExpanded = fields?.items?.items?.[0]?.benefitsExpanded || false;
|
|
27
35
|
const [desktopExpanded, setDesktopExpanded] = useState(itemsExpanded);
|
|
@@ -284,8 +292,8 @@ function ProductCardPanel({
|
|
|
284
292
|
setDesktopExpanded(prev => !prev);
|
|
285
293
|
};
|
|
286
294
|
|
|
287
|
-
const handleCtaClick = () => {
|
|
288
|
-
|
|
295
|
+
const handleCtaClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
296
|
+
onProductCardCtaClick?.(item as ProductCardFields, event);
|
|
289
297
|
};
|
|
290
298
|
|
|
291
299
|
return (
|
|
@@ -331,6 +339,7 @@ function ProductCardPanel({
|
|
|
331
339
|
priceSuffixBottom={item?.priceSuffixBottom}
|
|
332
340
|
hasAnyBadge={hasAnyBadge}
|
|
333
341
|
twoColumn={twoColumn}
|
|
342
|
+
enableBusinessSettings={enableBusinessSettings}
|
|
334
343
|
/>
|
|
335
344
|
</div>
|
|
336
345
|
);
|
|
@@ -425,12 +434,19 @@ export function ProductCardCarousel({
|
|
|
425
434
|
renderCheckPlans,
|
|
426
435
|
activeTab,
|
|
427
436
|
tabs,
|
|
437
|
+
onProductCardCtaClick,
|
|
438
|
+
enableBusinessSettings = false,
|
|
428
439
|
}: {
|
|
429
440
|
fields: CarouselWithProductCards;
|
|
430
441
|
onModalButtonClick?: (id?: string) => void;
|
|
431
442
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
432
443
|
activeTab?: string;
|
|
433
444
|
tabs?: string[];
|
|
445
|
+
onProductCardCtaClick?: (
|
|
446
|
+
item: ProductCardFields,
|
|
447
|
+
event: React.MouseEvent<HTMLElement>
|
|
448
|
+
) => void;
|
|
449
|
+
enableBusinessSettings?: boolean;
|
|
434
450
|
}) {
|
|
435
451
|
const allItems = fields?.items?.items || [];
|
|
436
452
|
|
|
@@ -456,6 +472,8 @@ export function ProductCardCarousel({
|
|
|
456
472
|
fields={tabFields as CarouselWithProductCards}
|
|
457
473
|
renderCheckPlans={renderCheckPlans}
|
|
458
474
|
isVisible={tab === activeTab}
|
|
475
|
+
onProductCardCtaClick={onProductCardCtaClick}
|
|
476
|
+
enableBusinessSettings={enableBusinessSettings}
|
|
459
477
|
/>
|
|
460
478
|
</div>
|
|
461
479
|
);
|
|
@@ -465,7 +483,12 @@ export function ProductCardCarousel({
|
|
|
465
483
|
}
|
|
466
484
|
|
|
467
485
|
return (
|
|
468
|
-
<ProductCardPanel
|
|
486
|
+
<ProductCardPanel
|
|
487
|
+
fields={fields}
|
|
488
|
+
renderCheckPlans={renderCheckPlans}
|
|
489
|
+
onProductCardCtaClick={onProductCardCtaClick}
|
|
490
|
+
enableBusinessSettings={enableBusinessSettings}
|
|
491
|
+
/>
|
|
469
492
|
);
|
|
470
493
|
}
|
|
471
494
|
|
|
@@ -8,8 +8,16 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
8
8
|
|
|
9
9
|
// Mock helper components
|
|
10
10
|
jest.mock("./helper", () => ({
|
|
11
|
-
ProductCardCarousel: ({ fields }: any) => (
|
|
12
|
-
<div data-testid="product-card-carousel">
|
|
11
|
+
ProductCardCarousel: ({ fields, onProductCardCtaClick }: any) => (
|
|
12
|
+
<div data-testid="product-card-carousel">
|
|
13
|
+
{fields?.title}
|
|
14
|
+
<button
|
|
15
|
+
data-testid="mock-cta"
|
|
16
|
+
onClick={e => onProductCardCtaClick?.({ sys: { id: "prod-1" } }, e)}
|
|
17
|
+
>
|
|
18
|
+
cta
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
13
21
|
),
|
|
14
22
|
TestimonialCarousel: ({ fields }: any) => (
|
|
15
23
|
<div data-testid="testimonial-carousel">{fields?.title}</div>
|
|
@@ -200,6 +208,68 @@ describe("Carousel", () => {
|
|
|
200
208
|
});
|
|
201
209
|
});
|
|
202
210
|
|
|
211
|
+
describe("analytics", () => {
|
|
212
|
+
it("forwards onProductCardCtaClick with the item and click event", () => {
|
|
213
|
+
const onProductCardCtaClick = jest.fn();
|
|
214
|
+
render(
|
|
215
|
+
<Carousel
|
|
216
|
+
{...defaultProps}
|
|
217
|
+
hasProductCards={true}
|
|
218
|
+
onProductCardCtaClick={onProductCardCtaClick}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
fireEvent.click(screen.getByTestId("mock-cta"));
|
|
222
|
+
expect(onProductCardCtaClick).toHaveBeenCalledTimes(1);
|
|
223
|
+
expect(onProductCardCtaClick.mock.calls[0][0]).toEqual({
|
|
224
|
+
sys: { id: "prod-1" },
|
|
225
|
+
});
|
|
226
|
+
// Second argument is the originating click event (needed by consumers to
|
|
227
|
+
// dispatch their own click analytics from the clicked element).
|
|
228
|
+
expect(onProductCardCtaClick.mock.calls[0][1]).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("attaches productCardsRef to the product-card container", () => {
|
|
232
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
233
|
+
render(
|
|
234
|
+
<Carousel
|
|
235
|
+
{...defaultProps}
|
|
236
|
+
hasProductCards={true}
|
|
237
|
+
productCardsRef={ref}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
241
|
+
expect(ref.current).toContainElement(
|
|
242
|
+
screen.getByTestId("product-card-carousel")
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("business theme", () => {
|
|
248
|
+
it("does not scope the business theme by default", () => {
|
|
249
|
+
const { container } = render(
|
|
250
|
+
<Carousel {...defaultProps} hasProductCards={true} />
|
|
251
|
+
);
|
|
252
|
+
expect(
|
|
253
|
+
container.querySelector('[data-theme="business-rebrand"]')
|
|
254
|
+
).not.toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("scopes the product cards to the business-rebrand theme when enabled", () => {
|
|
258
|
+
const { container } = render(
|
|
259
|
+
<Carousel
|
|
260
|
+
{...defaultProps}
|
|
261
|
+
hasProductCards={true}
|
|
262
|
+
enableBusinessSettings={true}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
const scope = container.querySelector('[data-theme="business-rebrand"]');
|
|
266
|
+
expect(scope).toBeInTheDocument();
|
|
267
|
+
expect(scope).toContainElement(
|
|
268
|
+
screen.getByTestId("product-card-carousel")
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
203
273
|
describe("testimonial cards", () => {
|
|
204
274
|
it("renders TestimonialCarousel when hasTestimonialCards is true", () => {
|
|
205
275
|
render(<Carousel {...defaultProps} hasTestimonialCards={true} />);
|
|
@@ -22,6 +22,9 @@ export const Carousel: React.FC<CarouselProps> = ({
|
|
|
22
22
|
renderCheckPlans,
|
|
23
23
|
testimonialAutoScroll = true,
|
|
24
24
|
showSwitch = false,
|
|
25
|
+
productCardsRef,
|
|
26
|
+
onProductCardCtaClick,
|
|
27
|
+
enableBusinessSettings = false,
|
|
25
28
|
}) => {
|
|
26
29
|
const baseClass = hasProductCards
|
|
27
30
|
? "px-3 py-9 md:py-20"
|
|
@@ -45,7 +48,11 @@ export const Carousel: React.FC<CarouselProps> = ({
|
|
|
45
48
|
)}
|
|
46
49
|
</div>
|
|
47
50
|
{hasProductCards && (
|
|
48
|
-
<div
|
|
51
|
+
<div
|
|
52
|
+
ref={productCardsRef}
|
|
53
|
+
data-theme={enableBusinessSettings ? "business-rebrand" : undefined}
|
|
54
|
+
className="flex flex-col gap-8"
|
|
55
|
+
>
|
|
49
56
|
{showSwitch && hasProductCards && tabs.length > 1 && (
|
|
50
57
|
<TabSwitch
|
|
51
58
|
tabs={tabs}
|
|
@@ -60,6 +67,8 @@ export const Carousel: React.FC<CarouselProps> = ({
|
|
|
60
67
|
fields={fields as CarouselWithProductCards}
|
|
61
68
|
activeTab={activeTab}
|
|
62
69
|
tabs={tabs}
|
|
70
|
+
onProductCardCtaClick={onProductCardCtaClick}
|
|
71
|
+
enableBusinessSettings={enableBusinessSettings}
|
|
63
72
|
/>
|
|
64
73
|
)}
|
|
65
74
|
</div>
|
|
@@ -136,6 +136,18 @@ export type CarouselProps = {
|
|
|
136
136
|
testimonialAutoScroll?: boolean;
|
|
137
137
|
onModalButtonClick?: (id?: string) => void;
|
|
138
138
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
139
|
+
/** Ref attached to the product-card ("speeds list") container so consumers
|
|
140
|
+
* can observe its scroll visibility (e.g. to fire their own view-tracking
|
|
141
|
+
* analytics). Only product-card carousels expose a measurable element. */
|
|
142
|
+
productCardsRef?: React.Ref<HTMLDivElement>;
|
|
143
|
+
/** Fired when a product card's CTA ("Select plan") is clicked, with the
|
|
144
|
+
* Contentful product-card entry and the originating click event. Consumers
|
|
145
|
+
* map this onto their click analytics (e.g. Segment "Site Clicked"). */
|
|
146
|
+
onProductCardCtaClick?: (
|
|
147
|
+
item: ProductCardFields,
|
|
148
|
+
event: React.MouseEvent<HTMLElement>
|
|
149
|
+
) => void;
|
|
150
|
+
enableBusinessSettings?: boolean;
|
|
139
151
|
};
|
|
140
152
|
|
|
141
153
|
export interface TabSwitchProps {
|