@windstream/react-shared-components 0.2.86 → 0.2.87

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.
Files changed (32) hide show
  1. package/dist/contentful/index.d.ts +19 -3
  2. package/dist/contentful/index.esm.js +4 -4
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +4 -4
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/package.json +1 -1
  8. package/src/contentful/blocks/accordion/index.test.tsx +53 -0
  9. package/src/contentful/blocks/accordion/index.tsx +7 -2
  10. package/src/contentful/blocks/accordion/types.ts +4 -0
  11. package/src/contentful/blocks/callout/{callout.stories.tsx → Callout.stories.tsx} +1 -1
  12. package/src/contentful/blocks/callout/field-props.test.tsx +184 -0
  13. package/src/contentful/blocks/callout/index.tsx +26 -4
  14. package/src/contentful/blocks/callout/types.ts +17 -1
  15. package/src/contentful/blocks/cards/blog-card/index.tsx +6 -2
  16. package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
  17. package/src/contentful/blocks/cards/floating-image-card/index.tsx +4 -2
  18. package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
  19. package/src/contentful/blocks/cards/full-image-card/index.tsx +24 -5
  20. package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
  21. package/src/contentful/blocks/cards/simple-card/index.tsx +21 -3
  22. package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
  23. package/src/contentful/blocks/comparison-table/index.test.tsx +75 -0
  24. package/src/contentful/blocks/comparison-table/index.tsx +15 -3
  25. package/src/contentful/blocks/comparison-table/types.ts +8 -2
  26. package/src/contentful/blocks/cta-callout/index.test.tsx +93 -1
  27. package/src/contentful/blocks/cta-callout/index.tsx +9 -2
  28. package/src/contentful/blocks/cta-callout/types.ts +3 -0
  29. package/src/contentful/blocks/modal/index.test.tsx +75 -2
  30. package/src/contentful/blocks/modal/index.tsx +11 -2
  31. package/src/contentful/blocks/modal/types.ts +4 -0
  32. package/src/contentful/blocks/navigation/{navigation.stories.tsx → Navigation.stories.tsx} +1 -1
@@ -97,7 +97,13 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
97
97
  >
98
98
  <NextImage
99
99
  src={card.image}
100
- alt={card.imageAltText ?? card.imageDescription ?? card.imageAlt ?? card.title ?? "card-image"}
100
+ alt={
101
+ card.imageAltText ??
102
+ card.imageDescription ??
103
+ card.imageAlt ??
104
+ card.title ??
105
+ "card-image"
106
+ }
101
107
  width={card.imageWidth || 400}
102
108
  height={card.imageHeight || 280}
103
109
  className={cx("w-full", !margin && "rounded-t-3xl")}
@@ -124,7 +130,13 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
124
130
  >
125
131
  <NextImage
126
132
  src={card.image}
127
- alt={card.imageAltText ?? card.imageDescription ?? card.imageAlt ?? card.title ?? "card-icon"}
133
+ alt={
134
+ card.imageAltText ??
135
+ card.imageDescription ??
136
+ card.imageAlt ??
137
+ card.title ??
138
+ "card-icon"
139
+ }
128
140
  width={imgWidth}
129
141
  height={imgHeight}
130
142
  className={cx(
@@ -144,6 +156,7 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
144
156
  as="h3"
145
157
  className="heading6 mb-2 text-text md:heading6"
146
158
  style={textColorStyle}
159
+ {...card.fieldProps?.title}
147
160
  >
148
161
  {card.title}
149
162
  </Text>
@@ -300,6 +313,7 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
300
313
  as="div"
301
314
  className="body1 text-text"
302
315
  style={textColorStyle}
316
+ {...card.fieldProps?.body}
303
317
  >
304
318
  {card.body}
305
319
  </Text>
@@ -309,6 +323,7 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
309
323
  as="div"
310
324
  className="body1 text-text"
311
325
  style={textColorStyle}
326
+ {...card.fieldProps?.richText}
312
327
  >
313
328
  {card.richText}
314
329
  </Text>
@@ -318,7 +333,10 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
318
333
  </div>
319
334
 
320
335
  {card.cta ? (
321
- <div className={cx(!isCtaOnly && "mt-2", ctaAlignmentClass)}>
336
+ <div
337
+ className={cx(!isCtaOnly && "mt-2", ctaAlignmentClass)}
338
+ {...card.fieldProps?.cta}
339
+ >
322
340
  <Button
323
341
  linkVariant="unstyled"
324
342
  linkClassName={cx(
@@ -1,6 +1,8 @@
1
1
  import type { CSSProperties, ReactNode } from "react";
2
2
  import type { ButtonProps } from "../../button/types";
3
3
 
4
+ import type { FieldProps } from "@shared/types/field-props";
5
+
4
6
  export type PinwheelColor =
5
7
  | "Blue-#307998"
6
8
  | "Green-#209A61"
@@ -54,6 +56,8 @@ export type Item = {
54
56
  /** Each entry uses a `label|value` separator. */
55
57
  workingHours?: string[];
56
58
  shadow?: boolean;
59
+ /** Per-field DOM attributes supplied by the consumer or parent Callout item. */
60
+ fieldProps?: FieldProps<"title" | "body" | "richText" | "cta">;
57
61
  };
58
62
 
59
63
  export type SimpleCardProps = {
@@ -41,6 +41,81 @@ describe("ComparisonTable", () => {
41
41
  expect(container.querySelector(".component-container")).toBeInTheDocument();
42
42
  });
43
43
 
44
+ describe("fieldProps", () => {
45
+ it("tags the block fields and preserves consumer-supplied row tags", () => {
46
+ const tag = (entry: string, field: string) => ({
47
+ "data-contentful-entry-id": entry,
48
+ "data-contentful-field-id": field,
49
+ "data-contentful-locale": "en-US",
50
+ });
51
+ const { container } = render(
52
+ <ComparisonTable
53
+ {...defaultProps}
54
+ fieldProps={{
55
+ title: tag("comparison", "title"),
56
+ table: tag("comparison", "table"),
57
+ disclaimer: tag("comparison", "disclaimer"),
58
+ }}
59
+ table={
60
+ <table>
61
+ <tbody>
62
+ <tr {...tag("row-1", "feature")}>
63
+ <td>Speed</td>
64
+ </tr>
65
+ <tr {...tag("row-2", "feature")}>
66
+ <td>Price</td>
67
+ </tr>
68
+ </tbody>
69
+ </table>
70
+ }
71
+ />
72
+ );
73
+ expect(screen.getByText("Compare Plans")).toHaveAttribute(
74
+ "data-contentful-field-id",
75
+ "title"
76
+ );
77
+ expect(
78
+ container.querySelector(".comparison-table-container")
79
+ ).toHaveAttribute("data-contentful-field-id", "table");
80
+ expect(
81
+ screen.getByText("* Terms and conditions apply").parentElement
82
+ ).toHaveAttribute("data-contentful-field-id", "disclaimer");
83
+ expect(screen.getByText("Speed").closest("tr")).toHaveAttribute(
84
+ "data-contentful-entry-id",
85
+ "row-1"
86
+ );
87
+ expect(screen.getByText("Price").closest("tr")).toHaveAttribute(
88
+ "data-contentful-entry-id",
89
+ "row-2"
90
+ );
91
+ container
92
+ .querySelectorAll("[data-contentful-field-id]")
93
+ .forEach(element => {
94
+ expect(element).toHaveAttribute("data-contentful-locale", "en-US");
95
+ });
96
+ });
97
+
98
+ it("supports partial fieldProps", () => {
99
+ const { container } = render(
100
+ <ComparisonTable
101
+ {...defaultProps}
102
+ fieldProps={{ table: { "data-contentful-field-id": "table" } }}
103
+ />
104
+ );
105
+ expect(
106
+ container.querySelectorAll("[data-contentful-field-id]")
107
+ ).toHaveLength(1);
108
+ expect(screen.getByText("Compare Plans")).not.toHaveAttribute(
109
+ "data-contentful-field-id"
110
+ );
111
+ });
112
+
113
+ it("does not add tagging attributes when fieldProps is omitted", () => {
114
+ const { container } = render(<ComparisonTable {...defaultProps} />);
115
+ expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
116
+ });
117
+ });
118
+
44
119
  it("renders the title as h2", () => {
45
120
  render(<ComparisonTable {...defaultProps} />);
46
121
  const title = screen.getByText("Compare Plans");
@@ -8,19 +8,31 @@ export const ComparisonTable: React.FC<ComparisonTableProps> = ({
8
8
  disclaimer,
9
9
  table,
10
10
  maxWidth = true,
11
+ fieldProps,
11
12
  }) => {
12
13
  return (
13
14
  <div className="component-container">
14
15
  <div
15
16
  className={`mx-5 mb-5 mt-8 md:mt-18 ${maxWidth ? "max-w-120 xl:mx-auto" : ""}`}
16
17
  >
17
- <Text as="h2" className="heading2 md:heading1 md:text-center">
18
+ <Text
19
+ as="h2"
20
+ className="heading2 md:heading1 md:text-center"
21
+ {...fieldProps?.title}
22
+ >
18
23
  {title}
19
24
  </Text>
20
- <div className="comparison-table-container mt-5 p-5 pt-0 md:mt-10 md:px-10 md:py-9">
25
+ <div
26
+ className="comparison-table-container mt-5 p-5 pt-0 md:mt-10 md:px-10 md:py-9"
27
+ {...fieldProps?.table}
28
+ >
21
29
  {table}
22
30
  </div>
23
- <Text as="div" className="micro mt-8 text-center">
31
+ <Text
32
+ as="div"
33
+ className="micro mt-8 text-center"
34
+ {...fieldProps?.disclaimer}
35
+ >
24
36
  {disclaimer}
25
37
  </Text>
26
38
  </div>
@@ -1,6 +1,12 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
1
5
  export type ComparisonTableProps = {
2
6
  title?: string;
3
- disclaimer?: React.ReactNode;
4
- table: React.ReactNode;
7
+ disclaimer?: ReactNode;
8
+ table: ReactNode;
5
9
  maxWidth?: boolean;
10
+ /** DOM attributes for the block fields. Tag individual rows in the consumer-supplied `table` node. */
11
+ fieldProps?: FieldProps<"title" | "table" | "disclaimer">;
6
12
  };
@@ -19,8 +19,12 @@ jest.mock("@shared/contentful/blocks/button", () => ({
19
19
  data-testid="cta-button"
20
20
  data-size={JSON.stringify(props.size)}
21
21
  data-href={props.href}
22
+ {...props.fieldProps}
22
23
  >
23
- {props.buttonLabel || props.children}
24
+ {props.preDefinedFunctionExecution === "check availability" &&
25
+ props.renderCheckPlans
26
+ ? props.renderCheckPlans()
27
+ : props.buttonLabel || props.children}
24
28
  </button>
25
29
  ),
26
30
  }));
@@ -37,6 +41,94 @@ describe("CtaCallout", () => {
37
41
  descriptionAlignment: "center",
38
42
  };
39
43
 
44
+ describe("fieldProps", () => {
45
+ it("tags all block fields without changing nested button tags", () => {
46
+ const tag = (field: string) => ({
47
+ "data-contentful-field-id": field,
48
+ "data-contentful-entry-id": "cta-callout",
49
+ "data-contentful-locale": "en-US",
50
+ });
51
+ const { container } = render(
52
+ <CtaCallout
53
+ {...defaultProps}
54
+ button={{
55
+ ...defaultProps.button,
56
+ fieldProps: { "data-contentful-field-id": "buttonLabel" },
57
+ }}
58
+ fieldProps={{
59
+ title: tag("title"),
60
+ subTitle: tag("subTitle"),
61
+ description: tag("description"),
62
+ button: tag("button"),
63
+ }}
64
+ />
65
+ );
66
+ expect(screen.getByText(defaultProps.title!)).toHaveAttribute(
67
+ "data-contentful-field-id",
68
+ "title"
69
+ );
70
+ expect(screen.getByText(defaultProps.subTitle!)).toHaveAttribute(
71
+ "data-contentful-field-id",
72
+ "subTitle"
73
+ );
74
+ expect(
75
+ screen.getByText("Choose the plan that works for you.").parentElement
76
+ ).toHaveAttribute("data-contentful-field-id", "description");
77
+ expect(screen.getByTestId("cta-button").parentElement).toHaveAttribute(
78
+ "data-contentful-field-id",
79
+ "button"
80
+ );
81
+ expect(screen.getByTestId("cta-button")).toHaveAttribute(
82
+ "data-contentful-field-id",
83
+ "buttonLabel"
84
+ );
85
+ container
86
+ .querySelectorAll("[data-contentful-entry-id]")
87
+ .forEach(element => {
88
+ expect(element).toHaveAttribute(
89
+ "data-contentful-entry-id",
90
+ "cta-callout"
91
+ );
92
+ expect(element).toHaveAttribute("data-contentful-locale", "en-US");
93
+ });
94
+ });
95
+
96
+ it("tags the CTA region for custom renderers", () => {
97
+ render(
98
+ <CtaCallout
99
+ button={{ preDefinedFunctionExecution: "check availability" }}
100
+ renderCheckPlans={() => <span>Custom check plans</span>}
101
+ fieldProps={{ button: { "data-contentful-field-id": "button" } }}
102
+ />
103
+ );
104
+ expect(screen.getByTestId("cta-button").parentElement).toHaveAttribute(
105
+ "data-contentful-field-id",
106
+ "button"
107
+ );
108
+ expect(screen.getByText("Custom check plans")).toBeInTheDocument();
109
+ });
110
+
111
+ it("does not add tagging attributes when fieldProps is omitted", () => {
112
+ const { container } = render(<CtaCallout {...defaultProps} />);
113
+ expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
114
+ });
115
+
116
+ it("supports partial fieldProps and does not create absent fields", () => {
117
+ const { container } = render(
118
+ <CtaCallout
119
+ title="Only title"
120
+ fieldProps={{
121
+ title: { "data-contentful-field-id": "title" },
122
+ description: { "data-contentful-field-id": "description" },
123
+ }}
124
+ />
125
+ );
126
+ expect(
127
+ container.querySelectorAll("[data-contentful-field-id]")
128
+ ).toHaveLength(1);
129
+ });
130
+ });
131
+
40
132
  it("renders the component", () => {
41
133
  const { container } = render(<CtaCallout {...defaultProps} />);
42
134
  expect(container.firstChild).toBeInTheDocument();
@@ -17,6 +17,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
17
17
  maxWidth = true,
18
18
  onModalButtonClick,
19
19
  renderCheckPlans,
20
+ fieldProps,
20
21
  }) => {
21
22
  const bgColorClasses: Record<ThemeKey, string> = {
22
23
  navy: "bg-[#00002D]",
@@ -28,7 +29,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
28
29
  };
29
30
  return (
30
31
  <div
31
- className={`${bgColorClasses[background]} component-container px-5 py-16 lg:px-13 lg:py-24 w-full`}
32
+ className={`${bgColorClasses[background]} component-container w-full px-5 py-16 lg:px-13 lg:py-24`}
32
33
  >
33
34
  <div
34
35
  className={`${maxWidth ? "mx-auto max-w-120" : ""} color-${color} flex flex-col ${color == "dark" ? "text-text" : "text-text-inverse"}`}
@@ -37,6 +38,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
37
38
  <Text
38
39
  as={enableHeading ? "h1" : "h2"}
39
40
  className={`heading2 lg:heading1 text-${contentAlignment}`}
41
+ {...fieldProps?.title}
40
42
  >
41
43
  {title}
42
44
  </Text>
@@ -45,6 +47,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
45
47
  <Text
46
48
  as="h3"
47
49
  className={`subheading3 pt-2 lg:subheading1 lg:pt-3 text-${contentAlignment}`}
50
+ {...fieldProps?.subTitle}
48
51
  >
49
52
  {subTitle}
50
53
  </Text>
@@ -52,12 +55,16 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
52
55
  {description && (
53
56
  <div
54
57
  className={`pt-2 text-body1 lg:pt-3 text-${descriptionAlignment} break-words [&_table]:w-full`}
58
+ {...fieldProps?.description}
55
59
  >
56
60
  {description}
57
61
  </div>
58
62
  )}
59
63
  {button || renderCheckPlans ? (
60
- <div className="flex justify-center pt-5 lg:pt-14">
64
+ <div
65
+ className="flex justify-center pt-5 lg:pt-14"
66
+ {...fieldProps?.button}
67
+ >
61
68
  <Button
62
69
  {...button}
63
70
  renderCheckPlans={renderCheckPlans}
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
3
4
  import { CheckPlansProps } from "@shared/types/micro-components";
4
5
 
5
6
  export type CtaCalloutProps = {
@@ -15,6 +16,8 @@ export type CtaCalloutProps = {
15
16
  maxWidth?: boolean;
16
17
  onModalButtonClick?: (id?: string) => void;
17
18
  renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
19
+ /** Consumer-supplied DOM attributes; `button` tags the CTA region, including custom renderers. */
20
+ fieldProps?: FieldProps<"title" | "subTitle" | "description" | "button">;
18
21
  };
19
22
 
20
23
  export type ThemeKey =
@@ -51,8 +51,10 @@ jest.mock("@shared/components/material-icon", () => ({
51
51
  }));
52
52
 
53
53
  jest.mock("@shared/components/text", () => ({
54
- Text: ({ as: Tag = "span", children, className }: any) => (
55
- <Tag className={className}>{children}</Tag>
54
+ Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
55
+ <Tag className={className} {...rest}>
56
+ {children}
57
+ </Tag>
56
58
  ),
57
59
  }));
58
60
 
@@ -74,6 +76,77 @@ describe("Modal", () => {
74
76
 
75
77
  beforeEach(() => jest.clearAllMocks());
76
78
 
79
+ describe("fieldProps", () => {
80
+ it("tags title, description and content without changing child tags", () => {
81
+ const tag = (field: string) => ({
82
+ "data-contentful-field-id": field,
83
+ "data-contentful-entry-id": "modal",
84
+ "data-contentful-locale": "en-US",
85
+ });
86
+ const { container } = render(
87
+ <Modal
88
+ {...defaultProps}
89
+ title="Tagged modal"
90
+ description="Tagged description"
91
+ content={<span {...tag("nested")}>Nested content</span>}
92
+ fieldProps={{
93
+ title: tag("title"),
94
+ description: tag("description"),
95
+ content: tag("content"),
96
+ }}
97
+ >
98
+ <span>Extra child</span>
99
+ </Modal>
100
+ );
101
+ expect(screen.getByText("Tagged modal")).toHaveAttribute(
102
+ "data-contentful-field-id",
103
+ "title"
104
+ );
105
+ expect(screen.getByText("Tagged description")).toHaveAttribute(
106
+ "data-contentful-field-id",
107
+ "description"
108
+ );
109
+ expect(screen.getByText("Nested content").parentElement).toHaveAttribute(
110
+ "data-contentful-field-id",
111
+ "content"
112
+ );
113
+ expect(screen.getByText("Nested content")).toHaveAttribute(
114
+ "data-contentful-field-id",
115
+ "nested"
116
+ );
117
+ expect(screen.getByText("Extra child").parentElement).toBe(
118
+ screen.getByText("Nested content").parentElement
119
+ );
120
+ container
121
+ .querySelectorAll("[data-contentful-field-id]")
122
+ .forEach(element => {
123
+ expect(element).toHaveAttribute("data-contentful-entry-id", "modal");
124
+ expect(element).toHaveAttribute("data-contentful-locale", "en-US");
125
+ });
126
+ });
127
+
128
+ it("supports partial fieldProps without creating absent fields", () => {
129
+ const { container } = render(
130
+ <Modal
131
+ {...defaultProps}
132
+ title="Title only"
133
+ fieldProps={{
134
+ title: { "data-contentful-field-id": "title" },
135
+ description: { "data-contentful-field-id": "description" },
136
+ }}
137
+ />
138
+ );
139
+ expect(
140
+ container.querySelectorAll("[data-contentful-field-id]")
141
+ ).toHaveLength(1);
142
+ });
143
+
144
+ it("does not add tagging attributes when fieldProps is omitted", () => {
145
+ const { container } = render(<Modal {...defaultProps} title="No tags" />);
146
+ expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
147
+ });
148
+ });
149
+
77
150
  describe("Rendering", () => {
78
151
  it("renders dialog root with open state", () => {
79
152
  render(<Modal {...defaultProps} />);
@@ -31,6 +31,7 @@ export const Modal: React.FC<ExtendedModalProps> = props => {
31
31
  bodyClassName,
32
32
  type = "modal",
33
33
  index = 0,
34
+ fieldProps,
34
35
  } = props;
35
36
 
36
37
  return (
@@ -93,18 +94,26 @@ export const Modal: React.FC<ExtendedModalProps> = props => {
93
94
  "mb-4 md:mb-[60px]"
94
95
  )}
95
96
  as="h2"
97
+ {...fieldProps?.title}
96
98
  >
97
99
  {title}
98
100
  </Text>
99
101
  ) : null}
100
102
 
101
103
  {description ? (
102
- <Text as="div" className="mb-8 sm:mb-5">
104
+ <Text
105
+ as="div"
106
+ className="mb-8 sm:mb-5"
107
+ {...fieldProps?.description}
108
+ >
103
109
  {description}
104
110
  </Text>
105
111
  ) : null}
106
112
 
107
- <div className="flex flex-col gap-4 sm:gap-5">
113
+ <div
114
+ className="flex flex-col gap-4 sm:gap-5"
115
+ {...fieldProps?.content}
116
+ >
108
117
  {content}
109
118
  {children ? children : null}
110
119
  </div>
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
3
5
  export type ModalProps = {
4
6
  isOpen: boolean;
5
7
  onRequestClose?: () => void;
@@ -9,4 +11,6 @@ export type ModalProps = {
9
11
  content?: React.ReactNode;
10
12
  children?: React.ReactNode;
11
13
  bodyClassName?: string;
14
+ /** DOM attributes for modal fields; `content` tags the region containing content and children. */
15
+ fieldProps?: FieldProps<"title" | "description" | "content">;
12
16
  };
@@ -7,7 +7,7 @@ import {
7
7
  UTILITY_LINKS,
8
8
  ACCOUNT_LINKS,
9
9
  SUPPORT_LINKS,
10
- } from "./navigation.stories.mocks";
10
+ } from "./Navigation.stories.mocks";
11
11
 
12
12
  const searchFormStyle = `
13
13
  form[name="searchForm"] {