@windstream/react-shared-components 0.2.82 → 0.2.83
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 +4 -0
- package/dist/contentful/index.esm.js +1 -1
- 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/styles.css +1 -1
- package/package.json +1 -1
- package/src/contentful/blocks/award-marquee/AwardMarquee.stories.tsx +45 -1
- package/src/contentful/blocks/award-marquee/index.test.tsx +203 -5
- package/src/contentful/blocks/award-marquee/index.tsx +77 -26
- package/src/contentful/blocks/award-marquee/types.ts +6 -0
|
@@ -92,17 +92,128 @@ describe("AwardMarquee", () => {
|
|
|
92
92
|
expect(icon.src).toContain("icon-0.png");
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
it("renders intrinsic icon dimensions when provided", () => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
it("renders intrinsic icon dimensions when provided (multi-entry, unchanged sizing)", () => {
|
|
96
|
+
const items = buildItems(2).map(item => ({
|
|
97
|
+
...item,
|
|
98
|
+
iconWidth: 320,
|
|
99
|
+
iconHeight: 180,
|
|
100
|
+
}));
|
|
101
|
+
render(<AwardMarquee items={items} />);
|
|
100
102
|
const icon = screen.getByAltText("Award 0");
|
|
101
103
|
expect(icon).toHaveAttribute("width", "320");
|
|
102
104
|
expect(icon).toHaveAttribute("height", "180");
|
|
103
105
|
expect(icon).toHaveClass("w-[200px]");
|
|
104
106
|
});
|
|
105
107
|
|
|
108
|
+
it("renders intrinsic icon dimensions when provided (single static entry)", () => {
|
|
109
|
+
const [item] = buildItems(1);
|
|
110
|
+
render(
|
|
111
|
+
<AwardMarquee items={[{ ...item, iconWidth: 1441, iconHeight: 622 }]} />
|
|
112
|
+
);
|
|
113
|
+
const icon = screen.getByAltText("Award 0");
|
|
114
|
+
expect(icon).toHaveAttribute("width", "1441");
|
|
115
|
+
expect(icon).toHaveAttribute("height", "622");
|
|
116
|
+
expect(icon).toHaveClass("w-full", "max-w-[1100px]", "h-auto");
|
|
117
|
+
expect(icon).not.toHaveClass("w-[200px]");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("renders a distinct mobile image for a single static entry when one is supplied", () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<AwardMarquee
|
|
123
|
+
items={[
|
|
124
|
+
{
|
|
125
|
+
iconUrl: "https://example.com/desktop-logo.png",
|
|
126
|
+
iconAlt: "Desktop Logo",
|
|
127
|
+
iconWidth: 1441,
|
|
128
|
+
iconHeight: 622,
|
|
129
|
+
mobileIconUrl: "https://example.com/mobile-logo.png",
|
|
130
|
+
mobileIconAlt: "Mobile Logo",
|
|
131
|
+
mobileIconWidth: 600,
|
|
132
|
+
mobileIconHeight: 400,
|
|
133
|
+
},
|
|
134
|
+
]}
|
|
135
|
+
scroll={false}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const mobileIcon = screen.getByAltText("Mobile Logo") as HTMLImageElement;
|
|
140
|
+
const desktopIcon = screen.getByAltText("Desktop Logo") as HTMLImageElement;
|
|
141
|
+
|
|
142
|
+
expect(mobileIcon.src).toContain("mobile-logo.png");
|
|
143
|
+
expect(mobileIcon).toHaveAttribute("width", "600");
|
|
144
|
+
expect(mobileIcon).toHaveAttribute("height", "400");
|
|
145
|
+
expect(mobileIcon).toHaveClass("sm:hidden", "max-w-[1100px]", "h-auto");
|
|
146
|
+
|
|
147
|
+
expect(desktopIcon.src).toContain("desktop-logo.png");
|
|
148
|
+
expect(desktopIcon).toHaveAttribute("width", "1441");
|
|
149
|
+
expect(desktopIcon).toHaveAttribute("height", "622");
|
|
150
|
+
expect(desktopIcon).toHaveClass(
|
|
151
|
+
"hidden",
|
|
152
|
+
"sm:block",
|
|
153
|
+
"max-w-[1100px]",
|
|
154
|
+
"h-auto"
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Exactly one wrapper div (not duplicated) still holds both images.
|
|
158
|
+
expect(
|
|
159
|
+
container.querySelectorAll("#Award-Marquee > div > div")
|
|
160
|
+
).toHaveLength(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("falls back to the desktop image at every breakpoint when no mobile image is supplied", () => {
|
|
164
|
+
render(
|
|
165
|
+
<AwardMarquee
|
|
166
|
+
items={[
|
|
167
|
+
{
|
|
168
|
+
iconUrl: "https://example.com/desktop-logo.png",
|
|
169
|
+
iconAlt: "Desktop Logo",
|
|
170
|
+
},
|
|
171
|
+
]}
|
|
172
|
+
scroll={false}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const icon = screen.getByAltText("Desktop Logo");
|
|
177
|
+
expect(icon).not.toHaveClass("sm:hidden");
|
|
178
|
+
expect(icon).not.toHaveClass("hidden", "sm:block");
|
|
179
|
+
expect(screen.queryAllByAltText("Desktop Logo")).toHaveLength(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("ignores the mobile image field for multi-entry and animated-carousel rendering", () => {
|
|
183
|
+
const itemWithMobile = {
|
|
184
|
+
iconUrl: "https://example.com/desktop-logo.png",
|
|
185
|
+
iconAlt: "Desktop Logo",
|
|
186
|
+
mobileIconUrl: "https://example.com/mobile-logo.png",
|
|
187
|
+
mobileIconAlt: "Mobile Logo",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const { container: multiContainer } = render(
|
|
191
|
+
<AwardMarquee
|
|
192
|
+
items={[itemWithMobile, { ...buildItems(1)[0] }]}
|
|
193
|
+
scroll={false}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
expect(multiContainer.querySelectorAll('[alt="Mobile Logo"]')).toHaveLength(
|
|
197
|
+
0
|
|
198
|
+
);
|
|
199
|
+
expect(screen.getByAltText("Desktop Logo")).not.toHaveClass("sm:hidden");
|
|
200
|
+
|
|
201
|
+
const { container: carouselContainer } = render(
|
|
202
|
+
<AwardMarquee items={[itemWithMobile]} scroll={true} />
|
|
203
|
+
);
|
|
204
|
+
// Split view for a single item never applies the mobile/desktop swap:
|
|
205
|
+
// the mobile half is already handled by the separate animated carousel,
|
|
206
|
+
// and the desktop half shows a single, unswapped image.
|
|
207
|
+
expect(
|
|
208
|
+
carouselContainer.querySelectorAll('[alt="Mobile Logo"]')
|
|
209
|
+
).toHaveLength(0);
|
|
210
|
+
const desktopHalfIcon = carouselContainer.querySelector(
|
|
211
|
+
".hidden.sm\\:block img"
|
|
212
|
+
);
|
|
213
|
+
expect(desktopHalfIcon).toHaveClass("w-full", "max-w-[1100px]", "h-auto");
|
|
214
|
+
expect(desktopHalfIcon).not.toHaveClass("sm:hidden");
|
|
215
|
+
});
|
|
216
|
+
|
|
106
217
|
it("omits intrinsic icon dimensions when unavailable", () => {
|
|
107
218
|
render(<AwardMarquee items={buildItems(1)} />);
|
|
108
219
|
const icon = screen.getByAltText("Award 0");
|
|
@@ -139,6 +250,93 @@ describe("AwardMarquee", () => {
|
|
|
139
250
|
expect(screen.getAllByText("Award 0")).toHaveLength(3);
|
|
140
251
|
});
|
|
141
252
|
|
|
253
|
+
it("centers a single static entry and hides the caption block when it has no caption text", () => {
|
|
254
|
+
const { container } = render(
|
|
255
|
+
<AwardMarquee
|
|
256
|
+
items={[{ iconUrl: "https://example.com/logo.png", iconAlt: "Logo" }]}
|
|
257
|
+
scroll={false}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
const wrapper = container.querySelector("#Award-Marquee > div > div");
|
|
261
|
+
expect(wrapper).toHaveClass("mx-auto", "w-full", "justify-center");
|
|
262
|
+
expect(wrapper).not.toHaveClass("ml-4", "w-auto");
|
|
263
|
+
|
|
264
|
+
const icon = screen.getByAltText("Logo");
|
|
265
|
+
expect(icon).toHaveClass("w-full", "max-w-[1100px]", "h-auto");
|
|
266
|
+
expect(icon).not.toHaveClass("w-[200px]");
|
|
267
|
+
|
|
268
|
+
// No title/description -> the 250px caption block takes no space.
|
|
269
|
+
expect(container.querySelector(".w-\\[250px\\]")).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("still renders the caption block for a single static entry that has caption text", () => {
|
|
273
|
+
const { container } = render(
|
|
274
|
+
<AwardMarquee items={buildItems(1)} scroll={false} />
|
|
275
|
+
);
|
|
276
|
+
expect(container.querySelector(".w-\\[250px\\]")).not.toBeNull();
|
|
277
|
+
expect(screen.getByText("Award 0")).toBeInTheDocument();
|
|
278
|
+
expect(screen.getByText("Description 0")).toBeInTheDocument();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("omits the caption block for any entry lacking caption text, even with multiple entries", () => {
|
|
282
|
+
const items: AwardMarqueeItem[] = [
|
|
283
|
+
...buildItems(1),
|
|
284
|
+
{ iconUrl: "https://example.com/logo-2.png", iconAlt: "Logo 2" },
|
|
285
|
+
];
|
|
286
|
+
const { container } = render(<AwardMarquee items={items} scroll={false} />);
|
|
287
|
+
// Only the first item (with title/description) gets a caption block.
|
|
288
|
+
expect(container.querySelectorAll(".w-\\[250px\\]")).toHaveLength(1);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("leaves multi-entry static rendering unchanged (fixed-width icon, ml-4 wrapper)", () => {
|
|
292
|
+
const { container } = render(
|
|
293
|
+
<AwardMarquee items={buildItems(2)} scroll={false} />
|
|
294
|
+
);
|
|
295
|
+
const wrappers = container.querySelectorAll("#Award-Marquee > div > div");
|
|
296
|
+
wrappers.forEach(wrapper => {
|
|
297
|
+
expect(wrapper).toHaveClass("ml-4", "w-auto");
|
|
298
|
+
expect(wrapper).not.toHaveClass("mx-auto", "justify-center");
|
|
299
|
+
});
|
|
300
|
+
const icon = screen.getByAltText("Award 0");
|
|
301
|
+
expect(icon).toHaveClass("w-[200px]");
|
|
302
|
+
expect(icon).not.toHaveClass("max-w-[1100px]");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("leaves the multi-entry animated carousel unchanged regardless of single-item styling", () => {
|
|
306
|
+
const { container } = render(
|
|
307
|
+
<AwardMarquee items={buildItems(5)} scroll={true} />
|
|
308
|
+
);
|
|
309
|
+
const icons = screen.getAllByAltText(/Award \d/);
|
|
310
|
+
icons.forEach(icon => expect(icon).toHaveClass("w-[200px]"));
|
|
311
|
+
const wrappers = container.querySelectorAll(".animate-award-marquee > div");
|
|
312
|
+
wrappers.forEach(wrapper => {
|
|
313
|
+
expect(wrapper).toHaveClass("ml-4", "w-auto");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("applies the single-static treatment only to the desktop (static) half of a split view", () => {
|
|
318
|
+
const { container } = render(
|
|
319
|
+
<AwardMarquee
|
|
320
|
+
items={[{ iconUrl: "https://example.com/logo.png", iconAlt: "Logo" }]}
|
|
321
|
+
scroll={true}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
const mobileIcon = container.querySelector(
|
|
325
|
+
".sm\\:hidden img"
|
|
326
|
+
) as HTMLImageElement;
|
|
327
|
+
const desktopIcon = container.querySelector(
|
|
328
|
+
".hidden.sm\\:block img"
|
|
329
|
+
) as HTMLImageElement;
|
|
330
|
+
|
|
331
|
+
// Mobile (animated) half keeps the original fixed-width treatment.
|
|
332
|
+
expect(mobileIcon).toHaveClass("w-[200px]");
|
|
333
|
+
expect(mobileIcon).not.toHaveClass("max-w-[1100px]");
|
|
334
|
+
|
|
335
|
+
// Desktop (static) half gets the large, centered single-entry treatment.
|
|
336
|
+
expect(desktopIcon).toHaveClass("w-full", "max-w-[1100px]", "h-auto");
|
|
337
|
+
expect(desktopIcon).not.toHaveClass("w-[200px]");
|
|
338
|
+
});
|
|
339
|
+
|
|
142
340
|
it("applies the anchorId to the wrapper in every render branch", () => {
|
|
143
341
|
const staticView = render(
|
|
144
342
|
<AwardMarquee
|
|
@@ -26,32 +26,76 @@ const renderAwardItem = (
|
|
|
26
26
|
iconAlt,
|
|
27
27
|
iconWidth,
|
|
28
28
|
iconHeight,
|
|
29
|
+
mobileIconUrl,
|
|
30
|
+
mobileIconAlt,
|
|
31
|
+
mobileIconWidth,
|
|
32
|
+
mobileIconHeight,
|
|
29
33
|
title,
|
|
30
34
|
description,
|
|
31
35
|
}: AwardMarqueeItem,
|
|
32
|
-
key: React.Key
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
key: React.Key,
|
|
37
|
+
// Only the single-entry, non-animated (static) rendering gets the large,
|
|
38
|
+
// centered treatment; the animated carousel is unaffected.
|
|
39
|
+
isSingleStatic = false,
|
|
40
|
+
// A distinct mobile image is only honored for the standalone static render
|
|
41
|
+
// (scroll={false}). When this is the "desktop half" of a responsive split
|
|
42
|
+
// (scroll={true} with <=3 items), the sm:hidden mobile side is already
|
|
43
|
+
// handled by the separate animated half, so a nested swap here would never
|
|
44
|
+
// be visible at any viewport.
|
|
45
|
+
allowMobileSwap = false
|
|
46
|
+
) => {
|
|
47
|
+
const hasCaption = Boolean(title || description);
|
|
48
|
+
const hasMobileIcon = allowMobileSwap && Boolean(mobileIconUrl);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
key={key}
|
|
53
|
+
className={cx(
|
|
54
|
+
"flex flex-shrink-0 items-center whitespace-nowrap",
|
|
55
|
+
isSingleStatic ? "mx-auto w-full justify-center" : "ml-4 w-auto"
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{hasMobileIcon ? (
|
|
59
|
+
<>
|
|
60
|
+
<img
|
|
61
|
+
src={mobileIconUrl}
|
|
62
|
+
alt={mobileIconAlt ?? iconAlt ?? ""}
|
|
63
|
+
width={mobileIconWidth}
|
|
64
|
+
height={mobileIconHeight}
|
|
65
|
+
className="h-auto w-full max-w-[1100px] sm:hidden"
|
|
66
|
+
/>
|
|
67
|
+
<img
|
|
68
|
+
src={iconUrl}
|
|
69
|
+
alt={iconAlt ?? ""}
|
|
70
|
+
width={iconWidth}
|
|
71
|
+
height={iconHeight}
|
|
72
|
+
className="hidden h-auto w-full max-w-[1100px] sm:block"
|
|
73
|
+
/>
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<img
|
|
77
|
+
src={iconUrl}
|
|
78
|
+
alt={iconAlt ?? ""}
|
|
79
|
+
width={iconWidth}
|
|
80
|
+
height={iconHeight}
|
|
81
|
+
className={
|
|
82
|
+
isSingleStatic ? "h-auto w-full max-w-[1100px]" : "w-[200px]"
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
{hasCaption && (
|
|
87
|
+
<div className="w-[250px] text-center">
|
|
88
|
+
<p className="flex justify-center text-center text-body3 font-bold">
|
|
89
|
+
{title}
|
|
90
|
+
</p>
|
|
91
|
+
<p className="flex flex-wrap whitespace-normal text-center text-footnote">
|
|
92
|
+
{description}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
52
96
|
</div>
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
);
|
|
98
|
+
};
|
|
55
99
|
|
|
56
100
|
export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
57
101
|
anchorId = "Award-Marquee",
|
|
@@ -91,20 +135,27 @@ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
|
91
135
|
);
|
|
92
136
|
};
|
|
93
137
|
|
|
94
|
-
const staticMarquee = (
|
|
138
|
+
const staticMarquee = (allowMobileSwap: boolean) => (
|
|
95
139
|
<div className="flex flex-nowrap items-center justify-around overflow-clip">
|
|
96
|
-
{items.map((item, index) =>
|
|
140
|
+
{items.map((item, index) =>
|
|
141
|
+
renderAwardItem(
|
|
142
|
+
item,
|
|
143
|
+
index,
|
|
144
|
+
items.length === 1,
|
|
145
|
+
allowMobileSwap && items.length === 1
|
|
146
|
+
)
|
|
147
|
+
)}
|
|
97
148
|
</div>
|
|
98
149
|
);
|
|
99
150
|
|
|
100
151
|
const marquee = !scroll ? (
|
|
101
|
-
staticMarquee
|
|
152
|
+
staticMarquee(true)
|
|
102
153
|
) : scrollAvailable ? (
|
|
103
154
|
animatedMarquee()
|
|
104
155
|
) : (
|
|
105
156
|
<>
|
|
106
157
|
<div className="sm:hidden">{animatedMarquee()}</div>
|
|
107
|
-
<div className="hidden sm:block">{staticMarquee}</div>
|
|
158
|
+
<div className="hidden sm:block">{staticMarquee(false)}</div>
|
|
108
159
|
</>
|
|
109
160
|
);
|
|
110
161
|
|
|
@@ -7,6 +7,12 @@ export type AwardMarqueeItem = {
|
|
|
7
7
|
iconAlt?: string;
|
|
8
8
|
iconWidth?: number;
|
|
9
9
|
iconHeight?: number;
|
|
10
|
+
// Optional mobile-specific image for the single-entry static display.
|
|
11
|
+
// Falls back to iconUrl at every breakpoint when omitted.
|
|
12
|
+
mobileIconUrl?: string;
|
|
13
|
+
mobileIconAlt?: string;
|
|
14
|
+
mobileIconWidth?: number;
|
|
15
|
+
mobileIconHeight?: number;
|
|
10
16
|
title?: string;
|
|
11
17
|
description?: string;
|
|
12
18
|
};
|