@windstream/react-shared-components 0.2.89 → 0.2.91
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 +14 -0
- package/dist/contentful/index.esm.js +2 -2
- 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 +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +55 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +15 -2
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +5 -0
- package/src/contentful/blocks/award-marquee/index.test.tsx +75 -0
- package/src/contentful/blocks/award-marquee/index.tsx +40 -19
- package/src/contentful/blocks/award-marquee/types.ts +6 -0
- package/src/contentful/blocks/callout/{Callout.stories.tsx → callout.stories.tsx} +1 -1
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +63 -3
- package/src/contentful/blocks/cart-retention-banner/index.tsx +21 -8
- package/src/contentful/blocks/cart-retention-banner/types.ts +4 -0
- package/src/contentful/blocks/floating-banner/index.test.tsx +54 -2
- package/src/contentful/blocks/floating-banner/index.tsx +13 -2
- package/src/contentful/blocks/floating-banner/types.ts +6 -0
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +176 -18
- package/src/contentful/blocks/image-promo-bar/index.tsx +45 -27
- package/src/contentful/blocks/image-promo-bar/types.ts +19 -1
- package/src/contentful/blocks/navigation/{Navigation.stories.tsx → navigation.stories.tsx} +1 -1
- package/src/contentful/blocks/primary-hero/index.tsx +1 -1
- package/tailwind.config.cjs +1 -1
|
@@ -15,6 +15,81 @@ const buildItems = (count: number): AwardMarqueeItem[] =>
|
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
17
|
describe("AwardMarquee", () => {
|
|
18
|
+
it("tags block heading/footer and per-award title/description", () => {
|
|
19
|
+
render(
|
|
20
|
+
<AwardMarquee
|
|
21
|
+
heading="Heading"
|
|
22
|
+
footerText="Footer"
|
|
23
|
+
items={[
|
|
24
|
+
{
|
|
25
|
+
iconUrl: "/award.png",
|
|
26
|
+
title: "Award title",
|
|
27
|
+
description: "Award description",
|
|
28
|
+
fieldProps: {
|
|
29
|
+
title: {
|
|
30
|
+
"data-contentful-entry-id": "award-1",
|
|
31
|
+
"data-contentful-field-id": "title",
|
|
32
|
+
"data-contentful-locale": "en-US",
|
|
33
|
+
},
|
|
34
|
+
description: {
|
|
35
|
+
"data-contentful-entry-id": "award-1",
|
|
36
|
+
"data-contentful-field-id": "description",
|
|
37
|
+
"data-contentful-locale": "en-US",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
]}
|
|
42
|
+
fieldProps={{
|
|
43
|
+
heading: { "data-contentful-field-id": "heading" },
|
|
44
|
+
footerText: { "data-contentful-field-id": "footerText" },
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
expect(screen.getByText("Heading")).toHaveAttribute(
|
|
49
|
+
"data-contentful-field-id",
|
|
50
|
+
"heading"
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByText("Footer")).toHaveAttribute(
|
|
53
|
+
"data-contentful-field-id",
|
|
54
|
+
"footerText"
|
|
55
|
+
);
|
|
56
|
+
expect(screen.getByText("Award title")).toHaveAttribute(
|
|
57
|
+
"data-contentful-field-id",
|
|
58
|
+
"title"
|
|
59
|
+
);
|
|
60
|
+
expect(screen.getByText("Award description")).toHaveAttribute(
|
|
61
|
+
"data-contentful-field-id",
|
|
62
|
+
"description"
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not duplicate item field tags in the animated marquee loop", () => {
|
|
67
|
+
const { container } = render(
|
|
68
|
+
<AwardMarquee
|
|
69
|
+
scroll={true}
|
|
70
|
+
items={buildItems(5).map((item, index) => ({
|
|
71
|
+
...item,
|
|
72
|
+
fieldProps: {
|
|
73
|
+
title: {
|
|
74
|
+
"data-contentful-entry-id": `award-${index}`,
|
|
75
|
+
"data-contentful-field-id": "title",
|
|
76
|
+
},
|
|
77
|
+
description: {
|
|
78
|
+
"data-contentful-entry-id": `award-${index}`,
|
|
79
|
+
"data-contentful-field-id": "description",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}))}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
expect(
|
|
86
|
+
container.querySelectorAll('[data-contentful-field-id="title"]')
|
|
87
|
+
).toHaveLength(5);
|
|
88
|
+
expect(
|
|
89
|
+
container.querySelectorAll('[data-contentful-field-id="description"]')
|
|
90
|
+
).toHaveLength(5);
|
|
91
|
+
});
|
|
92
|
+
|
|
18
93
|
it.each([
|
|
19
94
|
{ scroll: false, count: 2 },
|
|
20
95
|
{ scroll: true, count: 5 },
|
|
@@ -21,18 +21,7 @@ const BACKGROUND_CLASS: Record<AwardMarqueeBackground, string> = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const renderAwardItem = (
|
|
24
|
-
|
|
25
|
-
iconUrl,
|
|
26
|
-
iconAlt,
|
|
27
|
-
iconWidth,
|
|
28
|
-
iconHeight,
|
|
29
|
-
mobileIconUrl,
|
|
30
|
-
mobileIconAlt,
|
|
31
|
-
mobileIconWidth,
|
|
32
|
-
mobileIconHeight,
|
|
33
|
-
title,
|
|
34
|
-
description,
|
|
35
|
-
}: AwardMarqueeItem,
|
|
24
|
+
item: AwardMarqueeItem,
|
|
36
25
|
key: React.Key,
|
|
37
26
|
// Only the single-entry, non-animated (static) rendering gets the large,
|
|
38
27
|
// centered treatment; the animated carousel is unaffected.
|
|
@@ -42,8 +31,21 @@ const renderAwardItem = (
|
|
|
42
31
|
// (scroll={true} with <=3 items), the sm:hidden mobile side is already
|
|
43
32
|
// handled by the separate animated half, so a nested swap here would never
|
|
44
33
|
// be visible at any viewport.
|
|
45
|
-
allowMobileSwap = false
|
|
34
|
+
allowMobileSwap = false,
|
|
35
|
+
includeFieldProps = true
|
|
46
36
|
) => {
|
|
37
|
+
const {
|
|
38
|
+
iconUrl,
|
|
39
|
+
iconAlt,
|
|
40
|
+
iconWidth,
|
|
41
|
+
iconHeight,
|
|
42
|
+
mobileIconUrl,
|
|
43
|
+
mobileIconAlt,
|
|
44
|
+
mobileIconWidth,
|
|
45
|
+
mobileIconHeight,
|
|
46
|
+
title,
|
|
47
|
+
description,
|
|
48
|
+
} = item;
|
|
47
49
|
const hasCaption = Boolean(title || description);
|
|
48
50
|
const hasMobileIcon = allowMobileSwap && Boolean(mobileIconUrl);
|
|
49
51
|
|
|
@@ -85,10 +87,16 @@ const renderAwardItem = (
|
|
|
85
87
|
)}
|
|
86
88
|
{hasCaption && (
|
|
87
89
|
<div className="w-[250px] text-center">
|
|
88
|
-
<p
|
|
90
|
+
<p
|
|
91
|
+
className="flex justify-center text-center text-body3 font-bold"
|
|
92
|
+
{...(includeFieldProps ? item.fieldProps?.title : undefined)}
|
|
93
|
+
>
|
|
89
94
|
{title}
|
|
90
95
|
</p>
|
|
91
|
-
<p
|
|
96
|
+
<p
|
|
97
|
+
className="flex flex-wrap whitespace-normal text-center text-footnote"
|
|
98
|
+
{...(includeFieldProps ? item.fieldProps?.description : undefined)}
|
|
99
|
+
>
|
|
92
100
|
{description}
|
|
93
101
|
</p>
|
|
94
102
|
</div>
|
|
@@ -106,6 +114,7 @@ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
|
106
114
|
scroll = false,
|
|
107
115
|
backgroundColor = "white",
|
|
108
116
|
className,
|
|
117
|
+
fieldProps,
|
|
109
118
|
}) => {
|
|
110
119
|
const scrollAvailable = items.length > 3;
|
|
111
120
|
const shouldDuplicate = items.length < 8;
|
|
@@ -129,7 +138,9 @@ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
|
129
138
|
} as React.CSSProperties
|
|
130
139
|
}
|
|
131
140
|
>
|
|
132
|
-
{marqueeItems.map((item, index) =>
|
|
141
|
+
{marqueeItems.map((item, index) =>
|
|
142
|
+
renderAwardItem(item, index, false, false, index < items.length)
|
|
143
|
+
)}
|
|
133
144
|
</div>
|
|
134
145
|
</div>
|
|
135
146
|
);
|
|
@@ -142,7 +153,8 @@ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
|
142
153
|
item,
|
|
143
154
|
index,
|
|
144
155
|
items.length === 1,
|
|
145
|
-
allowMobileSwap && items.length === 1
|
|
156
|
+
allowMobileSwap && items.length === 1,
|
|
157
|
+
true
|
|
146
158
|
)
|
|
147
159
|
)}
|
|
148
160
|
</div>
|
|
@@ -161,10 +173,19 @@ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
|
161
173
|
|
|
162
174
|
return (
|
|
163
175
|
<div id={anchorId} className={className}>
|
|
164
|
-
{heading &&
|
|
176
|
+
{heading && (
|
|
177
|
+
<p className="heading2 mb-4 px-4 text-center" {...fieldProps?.heading}>
|
|
178
|
+
{heading}
|
|
179
|
+
</p>
|
|
180
|
+
)}
|
|
165
181
|
{marquee}
|
|
166
182
|
{footerText && (
|
|
167
|
-
<p
|
|
183
|
+
<p
|
|
184
|
+
className="subheading3 mt-4 px-4 text-center"
|
|
185
|
+
{...fieldProps?.footerText}
|
|
186
|
+
>
|
|
187
|
+
{footerText}
|
|
188
|
+
</p>
|
|
168
189
|
)}
|
|
169
190
|
</div>
|
|
170
191
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
2
|
+
|
|
1
3
|
export type AwardMarqueeSpeed = "fast" | "medium" | "slow";
|
|
2
4
|
|
|
3
5
|
export type AwardMarqueeBackground = "cream" | "gray90" | "white";
|
|
@@ -15,6 +17,8 @@ export type AwardMarqueeItem = {
|
|
|
15
17
|
mobileIconHeight?: number;
|
|
16
18
|
title?: string;
|
|
17
19
|
description?: string;
|
|
20
|
+
/** Inspector attributes for this repeated award item's text. */
|
|
21
|
+
fieldProps?: FieldProps<"title" | "description">;
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
export type AwardMarqueeProps = {
|
|
@@ -26,4 +30,6 @@ export type AwardMarqueeProps = {
|
|
|
26
30
|
scroll?: boolean;
|
|
27
31
|
backgroundColor?: AwardMarqueeBackground;
|
|
28
32
|
className?: string;
|
|
33
|
+
/** Inspector attributes for the block heading and footer text. */
|
|
34
|
+
fieldProps?: FieldProps<"heading" | "footerText">;
|
|
29
35
|
};
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
fourColumnItems,
|
|
9
9
|
fullImageItems,
|
|
10
10
|
withFloatingImageArgs,
|
|
11
|
-
} from "./
|
|
11
|
+
} from "./callout.stories.mocks";
|
|
12
12
|
|
|
13
13
|
import { DocsPage } from "@shared/stories/DocsTemplate";
|
|
14
14
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
@@ -11,8 +11,13 @@ jest.mock("@shared/components/text", () => ({
|
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
13
|
jest.mock("@shared/contentful/blocks/button", () => ({
|
|
14
|
-
Button: ({ children, onClick, buttonLabel, ...props }: any) => (
|
|
15
|
-
<button
|
|
14
|
+
Button: ({ children, onClick, buttonLabel, fieldProps, ...props }: any) => (
|
|
15
|
+
<button
|
|
16
|
+
onClick={onClick}
|
|
17
|
+
data-testid="banner-button"
|
|
18
|
+
{...props}
|
|
19
|
+
{...fieldProps}
|
|
20
|
+
>
|
|
16
21
|
{buttonLabel || children}
|
|
17
22
|
</button>
|
|
18
23
|
),
|
|
@@ -70,6 +75,59 @@ describe("CartRetentionBanner", () => {
|
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
describe("modal behavior", () => {
|
|
78
|
+
it("tags the heading, rich-text description and CTA", () => {
|
|
79
|
+
render(
|
|
80
|
+
<CartRetentionBanner
|
|
81
|
+
fields={defaultFields}
|
|
82
|
+
fieldProps={{
|
|
83
|
+
mainHeading: {
|
|
84
|
+
"data-contentful-field-id": "mainHeading",
|
|
85
|
+
"data-contentful-entry-id": "cart-banner",
|
|
86
|
+
"data-contentful-locale": "en-US",
|
|
87
|
+
},
|
|
88
|
+
description: {
|
|
89
|
+
"data-contentful-field-id": "description",
|
|
90
|
+
"data-contentful-entry-id": "cart-banner",
|
|
91
|
+
"data-contentful-locale": "en-US",
|
|
92
|
+
},
|
|
93
|
+
cta: {
|
|
94
|
+
"data-contentful-field-id": "cta",
|
|
95
|
+
"data-contentful-entry-id": "cart-banner",
|
|
96
|
+
"data-contentful-locale": "en-US",
|
|
97
|
+
},
|
|
98
|
+
}}
|
|
99
|
+
checkShouldShowBanner={() => true}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
act(() => {
|
|
103
|
+
jest.advanceTimersByTime(200);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(screen.getByText("Welcome back!")).toHaveAttribute(
|
|
107
|
+
"data-contentful-field-id",
|
|
108
|
+
"mainHeading"
|
|
109
|
+
);
|
|
110
|
+
expect(screen.getByTestId("rich-text").parentElement).toHaveAttribute(
|
|
111
|
+
"data-contentful-field-id",
|
|
112
|
+
"description"
|
|
113
|
+
);
|
|
114
|
+
expect(screen.getByTestId("banner-button")).toHaveAttribute(
|
|
115
|
+
"data-contentful-field-id",
|
|
116
|
+
"cta"
|
|
117
|
+
);
|
|
118
|
+
[
|
|
119
|
+
screen.getByText("Welcome back!"),
|
|
120
|
+
screen.getByTestId("rich-text").parentElement,
|
|
121
|
+
screen.getByTestId("banner-button"),
|
|
122
|
+
].forEach(element => {
|
|
123
|
+
expect(element).toHaveAttribute(
|
|
124
|
+
"data-contentful-entry-id",
|
|
125
|
+
"cart-banner"
|
|
126
|
+
);
|
|
127
|
+
expect(element).toHaveAttribute("data-contentful-locale", "en-US");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
73
131
|
it("renders a section with anchorId", () => {
|
|
74
132
|
render(<CartRetentionBanner fields={defaultFields} />);
|
|
75
133
|
const section = document.querySelector("section#cart-banner");
|
|
@@ -416,7 +474,9 @@ describe("CartRetentionBanner", () => {
|
|
|
416
474
|
|
|
417
475
|
describe("image and layout options", () => {
|
|
418
476
|
it("does not render an image when none is provided", () => {
|
|
419
|
-
render(
|
|
477
|
+
render(
|
|
478
|
+
<CartRetentionBanner fields={defaultFields} isInPopupContainer={true} />
|
|
479
|
+
);
|
|
420
480
|
expect(screen.queryByTestId("banner-image")).not.toBeInTheDocument();
|
|
421
481
|
});
|
|
422
482
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
CartRetentionBannerFieldProps,
|
|
6
|
+
TypeCartRetentionBannerFields,
|
|
7
|
+
} from "./types";
|
|
5
8
|
|
|
6
9
|
import { NextImage } from "@shared/components/next-image";
|
|
7
10
|
import { Text } from "@shared/components/text";
|
|
@@ -45,6 +48,7 @@ const isShapeBackgroundThemeKey = (
|
|
|
45
48
|
|
|
46
49
|
const CartRetentionBanner: React.FC<{
|
|
47
50
|
fields: TypeCartRetentionBannerFields;
|
|
51
|
+
fieldProps?: CartRetentionBannerFieldProps;
|
|
48
52
|
sys?: any;
|
|
49
53
|
isInPopupContainer?: boolean;
|
|
50
54
|
onClose?: () => void;
|
|
@@ -53,6 +57,7 @@ const CartRetentionBanner: React.FC<{
|
|
|
53
57
|
onBannerShown?: () => void;
|
|
54
58
|
}> = ({
|
|
55
59
|
fields,
|
|
60
|
+
fieldProps,
|
|
56
61
|
isInPopupContainer = false,
|
|
57
62
|
onClose,
|
|
58
63
|
onContinueCheckout,
|
|
@@ -174,15 +179,22 @@ const CartRetentionBanner: React.FC<{
|
|
|
174
179
|
"heading2",
|
|
175
180
|
hasSideImage ? "text-center md:text-left" : "text-center"
|
|
176
181
|
)}
|
|
182
|
+
{...fieldProps?.mainHeading}
|
|
177
183
|
>
|
|
178
184
|
{mainHeading || "Welcome back. Let's finish your order"}
|
|
179
185
|
</Text>
|
|
180
|
-
{description &&
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
{description && (
|
|
187
|
+
<div {...fieldProps?.description}>
|
|
188
|
+
{renderContentfulRichText(
|
|
189
|
+
toDocument(description?.json),
|
|
190
|
+
false,
|
|
191
|
+
cx(
|
|
192
|
+
"body2",
|
|
193
|
+
hasSideImage ? "text-center md:text-left" : "text-center"
|
|
194
|
+
)
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
186
198
|
<div
|
|
187
199
|
className={cx(
|
|
188
200
|
"flex w-full pt-5 md:px-4",
|
|
@@ -195,6 +207,7 @@ const CartRetentionBanner: React.FC<{
|
|
|
195
207
|
disableAnimation={cta?.disableAnimation ?? true}
|
|
196
208
|
animationType={cta?.animationType}
|
|
197
209
|
fullWidth={true}
|
|
210
|
+
fieldProps={cta?.fieldProps ?? fieldProps?.cta}
|
|
198
211
|
/>
|
|
199
212
|
</div>
|
|
200
213
|
</>
|
|
@@ -288,7 +301,7 @@ const CartRetentionBanner: React.FC<{
|
|
|
288
301
|
) : (
|
|
289
302
|
// overflow-x:clip contains the CTA hover-scale (keeps overflow-y);
|
|
290
303
|
// px keeps the full-width button off the modal border.
|
|
291
|
-
<div className="pop-up-body mb-5 w-full [overflow-x:clip]
|
|
304
|
+
<div className="pop-up-body mb-5 w-full px-3 [overflow-x:clip] md:px-6">
|
|
292
305
|
{wrappedBodyContent}
|
|
293
306
|
</div>
|
|
294
307
|
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnimationType } from "@shared/components/animation-wrapper";
|
|
2
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
2
3
|
|
|
3
4
|
export type CartRetentionBannerImagePosition = "left" | "right" | "center";
|
|
4
5
|
|
|
@@ -50,3 +51,6 @@ export interface TypeCartRetentionBannerFields {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export type TypeCartRetentionBanner = TypeCartRetentionBannerFields;
|
|
54
|
+
export type CartRetentionBannerFieldProps = FieldProps<
|
|
55
|
+
"mainHeading" | "description" | "cta"
|
|
56
|
+
>;
|
|
@@ -20,8 +20,10 @@ jest.mock("@shared/components/next-image", () => ({
|
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
jest.mock("@shared/components/text", () => ({
|
|
23
|
-
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
24
|
-
<Tag className={className}
|
|
23
|
+
Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
|
|
24
|
+
<Tag className={className} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</Tag>
|
|
25
27
|
),
|
|
26
28
|
}));
|
|
27
29
|
|
|
@@ -42,6 +44,56 @@ const defaultProps: FloatingBannerProps = {
|
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
describe("FloatingBanner", () => {
|
|
47
|
+
it("spreads fieldProps onto each rendered content field", () => {
|
|
48
|
+
const tag = (field: string) => ({
|
|
49
|
+
"data-contentful-field-id": field,
|
|
50
|
+
"data-contentful-entry-id": "floating-banner",
|
|
51
|
+
"data-contentful-locale": "en-US",
|
|
52
|
+
});
|
|
53
|
+
const { container } = render(
|
|
54
|
+
<FloatingBanner
|
|
55
|
+
{...defaultProps}
|
|
56
|
+
fieldProps={{
|
|
57
|
+
title: tag("title"),
|
|
58
|
+
subtitle: tag("subtitle"),
|
|
59
|
+
description: tag("description"),
|
|
60
|
+
disclaimer: tag("disclaimer"),
|
|
61
|
+
cta: tag("cta"),
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
["title", "subtitle", "description", "disclaimer", "cta"].forEach(field => {
|
|
66
|
+
expect(
|
|
67
|
+
container.querySelector(`[data-contentful-field-id="${field}"]`)
|
|
68
|
+
).toHaveAttribute("data-contentful-entry-id", "floating-banner");
|
|
69
|
+
});
|
|
70
|
+
container
|
|
71
|
+
.querySelectorAll("[data-contentful-field-id]")
|
|
72
|
+
.forEach(element =>
|
|
73
|
+
expect(element).toHaveAttribute("data-contentful-locale", "en-US")
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("supports partial fieldProps and omits tags when a field is not rendered", () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<FloatingBanner
|
|
80
|
+
title="Only title"
|
|
81
|
+
fieldProps={{
|
|
82
|
+
title: { "data-contentful-field-id": "title" },
|
|
83
|
+
description: { "data-contentful-field-id": "description" },
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
expect(
|
|
88
|
+
container.querySelectorAll("[data-contentful-field-id]")
|
|
89
|
+
).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not add tags when fieldProps is omitted", () => {
|
|
93
|
+
const { container } = render(<FloatingBanner {...defaultProps} />);
|
|
94
|
+
expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
45
97
|
describe("Rendering", () => {
|
|
46
98
|
it("renders with all props", () => {
|
|
47
99
|
render(<FloatingBanner {...defaultProps} />);
|
|
@@ -16,6 +16,7 @@ export const FloatingBanner: React.FC<FloatingBannerProps> = ({
|
|
|
16
16
|
color = "light",
|
|
17
17
|
enableHeading,
|
|
18
18
|
maxWidth = true,
|
|
19
|
+
fieldProps,
|
|
19
20
|
}) => {
|
|
20
21
|
const bgColorClasses: Record<ThemeKey, string> = {
|
|
21
22
|
blue: "bg-[#07B2E2]",
|
|
@@ -51,6 +52,7 @@ export const FloatingBanner: React.FC<FloatingBannerProps> = ({
|
|
|
51
52
|
<Text
|
|
52
53
|
as={enableHeading ? "h1" : "h2"}
|
|
53
54
|
className={`heading5 mb-3 text-center lg:w-[95%] lg:text-left`}
|
|
55
|
+
{...fieldProps?.title}
|
|
54
56
|
>
|
|
55
57
|
{title}
|
|
56
58
|
</Text>
|
|
@@ -59,6 +61,7 @@ export const FloatingBanner: React.FC<FloatingBannerProps> = ({
|
|
|
59
61
|
<Text
|
|
60
62
|
as={"h3"}
|
|
61
63
|
className={`heading3 mb-3 text-center lg:text-left`}
|
|
64
|
+
{...fieldProps?.subtitle}
|
|
62
65
|
>
|
|
63
66
|
{subtitle}
|
|
64
67
|
</Text>
|
|
@@ -67,13 +70,17 @@ export const FloatingBanner: React.FC<FloatingBannerProps> = ({
|
|
|
67
70
|
<Text
|
|
68
71
|
as="div"
|
|
69
72
|
className="body1 text-center lg:w-[95%] lg:text-left"
|
|
73
|
+
{...fieldProps?.description}
|
|
70
74
|
>
|
|
71
75
|
{description}
|
|
72
76
|
</Text>
|
|
73
77
|
)}
|
|
74
78
|
</div>
|
|
75
79
|
{cta && (
|
|
76
|
-
<div
|
|
80
|
+
<div
|
|
81
|
+
className="mt-5 flex flex-shrink-0 justify-center lg:mt-0 lg:items-center"
|
|
82
|
+
{...fieldProps?.cta}
|
|
83
|
+
>
|
|
77
84
|
<Button
|
|
78
85
|
buttonVariant="secondary"
|
|
79
86
|
{...cta}
|
|
@@ -84,7 +91,11 @@ export const FloatingBanner: React.FC<FloatingBannerProps> = ({
|
|
|
84
91
|
</div>
|
|
85
92
|
{disclaimer && (
|
|
86
93
|
<div>
|
|
87
|
-
<Text
|
|
94
|
+
<Text
|
|
95
|
+
as="div"
|
|
96
|
+
className="body3 mt-3 text-center"
|
|
97
|
+
{...fieldProps?.disclaimer}
|
|
98
|
+
>
|
|
88
99
|
{disclaimer}
|
|
89
100
|
</Text>
|
|
90
101
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
3
5
|
export type FloatingBannerProps = {
|
|
4
6
|
title?: string;
|
|
5
7
|
enableHeading?: boolean;
|
|
@@ -11,6 +13,10 @@ export type FloatingBannerProps = {
|
|
|
11
13
|
background?: "green" | "navy" | "white" | "purple" | "blue" | "yellow";
|
|
12
14
|
color?: "dark" | "light";
|
|
13
15
|
maxWidth?: boolean;
|
|
16
|
+
/** Consumer-supplied DOM attributes for the rendered content fields. */
|
|
17
|
+
fieldProps?: FieldProps<
|
|
18
|
+
"title" | "subtitle" | "description" | "disclaimer" | "cta"
|
|
19
|
+
>;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
export type ThemeKey =
|