@windstream/react-shared-components 0.2.72 → 0.2.73

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.
@@ -106,7 +106,7 @@ describe("AnchoredBottomBanner", () => {
106
106
  <AnchoredBottomBanner backgroundColor="blue" />
107
107
  );
108
108
  expect(
109
- container.querySelector(".bg-bg-fill-brand-supporting")
109
+ container.querySelector(".bg-bg-fill-brand-info")
110
110
  ).toBeInTheDocument();
111
111
  });
112
112
 
@@ -306,6 +306,7 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
306
306
  scheduledStart: "2026-05-18",
307
307
  scheduleEnd: "2026-05-20",
308
308
  daysToShow: ["ALL"],
309
+ enableCountdownTimer: true,
309
310
  countdownStartDateTime: "2026-05-19T11:00:00Z",
310
311
  countdownEndDateTime: "2026-05-19T13:00:00Z",
311
312
  ctaButtonLabel: "Flash sale",
@@ -327,6 +328,7 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
327
328
  {
328
329
  scheduledStart: "2026-05-17",
329
330
  scheduleEnd: "2026-05-19",
331
+ enableCountdownTimer: true,
330
332
  countdownEndDateTime: "2026-05-19T13:00:00Z",
331
333
  ctaButtonLabel: "Last day",
332
334
  },
@@ -383,6 +385,7 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
383
385
  scheduledStart: "2026-05-18",
384
386
  scheduleEnd: "2026-05-20",
385
387
  daysToShow: ["TUE"],
388
+ enableCountdownTimer: true,
386
389
  countdownEndDateTime: "2026-05-19T13:00:00Z",
387
390
  ctaButtonLabel: "Tuesday deal",
388
391
  },
@@ -403,6 +406,7 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
403
406
  {
404
407
  scheduledStart: "2026-05-18",
405
408
  scheduleEnd: "2026-05-20",
409
+ enableCountdownTimer: true,
406
410
  countdownEndDateTime: "2026-05-19T12:00:10Z",
407
411
  ctaButtonLabel: "Active message",
408
412
  },
@@ -414,6 +418,87 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
414
418
  expect(screen.getByText(/00H : 00M : 10/)).toBeInTheDocument();
415
419
  });
416
420
 
421
+ it("does not show the countdown when the active message disables the timer", () => {
422
+ render(
423
+ <AnchoredBottomBanner
424
+ ctaButtonLabel="Default label"
425
+ scheduledMessages={[
426
+ {
427
+ scheduledStart: "2026-05-18",
428
+ scheduleEnd: "2026-05-20",
429
+ enableCountdownTimer: false,
430
+ countdownEndDateTime: "2026-05-19T13:00:00Z",
431
+ ctaButtonLabel: "No timer message",
432
+ },
433
+ ]}
434
+ />
435
+ );
436
+ expect(screen.getByText("No timer message")).toBeInTheDocument();
437
+ expect(screen.queryByText(/H :/)).not.toBeInTheDocument();
438
+ });
439
+
440
+ it("does not show the active message's countdown when the flag is unset", () => {
441
+ render(
442
+ <AnchoredBottomBanner
443
+ ctaButtonLabel="Default label"
444
+ scheduledMessages={[
445
+ {
446
+ scheduledStart: "2026-05-18",
447
+ scheduleEnd: "2026-05-20",
448
+ countdownEndDateTime: "2026-05-19T13:00:00Z",
449
+ ctaButtonLabel: "Unset timer message",
450
+ },
451
+ ]}
452
+ />
453
+ );
454
+ expect(screen.getByText("Unset timer message")).toBeInTheDocument();
455
+ expect(screen.queryByText(/H :/)).not.toBeInTheDocument();
456
+ });
457
+
458
+ it("does not inherit the parent's enableCountdownTimer for an active message", () => {
459
+ render(
460
+ <AnchoredBottomBanner
461
+ ctaButtonLabel="Default label"
462
+ enableCountdownTimer={true}
463
+ countdownEndDateTime="2026-05-19T13:00:00Z"
464
+ scheduledMessages={[
465
+ {
466
+ scheduledStart: "2026-05-18",
467
+ scheduleEnd: "2026-05-20",
468
+ enableCountdownTimer: false,
469
+ countdownEndDateTime: "2026-05-19T13:00:00Z",
470
+ ctaButtonLabel: "Active message",
471
+ },
472
+ ]}
473
+ />
474
+ );
475
+ // The parent timer is enabled, but the active message disables it -> no countdown.
476
+ expect(screen.getByText("Active message")).toBeInTheDocument();
477
+ expect(screen.queryByText(/H :/)).not.toBeInTheDocument();
478
+ });
479
+
480
+ it("keeps the message active after its countdown ends, hiding only the countdown", () => {
481
+ render(
482
+ <AnchoredBottomBanner
483
+ ctaButtonLabel="Default label"
484
+ scheduledMessages={[
485
+ {
486
+ scheduledStart: "2026-05-18",
487
+ scheduleEnd: "2026-05-20",
488
+ enableCountdownTimer: true,
489
+ // Countdown already ended (11:00Z) while now is 12:00Z, but the
490
+ // display window (05-18..05-20) is still open.
491
+ countdownEndDateTime: "2026-05-19T11:00:00Z",
492
+ ctaButtonLabel: "Still showing message",
493
+ },
494
+ ]}
495
+ />
496
+ );
497
+ expect(screen.getByText("Still showing message")).toBeInTheDocument();
498
+ expect(screen.queryByText("Default label")).not.toBeInTheDocument();
499
+ expect(screen.queryByText(/H :/)).not.toBeInTheDocument();
500
+ });
501
+
417
502
  it("selects the first message whose window contains today", () => {
418
503
  render(
419
504
  <AnchoredBottomBanner
@@ -441,6 +526,48 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
441
526
  expect(screen.queryByText("Past message")).not.toBeInTheDocument();
442
527
  expect(screen.queryByText("Future message")).not.toBeInTheDocument();
443
528
  });
529
+
530
+ it("uses the active message's background, not the parent's", () => {
531
+ const { container } = render(
532
+ <AnchoredBottomBanner
533
+ backgroundColor="navy"
534
+ scheduledMessages={[
535
+ {
536
+ scheduledStart: "2026-05-18",
537
+ scheduleEnd: "2026-05-20",
538
+ backgroundColor: "green",
539
+ ctaButtonLabel: "Active message",
540
+ },
541
+ ]}
542
+ />
543
+ );
544
+ expect(container.querySelector(".bg-bg-fill-success")).toBeInTheDocument();
545
+ expect(
546
+ container.querySelector(".bg-bg-fill-inverse")
547
+ ).not.toBeInTheDocument();
548
+ });
549
+
550
+ it("uses the default background when the active message has none", () => {
551
+ const { container } = render(
552
+ <AnchoredBottomBanner
553
+ backgroundColor="navy"
554
+ scheduledMessages={[
555
+ {
556
+ scheduledStart: "2026-05-18",
557
+ scheduleEnd: "2026-05-20",
558
+ ctaButtonLabel: "Active message",
559
+ },
560
+ ]}
561
+ />
562
+ );
563
+ // No override on the message -> same default as a parent with no background.
564
+ expect(
565
+ container.querySelector(".bg-bg-fill-inverse")
566
+ ).not.toBeInTheDocument();
567
+ expect(
568
+ container.querySelector(".bg-bg-fill-brand-accent")
569
+ ).toBeInTheDocument();
570
+ });
444
571
  });
445
572
 
446
573
  describe("AnchoredBottomBanner scheduling timezone (America/New_York)", () => {
@@ -113,23 +113,16 @@ function dayMatches(daysToShow: string[] | undefined, nowMs: number): boolean {
113
113
  return normalized.includes(full) || normalized.includes(full.slice(0, 3));
114
114
  }
115
115
 
116
- // Active = today is within [scheduledStart, scheduleEnd], allowed by daysToShow,
117
- // and the message's countdown (when set) has not already ended. An expired
118
- // countdown makes the message inactive so it can't block another running one.
116
+ // Active = today is within [scheduledStart, scheduleEnd] and allowed by
117
+ // daysToShow. The countdown is independent: it hides once its own end passes
118
+ // (see the `countdown` memo), but the message itself stays active across its
119
+ // whole display window.
119
120
  function isMessageActive(message: ScheduledMessage, nowMs: number): boolean {
120
121
  const startMs = parseDisplayStart(message.scheduledStart);
121
122
  const endMs = parseDisplayEnd(message.scheduleEnd);
122
123
  const afterStart = startMs === undefined || nowMs >= startMs;
123
124
  const beforeEnd = endMs === undefined || nowMs < endMs;
124
- const countdownEndMs = parseCountdownDateTime(message.countdownEndDateTime);
125
- const countdownNotExpired =
126
- countdownEndMs === undefined || nowMs < countdownEndMs;
127
- return (
128
- afterStart &&
129
- beforeEnd &&
130
- countdownNotExpired &&
131
- dayMatches(message.daysToShow, nowMs)
132
- );
125
+ return afterStart && beforeEnd && dayMatches(message.daysToShow, nowMs);
133
126
  }
134
127
 
135
128
  function formatCountdown(totalSeconds: number) {
@@ -159,22 +152,12 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
159
152
  const backGroundColorClasses = {
160
153
  navy: "bg-bg-fill-inverse",
161
154
  green: "bg-bg-fill-success",
162
- blue: "bg-bg-fill-brand-supporting",
155
+ blue: "bg-bg-fill-brand-info",
163
156
  purple: "bg-bg-fill-brand-tertiary",
164
157
  yellow: "bg-bg-fill-brand-accent",
165
158
  white: "bg-white",
166
159
  };
167
160
 
168
- const bgClass = backgroundColor
169
- ? backGroundColorClasses[backgroundColor]
170
- : "bg-bg-fill-brand-accent";
171
-
172
- const isLightBackground =
173
- backgroundColor === "yellow" ||
174
- backgroundColor === "white" ||
175
- !backgroundColor;
176
- const textColorClass = isLightBackground ? "text-text-primary" : "text-white";
177
-
178
161
  const [nowMs, setNowMs] = useState(() => Date.now());
179
162
  const intervalRef = useRef<number | null>(null);
180
163
 
@@ -198,7 +181,14 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
198
181
  // records the pre-mount hasActiveMessage:false fallback.
199
182
  if (!hasMounted) return;
200
183
  onResolved?.({ hasActiveMessage, activeMessage, applicableWeekday });
201
- }, [hasMounted, hasActiveMessage, activeMessage, applicableWeekday, onResolved]);
184
+ }, [
185
+ hasMounted,
186
+ hasActiveMessage,
187
+ activeMessage,
188
+ applicableWeekday,
189
+ scheduledMessages,
190
+ onResolved,
191
+ ]);
202
192
 
203
193
  // Latest display-window end, so we keep ticking to let messages activate/expire.
204
194
  const scheduleMaxEndMs = useMemo(() => {
@@ -217,6 +207,13 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
217
207
  ? activeMessage?.countdownEndDateTime
218
208
  : countdownEndDateTime;
219
209
 
210
+ // The timer flag is never inherited: an active message uses its own
211
+ // enableCountdownTimer (undefined = off, same as the banner level); otherwise
212
+ // the banner's top-level flag applies.
213
+ const effectiveEnableCountdownTimer = hasActiveMessage
214
+ ? activeMessage?.enableCountdownTimer
215
+ : enableCountdownTimer;
216
+
220
217
  const endMs = useMemo(
221
218
  () => parseCountdownDateTime(effectiveCountdownEndDateTime),
222
219
  [effectiveCountdownEndDateTime]
@@ -227,8 +224,9 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
227
224
  );
228
225
 
229
226
  const isTimerValid = useMemo(() => {
230
- // Active messages always enable the timer; default uses enableCountdownTimer.
231
- if (!hasActiveMessage && !enableCountdownTimer) return false;
227
+ // Timer shows only when explicitly enabled for the resolved source
228
+ // (active message's own flag, or the banner's flag when none is active).
229
+ if (!effectiveEnableCountdownTimer) return false;
232
230
  if (endMs === undefined) return false;
233
231
  if (effectiveCountdownStartDateTime && startMs === undefined) return false;
234
232
  if (startMs !== undefined && startMs >= endMs) {
@@ -240,8 +238,7 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
240
238
  }
241
239
  return true;
242
240
  }, [
243
- hasActiveMessage,
244
- enableCountdownTimer,
241
+ effectiveEnableCountdownTimer,
245
242
  endMs,
246
243
  startMs,
247
244
  effectiveCountdownStartDateTime,
@@ -309,6 +306,20 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
309
306
  ? activeMessage?.ctaButtonTarget
310
307
  : ctaButtonTarget;
311
308
 
309
+ // Active message owns its background; when unset it uses the same default as
310
+ // the parent-with-no-background (bg-fill-brand-accent), not the parent's color.
311
+ const effectiveBackgroundColor = hasActiveMessage
312
+ ? activeMessage?.backgroundColor
313
+ : backgroundColor;
314
+ const bgClass = effectiveBackgroundColor
315
+ ? backGroundColorClasses[effectiveBackgroundColor]
316
+ : "bg-bg-fill-brand-accent";
317
+ const isLightBackground =
318
+ effectiveBackgroundColor === "yellow" ||
319
+ effectiveBackgroundColor === "white" ||
320
+ !effectiveBackgroundColor;
321
+ const textColorClass = isLightBackground ? "text-text-primary" : "text-white";
322
+
312
323
  return (
313
324
  <section id={anchorId}>
314
325
  <div
@@ -25,10 +25,14 @@ export interface ScheduledMessage {
25
25
  scheduleEnd?: string;
26
26
  // Weekdays the message may show on within the window. "ALL" (or empty) = every day.
27
27
  daysToShow?: ScheduledMessageDay[];
28
+ // Per-message timer toggle. The message never inherits the banner's
29
+ // enableCountdownTimer; the countdown shows only when this is explicitly true.
30
+ enableCountdownTimer?: boolean;
28
31
  // Countdown timer window (full timestamps).
29
32
  countdownStartDateTime?: string;
30
33
  countdownEndDateTime?: string;
31
34
  iconName?: string;
35
+ backgroundColor?: AnchoredBottomBannerProps["backgroundColor"];
32
36
  ctaButtonLabel?: string;
33
37
  ctaSuffixText?: string;
34
38
  ctaButtonLink?: string;
@@ -104,6 +104,7 @@
104
104
  --color-bg-fill-brand-hover: #45c187;
105
105
  --color-bg-fill-brand-selected: #24a76a;
106
106
  --color-bg-fill-brand-supporting: #07b2e2;
107
+ --color-bg-fill-brand-info: #07b2e2;
107
108
  --color-bg-fill-brand-tertiary: #931d69;
108
109
  --color-bg-fill-translucent: rgba(255, 255, 255, 0.5);
109
110
  --color-bg-fill-critical: #d40000;
@@ -243,6 +244,7 @@
243
244
  --color-bg-fill-brand-hover: #30aec5;
244
245
  --color-bg-fill-brand-selected: #007c93;
245
246
  --color-bg-fill-brand-supporting: #40afc9;
247
+ --color-bg-fill-brand-info: #40afc9;
246
248
  --color-bg-fill-brand-tertiary: #a33895;
247
249
  --color-bg-fill-inverse: #231f20;
248
250
  --color-bg-fill-inverse-active: #231f20;
@@ -300,6 +302,7 @@
300
302
  --color-bg-fill-brand-selected: #0393ba;
301
303
 
302
304
  --color-bg-fill-brand-supporting: #26b170;
305
+ --color-bg-fill-brand-info: #0393ba;
303
306
 
304
307
  /* Text colors */
305
308
  --color-text-brand: #0393ba;
@@ -158,6 +158,7 @@ module.exports = {
158
158
  hover: "var(--color-bg-fill-brand-hover)",
159
159
  selected: "var(--color-bg-fill-brand-selected)",
160
160
  supporting: "var(--color-bg-fill-brand-supporting)",
161
+ info: "var(--color-bg-fill-brand-info)",
161
162
  tertiary: "var(--color-bg-fill-brand-tertiary)",
162
163
  },
163
164
  critical: {