Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions cms-slider/src/components/CMSSlider/CMSSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ const CMSSlider = (props: CMSSliderProps) => {
const parentRef = useRef<HTMLDivElement>(null);

// Extract CMS collection items from Webflow slot
const { cmsCollectionComponentSlotRef, slideElements } =
useCMSCollectionItems("cmsCollectionComponentSlot");
const { cmsCollectionComponentSlotRef, items } = useCMSCollectionItems(
"cmsCollectionComponentSlot"
);

// Inject global styles into shadow DOM
useShadowGlobalStyles(parentRef);
Expand All @@ -50,7 +51,7 @@ const CMSSlider = (props: CMSSliderProps) => {
</div>

{/* Render slider once CMS items are extracted */}
{slideElements && slideElements.length > 0 && (
{items && items.length > 0 && (
<SlickSlider
infinite={infinite}
slidesToShow={slidesToShow}
Expand All @@ -61,8 +62,8 @@ const CMSSlider = (props: CMSSliderProps) => {
autoplaySpeed={autoplaySpeed}
swipeToSlide={true}
>
{slideElements.map((slide, index) => (
<SlideItem key={index} slide={slide} index={index} />
{items.map((item, index) => (
<SlideItem key={index} item={item} index={index} />
))}
</SlickSlider>
)}
Expand Down
14 changes: 7 additions & 7 deletions cms-slider/src/components/CMSSlider/SlideItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { useEffect, useRef } from "react";
/**
* Wrapper component that renders a single CMS collection item as a slide
*/
const SlideItem = (props: { slide: HTMLDivElement; index: number }) => {
const { slide, index } = props;
const slideRef = useRef<HTMLDivElement>(null);
const SlideItem = (props: { item: HTMLDivElement; index: number }) => {
const { item, index } = props;
const itemRef = useRef<HTMLDivElement>(null);

// Append the cloned slide element to the container
useEffect(() => {
if (slideRef.current) {
slideRef.current.appendChild(slide.cloneNode(true) as HTMLDivElement);
if (itemRef.current) {
itemRef.current.appendChild(item.cloneNode(true) as HTMLDivElement);
}
}, [slide]);
}, [item]);

return <div ref={slideRef} data-index={index}></div>;
return <div ref={itemRef} data-index={index}></div>;
};

export default SlideItem;
20 changes: 8 additions & 12 deletions cms-slider/src/components/CMSSlider/useCMSCollectionItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
*/
export function useCMSCollectionItems(slotName: string) {
const cmsCollectionComponentSlotRef = useRef<HTMLDivElement>(null);
const [slideElements, setSlideElements] = useState<HTMLDivElement[] | null>(
null
);
const [items, setItems] = useState<HTMLDivElement[] | null>(null);

useEffect(() => {
if (slideElements === null && cmsCollectionComponentSlotRef.current) {
if (items === null && cmsCollectionComponentSlotRef.current) {
// Find the slot element by name
const slot = cmsCollectionComponentSlotRef.current.querySelector(
`[name="${slotName}"]`
Expand All @@ -29,22 +27,20 @@ export function useCMSCollectionItems(slotName: string) {
)
) as HTMLDivElement[]
).map((slide) => slide.cloneNode(true) as HTMLDivElement);
setSlideElements(slides);
setItems(slides);
}
}
}
}, [cmsCollectionComponentSlotRef.current, slideElements]);
}, [cmsCollectionComponentSlotRef.current, items]);

// Filter out empty slides and memoize for performance
const memoizedSlideElements = useMemo(
() =>
slideElements?.filter((slide) => slide && slide.children.length > 0) ??
[],
[slideElements]
const memoizedItems = useMemo(
() => items?.filter((item) => item && item.children.length > 0) ?? [],
[items]
);

return {
cmsCollectionComponentSlotRef,
slideElements: memoizedSlideElements,
items: memoizedItems,
};
}
105 changes: 91 additions & 14 deletions cms-slider/src/hooks/useShadowGlobalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,97 @@ function copyGlobalStylesToShadow(shadowRoot: ShadowRoot) {
);

styleElements.forEach((el) => {
let clone: HTMLElement | null = null;

// Clone inline styles
if (el.tagName === "STYLE") {
clone = document.createElement("style");
(clone as HTMLStyleElement).textContent = el.textContent;
}
// Clone linked stylesheets
else if (el.tagName === "LINK" && (el as HTMLLinkElement).href) {
clone = document.createElement("link");
(clone as HTMLLinkElement).rel = "stylesheet";
(clone as HTMLLinkElement).href = (el as HTMLLinkElement).href;
const clone = cloneStyleElement(el);
if (clone) {
shadowRoot.appendChild(clone);
}

if (clone) shadowRoot.appendChild(clone);
});
}

/**
* Clones a style or link element for injection into shadow DOM.
*
* @param el - The style or link element to clone
* @returns A cloned HTMLElement ready for shadow DOM, or null if element cannot be cloned
*/
function cloneStyleElement(el: Element): HTMLElement | null {
// Clone inline <style> elements
if (el.tagName === "STYLE") {
return cloneInlineStyleElement(el as HTMLStyleElement);
}

// Clone external <link> stylesheets
if (el.tagName === "LINK" && (el as HTMLLinkElement).href) {
return cloneLinkElement(el as HTMLLinkElement);
}

return null;
}

/**
* Clones an inline style element, preserving its CSS content.
* Falls back to CSSOM extraction if textContent is empty.
*
* @param el - The style element to clone
* @returns A new style element with the same CSS content
*/
function cloneInlineStyleElement(el: HTMLStyleElement): HTMLStyleElement {
const clone = document.createElement("style");
const textContent = el.textContent?.trim();

// Use textContent if available, otherwise extract from CSSOM
if (textContent) {
clone.textContent = textContent;
} else {
clone.textContent = getStyleElementCSS(el);
}

return clone;
}

/**
* Clones a link element for external stylesheets.
*
* @param el - The link element to clone
* @returns A new link element pointing to the same stylesheet
*/
function cloneLinkElement(el: HTMLLinkElement): HTMLLinkElement {
const clone = document.createElement("link");
clone.rel = "stylesheet";
clone.href = el.href;
return clone;
}

/**
* Extracts CSS rules from a style element by accessing its associated stylesheet.
*
* This function is necessary because sometimes `element.textContent` is empty or unreliable
* for dynamically created style elements, but the actual CSS rules are accessible via the
* CSSOM (CSS Object Model) through `document.styleSheets`.
*
* @param el - The HTML element to extract CSS from (should be a style element)
* @returns The concatenated CSS text from all rules in the stylesheet, or empty string if the element is not a style element or if the stylesheet is not found.
*/
function getStyleElementCSS(el: HTMLElement) {
if (!(el instanceof HTMLStyleElement)) {
return "";
}

// Find the CSSStyleSheet object associated with this style element
const sheet = Array.from(document.styleSheets).find(
(s) => s.ownerNode === el
);

if (!sheet) return "";

try {
// Extract and concatenate all CSS rules from the stylesheet
return Array.from(sheet.cssRules)
.map((rule) => rule.cssText)
.join("\n");
} catch (e) {
// CORS restrictions prevent reading cross-origin stylesheets
console.warn("Unable to read CSS rules (maybe cross-origin):", e);
return "";
}
}