@windstream/react-shared-components 0.2.62 → 0.2.64
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 +8 -1
- 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 +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.d.ts +1 -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 +1 -1
- package/src/components/select-plan-button/index.tsx +6 -2
- package/src/components/select-plan-button/types.ts +1 -0
- package/src/components/video-link/vimeo-embed.tsx +19 -10
- package/src/contentful/blocks/cards/product-card/index.tsx +50 -24
- package/src/contentful/blocks/cards/product-card/types.ts +7 -0
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +34 -8
- package/src/contentful/blocks/carousel/helper.test.tsx +127 -10
- package/src/contentful/blocks/carousel/helper.tsx +114 -21
- package/src/contentful/blocks/image-promo-bar/index.tsx +2 -2
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +12 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +17 -2
|
@@ -25,19 +25,23 @@ function ProductCardPanel({
|
|
|
25
25
|
}) {
|
|
26
26
|
const itemsExpanded = fields?.items?.items?.[0]?.benefitsExpanded || false;
|
|
27
27
|
const [desktopExpanded, setDesktopExpanded] = useState(itemsExpanded);
|
|
28
|
-
const [mobileExpandedStates, setMobileExpandedStates] = useState<
|
|
29
|
-
Record<number, boolean>
|
|
30
|
-
>({});
|
|
31
28
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
32
29
|
const [maxIndex, setMaxIndex] = useState(0);
|
|
33
30
|
const [swipeOffset, setSwipeOffset] = useState(0);
|
|
34
31
|
const [isSwiping, setIsSwiping] = useState(false);
|
|
35
32
|
const items = fields?.items?.items || [];
|
|
36
|
-
|
|
33
|
+
// Prefer the CMS-provided `twoColumn` layout flag.
|
|
34
|
+
// When unset, fall back to the count heuristic: two-column once there are
|
|
35
|
+
// more than 3 cards; 3 or fewer stack vertically.
|
|
36
|
+
const twoColumn = fields?.twoColumn ?? items.length > 3;
|
|
37
37
|
const isCarousel = items.length > 2;
|
|
38
|
+
// When any card is flagged best-value, every card reserves the banner height
|
|
39
|
+
// so plan name/price stay aligned across a two-column row.
|
|
40
|
+
const hasAnyBadge = items.some((item: any) => item?.highlighted);
|
|
38
41
|
const showArrows = fields?.showArrows !== false && isCarousel;
|
|
39
42
|
const cardsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
40
43
|
const windowRef = useRef<HTMLDivElement | null>(null);
|
|
44
|
+
const mobileGridRef = useRef<HTMLDivElement | null>(null);
|
|
41
45
|
const touchStartX = useRef<number>(0);
|
|
42
46
|
|
|
43
47
|
// Determine how many cards fit so we never scroll past the last card
|
|
@@ -121,7 +125,9 @@ function ProductCardPanel({
|
|
|
121
125
|
setSwipeOffset(0);
|
|
122
126
|
}, [isSwiping, swipeOffset, prevSlide, nextSlide]);
|
|
123
127
|
|
|
124
|
-
// Equalize card heights
|
|
128
|
+
// Equalize card heights. Re-runs whenever the panel becomes visible so a tab
|
|
129
|
+
// that was rendered while hidden (display:none) gets its heights recalculated
|
|
130
|
+
// once it is shown. Residential (single panel, always visible) is unaffected.
|
|
125
131
|
useEffect(() => {
|
|
126
132
|
if (!isCarousel || !isVisible) return;
|
|
127
133
|
|
|
@@ -129,7 +135,7 @@ function ProductCardPanel({
|
|
|
129
135
|
const cards = cardsRef.current.filter(Boolean) as HTMLDivElement[];
|
|
130
136
|
if (cards.length === 0) return;
|
|
131
137
|
|
|
132
|
-
// Reset heights first
|
|
138
|
+
// Reset heights first so each card reports its natural height
|
|
133
139
|
cards.forEach(card => {
|
|
134
140
|
card.style.height = "auto";
|
|
135
141
|
});
|
|
@@ -137,21 +143,112 @@ function ProductCardPanel({
|
|
|
137
143
|
// Find the tallest card
|
|
138
144
|
const maxHeight = Math.max(...cards.map(card => card.offsetHeight));
|
|
139
145
|
|
|
146
|
+
if (maxHeight <= 0) return;
|
|
140
147
|
// Set all cards to the max height
|
|
141
148
|
cards.forEach(card => {
|
|
142
149
|
card.style.height = `${maxHeight}px`;
|
|
143
150
|
});
|
|
144
151
|
};
|
|
145
152
|
|
|
146
|
-
// Run
|
|
153
|
+
// Run immediately for content changes while already visible.
|
|
147
154
|
equalizeHeights();
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
const rafId = requestAnimationFrame(equalizeHeights);
|
|
157
|
+
|
|
158
|
+
// Re-run after a short delay to account for late-loading content (fonts/images)
|
|
150
159
|
const timeoutId = setTimeout(equalizeHeights, 100);
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
window.addEventListener("resize", equalizeHeights);
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
cancelAnimationFrame(rafId);
|
|
165
|
+
clearTimeout(timeoutId);
|
|
166
|
+
window.removeEventListener("resize", equalizeHeights);
|
|
167
|
+
};
|
|
153
168
|
}, [isCarousel, isVisible, desktopExpanded, items.length]);
|
|
154
169
|
|
|
170
|
+
// Two-column mobile: match each card's sections to the tallest card in the
|
|
171
|
+
// same row (pair) so section spacing and the CTA align per row. Desktop keeps
|
|
172
|
+
// its fixed section heights and is left untouched.
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!twoColumn || !isVisible) return;
|
|
175
|
+
|
|
176
|
+
const selectors = [
|
|
177
|
+
"[data-pc-header]",
|
|
178
|
+
"[data-pc-description]",
|
|
179
|
+
"[data-pc-gift]",
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const clear = () => {
|
|
183
|
+
const grid = mobileGridRef.current;
|
|
184
|
+
if (!grid) return;
|
|
185
|
+
selectors.forEach(sel =>
|
|
186
|
+
grid.querySelectorAll<HTMLElement>(sel).forEach(el => {
|
|
187
|
+
el.style.removeProperty("min-height");
|
|
188
|
+
el.style.removeProperty("display");
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// `important` is required because Tailwind utilities are generated with
|
|
194
|
+
// !important in this project (tailwind.config `important: true`).
|
|
195
|
+
const equalizePairs = (sel: string, collapseEmpty = false) => {
|
|
196
|
+
const grid = mobileGridRef.current;
|
|
197
|
+
if (!grid) return;
|
|
198
|
+
const els = Array.from(grid.querySelectorAll<HTMLElement>(sel));
|
|
199
|
+
// Reset before measuring so a previous pass doesn't inflate the result.
|
|
200
|
+
els.forEach(el => {
|
|
201
|
+
el.style.removeProperty("min-height");
|
|
202
|
+
el.style.removeProperty("display");
|
|
203
|
+
});
|
|
204
|
+
for (let i = 0; i < els.length; i += 2) {
|
|
205
|
+
const pair = els.slice(i, i + 2);
|
|
206
|
+
// Collapse a row (or lone trailing card) whose gift slots are all empty
|
|
207
|
+
// so no space is reserved when the row has no gift.
|
|
208
|
+
if (
|
|
209
|
+
collapseEmpty &&
|
|
210
|
+
pair.every(el => el.hasAttribute("data-pc-gift-empty"))
|
|
211
|
+
) {
|
|
212
|
+
pair.forEach(el =>
|
|
213
|
+
el.style.setProperty("display", "none", "important")
|
|
214
|
+
);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (pair.length < 2) break;
|
|
218
|
+
const max = Math.max(
|
|
219
|
+
...pair.map(el => el.getBoundingClientRect().height)
|
|
220
|
+
);
|
|
221
|
+
pair.forEach(el =>
|
|
222
|
+
el.style.setProperty("min-height", `${max}px`, "important")
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const equalize = () => {
|
|
228
|
+
// Only the two-column mobile breakpoint needs JS equalization.
|
|
229
|
+
if (!window.matchMedia("(max-width: 767px)").matches) {
|
|
230
|
+
clear();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
equalizePairs("[data-pc-header]");
|
|
234
|
+
equalizePairs("[data-pc-description]");
|
|
235
|
+
equalizePairs("[data-pc-gift]", true);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const rafId = requestAnimationFrame(equalize);
|
|
239
|
+
const t1 = setTimeout(equalize, 150);
|
|
240
|
+
const t2 = setTimeout(equalize, 500);
|
|
241
|
+
window.addEventListener("resize", equalize);
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
cancelAnimationFrame(rafId);
|
|
245
|
+
clearTimeout(t1);
|
|
246
|
+
clearTimeout(t2);
|
|
247
|
+
window.removeEventListener("resize", equalize);
|
|
248
|
+
clear();
|
|
249
|
+
};
|
|
250
|
+
}, [twoColumn, isVisible, desktopExpanded, items.length]);
|
|
251
|
+
|
|
155
252
|
if (!items.length && !fields?.title) {
|
|
156
253
|
return null;
|
|
157
254
|
}
|
|
@@ -174,20 +271,12 @@ function ProductCardPanel({
|
|
|
174
271
|
badgeIcon: item.innerBadgeIcon?.url || "",
|
|
175
272
|
};
|
|
176
273
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
: desktopExpanded;
|
|
274
|
+
// Shared expand state so toggling "More details" on one card opens the
|
|
275
|
+
// section on the adjacent cards too (matches the residential behavior).
|
|
276
|
+
const isExpanded = desktopExpanded;
|
|
181
277
|
|
|
182
278
|
const handleToggle = () => {
|
|
183
|
-
|
|
184
|
-
setMobileExpandedStates(prev => ({
|
|
185
|
-
...prev,
|
|
186
|
-
[globalIndex]: !prev[globalIndex],
|
|
187
|
-
}));
|
|
188
|
-
} else {
|
|
189
|
-
setDesktopExpanded(!desktopExpanded);
|
|
190
|
-
}
|
|
279
|
+
setDesktopExpanded(prev => !prev);
|
|
191
280
|
};
|
|
192
281
|
|
|
193
282
|
const handleCtaClick = () => {
|
|
@@ -234,6 +323,8 @@ function ProductCardPanel({
|
|
|
234
323
|
renderCheckPlans={renderCheckPlans}
|
|
235
324
|
cta={item.cta}
|
|
236
325
|
priceSuffixBottom={item?.priceSuffixBottom}
|
|
326
|
+
hasAnyBadge={hasAnyBadge}
|
|
327
|
+
twoColumn={twoColumn}
|
|
237
328
|
/>
|
|
238
329
|
</div>
|
|
239
330
|
);
|
|
@@ -241,6 +332,7 @@ function ProductCardPanel({
|
|
|
241
332
|
|
|
242
333
|
return !isCarousel ? (
|
|
243
334
|
<div
|
|
335
|
+
ref={mobileGridRef}
|
|
244
336
|
className={cx(
|
|
245
337
|
"w-full max-w-[1280px] gap-4",
|
|
246
338
|
twoColumn
|
|
@@ -254,6 +346,7 @@ function ProductCardPanel({
|
|
|
254
346
|
<>
|
|
255
347
|
{/* Mobile/Tablet View: Vertical Stack (or two-column grid) */}
|
|
256
348
|
<div
|
|
349
|
+
ref={mobileGridRef}
|
|
257
350
|
className={cx(
|
|
258
351
|
"md:hidden",
|
|
259
352
|
twoColumn ? "grid grid-cols-2 gap-4" : "flex flex-col gap-6"
|
|
@@ -164,7 +164,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
164
164
|
</div>
|
|
165
165
|
)}
|
|
166
166
|
</div>
|
|
167
|
-
<
|
|
167
|
+
<div className="flex w-full flex-1 shrink-0 items-center justify-center lg:w-auto">
|
|
168
168
|
{/* Media Section: only when no Video Link is attached at all */}
|
|
169
169
|
{image && !hasVideoLink && (
|
|
170
170
|
<div
|
|
@@ -203,7 +203,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
|
|
|
203
203
|
isAboveTheFold={isAboveTheFold}
|
|
204
204
|
onVideoEvent={onVideoEvent}
|
|
205
205
|
/>
|
|
206
|
-
</
|
|
206
|
+
</div>
|
|
207
207
|
</div>
|
|
208
208
|
</div>
|
|
209
209
|
|
|
@@ -276,6 +276,18 @@ describe("renderContentfulRichText", () => {
|
|
|
276
276
|
const a = container.querySelector("a")!;
|
|
277
277
|
expect(a).toHaveClass("custom-link");
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
it("uses a separate class for links inside list items", () => {
|
|
281
|
+
const list = makeList(false, ["List link"]);
|
|
282
|
+
list.content[0].content = [makeHyperlink("/list-link", "List link")];
|
|
283
|
+
const doc = makeDoc(makeHyperlink("/paragraph-link", "Paragraph link"), list);
|
|
284
|
+
|
|
285
|
+
const { container } = render(<>{renderContentfulRichText(doc)}</>);
|
|
286
|
+
const links = container.querySelectorAll("a");
|
|
287
|
+
|
|
288
|
+
expect(links[0]).toHaveClass("body1", "font-bold");
|
|
289
|
+
expect(links[1]).toHaveClass("body2");
|
|
290
|
+
});
|
|
279
291
|
});
|
|
280
292
|
|
|
281
293
|
describe("Custom options merge", () => {
|
|
@@ -267,10 +267,23 @@ export function renderContentfulRichText(
|
|
|
267
267
|
className: string = "body1",
|
|
268
268
|
linkClassName: string = "body1 font-bold",
|
|
269
269
|
options?: Options,
|
|
270
|
-
links?: any
|
|
270
|
+
links?: any,
|
|
271
|
+
listLinkClassName: string = "body2"
|
|
271
272
|
): ReactNode | null {
|
|
272
273
|
if (!doc || !Array.isArray(doc.content)) return null;
|
|
273
274
|
|
|
275
|
+
const listItemHyperlinks = new WeakSet<object>();
|
|
276
|
+
const collectListItemHyperlinks = (node: any, isInListItem = false) => {
|
|
277
|
+
const isListItem = isInListItem || node?.nodeType === BLOCKS.LIST_ITEM;
|
|
278
|
+
if (isListItem && node?.nodeType === INLINES.HYPERLINK) {
|
|
279
|
+
listItemHyperlinks.add(node);
|
|
280
|
+
}
|
|
281
|
+
node?.content?.forEach((child: any) =>
|
|
282
|
+
collectListItemHyperlinks(child, isListItem)
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
collectListItemHyperlinks(doc);
|
|
286
|
+
|
|
274
287
|
const defaultOptions = getDefaultOptions(links);
|
|
275
288
|
const merged: Options = {
|
|
276
289
|
...defaultOptions,
|
|
@@ -301,7 +314,9 @@ export function renderContentfulRichText(
|
|
|
301
314
|
target={shouldOpenBlank ? "_blank" : undefined}
|
|
302
315
|
rel={shouldOpenBlank ? "noopener" : undefined}
|
|
303
316
|
variant="default"
|
|
304
|
-
className={
|
|
317
|
+
className={
|
|
318
|
+
listItemHyperlinks.has(node) ? listLinkClassName : linkClassName
|
|
319
|
+
}
|
|
305
320
|
>
|
|
306
321
|
{children}
|
|
307
322
|
</Link>
|