@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.
@@ -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 [item] = buildItems(1);
97
- render(
98
- <AwardMarquee items={[{ ...item, iconWidth: 320, iconHeight: 180 }]} />
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
- <div
35
- key={key}
36
- className="ml-4 flex w-auto flex-shrink-0 items-center whitespace-nowrap"
37
- >
38
- <img
39
- src={iconUrl}
40
- alt={iconAlt ?? ""}
41
- width={iconWidth}
42
- height={iconHeight}
43
- className="w-[200px]"
44
- />
45
- <div className="w-[250px] text-center">
46
- <p className="flex justify-center text-center text-body3 font-bold">
47
- {title}
48
- </p>
49
- <p className="flex flex-wrap whitespace-normal text-center text-footnote">
50
- {description}
51
- </p>
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
- </div>
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) => renderAwardItem(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
  };