@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.
- package/dist/contentful/index.d.ts +2 -0
- package/dist/contentful/index.esm.js +1 -1
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +1 -1
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +128 -1
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +39 -28
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +4 -0
- package/src/styles/globals.css +3 -0
- package/tailwind.config.cjs +1 -0
|
@@ -106,7 +106,7 @@ describe("AnchoredBottomBanner", () => {
|
|
|
106
106
|
<AnchoredBottomBanner backgroundColor="blue" />
|
|
107
107
|
);
|
|
108
108
|
expect(
|
|
109
|
-
container.querySelector(".bg-bg-fill-brand-
|
|
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]
|
|
117
|
-
//
|
|
118
|
-
// countdown
|
|
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
|
-
|
|
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-
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
231
|
-
|
|
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
|
-
|
|
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;
|
package/src/styles/globals.css
CHANGED
|
@@ -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;
|
package/tailwind.config.cjs
CHANGED
|
@@ -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: {
|