@windstream/react-shared-components 0.2.38 → 0.2.41
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 +1 -2
- 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 +1 -1
- package/dist/index.d.ts +3 -3
- 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/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/brand-button/index.tsx +4 -4
- package/src/contentful/blocks/button/Button.stories.tsx +0 -1
- package/src/contentful/blocks/button/index.test.tsx +109 -8
- package/src/contentful/blocks/button/index.tsx +64 -18
- package/src/contentful/blocks/button/types.ts +1 -1
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +18 -42
- package/src/contentful/blocks/cards/blog-card/index.tsx +45 -63
- package/src/contentful/blocks/cards/blog-card/types.ts +0 -1
- package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +69 -2
- package/src/contentful/blocks/cards/floating-image-card/index.tsx +7 -4
- package/src/contentful/blocks/carousel/helper.test.tsx +33 -10
- package/src/contentful/blocks/carousel/index.test.tsx +6 -8
- package/src/contentful/blocks/carousel/types.test.ts +10 -0
- package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +0 -1
- package/src/contentful/blocks/dynamic-tabs/ordered-tab-views.tsx +1 -1
- package/src/contentful/blocks/ordered-list/index.tsx +2 -2
- package/src/contentful/blocks/primary-hero/index.tsx +6 -2
- package/src/hooks/use-carousel-swipe.test.ts +26 -6
- package/src/types/micro-components.ts +2 -2
|
@@ -9,10 +9,17 @@ import { BrandButton, ButtonVariantsT } from "@shared/components/brand-button";
|
|
|
9
9
|
import { Button as CoreButton } from "@shared/components/button";
|
|
10
10
|
import { Link } from "@shared/components/link";
|
|
11
11
|
import { MaterialIcon } from "@shared/components/material-icon";
|
|
12
|
+
import type { IconName } from "@shared/components/material-icon/types";
|
|
12
13
|
import { Text } from "@shared/components/text";
|
|
13
14
|
import { cx } from "@shared/utils";
|
|
14
15
|
import { buildPreservedQueryHref } from "@shared/utils/utm";
|
|
15
16
|
|
|
17
|
+
const textVariantClasses: Record<string, string> = {
|
|
18
|
+
primary_brand: "text-text-brand",
|
|
19
|
+
primary_inverse: "text-text",
|
|
20
|
+
secondary: "text-text",
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
export const Button: React.FC<ButtonProps> = props => {
|
|
17
24
|
const {
|
|
18
25
|
showButtonAs = "solid",
|
|
@@ -33,8 +40,9 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
33
40
|
idNameToOpen,
|
|
34
41
|
onModalButtonClick,
|
|
35
42
|
onClick,
|
|
36
|
-
iconName,
|
|
37
43
|
iconFill = 0,
|
|
44
|
+
buttonIconName,
|
|
45
|
+
buttonIconPosition,
|
|
38
46
|
size,
|
|
39
47
|
iconSize = 24,
|
|
40
48
|
preserveQueryParameters,
|
|
@@ -42,6 +50,13 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
42
50
|
animationType = DEFAULT_BUTTON_ANIMATION_TYPE,
|
|
43
51
|
...rest
|
|
44
52
|
} = props;
|
|
53
|
+
|
|
54
|
+
const isLeftIcon = buttonIconPosition === "left";
|
|
55
|
+
const matIconName = buttonIconName as IconName | undefined;
|
|
56
|
+
const brandButtonClassName = cx(
|
|
57
|
+
buttonClassName,
|
|
58
|
+
isLeftIcon && matIconName ? "flex-row-reverse" : ""
|
|
59
|
+
);
|
|
45
60
|
|
|
46
61
|
if (
|
|
47
62
|
preDefinedFunctionExecution === "check availability" &&
|
|
@@ -116,7 +131,6 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
116
131
|
: href && preserveQueryParameters && currentSearch
|
|
117
132
|
? buildPreservedQueryHref(href, currentSearch)
|
|
118
133
|
: href;
|
|
119
|
-
|
|
120
134
|
let CTA = (
|
|
121
135
|
<CoreButton onClick={buttonClick} className={buttonClassName} {...rest} />
|
|
122
136
|
);
|
|
@@ -132,9 +146,12 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
132
146
|
text={buttonLabel}
|
|
133
147
|
label={buttonPrefix}
|
|
134
148
|
fullWidth={fullWidth}
|
|
135
|
-
buttonClassName={
|
|
149
|
+
buttonClassName={brandButtonClassName}
|
|
136
150
|
size={size}
|
|
137
151
|
onClick={linkClick}
|
|
152
|
+
iconName={matIconName}
|
|
153
|
+
iconSize={iconSize}
|
|
154
|
+
iconFill={iconFill}
|
|
138
155
|
disableAnimation={disableAnimation}
|
|
139
156
|
animationType={animationType}
|
|
140
157
|
/>
|
|
@@ -147,7 +164,10 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
147
164
|
fullWidth={fullWidth}
|
|
148
165
|
size={size}
|
|
149
166
|
onClick={buttonClick}
|
|
150
|
-
|
|
167
|
+
iconName={matIconName}
|
|
168
|
+
iconSize={iconSize}
|
|
169
|
+
iconFill={iconFill}
|
|
170
|
+
buttonClassName={brandButtonClassName}
|
|
151
171
|
disableAnimation={disableAnimation}
|
|
152
172
|
animationType={animationType}
|
|
153
173
|
/>
|
|
@@ -160,16 +180,34 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
160
180
|
href={resolvedHref}
|
|
161
181
|
target={target}
|
|
162
182
|
variant={linkVariant || "default"}
|
|
163
|
-
className={
|
|
183
|
+
className={cx(
|
|
184
|
+
matIconName &&
|
|
185
|
+
"label1 inline-flex items-center gap-2 no-underline",
|
|
186
|
+
textVariantClasses[buttonVariant],
|
|
187
|
+
linkClassName
|
|
188
|
+
)}
|
|
164
189
|
onClick={linkClick}
|
|
165
190
|
>
|
|
166
|
-
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
{matIconName ? (
|
|
192
|
+
<>
|
|
193
|
+
{isLeftIcon ? (
|
|
194
|
+
<MaterialIcon
|
|
195
|
+
name={matIconName as IconName}
|
|
196
|
+
size={iconSize}
|
|
197
|
+
fill={iconFill}
|
|
198
|
+
/>
|
|
199
|
+
) : null}
|
|
200
|
+
{buttonLabel}
|
|
201
|
+
{!isLeftIcon ? (
|
|
202
|
+
<MaterialIcon
|
|
203
|
+
name={matIconName as IconName}
|
|
204
|
+
size={iconSize}
|
|
205
|
+
fill={iconFill}
|
|
206
|
+
/>
|
|
207
|
+
) : null}
|
|
208
|
+
</>
|
|
209
|
+
) : (
|
|
210
|
+
<>{buttonLabel}</>
|
|
173
211
|
)}
|
|
174
212
|
</Link>
|
|
175
213
|
);
|
|
@@ -184,10 +222,10 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
184
222
|
text={buttonLabel}
|
|
185
223
|
label={buttonPrefix}
|
|
186
224
|
fullWidth={fullWidth}
|
|
187
|
-
buttonClassName={
|
|
225
|
+
buttonClassName={brandButtonClassName}
|
|
188
226
|
size={size}
|
|
189
227
|
onClick={linkClick}
|
|
190
|
-
iconName={
|
|
228
|
+
iconName={matIconName}
|
|
191
229
|
iconSize={iconSize}
|
|
192
230
|
iconFill={iconFill}
|
|
193
231
|
disableAnimation={disableAnimation}
|
|
@@ -200,10 +238,10 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
200
238
|
text={buttonLabel}
|
|
201
239
|
label={buttonPrefix}
|
|
202
240
|
fullWidth={fullWidth}
|
|
203
|
-
buttonClassName={
|
|
241
|
+
buttonClassName={brandButtonClassName}
|
|
204
242
|
size={size}
|
|
205
243
|
onClick={buttonClick}
|
|
206
|
-
iconName={
|
|
244
|
+
iconName={matIconName}
|
|
207
245
|
iconSize={iconSize}
|
|
208
246
|
iconFill={iconFill}
|
|
209
247
|
disableAnimation={disableAnimation}
|
|
@@ -237,12 +275,20 @@ export const Button: React.FC<ButtonProps> = props => {
|
|
|
237
275
|
)}
|
|
238
276
|
aria-label={buttonLabel}
|
|
239
277
|
>
|
|
278
|
+
{isLeftIcon && matIconName ? (
|
|
279
|
+
<MaterialIcon
|
|
280
|
+
name={matIconName as IconName}
|
|
281
|
+
fill={iconFill}
|
|
282
|
+
size={iconSize}
|
|
283
|
+
weight="200"
|
|
284
|
+
/>
|
|
285
|
+
) : null}
|
|
240
286
|
<Text aria-label={buttonLabel} className="label1 text-nowrap">
|
|
241
287
|
{buttonLabel}
|
|
242
288
|
</Text>
|
|
243
|
-
{
|
|
289
|
+
{matIconName && !isLeftIcon && (
|
|
244
290
|
<MaterialIcon
|
|
245
|
-
name={
|
|
291
|
+
name={matIconName as IconName}
|
|
246
292
|
fill={iconFill}
|
|
247
293
|
size={iconSize}
|
|
248
294
|
weight="200"
|
|
@@ -14,14 +14,6 @@ jest.mock("next/link", () => {
|
|
|
14
14
|
return MockLink;
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
jest.mock("@shared/components/material-icon", () => ({
|
|
18
|
-
MaterialIcon: ({ name }: any) => (
|
|
19
|
-
<span data-testid="material-icon" data-name={name}>
|
|
20
|
-
{name}
|
|
21
|
-
</span>
|
|
22
|
-
),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
17
|
jest.mock("@shared/components/text", () => ({
|
|
26
18
|
Text: ({ children, className, ...rest }: any) => (
|
|
27
19
|
<span className={className} {...rest}>
|
|
@@ -74,16 +66,6 @@ describe("BlogCard", () => {
|
|
|
74
66
|
expect(screen.getByText("•")).toBeInTheDocument();
|
|
75
67
|
});
|
|
76
68
|
|
|
77
|
-
it("renders read more link", () => {
|
|
78
|
-
render(<BlogCard {...defaultProps} />);
|
|
79
|
-
expect(screen.getByText("Read more")).toBeInTheDocument();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("renders custom readMoreText", () => {
|
|
83
|
-
render(<BlogCard {...defaultProps} readMoreText="Continue reading" />);
|
|
84
|
-
expect(screen.getByText("Continue reading")).toBeInTheDocument();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
69
|
it("does not render date separator when date is not provided", () => {
|
|
88
70
|
render(<BlogCard {...defaultProps} date={undefined} />);
|
|
89
71
|
expect(screen.queryByText("•")).not.toBeInTheDocument();
|
|
@@ -171,6 +153,24 @@ describe("BlogCard", () => {
|
|
|
171
153
|
});
|
|
172
154
|
});
|
|
173
155
|
|
|
156
|
+
describe("Navigation", () => {
|
|
157
|
+
it("renders a link to href when title is missing", () => {
|
|
158
|
+
const { container } = render(
|
|
159
|
+
<BlogCard {...defaultProps} title={undefined} />
|
|
160
|
+
);
|
|
161
|
+
const links = container.querySelectorAll("a");
|
|
162
|
+
expect(links).toHaveLength(1);
|
|
163
|
+
expect(links[0]).toHaveAttribute("href", "/blog/test-post");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("renders exactly one link when title is present", () => {
|
|
167
|
+
const { container } = render(<BlogCard {...defaultProps} />);
|
|
168
|
+
const links = container.querySelectorAll("a");
|
|
169
|
+
expect(links).toHaveLength(1);
|
|
170
|
+
expect(links[0]).toHaveAttribute("href", "/blog/test-post");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
174
|
describe("Optional props", () => {
|
|
175
175
|
it("renders without title", () => {
|
|
176
176
|
const { container } = render(
|
|
@@ -185,29 +185,5 @@ describe("BlogCard", () => {
|
|
|
185
185
|
);
|
|
186
186
|
expect(container.querySelector("article")).toBeInTheDocument();
|
|
187
187
|
});
|
|
188
|
-
|
|
189
|
-
it("sets aria-label on read more link", () => {
|
|
190
|
-
render(<BlogCard {...defaultProps} />);
|
|
191
|
-
const links = screen.getAllByRole("link");
|
|
192
|
-
const readMoreLink = links.find(link =>
|
|
193
|
-
link.getAttribute("aria-label")?.includes("Read more")
|
|
194
|
-
);
|
|
195
|
-
expect(readMoreLink).toHaveAttribute(
|
|
196
|
-
"aria-label",
|
|
197
|
-
"Read more about Test Blog Post"
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("uses fallback aria-label when title is not provided", () => {
|
|
202
|
-
render(<BlogCard {...defaultProps} title={undefined} />);
|
|
203
|
-
const links = screen.getAllByRole("link");
|
|
204
|
-
const readMoreLink = links.find(link =>
|
|
205
|
-
link.getAttribute("aria-label")?.includes("Read more")
|
|
206
|
-
);
|
|
207
|
-
expect(readMoreLink).toHaveAttribute(
|
|
208
|
-
"aria-label",
|
|
209
|
-
"Read more about this article"
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
188
|
});
|
|
213
189
|
});
|
|
@@ -2,8 +2,6 @@ import React from "react";
|
|
|
2
2
|
import { BlogCardProps } from "./types";
|
|
3
3
|
import * as _NextLink from "next/link";
|
|
4
4
|
|
|
5
|
-
// import { Button } from "@shared/components/button";
|
|
6
|
-
import { MaterialIcon } from "@shared/components/material-icon";
|
|
7
5
|
import { Text } from "@shared/components/text";
|
|
8
6
|
|
|
9
7
|
// CJS/ESM interop: next/link uses standard __esModule exports so this is safe.
|
|
@@ -21,13 +19,12 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
21
19
|
category,
|
|
22
20
|
image,
|
|
23
21
|
imageComponent: ImageComponent,
|
|
24
|
-
readMoreText = "Read more",
|
|
25
22
|
asGrid = true,
|
|
26
23
|
index,
|
|
27
24
|
}: BlogCardProps) => {
|
|
28
25
|
const parentClassName = asGrid
|
|
29
|
-
? "flex h-full flex-col overflow-hidden rounded-card-lg bg-white shadow-drop transition-all duration-200 hover:-translate-y-0.5 hover:shadow-cardDrop"
|
|
30
|
-
: `callout-card w-full flex h-full flex-col overflow-hidden`;
|
|
26
|
+
? "group relative flex h-full flex-col overflow-hidden rounded-card-lg bg-white shadow-drop transition-all duration-200 hover:-translate-y-0.5 hover:shadow-cardDrop"
|
|
27
|
+
: `callout-card group relative w-full flex h-full flex-col overflow-hidden`;
|
|
31
28
|
|
|
32
29
|
return (
|
|
33
30
|
<article
|
|
@@ -38,42 +35,40 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
38
35
|
>
|
|
39
36
|
{/* Image (hidden in list view) */}
|
|
40
37
|
{asGrid && (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
ImageComponent
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/>
|
|
53
|
-
) : (
|
|
54
|
-
<img
|
|
55
|
-
src={image.src}
|
|
56
|
-
alt={image.alt}
|
|
57
|
-
width={image.width}
|
|
58
|
-
height={image.height}
|
|
59
|
-
loading="lazy"
|
|
60
|
-
decoding="async"
|
|
61
|
-
className="h-full w-full object-cover transition-transform duration-300 hover:scale-[1.03]"
|
|
62
|
-
/>
|
|
63
|
-
)
|
|
38
|
+
<div className="block h-[232px] w-full flex-shrink-0 overflow-hidden bg-gray-100">
|
|
39
|
+
{image ? (
|
|
40
|
+
ImageComponent ? (
|
|
41
|
+
<ImageComponent
|
|
42
|
+
src={image.src}
|
|
43
|
+
alt={image.alt}
|
|
44
|
+
width={image.width}
|
|
45
|
+
height={image.height}
|
|
46
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
47
|
+
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
48
|
+
/>
|
|
64
49
|
) : (
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
<img
|
|
51
|
+
src={image.src}
|
|
52
|
+
alt={image.alt}
|
|
53
|
+
width={image.width}
|
|
54
|
+
height={image.height}
|
|
55
|
+
loading="lazy"
|
|
56
|
+
decoding="async"
|
|
57
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
68
58
|
/>
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
)
|
|
60
|
+
) : (
|
|
61
|
+
<div
|
|
62
|
+
className="h-full w-full bg-gradient-to-br from-gray-200 to-gray-100"
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
72
67
|
)}
|
|
73
68
|
|
|
74
69
|
{/* Body */}
|
|
75
70
|
<div
|
|
76
|
-
className={`flex flex-1 flex-col
|
|
71
|
+
className={`flex flex-1 flex-col ${
|
|
77
72
|
asGrid ? "gap-5 p-6 md:p-8" : "gap-1 border-b p-3 md:p-4"
|
|
78
73
|
}`}
|
|
79
74
|
>
|
|
@@ -94,12 +89,14 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
94
89
|
|
|
95
90
|
{/* Title */}
|
|
96
91
|
{title && (
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
<Text as="h2" className="heading6">
|
|
93
|
+
<Link
|
|
94
|
+
href={href}
|
|
95
|
+
className="m-0 line-clamp-3 font-black transition-colors duration-150 after:absolute after:inset-0 after:z-10 after:content-[''] hover:text-text-brand group-hover:text-text-brand"
|
|
96
|
+
>
|
|
97
|
+
{title}
|
|
98
|
+
</Link>
|
|
99
|
+
</Text>
|
|
103
100
|
)}
|
|
104
101
|
|
|
105
102
|
{/* Excerpt */}
|
|
@@ -108,29 +105,14 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
108
105
|
{description}
|
|
109
106
|
</Text>
|
|
110
107
|
)}
|
|
111
|
-
|
|
112
|
-
{/* Read more (hidden in list view) */}
|
|
113
|
-
{asGrid && (
|
|
114
|
-
<Link
|
|
115
|
-
href={href}
|
|
116
|
-
className="group mt-auto inline-flex items-center justify-start gap-2 pt-3 text-sm text-text-brand no-underline"
|
|
117
|
-
aria-label={`${readMoreText} about ${title || "this article"}`}
|
|
118
|
-
>
|
|
119
|
-
<Text
|
|
120
|
-
aria-label={`${readMoreText} about ${title || "this article"}`}
|
|
121
|
-
className="label1 text-nowrap"
|
|
122
|
-
>
|
|
123
|
-
{readMoreText}
|
|
124
|
-
</Text>
|
|
125
|
-
<MaterialIcon
|
|
126
|
-
name="expand_circle_right"
|
|
127
|
-
fill={1}
|
|
128
|
-
size={24}
|
|
129
|
-
weight="200"
|
|
130
|
-
/>
|
|
131
|
-
</Link>
|
|
132
|
-
)}
|
|
133
108
|
</div>
|
|
109
|
+
|
|
110
|
+
{/* Fallback stretched link keeps the card navigable when no title link is rendered */}
|
|
111
|
+
{!title && (
|
|
112
|
+
<Link href={href} className="absolute inset-0 z-10">
|
|
113
|
+
<span className="sr-only">{category ?? "Read more"}</span>
|
|
114
|
+
</Link>
|
|
115
|
+
)}
|
|
134
116
|
</article>
|
|
135
117
|
);
|
|
136
118
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { FloatingImageCard } from "./index";
|
|
3
3
|
|
|
4
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import "@testing-library/jest-dom/jest-globals";
|
|
4
6
|
import { render, screen } from "@testing-library/react";
|
|
5
7
|
|
|
6
8
|
jest.mock("@shared/components/next-image", () => ({
|
|
7
|
-
NextImage: ({ src, alt, width, height, className, onClick }: any) => (
|
|
9
|
+
NextImage: ({ src, alt, width, height, className, onClick, ...rest }: any) => (
|
|
8
10
|
<img
|
|
9
11
|
data-testid="next-image"
|
|
10
12
|
src={src}
|
|
@@ -13,6 +15,7 @@ jest.mock("@shared/components/next-image", () => ({
|
|
|
13
15
|
height={height}
|
|
14
16
|
className={className}
|
|
15
17
|
onClick={onClick}
|
|
18
|
+
{...rest}
|
|
16
19
|
/>
|
|
17
20
|
),
|
|
18
21
|
}));
|
|
@@ -98,6 +101,68 @@ describe("FloatingImageCard", () => {
|
|
|
98
101
|
render(<FloatingImageCard card={defaultCard} />);
|
|
99
102
|
expect(screen.queryByTestId("cta-button")).not.toBeInTheDocument();
|
|
100
103
|
});
|
|
104
|
+
|
|
105
|
+
it("renders Button when buttonIconName is provided", () => {
|
|
106
|
+
const card = {
|
|
107
|
+
...defaultCard,
|
|
108
|
+
cta: {
|
|
109
|
+
buttonLabel: "See Jobs",
|
|
110
|
+
href: "/jobs",
|
|
111
|
+
showButtonAs: "text" as const,
|
|
112
|
+
buttonIconName: "expand_circle_right",
|
|
113
|
+
buttonIconPosition: "left" as const,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
render(<FloatingImageCard card={card} />);
|
|
118
|
+
expect(screen.getByTestId("cta-button")).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("renders Button for primary_inverse text CTA variant", () => {
|
|
122
|
+
const card = {
|
|
123
|
+
...defaultCard,
|
|
124
|
+
cta: {
|
|
125
|
+
buttonLabel: "See Jobs",
|
|
126
|
+
href: "/jobs",
|
|
127
|
+
showButtonAs: "text" as const,
|
|
128
|
+
buttonVariant: "primary_inverse" as const,
|
|
129
|
+
buttonIconName: "expand_circle_right",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
render(<FloatingImageCard card={card} />);
|
|
134
|
+
expect(screen.getByTestId("cta-button")).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("renders Button for primary_navy alias", () => {
|
|
138
|
+
const card = {
|
|
139
|
+
...defaultCard,
|
|
140
|
+
cta: {
|
|
141
|
+
buttonLabel: "See Jobs",
|
|
142
|
+
href: "/jobs",
|
|
143
|
+
showButtonAs: "text" as const,
|
|
144
|
+
buttonVariant: "primary_navy" as any,
|
|
145
|
+
buttonIconName: "expand_circle_right",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
render(<FloatingImageCard card={card} />);
|
|
150
|
+
expect(screen.getByTestId("cta-button")).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("renders authored icon CTA when buttonIconName is set even if href is missing", () => {
|
|
154
|
+
const card = {
|
|
155
|
+
...defaultCard,
|
|
156
|
+
cta: {
|
|
157
|
+
buttonLabel: "See Jobs",
|
|
158
|
+
showButtonAs: "text" as const,
|
|
159
|
+
buttonIconName: "expand_circle_right",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
render(<FloatingImageCard card={card} />);
|
|
164
|
+
expect(screen.getByTestId("cta-button")).toBeInTheDocument();
|
|
165
|
+
});
|
|
101
166
|
});
|
|
102
167
|
|
|
103
168
|
describe("CTA alignment", () => {
|
|
@@ -158,7 +223,9 @@ describe("FloatingImageCard", () => {
|
|
|
158
223
|
|
|
159
224
|
describe("Image link", () => {
|
|
160
225
|
it("makes image clickable when imageLink is provided", () => {
|
|
161
|
-
const windowOpen = jest
|
|
226
|
+
const windowOpen = jest
|
|
227
|
+
.spyOn(window, "open")
|
|
228
|
+
.mockImplementation(() => null);
|
|
162
229
|
const card = {
|
|
163
230
|
...defaultCard,
|
|
164
231
|
imageLink: "https://example.com",
|
|
@@ -106,11 +106,14 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
|
|
|
106
106
|
<div className={cx(ctaAlignmentClass, "mt-auto")}>
|
|
107
107
|
{cta ? (
|
|
108
108
|
<Button
|
|
109
|
-
linkVariant="unstyled"
|
|
110
|
-
linkClassName="label1 flex items-center gap-2 text-text"
|
|
111
109
|
{...cta}
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
showButtonAs={
|
|
111
|
+
cta.showButtonAs || (cta.buttonIconName ? undefined : "text-with-icon")
|
|
112
|
+
}
|
|
113
|
+
iconName={
|
|
114
|
+
cta.iconName || (cta.buttonIconName ? undefined : "expand_circle_right")
|
|
115
|
+
}
|
|
116
|
+
iconFill={cta.iconFill ?? 1}
|
|
114
117
|
// Button animates ONLY when BOTH are explicitly enabled:
|
|
115
118
|
// - the parent Callout animation is enabled (disableAnimation === false)
|
|
116
119
|
// - the button's own disableAnimation is enabled (=== false)
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
CarouselWithTestimonialCards,
|
|
8
8
|
} from "./types";
|
|
9
9
|
|
|
10
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
10
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
11
11
|
|
|
12
12
|
// Mock sub-components
|
|
13
13
|
jest.mock("@shared/components/button", () => ({
|
|
@@ -84,6 +84,12 @@ jest.mock("@shared/utils", () => ({
|
|
|
84
84
|
}));
|
|
85
85
|
|
|
86
86
|
describe("ProductCardCarousel", () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
|
89
|
+
configurable: true,
|
|
90
|
+
get: () => 900,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
87
93
|
const createProductFields = (
|
|
88
94
|
items: any[] = []
|
|
89
95
|
): CarouselWithProductCards => ({
|
|
@@ -208,39 +214,56 @@ describe("ProductCardCarousel", () => {
|
|
|
208
214
|
expect(card).toHaveAttribute("data-plan", "500 Mbps");
|
|
209
215
|
});
|
|
210
216
|
|
|
211
|
-
it("renders
|
|
217
|
+
it("renders Next arrow and hides Previous on first slide in carousel mode", async () => {
|
|
212
218
|
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
213
219
|
...sampleProduct,
|
|
214
220
|
sys: { id: `prod-${i}` },
|
|
215
221
|
}));
|
|
216
222
|
const fields = createProductFields(items);
|
|
217
223
|
render(<ProductCardCarousel fields={fields} />);
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
|
|
225
|
+
fireEvent(window, new Event("resize"));
|
|
226
|
+
|
|
227
|
+
await waitFor(() =>
|
|
228
|
+
expect(screen.getByLabelText("Next")).toBeInTheDocument()
|
|
229
|
+
);
|
|
230
|
+
expect(screen.queryByLabelText("Previous")).not.toBeInTheDocument();
|
|
220
231
|
});
|
|
221
232
|
|
|
222
|
-
it("
|
|
233
|
+
it("shows Previous after moving forward, then hides it after returning to first slide", async () => {
|
|
223
234
|
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
224
235
|
...sampleProduct,
|
|
225
236
|
sys: { id: `prod-${i}` },
|
|
226
237
|
}));
|
|
227
238
|
const fields = createProductFields(items);
|
|
228
239
|
render(<ProductCardCarousel fields={fields} />);
|
|
229
|
-
|
|
230
|
-
fireEvent
|
|
231
|
-
|
|
240
|
+
|
|
241
|
+
fireEvent(window, new Event("resize"));
|
|
242
|
+
|
|
243
|
+
const nextBtn = await screen.findByLabelText("Next");
|
|
244
|
+
fireEvent.click(nextBtn);
|
|
245
|
+
|
|
246
|
+
const prevBtn = await screen.findByLabelText("Previous");
|
|
232
247
|
expect(prevBtn).toBeInTheDocument();
|
|
248
|
+
|
|
249
|
+
fireEvent.click(prevBtn);
|
|
250
|
+
|
|
251
|
+
expect(screen.queryByLabelText("Previous")).not.toBeInTheDocument();
|
|
233
252
|
});
|
|
234
253
|
|
|
235
|
-
it("calls nextSlide on Next button click", () => {
|
|
254
|
+
it("calls nextSlide on Next button click", async () => {
|
|
236
255
|
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
237
256
|
...sampleProduct,
|
|
238
257
|
sys: { id: `prod-${i}` },
|
|
239
258
|
}));
|
|
240
259
|
const fields = createProductFields(items);
|
|
241
260
|
render(<ProductCardCarousel fields={fields} />);
|
|
242
|
-
|
|
261
|
+
|
|
262
|
+
fireEvent(window, new Event("resize"));
|
|
263
|
+
|
|
264
|
+
const nextBtn = await screen.findByLabelText("Next");
|
|
243
265
|
fireEvent.click(nextBtn);
|
|
266
|
+
|
|
244
267
|
expect(nextBtn).toBeInTheDocument();
|
|
245
268
|
});
|
|
246
269
|
|
|
@@ -152,13 +152,12 @@ describe("Carousel", () => {
|
|
|
152
152
|
expect(screen.getByTestId("product-card-carousel")).toBeInTheDocument();
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
it("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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", () => {
|