@stratakit/foundations
Version:
Foundational pieces of StrataKit
110 lines (109 loc) • 3.88 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 DEFAULT_ICON_HASH = "#icon";
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-_sk-size": size,
className: cx("\u{1F95D}Icon", props.className),
ref: forwardedRef,
children: hrefBase ? /* @__PURE__ */ jsx("use", { href: toIconHref(hrefBase) }) : null
}
);
});
function toIconHref(hrefBase) {
if (!hrefBase.includes("#")) return `${hrefBase}${DEFAULT_ICON_HASH}`;
return hrefBase;
}
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[/* @__PURE__ */ 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 resourceUrl = new URL(rawHref, ownerDocument.baseURI);
const hash = resourceUrl.hash || DEFAULT_ICON_HASH;
resourceUrl.hash = "";
const response = await fetch(resourceUrl.href, { signal });
if (!response.ok) {
throw new Error(`Failed to fetch ${resourceUrl.href}`);
}
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}--${hash.slice(1)}`;
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
};