@windstream/react-shared-components 0.2.75 → 0.2.77

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.77",
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;
@@ -52,7 +52,7 @@ export const Button: React.FC<ButtonProps> = props => {
52
52
  animationType = DEFAULT_BUTTON_ANIMATION_TYPE,
53
53
  ...rest
54
54
  } = props;
55
-
55
+
56
56
  const isLeftIcon = buttonIconPosition === "left";
57
57
  const matIconName = buttonIconName as IconName | undefined;
58
58
  const brandButtonClassName = cx(
@@ -152,6 +152,7 @@ export const Button: React.FC<ButtonProps> = props => {
152
152
  <BrandButton
153
153
  as="a"
154
154
  href={resolvedHref}
155
+ target={target}
155
156
  id={anchorId}
156
157
  variant={buttonVariant}
157
158
  text={buttonLabel}
@@ -170,6 +171,7 @@ export const Button: React.FC<ButtonProps> = props => {
170
171
  <BrandButton
171
172
  id={anchorId}
172
173
  variant={buttonVariant}
174
+ target={target}
173
175
  text={buttonLabel}
174
176
  label={buttonPrefix}
175
177
  fullWidth={fullWidth}
@@ -192,8 +194,7 @@ export const Button: React.FC<ButtonProps> = props => {
192
194
  target={target}
193
195
  variant={linkVariant || "default"}
194
196
  className={cx(
195
- matIconName &&
196
- "label1 inline-flex items-center gap-2 no-underline",
197
+ matIconName && "label1 inline-flex items-center gap-2 no-underline",
197
198
  textVariantClasses[buttonVariant],
198
199
  linkClassName
199
200
  )}
@@ -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
- {...cta}
79
78
  buttonVariant="secondary"
79
+ {...cta}
80
80
  size={{ base: "medium", md: "large" }}
81
81
  ></Button>
82
82
  </div>