@windstream/react-shared-components 0.2.75 → 0.2.76

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.75",
3
+ "version": "0.2.76",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -78,7 +78,17 @@ export function AnimationWrapper({
78
78
  transition,
79
79
  ...motionProps
80
80
  }: AnimationWrapperProps) {
81
- const prefersReducedMotion = useReducedMotion();
81
+ // useReducedMotion() reads window.matchMedia, which is unavailable during
82
+ // SSR (always resolves falsy server-side). Gating on it immediately would
83
+ // make the client's first render diverge from the server-rendered markup
84
+ // on any machine with OS-level "Reduce motion" enabled, causing a
85
+ // hydration mismatch (extra framer-motion attributes like tabIndex) that
86
+ // can force React to remount the whole tree mid-click. Only honor it once
87
+ // mounted, so the first client render always matches SSR.
88
+ const [isMounted, setIsMounted] = React.useState(false);
89
+ React.useEffect(() => setIsMounted(true), []);
90
+ const reducedMotionPreferred = useReducedMotion();
91
+ const prefersReducedMotion = isMounted && reducedMotionPreferred;
82
92
 
83
93
  const child = React.Children.only(children) as React.ReactElement<
84
94
  Record<string, unknown>
@@ -13,7 +13,7 @@ const meta: Meta<typeof AwardMarquee> = {
13
13
  page: DocsPage,
14
14
  description: {
15
15
  component:
16
- "Server-rendered marquee of award badges. Animates when `scroll` is set; with 3 or fewer items it animates on mobile and falls back to a static row at the `sm` breakpoint (CSS-driven, no client-side JS).",
16
+ "Server-rendered marquee of award badges. Optional `heading` and `footerText` display gold text above and below the awards, with divider lines around the heading. Animates when `scroll` is set; with 3 or fewer items it animates on mobile and falls back to a static row at the `sm` breakpoint (CSS-driven, no client-side JS).",
17
17
  },
18
18
  },
19
19
  },
@@ -48,3 +48,11 @@ export const CreamBackground: Story = {
48
48
  backgroundColor: "cream",
49
49
  },
50
50
  };
51
+
52
+ export const WithHeadingAndFooter: Story = {
53
+ args: {
54
+ heading: "As seen on",
55
+ footerText: "And over 450+ news sites",
56
+ scroll: false,
57
+ },
58
+ };
@@ -15,6 +15,70 @@ const buildItems = (count: number): AwardMarqueeItem[] =>
15
15
  }));
16
16
 
17
17
  describe("AwardMarquee", () => {
18
+ it.each([
19
+ { scroll: false, count: 2 },
20
+ { scroll: true, count: 5 },
21
+ { scroll: true, count: 2 },
22
+ ])(
23
+ "renders heading and footer once with $count items and scroll=$scroll",
24
+ ({ scroll, count }) => {
25
+ const { container } = render(
26
+ <AwardMarquee
27
+ items={buildItems(count)}
28
+ scroll={scroll}
29
+ heading="As seen on"
30
+ footerText="And over 450+ news sites"
31
+ />
32
+ );
33
+
34
+ const heading = screen.getByText("As seen on");
35
+ const footer = screen.getByText("And over 450+ news sites");
36
+ expect(screen.getAllByText("As seen on")).toHaveLength(1);
37
+ expect(screen.getAllByText("And over 450+ news sites")).toHaveLength(1);
38
+ expect(heading).toHaveClass("heading2");
39
+ expect(footer).toHaveClass("subheading3");
40
+ expect(heading).not.toHaveClass("text-xs");
41
+ expect(footer).not.toHaveClass("text-xs");
42
+ expect(container.querySelector("#Award-Marquee")?.firstElementChild).toBe(
43
+ heading
44
+ );
45
+ expect(container.querySelector("#Award-Marquee")?.lastElementChild).toBe(
46
+ footer
47
+ );
48
+ expect(screen.getAllByText("Award 0").length).toBeGreaterThan(0);
49
+ expect(screen.getAllByText("Description 0").length).toBeGreaterThan(0);
50
+ }
51
+ );
52
+
53
+ it("allows heading and footer to be set independently", () => {
54
+ const { rerender } = render(
55
+ <AwardMarquee items={buildItems(1)} heading="As seen on" />
56
+ );
57
+ expect(screen.getByText("As seen on")).toBeInTheDocument();
58
+ expect(
59
+ screen.queryByText("And over 450+ news sites")
60
+ ).not.toBeInTheDocument();
61
+
62
+ rerender(
63
+ <AwardMarquee
64
+ items={buildItems(1)}
65
+ footerText="And over 450+ news sites"
66
+ />
67
+ );
68
+ expect(screen.queryByText("As seen on")).not.toBeInTheDocument();
69
+ expect(screen.getByText("And over 450+ news sites")).toBeInTheDocument();
70
+ });
71
+
72
+ it("does not add text or divider lines when fields are empty or omitted", () => {
73
+ const { container, rerender } = render(
74
+ <AwardMarquee items={buildItems(1)} />
75
+ );
76
+ const originalMarkup = container.innerHTML;
77
+ rerender(<AwardMarquee items={buildItems(1)} heading="" footerText="" />);
78
+ expect(container.innerHTML).toBe(originalMarkup);
79
+ expect(container.querySelector('[aria-hidden="true"]')).toBeNull();
80
+ });
81
+
18
82
  it("renders award items synchronously (SSR, no hydration gate)", () => {
19
83
  render(<AwardMarquee items={buildItems(2)} />);
20
84
  expect(screen.getByText("Award 0")).toBeInTheDocument();
@@ -31,9 +95,7 @@ describe("AwardMarquee", () => {
31
95
  it("renders intrinsic icon dimensions when provided", () => {
32
96
  const [item] = buildItems(1);
33
97
  render(
34
- <AwardMarquee
35
- items={[{ ...item, iconWidth: 320, iconHeight: 180 }]}
36
- />
98
+ <AwardMarquee items={[{ ...item, iconWidth: 320, iconHeight: 180 }]} />
37
99
  );
38
100
  const icon = screen.getByAltText("Award 0");
39
101
  expect(icon).toHaveAttribute("width", "320");
@@ -99,7 +161,11 @@ describe("AwardMarquee", () => {
99
161
  ).not.toBeNull();
100
162
 
101
163
  const splitView = render(
102
- <AwardMarquee items={buildItems(2)} scroll={true} anchorId="split-anchor" />
164
+ <AwardMarquee
165
+ items={buildItems(2)}
166
+ scroll={true}
167
+ anchorId="split-anchor"
168
+ />
103
169
  );
104
170
  expect(splitView.container.querySelector("#split-anchor")).not.toBeNull();
105
171
  });
@@ -55,6 +55,8 @@ const renderAwardItem = (
55
55
 
56
56
  export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
57
57
  anchorId = "Award-Marquee",
58
+ heading,
59
+ footerText,
58
60
  items = [],
59
61
  speed = "medium",
60
62
  scroll = false,
@@ -95,27 +97,24 @@ export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
95
97
  </div>
96
98
  );
97
99
 
98
- // No scroll: always static at every breakpoint.
99
- if (!scroll)
100
- return (
101
- <div id={anchorId} className={className}>
102
- {staticMarquee}
103
- </div>
104
- );
105
-
106
- // Scroll with >3 items: animate at every breakpoint.
107
- if (scrollAvailable)
108
- return (
109
- <div id={anchorId} className={className}>
110
- {animatedMarquee()}
111
- </div>
112
- );
100
+ const marquee = !scroll ? (
101
+ staticMarquee
102
+ ) : scrollAvailable ? (
103
+ animatedMarquee()
104
+ ) : (
105
+ <>
106
+ <div className="sm:hidden">{animatedMarquee()}</div>
107
+ <div className="hidden sm:block">{staticMarquee}</div>
108
+ </>
109
+ );
113
110
 
114
- // Scroll with <=3 items: animate on mobile, static on >= sm (CSS-driven).
115
111
  return (
116
112
  <div id={anchorId} className={className}>
117
- <div className="sm:hidden">{animatedMarquee()}</div>
118
- <div className="hidden sm:block">{staticMarquee}</div>
113
+ {heading && <p className="heading2 mb-4 px-4 text-center">{heading}</p>}
114
+ {marquee}
115
+ {footerText && (
116
+ <p className="subheading3 mt-4 px-4 text-center">{footerText}</p>
117
+ )}
119
118
  </div>
120
119
  );
121
120
  };
@@ -13,6 +13,8 @@ export type AwardMarqueeItem = {
13
13
 
14
14
  export type AwardMarqueeProps = {
15
15
  anchorId?: string;
16
+ heading?: string;
17
+ footerText?: string;
16
18
  items: AwardMarqueeItem[];
17
19
  speed?: AwardMarqueeSpeed;
18
20
  scroll?: boolean;
@@ -75,8 +75,8 @@ export const FloatingBanner: React.FC<FloatingBannerProps> = ({
75
75
  {cta && (
76
76
  <div className="mt-5 flex flex-shrink-0 justify-center lg:mt-0 lg:items-center">
77
77
  <Button
78
+ buttonVariant="secondary"
78
79
  {...cta}
79
- buttonVariant="secondary"
80
80
  size={{ base: "medium", md: "large" }}
81
81
  ></Button>
82
82
  </div>