@windstream/react-shared-components 0.2.86 → 0.2.88
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 +20 -4
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +4 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/checklist/index.test.tsx +31 -1
- package/src/components/checklist/index.tsx +1 -0
- package/src/components/checklist/types.ts +2 -0
- package/src/contentful/blocks/accordion/index.test.tsx +53 -0
- package/src/contentful/blocks/accordion/index.tsx +7 -2
- package/src/contentful/blocks/accordion/types.ts +4 -0
- package/src/contentful/blocks/callout/{callout.stories.tsx → Callout.stories.tsx} +1 -1
- package/src/contentful/blocks/callout/field-props.test.tsx +184 -0
- package/src/contentful/blocks/callout/index.tsx +26 -4
- package/src/contentful/blocks/callout/types.ts +17 -1
- package/src/contentful/blocks/cards/blog-card/index.tsx +6 -2
- package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
- package/src/contentful/blocks/cards/floating-image-card/index.tsx +4 -2
- package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
- package/src/contentful/blocks/cards/full-image-card/index.tsx +24 -5
- package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +29 -4
- package/src/contentful/blocks/cards/product-card/index.tsx +46 -44
- package/src/contentful/blocks/cards/product-card/types.ts +1 -1
- package/src/contentful/blocks/cards/simple-card/index.tsx +21 -3
- package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +24 -0
- package/src/contentful/blocks/carousel/helper.tsx +1 -1
- package/src/contentful/blocks/comparison-table/index.test.tsx +75 -0
- package/src/contentful/blocks/comparison-table/index.tsx +15 -3
- package/src/contentful/blocks/comparison-table/types.ts +8 -2
- package/src/contentful/blocks/cta-callout/index.test.tsx +93 -1
- package/src/contentful/blocks/cta-callout/index.tsx +9 -2
- package/src/contentful/blocks/cta-callout/types.ts +3 -0
- package/src/contentful/blocks/modal/index.test.tsx +75 -2
- package/src/contentful/blocks/modal/index.tsx +11 -2
- package/src/contentful/blocks/modal/types.ts +4 -0
- package/src/contentful/blocks/navigation/{navigation.stories.tsx → Navigation.stories.tsx} +1 -1
package/package.json
CHANGED
|
@@ -19,11 +19,18 @@ jest.mock("@shared/components/list", () => ({
|
|
|
19
19
|
ListItem: ({
|
|
20
20
|
children,
|
|
21
21
|
className,
|
|
22
|
+
...rest
|
|
22
23
|
}: {
|
|
23
24
|
children: React.ReactNode;
|
|
24
25
|
className?: string;
|
|
25
26
|
}) => (
|
|
26
|
-
<li
|
|
27
|
+
<li
|
|
28
|
+
data-testid="list-item"
|
|
29
|
+
className={className}
|
|
30
|
+
{...Object.fromEntries(
|
|
31
|
+
Object.entries(rest).filter(([k]) => k.startsWith("data-"))
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
27
34
|
{children}
|
|
28
35
|
</li>
|
|
29
36
|
),
|
|
@@ -109,6 +116,29 @@ describe("Checklist", () => {
|
|
|
109
116
|
});
|
|
110
117
|
});
|
|
111
118
|
|
|
119
|
+
describe("fieldProps", () => {
|
|
120
|
+
it("spreads each structured item's fieldProps onto its own row", () => {
|
|
121
|
+
render(
|
|
122
|
+
<Checklist
|
|
123
|
+
items={[
|
|
124
|
+
{ title: "One", fieldProps: { "data-contentful-entry-id": "a" } },
|
|
125
|
+
{ title: "Two", fieldProps: { "data-contentful-entry-id": "b" } },
|
|
126
|
+
]}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
const rows = screen.getAllByTestId("list-item");
|
|
130
|
+
expect(rows[0]).toHaveAttribute("data-contentful-entry-id", "a");
|
|
131
|
+
expect(rows[1]).toHaveAttribute("data-contentful-entry-id", "b");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders no tagging attributes for plain or untagged items", () => {
|
|
135
|
+
render(<Checklist items={["Plain", { title: "Structured" }]} />);
|
|
136
|
+
screen.getAllByTestId("list-item").forEach(row => {
|
|
137
|
+
expect(row).not.toHaveAttribute("data-contentful-entry-id");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
112
142
|
describe("icons", () => {
|
|
113
143
|
it("renders check icons by default", () => {
|
|
114
144
|
render(<Checklist items={["Item 1"]} />);
|
|
@@ -74,6 +74,7 @@ export const Checklist: React.FC<ChecklistProps> = props => {
|
|
|
74
74
|
`${showIcons ? "flex" : ""} ${iconPosition === "top" ? "items-start" : "items-center"} gap-2 text-text-secondary`,
|
|
75
75
|
listItemClassName
|
|
76
76
|
)}
|
|
77
|
+
{...(structured ? item.fieldProps : undefined)}
|
|
77
78
|
>
|
|
78
79
|
{showIcons && hasCustomIcon ? (
|
|
79
80
|
<div className="flex shrink-0 leading-none">
|
|
@@ -10,6 +10,59 @@ const items: AccordionItem[] = [
|
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
describe("Contentful Accordion", () => {
|
|
13
|
+
describe("fieldProps", () => {
|
|
14
|
+
const tag = (field: string) => ({
|
|
15
|
+
"data-contentful-field-id": field,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("spreads block-level title fieldProps onto the heading", () => {
|
|
19
|
+
render(
|
|
20
|
+
<Accordion
|
|
21
|
+
items={items}
|
|
22
|
+
title="FAQ"
|
|
23
|
+
fieldProps={{ title: tag("title") }}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText("FAQ")).toHaveAttribute(
|
|
27
|
+
"data-contentful-field-id",
|
|
28
|
+
"title"
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("spreads per-item title and description fieldProps", () => {
|
|
33
|
+
render(
|
|
34
|
+
<Accordion
|
|
35
|
+
items={[
|
|
36
|
+
{
|
|
37
|
+
...items[0],
|
|
38
|
+
fieldProps: {
|
|
39
|
+
title: tag("itemTitle"),
|
|
40
|
+
description: tag("itemDescription"),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
items[1],
|
|
44
|
+
]}
|
|
45
|
+
title="FAQ"
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
expect(document.getElementById("item-1")).toHaveAttribute(
|
|
49
|
+
"data-contentful-field-id",
|
|
50
|
+
"itemTitle"
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByText("Answer one")).toHaveAttribute(
|
|
53
|
+
"data-contentful-field-id",
|
|
54
|
+
"itemDescription"
|
|
55
|
+
);
|
|
56
|
+
expect(document.getElementById("item-2")).not.toHaveAttribute(
|
|
57
|
+
"data-contentful-field-id"
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders no tagging attributes when fieldProps is omitted", () => {
|
|
62
|
+
const { container } = render(<Accordion items={items} title="FAQ" />);
|
|
63
|
+
expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
13
66
|
beforeEach(() => {
|
|
14
67
|
Object.defineProperty(window, "location", {
|
|
15
68
|
writable: true,
|
|
@@ -18,6 +18,7 @@ export const Accordion: React.FC<AccordionProps> = ({
|
|
|
18
18
|
hashAutoExpand = false,
|
|
19
19
|
descriptionMaxWidth = "max-w-[980px]",
|
|
20
20
|
itemContainerClassName,
|
|
21
|
+
fieldProps,
|
|
21
22
|
}) => {
|
|
22
23
|
const [expandedItems, setExpandedItems] = useState<string[]>(
|
|
23
24
|
initialExpandedItemsProp
|
|
@@ -64,7 +65,8 @@ export const Accordion: React.FC<AccordionProps> = ({
|
|
|
64
65
|
<Text
|
|
65
66
|
as={enableHeading ? "h1" : "h2"}
|
|
66
67
|
className="heading2 mb-10 pt-12 text-left text-text md:heading1 lg:text-center"
|
|
67
|
-
|
|
68
|
+
{...fieldProps?.title}
|
|
69
|
+
>
|
|
68
70
|
{title}
|
|
69
71
|
</Text>
|
|
70
72
|
<div className={`pt-6 md:pt-10 ${itemContainerClassName ?? ""}`}>
|
|
@@ -79,6 +81,7 @@ export const Accordion: React.FC<AccordionProps> = ({
|
|
|
79
81
|
key={itemKey}
|
|
80
82
|
id={item.anchorId}
|
|
81
83
|
className="snap-start scroll-mt-48 lg:scroll-mt-60"
|
|
84
|
+
{...item.fieldProps?.title}
|
|
82
85
|
>
|
|
83
86
|
<AccordionComponent
|
|
84
87
|
title={item.title}
|
|
@@ -96,7 +99,9 @@ export const Accordion: React.FC<AccordionProps> = ({
|
|
|
96
99
|
})}
|
|
97
100
|
>
|
|
98
101
|
<div className={descriptionMaxWidth}>
|
|
99
|
-
<Text as="div"
|
|
102
|
+
<Text as="div" {...item.fieldProps?.description}>
|
|
103
|
+
{item.description}
|
|
104
|
+
</Text>
|
|
100
105
|
</div>
|
|
101
106
|
</AccordionComponent>
|
|
102
107
|
{index < items.length - 1 && (
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
3
5
|
export type AccordionItem = {
|
|
4
6
|
anchorId?: string;
|
|
5
7
|
title: string;
|
|
6
8
|
description: React.ReactNode;
|
|
9
|
+
fieldProps?: FieldProps<"title" | "description">;
|
|
7
10
|
};
|
|
8
11
|
|
|
9
12
|
export type AccordionProps = {
|
|
@@ -22,6 +25,7 @@ export type AccordionProps = {
|
|
|
22
25
|
descriptionMaxWidth?: string;
|
|
23
26
|
/** Custom class for the accordion list wrapper */
|
|
24
27
|
itemContainerClassName?: string;
|
|
28
|
+
fieldProps?: FieldProps<"title">;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export type ThemeKey =
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
fourColumnItems,
|
|
9
9
|
fullImageItems,
|
|
10
10
|
withFloatingImageArgs,
|
|
11
|
-
} from "./
|
|
11
|
+
} from "./Callout.stories.mocks";
|
|
12
12
|
|
|
13
13
|
import { DocsPage } from "@shared/stories/DocsTemplate";
|
|
14
14
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Callout } from "./index";
|
|
3
|
+
import type { CalloutItem } from "./types";
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
const tag = (entry: string, field: string) => ({
|
|
8
|
+
"data-contentful-entry-id": entry,
|
|
9
|
+
"data-contentful-field-id": field,
|
|
10
|
+
"data-contentful-locale": "en-US",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("Callout fieldProps", () => {
|
|
14
|
+
it("tags the block fields and retains CTA behavior", () => {
|
|
15
|
+
const onClick = jest.fn();
|
|
16
|
+
const { container } = render(
|
|
17
|
+
<Callout
|
|
18
|
+
items={[]}
|
|
19
|
+
title="Callout title"
|
|
20
|
+
subtitle="Callout subtitle"
|
|
21
|
+
description="Callout description"
|
|
22
|
+
finePrint="Callout fine print"
|
|
23
|
+
cta={{ buttonLabel: "Callout CTA", onClick }}
|
|
24
|
+
fieldProps={{
|
|
25
|
+
title: tag("callout", "title"),
|
|
26
|
+
subtitle: tag("callout", "subtitle"),
|
|
27
|
+
description: tag("callout", "description"),
|
|
28
|
+
finePrint: tag("callout", "finePrint"),
|
|
29
|
+
cta: tag("callout", "cta"),
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
["title", "subtitle", "description", "finePrint", "cta"].forEach(field => {
|
|
34
|
+
expect(
|
|
35
|
+
container.querySelector(`[data-contentful-field-id="${field}"]`)
|
|
36
|
+
).toHaveAttribute("data-contentful-entry-id", "callout");
|
|
37
|
+
});
|
|
38
|
+
fireEvent.click(screen.getByRole("button", { name: "Callout CTA" }));
|
|
39
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("preserves CTA entry tags when both the block and CTA supply attributes", () => {
|
|
43
|
+
render(
|
|
44
|
+
<Callout
|
|
45
|
+
items={[]}
|
|
46
|
+
cta={{
|
|
47
|
+
buttonLabel: "Nested CTA",
|
|
48
|
+
fieldProps: tag("button", "buttonLabel"),
|
|
49
|
+
}}
|
|
50
|
+
fieldProps={{ cta: tag("callout", "cta") }}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByRole("button", { name: "Nested CTA" })).toHaveAttribute(
|
|
54
|
+
"data-contentful-entry-id",
|
|
55
|
+
"button"
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("tags custom CTA renderers without requiring them to forward attributes", () => {
|
|
60
|
+
const onClick = jest.fn();
|
|
61
|
+
render(
|
|
62
|
+
<Callout
|
|
63
|
+
items={[]}
|
|
64
|
+
cta={{
|
|
65
|
+
preDefinedFunctionExecution: "check availability",
|
|
66
|
+
renderCheckPlans: () => <button onClick={onClick}>Custom CTA</button>,
|
|
67
|
+
}}
|
|
68
|
+
fieldProps={{ cta: tag("callout", "cta") }}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
const button = screen.getByRole("button", { name: "Custom CTA" });
|
|
72
|
+
expect(button.parentElement).toHaveAttribute(
|
|
73
|
+
"data-contentful-field-id",
|
|
74
|
+
"cta"
|
|
75
|
+
);
|
|
76
|
+
fireEvent.click(button);
|
|
77
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it.each([
|
|
81
|
+
{ cardsWidth: true, disableAnimation: true },
|
|
82
|
+
{ cardsWidth: true, disableAnimation: false },
|
|
83
|
+
{ cardsWidth: false, disableAnimation: true },
|
|
84
|
+
{ cardsWidth: false, disableAnimation: false },
|
|
85
|
+
])("tags every card variant with layout/animation options %p", options => {
|
|
86
|
+
const items: CalloutItem[] = [
|
|
87
|
+
{
|
|
88
|
+
cardType: "simple",
|
|
89
|
+
title: "Simple title",
|
|
90
|
+
body: "Simple body",
|
|
91
|
+
richText: "Simple rich text",
|
|
92
|
+
cta: { buttonLabel: "Simple CTA" },
|
|
93
|
+
fieldProps: {
|
|
94
|
+
title: tag("simple", "title"),
|
|
95
|
+
body: tag("simple", "body"),
|
|
96
|
+
richText: tag("simple", "richText"),
|
|
97
|
+
cta: tag("simple", "cta"),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
cardType: "blog",
|
|
102
|
+
title: "Blog title",
|
|
103
|
+
slug: "/blog",
|
|
104
|
+
shortDescription: "Blog excerpt",
|
|
105
|
+
fieldProps: {
|
|
106
|
+
title: tag("blog", "title"),
|
|
107
|
+
shortDescription: tag("blog", "shortDescription"),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
cardType: "fullImage",
|
|
112
|
+
title: "Full image title",
|
|
113
|
+
description: "Full image description",
|
|
114
|
+
caption: "Full image caption",
|
|
115
|
+
cta: { buttonLabel: "Full image CTA" },
|
|
116
|
+
fieldProps: {
|
|
117
|
+
title: tag("full", "title"),
|
|
118
|
+
description: tag("full", "description"),
|
|
119
|
+
caption: tag("full", "caption"),
|
|
120
|
+
cta: tag("full", "cta"),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
cardType: "floatingImage",
|
|
125
|
+
header: "Floating header",
|
|
126
|
+
body: "Floating body",
|
|
127
|
+
cta: { buttonLabel: "Floating CTA" },
|
|
128
|
+
fieldProps: {
|
|
129
|
+
header: tag("floating", "header"),
|
|
130
|
+
body: tag("floating", "body"),
|
|
131
|
+
cta: tag("floating", "cta"),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ title: "Untagged item" },
|
|
135
|
+
];
|
|
136
|
+
const { container } = render(<Callout items={items} {...options} />);
|
|
137
|
+
items.forEach(item => {
|
|
138
|
+
Object.values(item.fieldProps ?? {}).forEach(attributes => {
|
|
139
|
+
const element = container.querySelector(
|
|
140
|
+
`[data-contentful-entry-id="${attributes["data-contentful-entry-id"]}"][data-contentful-field-id="${attributes["data-contentful-field-id"]}"]`
|
|
141
|
+
);
|
|
142
|
+
expect(element).toBeInTheDocument();
|
|
143
|
+
expect(element).toHaveAttribute("data-contentful-locale", "en-US");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
expect(screen.getByText("Untagged item")).not.toHaveAttribute(
|
|
147
|
+
"data-contentful-field-id"
|
|
148
|
+
);
|
|
149
|
+
expect(screen.getByText("Blog title").closest("h2")).toHaveAttribute(
|
|
150
|
+
"data-contentful-entry-id",
|
|
151
|
+
"blog"
|
|
152
|
+
);
|
|
153
|
+
expect(screen.getByText("Simple body")).toHaveAttribute(
|
|
154
|
+
"data-contentful-field-id",
|
|
155
|
+
"body"
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("supports partial fieldProps without creating absent fields", () => {
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<Callout
|
|
162
|
+
items={[]}
|
|
163
|
+
title="Only title"
|
|
164
|
+
fieldProps={{
|
|
165
|
+
title: tag("callout", "title"),
|
|
166
|
+
description: tag("callout", "description"),
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
expect(
|
|
171
|
+
container.querySelectorAll("[data-contentful-field-id]")
|
|
172
|
+
).toHaveLength(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does not add tagging attributes when fieldProps is omitted", () => {
|
|
176
|
+
const { container } = render(
|
|
177
|
+
<Callout
|
|
178
|
+
title="No tags"
|
|
179
|
+
items={[{ title: "Card", body: "Body", cta: { buttonLabel: "CTA" } }]}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -82,6 +82,7 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
82
82
|
noGutter = false,
|
|
83
83
|
disableAnimation,
|
|
84
84
|
animationType = "lift",
|
|
85
|
+
fieldProps,
|
|
85
86
|
}) => {
|
|
86
87
|
const itemCount = items?.length ?? 0;
|
|
87
88
|
const desktopCols = clampCol(
|
|
@@ -142,6 +143,12 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
142
143
|
image={blogItem.cover}
|
|
143
144
|
imageAltText={blogItem.imageAltText}
|
|
144
145
|
asGrid={false}
|
|
146
|
+
fieldProps={{
|
|
147
|
+
...item.fieldProps,
|
|
148
|
+
description:
|
|
149
|
+
item.fieldProps?.shortDescription ??
|
|
150
|
+
item.fieldProps?.description,
|
|
151
|
+
}}
|
|
145
152
|
{...widthProps}
|
|
146
153
|
/>
|
|
147
154
|
);
|
|
@@ -197,7 +204,9 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
197
204
|
>
|
|
198
205
|
<div
|
|
199
206
|
className={cx(
|
|
200
|
-
noGutter
|
|
207
|
+
noGutter
|
|
208
|
+
? "p-0"
|
|
209
|
+
: "mx-5 mb-5 mt-12 md:mx-0 md:mb-0 md:mt-0 md:px-6 md:pb-8 md:pt-16 lg:px-0 lg:py-20",
|
|
201
210
|
maxWidth && "max-w-120 xl:mx-auto",
|
|
202
211
|
color === "dark" ? "text-text" : "text-white",
|
|
203
212
|
innerClassName
|
|
@@ -209,6 +218,7 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
209
218
|
<Text
|
|
210
219
|
as={enableHeading ? "h1" : "h2"}
|
|
211
220
|
className="heading2 md:heading1 md:text-center"
|
|
221
|
+
{...fieldProps?.title}
|
|
212
222
|
>
|
|
213
223
|
{title}
|
|
214
224
|
</Text>
|
|
@@ -217,12 +227,17 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
217
227
|
<Text
|
|
218
228
|
as={enableHeading ? "h2" : "h3"}
|
|
219
229
|
className="subheading3 mt-2 md:subheading1 md:text-center lg:mt-3"
|
|
230
|
+
{...fieldProps?.subtitle}
|
|
220
231
|
>
|
|
221
232
|
{subtitle}
|
|
222
233
|
</Text>
|
|
223
234
|
)}
|
|
224
235
|
{description && (
|
|
225
|
-
<Text
|
|
236
|
+
<Text
|
|
237
|
+
as="p"
|
|
238
|
+
className="body1 mt-4 text-center md:mt-6"
|
|
239
|
+
{...fieldProps?.description}
|
|
240
|
+
>
|
|
226
241
|
{description}
|
|
227
242
|
</Text>
|
|
228
243
|
)}
|
|
@@ -253,7 +268,10 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
253
268
|
)}
|
|
254
269
|
</div>
|
|
255
270
|
{(cta || finePrint) && (
|
|
256
|
-
<div
|
|
271
|
+
<div
|
|
272
|
+
className="flex flex-col items-center gap-4"
|
|
273
|
+
{...(cta ? fieldProps?.cta : undefined)}
|
|
274
|
+
>
|
|
257
275
|
{cta ? (
|
|
258
276
|
<Button
|
|
259
277
|
linkClassName="label1"
|
|
@@ -264,7 +282,11 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
264
282
|
</Button>
|
|
265
283
|
) : null}
|
|
266
284
|
{finePrint ? (
|
|
267
|
-
<Text
|
|
285
|
+
<Text
|
|
286
|
+
as="div"
|
|
287
|
+
className="footnote text-center text-text"
|
|
288
|
+
{...fieldProps?.finePrint}
|
|
289
|
+
>
|
|
268
290
|
{finePrint}
|
|
269
291
|
</Text>
|
|
270
292
|
) : null}
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import type { ButtonProps } from "../button/types";
|
|
3
3
|
|
|
4
4
|
import type { AnimationType } from "@shared/components/animation-wrapper";
|
|
5
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
5
6
|
|
|
6
7
|
export type CalloutCardType = "simple" | "blog" | "fullImage" | "floatingImage";
|
|
7
8
|
|
|
@@ -16,6 +17,17 @@ export type CalloutItem = {
|
|
|
16
17
|
* Callout (e.g. one full-image + two simple cards).
|
|
17
18
|
*/
|
|
18
19
|
cardType?: CalloutCardType;
|
|
20
|
+
/** Per-field DOM attributes for the selected card variant. */
|
|
21
|
+
fieldProps?: FieldProps<
|
|
22
|
+
| "title"
|
|
23
|
+
| "shortDescription"
|
|
24
|
+
| "description"
|
|
25
|
+
| "header"
|
|
26
|
+
| "body"
|
|
27
|
+
| "richText"
|
|
28
|
+
| "caption"
|
|
29
|
+
| "cta"
|
|
30
|
+
>;
|
|
19
31
|
// Permissive shape — concrete card components validate their own subset.
|
|
20
32
|
[key: string]: any;
|
|
21
33
|
};
|
|
@@ -30,7 +42,7 @@ export type CalloutProps = {
|
|
|
30
42
|
finePrint?: ReactNode;
|
|
31
43
|
cta?: CalloutCtaProps;
|
|
32
44
|
applyBoxShadow?: boolean;
|
|
33
|
-
cardStackingMobile?: boolean;
|
|
45
|
+
cardStackingMobile?: boolean;
|
|
34
46
|
color?: "dark" | "light";
|
|
35
47
|
/**
|
|
36
48
|
* When `true` (default) cards are laid out in a responsive Tailwind
|
|
@@ -74,4 +86,8 @@ export type CalloutProps = {
|
|
|
74
86
|
disableAnimation?: boolean;
|
|
75
87
|
/** Animation type(s) applied to each card. */
|
|
76
88
|
animationType?: AnimationType | AnimationType[];
|
|
89
|
+
/** Consumer-supplied Inspector Mode attributes. `cta` tags the CTA/fine-print region; nested CTA fieldProps remain unchanged. */
|
|
90
|
+
fieldProps?: FieldProps<
|
|
91
|
+
"title" | "subtitle" | "description" | "finePrint" | "cta"
|
|
92
|
+
>;
|
|
77
93
|
};
|
|
@@ -22,6 +22,7 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
22
22
|
imageComponent: ImageComponent,
|
|
23
23
|
asGrid = true,
|
|
24
24
|
index,
|
|
25
|
+
fieldProps,
|
|
25
26
|
}: BlogCardProps) => {
|
|
26
27
|
const imageAlt =
|
|
27
28
|
imageAltText ??
|
|
@@ -96,7 +97,7 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
96
97
|
|
|
97
98
|
{/* Title */}
|
|
98
99
|
{title && (
|
|
99
|
-
<Text as="h2" className="heading6">
|
|
100
|
+
<Text as="h2" className="heading6" {...fieldProps?.title}>
|
|
100
101
|
<Link
|
|
101
102
|
href={href}
|
|
102
103
|
className="m-0 line-clamp-3 font-black transition-colors duration-150 after:absolute after:inset-0 after:z-10 after:content-[''] hover:text-text-brand group-hover:text-text-brand"
|
|
@@ -108,7 +109,10 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
108
109
|
|
|
109
110
|
{/* Excerpt */}
|
|
110
111
|
{description && (
|
|
111
|
-
<Text
|
|
112
|
+
<Text
|
|
113
|
+
className="body1 m-0 line-clamp-3 flex-1 text-text"
|
|
114
|
+
{...fieldProps?.description}
|
|
115
|
+
>
|
|
112
116
|
{description}
|
|
113
117
|
</Text>
|
|
114
118
|
)}
|
|
@@ -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 interface BlogCardImageProps {
|
|
4
6
|
src: string;
|
|
5
7
|
alt: string;
|
|
@@ -32,4 +34,6 @@ export interface BlogCardProps {
|
|
|
32
34
|
imageComponent?: React.ComponentType<BlogCardImageProps>;
|
|
33
35
|
asGrid?: boolean;
|
|
34
36
|
index?: number;
|
|
37
|
+
/** Per-field DOM attributes supplied by the consumer or parent Callout item. */
|
|
38
|
+
fieldProps?: FieldProps<"title" | "description">;
|
|
35
39
|
}
|
|
@@ -33,6 +33,7 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
|
|
|
33
33
|
isPromoBarImg = false,
|
|
34
34
|
shadow = false,
|
|
35
35
|
disableAnimation,
|
|
36
|
+
fieldProps,
|
|
36
37
|
} = card;
|
|
37
38
|
|
|
38
39
|
const ctaAlignmentClass = ctaAlignment
|
|
@@ -102,7 +103,7 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
|
|
|
102
103
|
) : null}
|
|
103
104
|
|
|
104
105
|
{header ? (
|
|
105
|
-
<Text as="h4" className="heading6 text-text">
|
|
106
|
+
<Text as="h4" className="heading6 text-text" {...fieldProps?.header}>
|
|
106
107
|
{header}
|
|
107
108
|
</Text>
|
|
108
109
|
) : null}
|
|
@@ -111,12 +112,13 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
|
|
|
111
112
|
<Text
|
|
112
113
|
as="div"
|
|
113
114
|
className="body1 -mt-1 mb-0 flex-1 text-text md:-mt-2 lg:mt-2"
|
|
115
|
+
{...fieldProps?.body}
|
|
114
116
|
>
|
|
115
117
|
{body}
|
|
116
118
|
</Text>
|
|
117
119
|
) : null}
|
|
118
120
|
|
|
119
|
-
<div className={cx(ctaAlignmentClass, "mt-auto")}>
|
|
121
|
+
<div className={cx(ctaAlignmentClass, "mt-auto")} {...fieldProps?.cta}>
|
|
120
122
|
{cta ? (
|
|
121
123
|
<Button
|
|
122
124
|
{...cta}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { 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 FloatingImageAsset = {
|
|
5
7
|
href: string;
|
|
6
8
|
title?: string;
|
|
@@ -27,6 +29,8 @@ export type FloatingImageCardItem = {
|
|
|
27
29
|
shadow?: boolean;
|
|
28
30
|
/** Disables the CTA hover/tap animation when true. */
|
|
29
31
|
disableAnimation?: boolean;
|
|
32
|
+
/** Per-field DOM attributes supplied by the consumer or parent Callout item. */
|
|
33
|
+
fieldProps?: FieldProps<"header" | "body" | "cta">;
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
export type FloatingImageCardProps = {
|
|
@@ -25,6 +25,7 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
|
|
|
25
25
|
contentClassName,
|
|
26
26
|
imageClassName,
|
|
27
27
|
shadow = true,
|
|
28
|
+
fieldProps,
|
|
28
29
|
} = card;
|
|
29
30
|
|
|
30
31
|
const target = cta || link;
|
|
@@ -36,7 +37,13 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
|
|
|
36
37
|
const img = (
|
|
37
38
|
<NextImage
|
|
38
39
|
src={image.href}
|
|
39
|
-
alt={
|
|
40
|
+
alt={
|
|
41
|
+
imageAltText ??
|
|
42
|
+
image.description ??
|
|
43
|
+
image.title ??
|
|
44
|
+
title ??
|
|
45
|
+
"card-image"
|
|
46
|
+
}
|
|
40
47
|
width={image.width || 400}
|
|
41
48
|
height={image.height || 280}
|
|
42
49
|
className="w-full"
|
|
@@ -53,7 +60,11 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
|
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
const titleNode = title ? (
|
|
56
|
-
<Text
|
|
63
|
+
<Text
|
|
64
|
+
as="h3"
|
|
65
|
+
className="body1 text-text xl:heading6"
|
|
66
|
+
{...fieldProps?.title}
|
|
67
|
+
>
|
|
57
68
|
{title}
|
|
58
69
|
</Text>
|
|
59
70
|
) : null;
|
|
@@ -101,19 +112,27 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
|
|
|
101
112
|
titleNode
|
|
102
113
|
)}
|
|
103
114
|
{description ? (
|
|
104
|
-
<Text
|
|
115
|
+
<Text
|
|
116
|
+
as="p"
|
|
117
|
+
className="body2 mt-4 text-text md:body1"
|
|
118
|
+
{...fieldProps?.description}
|
|
119
|
+
>
|
|
105
120
|
{description}
|
|
106
121
|
</Text>
|
|
107
122
|
) : null}
|
|
108
123
|
</div>
|
|
109
124
|
{caption ? (
|
|
110
|
-
<Text
|
|
125
|
+
<Text
|
|
126
|
+
as="p"
|
|
127
|
+
className="footnote mt-5 text-text-secondary"
|
|
128
|
+
{...fieldProps?.caption}
|
|
129
|
+
>
|
|
111
130
|
{caption}
|
|
112
131
|
</Text>
|
|
113
132
|
) : null}
|
|
114
133
|
</div>
|
|
115
134
|
{cta ? (
|
|
116
|
-
<div className="mt-2 flex items-end">
|
|
135
|
+
<div className="mt-2 flex items-end" {...fieldProps?.cta}>
|
|
117
136
|
<Button
|
|
118
137
|
linkVariant="unstyled"
|
|
119
138
|
linkClassName="label1 flex items-center gap-2 text-text"
|