@windstream/react-shared-components 0.2.45 → 0.2.47
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 -1
- package/dist/contentful/index.esm.js +3 -3
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +151 -3
- package/dist/index.d.ts +152 -4
- package/dist/index.esm.js +6 -6
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +2 -2
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +2 -2
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -2
- package/src/components/brand-button/index.tsx +2 -2
- package/src/components/video-link/index.test.tsx +1 -1
- package/src/components/video-link/index.tsx +6 -6
- package/src/contentful/blocks/email-input-block/index.test.tsx +189 -161
- package/src/contentful/blocks/fiber-form/constants.ts +81 -0
- package/src/contentful/blocks/fiber-form/index.tsx +557 -0
- package/src/contentful/blocks/fiber-form/types.ts +92 -0
- package/src/contentful/blocks/primary-hero/index.tsx +1 -1
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +118 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +72 -13
- package/src/index.ts +20 -0
|
@@ -46,6 +46,12 @@ jest.mock("@shared/components/checklist", () => ({
|
|
|
46
46
|
),
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
|
+
jest.mock("@shared/components/video-link", () => ({
|
|
50
|
+
VideoLink: ({ videoLink }: any) => (
|
|
51
|
+
<div data-testid="video-link" data-video-link={JSON.stringify(videoLink)} />
|
|
52
|
+
),
|
|
53
|
+
}));
|
|
54
|
+
|
|
49
55
|
// Helper to build a Contentful Document
|
|
50
56
|
function makeDoc(...content: any[]): Document {
|
|
51
57
|
return {
|
|
@@ -334,6 +340,22 @@ describe("renderContentfulRichText", () => {
|
|
|
334
340
|
const emptySpan = Array.from(spans).find(s => s.textContent === "");
|
|
335
341
|
expect(emptySpan).toBeDefined();
|
|
336
342
|
});
|
|
343
|
+
|
|
344
|
+
it("renders componentVideoLink inline entry (REST shape)", () => {
|
|
345
|
+
const doc = makeDoc({
|
|
346
|
+
nodeType: BLOCKS.PARAGRAPH,
|
|
347
|
+
data: {},
|
|
348
|
+
content: [
|
|
349
|
+
makeInlineEntry("componentVideoLink", {
|
|
350
|
+
link: "https://vimeo.com/123",
|
|
351
|
+
}),
|
|
352
|
+
],
|
|
353
|
+
});
|
|
354
|
+
const { container } = render(<>{renderContentfulRichText(doc)}</>);
|
|
355
|
+
expect(
|
|
356
|
+
container.querySelector('[data-testid="video-link"]')
|
|
357
|
+
).toBeInTheDocument();
|
|
358
|
+
});
|
|
337
359
|
});
|
|
338
360
|
|
|
339
361
|
describe("Embedded entry rendering via defaultOptions", () => {
|
|
@@ -354,6 +376,74 @@ describe("renderContentfulRichText", () => {
|
|
|
354
376
|
const { container } = render(<>{renderContentfulRichText(doc)}</>);
|
|
355
377
|
expect(container.querySelector("aside")).not.toBeInTheDocument();
|
|
356
378
|
});
|
|
379
|
+
|
|
380
|
+
it("renders componentVideoLink block entry (REST shape)", () => {
|
|
381
|
+
const doc = makeDoc(
|
|
382
|
+
makeEmbeddedEntry("componentVideoLink", {
|
|
383
|
+
image: { fields: { file: { url: "https://cdn.com/poster.jpg" } } },
|
|
384
|
+
link: "https://youtube.com/watch?v=123",
|
|
385
|
+
videoPopup: true,
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
const { container } = render(<>{renderContentfulRichText(doc)}</>);
|
|
389
|
+
const el = container.querySelector('[data-testid="video-link"]')!;
|
|
390
|
+
expect(el).toBeInTheDocument();
|
|
391
|
+
const videoLink = JSON.parse(el.getAttribute("data-video-link")!);
|
|
392
|
+
expect(videoLink).toMatchObject({
|
|
393
|
+
image: "https://cdn.com/poster.jpg",
|
|
394
|
+
link: "https://youtube.com/watch?v=123",
|
|
395
|
+
videoPopup: true,
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("renders ComponentVideoLink (GraphQL __typename) block entry resolved from links", () => {
|
|
400
|
+
const unresolvedNode: any = {
|
|
401
|
+
nodeType: BLOCKS.EMBEDDED_ENTRY,
|
|
402
|
+
data: { target: { sys: { id: "video-1" } } },
|
|
403
|
+
content: [],
|
|
404
|
+
};
|
|
405
|
+
const doc = makeDoc(unresolvedNode);
|
|
406
|
+
const links = {
|
|
407
|
+
entries: {
|
|
408
|
+
block: [
|
|
409
|
+
{
|
|
410
|
+
__typename: "ComponentVideoLink",
|
|
411
|
+
sys: { id: "video-1" },
|
|
412
|
+
videoSourceType: "contentful",
|
|
413
|
+
videoAssetUrl: "https://videos.ctfassets.net/a/b/c.mp4",
|
|
414
|
+
videoAssetType: "video/mp4",
|
|
415
|
+
title: "My video",
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
inline: [],
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
const { container } = render(
|
|
422
|
+
<>{renderContentfulRichText(doc, undefined, "body1", "body1 font-bold", undefined, links)}</>
|
|
423
|
+
);
|
|
424
|
+
const el = container.querySelector('[data-testid="video-link"]')!;
|
|
425
|
+
expect(el).toBeInTheDocument();
|
|
426
|
+
const videoLink = JSON.parse(el.getAttribute("data-video-link")!);
|
|
427
|
+
expect(videoLink).toMatchObject({
|
|
428
|
+
videoSourceType: "contentful",
|
|
429
|
+
videoAssetUrl: "https://videos.ctfassets.net/a/b/c.mp4",
|
|
430
|
+
videoAssetType: "video/mp4",
|
|
431
|
+
title: "My video",
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("returns null for a GraphQL-style entry that cannot be resolved from links", () => {
|
|
436
|
+
const unresolvedNode: any = {
|
|
437
|
+
nodeType: BLOCKS.EMBEDDED_ENTRY,
|
|
438
|
+
data: { target: { sys: { id: "missing" } } },
|
|
439
|
+
content: [],
|
|
440
|
+
};
|
|
441
|
+
const doc = makeDoc(unresolvedNode);
|
|
442
|
+
const { container } = render(<>{renderContentfulRichText(doc)}</>);
|
|
443
|
+
expect(
|
|
444
|
+
container.querySelector('[data-testid="video-link"]')
|
|
445
|
+
).not.toBeInTheDocument();
|
|
446
|
+
});
|
|
357
447
|
});
|
|
358
448
|
|
|
359
449
|
describe("Embedded asset via defaultOptions", () => {
|
|
@@ -659,6 +749,34 @@ describe("useContentfulRichText", () => {
|
|
|
659
749
|
expect(container.querySelector("aside")).not.toBeInTheDocument();
|
|
660
750
|
});
|
|
661
751
|
|
|
752
|
+
it("resolves a GraphQL-style componentVideoLink entry via config.links", () => {
|
|
753
|
+
const unresolvedNode: any = {
|
|
754
|
+
nodeType: BLOCKS.EMBEDDED_ENTRY,
|
|
755
|
+
data: { target: { sys: { id: "video-2" } } },
|
|
756
|
+
content: [],
|
|
757
|
+
};
|
|
758
|
+
const doc = makeDoc(unresolvedNode);
|
|
759
|
+
const links = {
|
|
760
|
+
entries: {
|
|
761
|
+
block: [
|
|
762
|
+
{
|
|
763
|
+
__typename: "ComponentVideoLink",
|
|
764
|
+
sys: { id: "video-2" },
|
|
765
|
+
link: "https://youtube.com/watch?v=abc",
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
inline: [],
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
const { result } = renderHook(() =>
|
|
772
|
+
useContentfulRichText(doc, undefined, { links })
|
|
773
|
+
);
|
|
774
|
+
const { container } = render(<>{result.current}</>);
|
|
775
|
+
expect(
|
|
776
|
+
container.querySelector('[data-testid="video-link"]')
|
|
777
|
+
).toBeInTheDocument();
|
|
778
|
+
});
|
|
779
|
+
|
|
662
780
|
it("renders inline entry with componentCheckList type", () => {
|
|
663
781
|
const doc = makeDoc({
|
|
664
782
|
nodeType: BLOCKS.PARAGRAPH,
|
|
@@ -14,6 +14,8 @@ import { Checklist } from "@shared/components/checklist";
|
|
|
14
14
|
import { Link } from "@shared/components/link";
|
|
15
15
|
import { MaterialIcon } from "@shared/components/material-icon";
|
|
16
16
|
import { Text } from "@shared/components/text";
|
|
17
|
+
import { VideoLink } from "@shared/components/video-link";
|
|
18
|
+
import type { VideoLinkProps } from "@shared/components/video-link";
|
|
17
19
|
import { toDocument } from "@shared/utils/contentful/to-document";
|
|
18
20
|
|
|
19
21
|
const isEmptyParagraph = (node: any): boolean =>
|
|
@@ -23,6 +25,49 @@ const isEmptyParagraph = (node: any): boolean =>
|
|
|
23
25
|
child?.nodeType === "text" && (!child.value || child.value.trim() === "")
|
|
24
26
|
);
|
|
25
27
|
|
|
28
|
+
// Resolves an embedded entry node's data. REST/CDA responses already resolve
|
|
29
|
+
// `data.target` to the full entry (fields + sys.contentType). GraphQL responses
|
|
30
|
+
// only include the sys.id reference, so the real entry must be looked up in the
|
|
31
|
+
// `links.entries.block`/`links.entries.inline` arrays returned alongside `json`.
|
|
32
|
+
const resolveEmbeddedEntry = (node: any, links?: any): any => {
|
|
33
|
+
const target = node?.data?.target;
|
|
34
|
+
if (target?.fields || target?.sys?.contentType) return target;
|
|
35
|
+
const id = target?.sys?.id;
|
|
36
|
+
if (!id) return null;
|
|
37
|
+
const pool = [
|
|
38
|
+
...(links?.entries?.block ?? []),
|
|
39
|
+
...(links?.entries?.inline ?? []),
|
|
40
|
+
];
|
|
41
|
+
return pool.find((entry: any) => entry?.sys?.id === id) ?? null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Supports both REST (sys.contentType.sys.id) and GraphQL (__typename) shapes.
|
|
45
|
+
const getEntryTypeId = (entry: any): string =>
|
|
46
|
+
entry?.sys?.contentType?.sys?.id ?? entry?.__typename ?? "";
|
|
47
|
+
|
|
48
|
+
// REST entries expose data under `fields`; GraphQL entries have fields flattened.
|
|
49
|
+
const getEntryFields = (entry: any): any => entry?.fields ?? entry ?? {};
|
|
50
|
+
|
|
51
|
+
const toVideoLinkProps = (fields: any): VideoLinkProps => ({
|
|
52
|
+
image:
|
|
53
|
+
fields?.image?.fields?.file?.url ??
|
|
54
|
+
fields?.image?.url ??
|
|
55
|
+
fields?.image,
|
|
56
|
+
link: fields?.link,
|
|
57
|
+
videoPopup: Boolean(fields?.videoPopup),
|
|
58
|
+
videoSourceType: fields?.videoSourceType,
|
|
59
|
+
videoAssetUrl:
|
|
60
|
+
fields?.videoAssetUrl?.fields?.file?.url ??
|
|
61
|
+
fields?.videoAssetUrl?.url ??
|
|
62
|
+
fields?.videoAssetUrl,
|
|
63
|
+
videoAssetType:
|
|
64
|
+
fields?.videoAssetUrl?.fields?.file?.contentType ??
|
|
65
|
+
fields?.videoAssetUrl?.contentType ??
|
|
66
|
+
fields?.videoAssetType,
|
|
67
|
+
title: fields?.title ?? fields?.entryTitle,
|
|
68
|
+
description: fields?.description,
|
|
69
|
+
});
|
|
70
|
+
|
|
26
71
|
// A link is "external" only when it belongs to a different app than the one
|
|
27
72
|
// currently rendering this shared component (business vs. main/www are separate
|
|
28
73
|
// apps). Matching is environment-agnostic so a prod link like
|
|
@@ -80,7 +125,7 @@ const isExternalUrl = (url: string): boolean => {
|
|
|
80
125
|
}
|
|
81
126
|
};
|
|
82
127
|
|
|
83
|
-
const
|
|
128
|
+
const getDefaultOptions = (links?: any): Options => ({
|
|
84
129
|
renderMark: {
|
|
85
130
|
[MARKS.BOLD]: text => <strong className="font-black">{text}</strong>,
|
|
86
131
|
[MARKS.ITALIC]: text => <em>{text}</em>,
|
|
@@ -158,26 +203,33 @@ const defaultOptions: Options = {
|
|
|
158
203
|
},
|
|
159
204
|
|
|
160
205
|
[BLOCKS.EMBEDDED_ENTRY]: node => {
|
|
161
|
-
const entry = (node
|
|
162
|
-
|
|
206
|
+
const entry = resolveEmbeddedEntry(node, links);
|
|
207
|
+
if (!entry) return null;
|
|
208
|
+
const ctid = getEntryTypeId(entry);
|
|
209
|
+
const fields = getEntryFields(entry);
|
|
163
210
|
switch (ctid) {
|
|
164
211
|
case "callout":
|
|
165
212
|
return (
|
|
166
213
|
<aside
|
|
167
214
|
style={{ border: "1px solid #ddd", padding: 12, borderRadius: 8 }}
|
|
168
215
|
>
|
|
169
|
-
<strong>{
|
|
170
|
-
<div>{
|
|
216
|
+
<strong>{fields.title}</strong>
|
|
217
|
+
<div>{fields.body}</div>
|
|
171
218
|
</aside>
|
|
172
219
|
);
|
|
220
|
+
case "componentVideoLink":
|
|
221
|
+
case "ComponentVideoLink":
|
|
222
|
+
return <VideoLink videoLink={toVideoLinkProps(fields)} />;
|
|
173
223
|
default:
|
|
174
224
|
return null;
|
|
175
225
|
}
|
|
176
226
|
},
|
|
177
227
|
|
|
178
228
|
[INLINES.EMBEDDED_ENTRY]: node => {
|
|
179
|
-
const entry = (node
|
|
180
|
-
|
|
229
|
+
const entry = resolveEmbeddedEntry(node, links);
|
|
230
|
+
if (!entry) return null;
|
|
231
|
+
const ctid = getEntryTypeId(entry);
|
|
232
|
+
const fields = getEntryFields(entry);
|
|
181
233
|
switch (ctid) {
|
|
182
234
|
case "componentCheckList":
|
|
183
235
|
return (
|
|
@@ -193,15 +245,18 @@ const defaultOptions: Options = {
|
|
|
193
245
|
fontSize: "0.9em",
|
|
194
246
|
}}
|
|
195
247
|
>
|
|
196
|
-
✅ {
|
|
248
|
+
✅ {fields.title}
|
|
197
249
|
</span>
|
|
198
250
|
);
|
|
251
|
+
case "componentVideoLink":
|
|
252
|
+
case "ComponentVideoLink":
|
|
253
|
+
return <VideoLink videoLink={toVideoLinkProps(fields)} />;
|
|
199
254
|
default:
|
|
200
|
-
return <span>{
|
|
255
|
+
return <span>{fields?.title ?? ""}</span>;
|
|
201
256
|
}
|
|
202
257
|
},
|
|
203
258
|
},
|
|
204
|
-
};
|
|
259
|
+
});
|
|
205
260
|
/**
|
|
206
261
|
* Utility function to render Contentful Rich Text.
|
|
207
262
|
* Safe to use inside .map() loops.
|
|
@@ -211,10 +266,12 @@ export function renderContentfulRichText(
|
|
|
211
266
|
target?: boolean,
|
|
212
267
|
className: string = "body1",
|
|
213
268
|
linkClassName: string = "body1 font-bold",
|
|
214
|
-
options?: Options
|
|
269
|
+
options?: Options,
|
|
270
|
+
links?: any
|
|
215
271
|
): ReactNode | null {
|
|
216
272
|
if (!doc || !Array.isArray(doc.content)) return null;
|
|
217
273
|
|
|
274
|
+
const defaultOptions = getDefaultOptions(links);
|
|
218
275
|
const merged: Options = {
|
|
219
276
|
...defaultOptions,
|
|
220
277
|
...options,
|
|
@@ -260,10 +317,11 @@ export function renderContentfulRichText(
|
|
|
260
317
|
export function useContentfulRichText(
|
|
261
318
|
doc: Document | null | undefined,
|
|
262
319
|
options?: Options,
|
|
263
|
-
config?: { boldClassName?: string }
|
|
320
|
+
config?: { boldClassName?: string; links?: any }
|
|
264
321
|
): ReactNode | null {
|
|
265
322
|
return useMemo(() => {
|
|
266
323
|
if (!doc || !Array.isArray(doc.content)) return null;
|
|
324
|
+
const defaultOptions = getDefaultOptions(config?.links);
|
|
267
325
|
const merged: Options = {
|
|
268
326
|
...defaultOptions,
|
|
269
327
|
renderMark: {
|
|
@@ -280,7 +338,7 @@ export function useContentfulRichText(
|
|
|
280
338
|
renderNode: { ...defaultOptions.renderNode, ...options?.renderNode },
|
|
281
339
|
};
|
|
282
340
|
return documentToReactComponents(doc, merged);
|
|
283
|
-
}, [doc, options, config?.boldClassName]);
|
|
341
|
+
}, [doc, options, config?.boldClassName, config?.links]);
|
|
284
342
|
}
|
|
285
343
|
// 1. Remove useMemo and React Hook imports from this specific function's logic
|
|
286
344
|
export function renderContentfulRichTextTable(
|
|
@@ -289,6 +347,7 @@ export function renderContentfulRichTextTable(
|
|
|
289
347
|
) {
|
|
290
348
|
if (!doc || !Array.isArray(doc.content)) return null;
|
|
291
349
|
|
|
350
|
+
const defaultOptions = getDefaultOptions(links);
|
|
292
351
|
const options: Options = {
|
|
293
352
|
...defaultOptions, // Spread defaultOptions so <p> and <strong> still work!
|
|
294
353
|
renderMark: {
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,9 @@ export type { SpinnerProps } from "./components/spinner";
|
|
|
29
29
|
export { Text } from "./components/text";
|
|
30
30
|
export type { TextProps, TextVariant } from "./components/text";
|
|
31
31
|
|
|
32
|
+
export { VideoLink } from "./components/video-link";
|
|
33
|
+
export type { VideoLinkProps } from "./components/video-link";
|
|
34
|
+
|
|
32
35
|
export { CallButton } from "./components/call-button";
|
|
33
36
|
export type { CallButtonProps } from "./components/call-button";
|
|
34
37
|
|
|
@@ -83,6 +86,23 @@ export type { SkeletonProps } from "./components/skeleton";
|
|
|
83
86
|
export { Tooltip } from "./components/tooltip";
|
|
84
87
|
export type { ToolTipProps } from "./components/tooltip";
|
|
85
88
|
|
|
89
|
+
export {
|
|
90
|
+
FiberForm,
|
|
91
|
+
fiberFormAddressCollectionFormSchema,
|
|
92
|
+
fiberFormInputInitialValues,
|
|
93
|
+
fiberFormInputList,
|
|
94
|
+
} from "./contentful/blocks/fiber-form";
|
|
95
|
+
export type {
|
|
96
|
+
FiberAddress,
|
|
97
|
+
FiberAddressAutocompleteProps,
|
|
98
|
+
FiberAddressAutocompleteResult,
|
|
99
|
+
FiberAddressCandidate,
|
|
100
|
+
FiberAddressUnit,
|
|
101
|
+
FiberFormInputList,
|
|
102
|
+
FiberFormProps,
|
|
103
|
+
FiberFormValues,
|
|
104
|
+
} from "./contentful/blocks/fiber-form";
|
|
105
|
+
|
|
86
106
|
export { AnimationWrapper } from "./components/animation-wrapper";
|
|
87
107
|
export { DEFAULT_BUTTON_ANIMATION_TYPE } from "./components/animation-wrapper";
|
|
88
108
|
export type {
|