UNPKG

@stratakit/foundations

Version:
110 lines (109 loc) 3.79 kB
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 };