@stratakit/foundations
Version:
Foundational pieces of StrataKit
110 lines (109 loc) • 3.79 kB
JavaScript
import { jsx } from "react/jsx-runtime";
import * as React from "react";
import { Role } from "@ariakit/react/role";
import cx from "classnames";
import { useLatestRef, useSafeContext } from "./~hooks.js";
import { forwardRef, getOwnerDocument, parseDOM } from "./~utils.js";
import {
HtmlSanitizerContext,
spriteSheetId,
useRootNode
} from "./Root.internal.js";
const Icon = forwardRef((props, forwardedRef) => {
const { href: hrefProp, size, alt, ...rest } = props;
const isDecorative = !alt;
const hrefBase = useNormalizedHrefBase(hrefProp);
return /* @__PURE__ */ jsx(
Role.svg,
{
"aria-hidden": isDecorative ? "true" : void 0,
role: isDecorative ? void 0 : "img",
"aria-label": isDecorative ? void 0 : alt,
...rest,
"data-kiwi-size": size,
className: cx("\u{1F95D}-icon", props.className),
ref: forwardedRef,
children: hrefBase ? /* @__PURE__ */ jsx("use", { href: toIconHref(hrefBase, size) }) : null
}
);
});
DEV: Icon.displayName = "Icon";
function toIconHref(hrefBase, size) {
const separator = hrefBase.includes("#") ? "--" : "#";
const suffix = toIconId(size);
return `${hrefBase}${separator}${suffix}`;
}
function toIconId(size) {
if (size === "large") return "icon-large";
return "icon";
}
function useNormalizedHrefBase(rawHref) {
const generatedId = React.useId();
const sanitizeHtml = useLatestRef(useSafeContext(HtmlSanitizerContext));
const rootNode = useRootNode();
const inlineHref = React.useRef(void 0);
const getClientSnapshot = () => {
const ownerDocument = getOwnerDocument(rootNode);
if (!rawHref || !ownerDocument) return void 0;
if (isHttpProtocol(rawHref, ownerDocument)) return rawHref;
return inlineHref.current;
};
const subscribe = React.useCallback(
(notify) => {
const ownerDocument = getOwnerDocument(rootNode);
const spriteSheet = ownerDocument?.getElementById(spriteSheetId);
if (!rawHref || !ownerDocument || !spriteSheet) return () => {
};
if (isHttpProtocol(rawHref, ownerDocument)) return () => {
};
const cache = spriteSheet[Symbol.for("\u{1F95D}")]?.icons;
if (!cache) return () => {
};
const prefix = `\u{1F95D}${generatedId}`;
if (cache.has(rawHref)) {
inlineHref.current = cache.get(rawHref);
notify();
return () => {
};
}
const abortController = new AbortController();
const { signal } = abortController;
(async () => {
try {
const response = await fetch(rawHref, { signal });
if (!response.ok) throw new Error(`Failed to fetch ${rawHref}`);
const fetchedSvgString = sanitizeHtml.current(await response.text());
const parsedSvgContent = parseDOM(fetchedSvgString, {
ownerDocument
});
const symbols = parsedSvgContent.querySelectorAll("symbol");
for (const symbol of symbols) {
symbol.id = `${prefix}--${symbol.id}`;
if (ownerDocument.getElementById(symbol.id)) continue;
spriteSheet.appendChild(symbol.cloneNode(true));
}
inlineHref.current = `#${prefix}`;
cache.set(rawHref, inlineHref.current);
if (!signal.aborted) notify();
} catch (error) {
if (signal.aborted) return;
console.error(error);
}
})();
return () => abortController.abort();
},
[rawHref, rootNode, sanitizeHtml, generatedId]
);
return React.useSyncExternalStore(
subscribe,
getClientSnapshot,
() => rawHref
);
}
function isHttpProtocol(url, ownerDocument) {
const { protocol } = new URL(url, ownerDocument.baseURI);
return ["http:", "https:"].includes(protocol);
}
export {
Icon
};