@windstream/react-shared-components 0.2.66 → 0.2.68
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 +13 -4
- 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 +3 -3
- 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 +2 -2
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/video-link/contentful-video-embed.test.tsx +44 -1
- package/src/components/video-link/index.test.tsx +117 -1
- package/src/components/video-link/index.tsx +31 -2
- package/src/components/video-link/vimeo-embed.analytics.test.tsx +112 -0
- package/src/components/video-link/vimeo-embed.test.tsx +13 -0
- package/src/components/video-link/vimeo-embed.tsx +2 -1
- package/src/components/video-link/youtube-embed.test.tsx +75 -3
- package/src/components/video-link/youtube-embed.tsx +2 -1
- package/src/components/video-link/youtube-iframe-api-loader.test.ts +108 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +11 -2
- package/src/contentful/blocks/cards/product-card/index.tsx +25 -22
- package/src/contentful/blocks/cards/product-card/types.ts +3 -0
- package/src/contentful/blocks/carousel/helper.tsx +6 -0
- package/src/contentful/blocks/dynamic-form/DynamicForm.stories.tsx +3 -0
- package/src/contentful/blocks/dynamic-form/render.test.tsx +25 -0
- package/src/contentful/blocks/dynamic-form/render.tsx +15 -0
- package/src/contentful/blocks/dynamic-form/types.ts +8 -3
- package/src/contentful/blocks/email-input-block/index.test.tsx +4 -4
- package/src/contentful/blocks/email-input-block/index.tsx +3 -3
|
@@ -30,6 +30,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
30
30
|
planSubtext,
|
|
31
31
|
speed,
|
|
32
32
|
price,
|
|
33
|
+
priceUnit = "/mo",
|
|
33
34
|
description,
|
|
34
35
|
bestValue = false,
|
|
35
36
|
bestValueText = "best value",
|
|
@@ -69,9 +70,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
69
70
|
// stacks keep the standard sizes at every breakpoint.
|
|
70
71
|
const planNameClass = twoColumn ? "label3 md:subheading2" : "subheading2";
|
|
71
72
|
const priceIntegerClass = twoColumn
|
|
72
|
-
? "heading2 md:
|
|
73
|
-
: "
|
|
74
|
-
const priceUnitClass = twoColumn ? "label3
|
|
73
|
+
? "heading2 md:subheading5"
|
|
74
|
+
: "subheading5";
|
|
75
|
+
const priceUnitClass = twoColumn ? "label3" : "label1";
|
|
75
76
|
|
|
76
77
|
return (
|
|
77
78
|
<article className="relative flex h-full w-full max-w-[392px] flex-col">
|
|
@@ -115,15 +116,12 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
115
116
|
)}
|
|
116
117
|
>
|
|
117
118
|
{planName}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)}
|
|
125
|
-
>
|
|
126
|
-
{planSubtext}
|
|
119
|
+
{planSubtext ? (
|
|
120
|
+
<>
|
|
121
|
+
<br />
|
|
122
|
+
{planSubtext}
|
|
123
|
+
</>
|
|
124
|
+
) : null}
|
|
127
125
|
</Text>
|
|
128
126
|
</div>
|
|
129
127
|
<div className="flex flex-col items-start md:items-end">
|
|
@@ -155,14 +153,14 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
155
153
|
isDark ? "text-text-inverse" : "text-text"
|
|
156
154
|
)}
|
|
157
155
|
>
|
|
158
|
-
{price.split(".")[1]}
|
|
156
|
+
{`${price.split(".")[1]}${priceUnit}`}
|
|
159
157
|
</Text>
|
|
160
158
|
</div>
|
|
161
159
|
{priceSuffixBottom ? (
|
|
162
160
|
<Text
|
|
163
161
|
as="span"
|
|
164
162
|
className={cx(
|
|
165
|
-
"
|
|
163
|
+
"micro text-nowrap",
|
|
166
164
|
isDark ? "text-text-inverse" : "text-text"
|
|
167
165
|
)}
|
|
168
166
|
>
|
|
@@ -176,7 +174,8 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
176
174
|
<section data-pc-description>
|
|
177
175
|
<Text
|
|
178
176
|
className={cx(
|
|
179
|
-
"footnote md:body2
|
|
177
|
+
twoColumn ? "footnote md:body2" : "body2",
|
|
178
|
+
"md:h-[56px]",
|
|
180
179
|
isDark ? "text-text-inverse" : "text-text"
|
|
181
180
|
)}
|
|
182
181
|
>
|
|
@@ -189,18 +188,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
189
188
|
<div data-pc-gift className="min-h-[76px]">
|
|
190
189
|
<div
|
|
191
190
|
className={cx(
|
|
192
|
-
"flex-col
|
|
191
|
+
"flex w-full flex-col gap-3 rounded-surface-sm p-2",
|
|
193
192
|
isDark ? "bg-bg-surface-inverse" : "bg-bg-surface-info"
|
|
194
193
|
)}
|
|
195
194
|
>
|
|
196
195
|
{giftBadge?.map((badge: any) => {
|
|
197
196
|
return (
|
|
198
|
-
<div
|
|
197
|
+
<div
|
|
198
|
+
key={badge.title}
|
|
199
|
+
className="flex w-full items-center gap-2"
|
|
200
|
+
>
|
|
199
201
|
<Image
|
|
200
202
|
src={badge.icon}
|
|
201
203
|
alt={badge.title}
|
|
202
|
-
width={
|
|
203
|
-
height={
|
|
204
|
+
width={20}
|
|
205
|
+
height={20}
|
|
204
206
|
/>
|
|
205
207
|
<Text
|
|
206
208
|
as="div"
|
|
@@ -290,20 +292,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
290
292
|
iconClassName="mt-0"
|
|
291
293
|
iconColor={isDark ? "yellow" : "green"}
|
|
292
294
|
listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
|
|
293
|
-
listItemClassName={
|
|
295
|
+
listItemClassName={`body3 ${isDark ? "text-text-inverse" : "text-text"}`}
|
|
294
296
|
/>
|
|
295
297
|
</div>
|
|
296
298
|
{innerBadge?.badgeIcon ? (
|
|
297
|
-
<div className="mt-3 flex items-center gap-
|
|
299
|
+
<div className="mt-3 flex items-center gap-1 text-left">
|
|
298
300
|
<Image
|
|
299
301
|
src={innerBadge.badgeIcon}
|
|
300
302
|
alt="Inner Badge"
|
|
301
303
|
width={24}
|
|
302
304
|
height={24}
|
|
305
|
+
className="ml-1 h-auto pr-2"
|
|
303
306
|
/>
|
|
304
307
|
<span
|
|
305
308
|
className={cx(
|
|
306
|
-
"
|
|
309
|
+
"body3",
|
|
307
310
|
isDark ? "text-text-inverse" : "text-text"
|
|
308
311
|
)}
|
|
309
312
|
>
|
|
@@ -10,6 +10,9 @@ export interface ProductCardProps {
|
|
|
10
10
|
techType?: string;
|
|
11
11
|
ismaxSpeed?: boolean;
|
|
12
12
|
price: string;
|
|
13
|
+
/** The price unit rendered after the cents, e.g. "/mo" or "/mo*". Defaults
|
|
14
|
+
* to "/mo". Lets Contentful control a trailing disclaimer star. */
|
|
15
|
+
priceUnit?: string;
|
|
13
16
|
priceSuffixBottom?: string;
|
|
14
17
|
description: string;
|
|
15
18
|
bestValue?: boolean;
|
|
@@ -263,6 +263,11 @@ function ProductCardPanel({
|
|
|
263
263
|
const planSubtext = planContent?.[1] || "";
|
|
264
264
|
const priceCents = item?.priceSuffix?.split("/")?.[0] || "00";
|
|
265
265
|
const formattedPrice = `${item?.price || "0"}.${priceCents}`;
|
|
266
|
+
// Preserve the full unit after the cents (e.g. "/mo*") so a Contentful
|
|
267
|
+
// disclaimer star renders; fall back to "/mo" when no unit is present.
|
|
268
|
+
const suffixParts = item?.priceSuffix?.split("/") || [];
|
|
269
|
+
const priceUnit =
|
|
270
|
+
suffixParts.length > 1 ? `/${suffixParts.slice(1).join("/")}` : "/mo";
|
|
266
271
|
const features = item?.benefits?.items || [];
|
|
267
272
|
const bestValue = item?.highlighted || false;
|
|
268
273
|
const rawBadge = item?.giftRewards?.list?.items || [];
|
|
@@ -308,6 +313,7 @@ function ProductCardPanel({
|
|
|
308
313
|
techType={item?.techType || ""}
|
|
309
314
|
ismaxSpeed={item?.ismaxSpeed || ""}
|
|
310
315
|
price={formattedPrice}
|
|
316
|
+
priceUnit={priceUnit}
|
|
311
317
|
description={item?.productCardDescription || ""}
|
|
312
318
|
giftBadge={rawBadge}
|
|
313
319
|
innerBadge={innerBadge}
|
|
@@ -57,6 +57,31 @@ describe("dynamicRenderingWithGroupings", () => {
|
|
|
57
57
|
expect(screen.getByText("Custom Content")).toBeInTheDocument();
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
it("renders a full-width divider by default", () => {
|
|
61
|
+
const { container } = renderGroups([{ itemType: "divider" }]);
|
|
62
|
+
const divider = container.querySelector<HTMLElement>('[role="separator"]');
|
|
63
|
+
expect(divider).toBeInTheDocument();
|
|
64
|
+
expect(divider).toHaveClass("w-full");
|
|
65
|
+
expect(divider?.style.height).toBe("1px");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renders a half-width divider with a custom thickness", () => {
|
|
69
|
+
const { container } = renderGroups([
|
|
70
|
+
{ itemType: "divider", dividerLength: false, dividerThickness: 4 },
|
|
71
|
+
]);
|
|
72
|
+
const divider = container.querySelector<HTMLElement>('[role="separator"]');
|
|
73
|
+
expect(divider).toHaveClass("w-1/2");
|
|
74
|
+
expect(divider?.style.height).toBe("4px");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("clamps a divider thickness below 1 to 1px", () => {
|
|
78
|
+
const { container } = renderGroups([
|
|
79
|
+
{ itemType: "divider", dividerThickness: 0 },
|
|
80
|
+
]);
|
|
81
|
+
const divider = container.querySelector<HTMLElement>('[role="separator"]');
|
|
82
|
+
expect(divider?.style.height).toBe("1px");
|
|
83
|
+
});
|
|
84
|
+
|
|
60
85
|
it("renders a conditional item", () => {
|
|
61
86
|
renderGroups([
|
|
62
87
|
{
|
|
@@ -7,6 +7,7 @@ import FormFieldElement from "./form-field";
|
|
|
7
7
|
import { DynamicFormItem } from "./types";
|
|
8
8
|
import { groupFieldsForLayout } from "./utils";
|
|
9
9
|
|
|
10
|
+
import { Divider } from "@shared/components/divider";
|
|
10
11
|
import { Text } from "@shared/components/text";
|
|
11
12
|
|
|
12
13
|
type FormValues = Record<string, any>;
|
|
@@ -46,6 +47,20 @@ const renderItem = (
|
|
|
46
47
|
/>
|
|
47
48
|
);
|
|
48
49
|
}
|
|
50
|
+
if (item.itemType === "divider") {
|
|
51
|
+
const strokeWidth = Math.max(item.dividerThickness || 1, 1);
|
|
52
|
+
const widthClass = item.dividerLength === false ? "w-1/2" : "w-full";
|
|
53
|
+
return (
|
|
54
|
+
<div key={key} className="flex w-full justify-center">
|
|
55
|
+
<Divider
|
|
56
|
+
role="separator"
|
|
57
|
+
aria-label="Content divider"
|
|
58
|
+
className={widthClass}
|
|
59
|
+
style={{ height: `${strokeWidth}px` }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
49
64
|
return <React.Fragment key={key}>{item.content}</React.Fragment>;
|
|
50
65
|
};
|
|
51
66
|
|
|
@@ -69,14 +69,19 @@ export interface ConditionalFieldConfig {
|
|
|
69
69
|
grouped?: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export interface DynamicFormDividerConfig {
|
|
73
|
+
dividerLength?: boolean;
|
|
74
|
+
dividerThickness?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
/**
|
|
73
|
-
* A flat list item rendered by the form
|
|
74
|
-
*
|
|
75
|
-
* the app rendered itself and passes through).
|
|
78
|
+
* A flat list item rendered by the form: a form field, conditional field,
|
|
79
|
+
* divider, or custom React node.
|
|
76
80
|
*/
|
|
77
81
|
export type DynamicFormItem =
|
|
78
82
|
| ({ itemType: "formField" } & DynamicFormFieldConfig)
|
|
79
83
|
| ({ itemType: "conditional" } & ConditionalFieldConfig)
|
|
84
|
+
| ({ itemType: "divider"; fieldId?: string } & DynamicFormDividerConfig)
|
|
80
85
|
| { itemType: "custom"; fieldId?: string; content: React.ReactNode };
|
|
81
86
|
|
|
82
87
|
export interface DynamicFormPayload {
|
|
@@ -32,24 +32,24 @@ describe("EmailInputBlock", () => {
|
|
|
32
32
|
render(<EmailInputBlock {...defaultProps} title="Join us" />);
|
|
33
33
|
const title = screen.getByText("Join us");
|
|
34
34
|
expect(title).toBeInTheDocument();
|
|
35
|
-
expect(title.tagName).toBe("
|
|
35
|
+
expect(title.tagName).toBe("H2");
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("does not render title when not provided", () => {
|
|
39
39
|
const { container } = render(<EmailInputBlock {...defaultProps} />);
|
|
40
|
-
expect(container.querySelector("
|
|
40
|
+
expect(container.querySelector("h2")).not.toBeInTheDocument();
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("renders subTitle when provided", () => {
|
|
44
44
|
render(<EmailInputBlock {...defaultProps} subTitle="Stay updated" />);
|
|
45
45
|
const subtitle = screen.getByText("Stay updated");
|
|
46
46
|
expect(subtitle).toBeInTheDocument();
|
|
47
|
-
expect(subtitle.tagName).toBe("
|
|
47
|
+
expect(subtitle.tagName).toBe("H3");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
it("does not render subTitle when not provided", () => {
|
|
51
51
|
const { container } = render(<EmailInputBlock {...defaultProps} />);
|
|
52
|
-
expect(container.querySelector("
|
|
52
|
+
expect(container.querySelector("h3")).not.toBeInTheDocument();
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it("renders emailInputDescription when provided", () => {
|
|
@@ -111,12 +111,12 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
111
111
|
<div className="w-full">
|
|
112
112
|
<div className="lg:w-[548px]">
|
|
113
113
|
{title && (
|
|
114
|
-
<
|
|
114
|
+
<h2 className={`mb-3 text-[2.25rem] leading-[2.5rem] font-black md:text-heading1 md:font-black ${textColor}`}>{title}</h2>
|
|
115
115
|
)}
|
|
116
116
|
{subTitle && (
|
|
117
|
-
<
|
|
117
|
+
<h3 className={`mb-3 text-heading1 font-black ${textColor}`}>
|
|
118
118
|
{subTitle}
|
|
119
|
-
</
|
|
119
|
+
</h3>
|
|
120
120
|
)}
|
|
121
121
|
|
|
122
122
|
{emailInputDescription && (
|