UNPKG

react-svg

Version:

A React component that injects SVG into the DOM.

1 lines 15.9 kB
{"version":3,"file":"react-svg.mjs","names":[],"sources":["../src/owner-window.ts","../src/ReactSVG.tsx"],"sourcesContent":["// Hat-tip: https://github.com/mui/material-ui/tree/master/packages/mui-utils/src.\n\nconst ownerWindow = (node?: Node | null) => {\n const doc = node?.ownerDocument || document\n return doc.defaultView || window\n}\n\nexport default ownerWindow\n","import { SVGInjector } from '@tanem/svg-injector'\nimport * as React from 'react'\n\nimport ownerWindow from './owner-window'\nimport type { Props, WrapperType } from './types'\n\nconst svgNamespace = 'http://www.w3.org/2000/svg'\nconst xlinkNamespace = 'http://www.w3.org/1999/xlink'\n\n// Random prefix avoids ID collisions when multiple copies of react-svg are\n// bundled (e.g. microfrontends). The counter ensures each component instance\n// within the same bundle gets a unique ID.\nconst idPrefix = `react-svg-${Math.random().toString(36).slice(2, 6)}`\nlet idCounter = 0\n\n// forwardRef is still required: function components only accept a `ref` prop\n// directly from React 19 onwards, and the supported floor is React 16.8.\n//\n// The type annotation keeps declaration emit from inlining the interfaces\n// `Props` is built from, which aren't exported.\n// eslint-disable-next-line @eslint-react/no-forward-ref\nexport const ReactSVG: React.ForwardRefExoticComponent<\n Props & React.RefAttributes<WrapperType>\n> = React.forwardRef<WrapperType, Props>(\n (\n {\n afterInjection = () => undefined,\n beforeInjection = () => undefined,\n desc = '',\n evalScripts = 'never',\n fallback: Fallback,\n httpRequestWithCredentials = false,\n loading: Loading,\n loadingDelay = 0,\n onError = () => undefined,\n renumerateIRIElements = true,\n src,\n title = '',\n useRequestCache = true,\n wrapper = 'div',\n ...rest\n },\n forwardedRef,\n ) => {\n const [hasError, setHasError] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n\n // `loading` is held back until the delay elapses, so an injection that\n // resolves sooner - a warm cache, localhost, a file:// read - never paints\n // an indicator at all. The initial value has to account for the delay\n // rather than start false: effects run after paint, so initialising to\n // false would cost the default a frame without the loader.\n const [hasLoadingDelayElapsed, setHasLoadingDelayElapsed] = React.useState(\n loadingDelay <= 0,\n )\n\n // Bumped whenever an injection starts, so the delay effect below can tell\n // one injection from the next. `isLoading` can't carry that on its own: a\n // re-injection that begins while the previous request is still in flight\n // leaves it true throughout, so with an unchanged `loadingDelay` neither of\n // that effect's other dependencies would change and the timer would never\n // re-arm.\n //\n // Skipped for the first injection, so mount still settles in one render -\n // every value that effect writes already holds its default there.\n const [injectionId, setInjectionId] = React.useState(0)\n const hasInjectedRef = React.useRef(false)\n\n const reactWrapperRef = React.useRef<WrapperType | null>(null)\n\n // The callbacks are read through a ref so that changing them - which inline\n // arrow props do on every render - doesn't tear down and re-run the\n // injection. Declared before the injection effect so it always holds the\n // current props by the time that effect runs.\n const callbacksRef = React.useRef({\n afterInjection,\n beforeInjection,\n onError,\n })\n React.useEffect(() => {\n callbacksRef.current = { afterInjection, beforeInjection, onError }\n })\n\n // Read through a ref for the same reason, and declared here so it is\n // current by the time the injection effect runs: that effect has to clear\n // the elapsed flag, but listing `loadingDelay` as a dependency would make\n // changing the delay re-inject.\n const loadingDelayRef = React.useRef(loadingDelay)\n React.useEffect(() => {\n loadingDelayRef.current = loadingDelay\n })\n\n const refCallback = React.useCallback(\n (reactWrapper: WrapperType | null) => {\n reactWrapperRef.current = reactWrapper\n if (typeof forwardedRef === 'function') {\n forwardedRef(reactWrapper)\n } else if (forwardedRef) {\n forwardedRef.current = reactWrapper\n }\n },\n [forwardedRef],\n )\n\n // Only props that affect the injected SVG are listed as dependencies. Props\n // spread onto the React wrapper (className, style, event handlers, ...) are\n // applied by React itself and don't warrant a re-injection.\n React.useEffect(() => {\n const reactWrapper = reactWrapperRef.current\n\n /* istanbul ignore next */\n if (!(reactWrapper instanceof ownerWindow(reactWrapper).Node)) {\n return\n }\n\n // Guards against a teardown - unmount, or a dependency change that starts\n // a fresh injection - landing while the previous injection is still in\n // flight. The stale callbacks must not touch state, but errors are still\n // reported.\n let isActive = true\n let nonReactWrapper: WrapperType | null = null\n\n const removeSVG = () => {\n if (nonReactWrapper?.parentNode) {\n nonReactWrapper.parentNode.removeChild(nonReactWrapper)\n nonReactWrapper = null\n }\n }\n\n // A new injection is starting, so any result from the previous one is\n // stale. On mount the three flags already hold these defaults and React\n // bails out - and `injectionId` is skipped there for the same reason - so\n // this only re-renders when a dependency actually changed.\n //\n // The elapsed flag has to be cleared here rather than left to the delay\n // effect below: that effect only reacts once this render has committed,\n // and the render in between would still see a flag left true by the\n // previous injection and mount `loading` regardless of the delay. It\n // takes `loadingDelay <= 0` rather than a plain false for the reason the\n // initial state does - the default would otherwise lose a frame to a\n // re-injection.\n /* eslint-disable @eslint-react/set-state-in-effect */\n setHasError(false)\n setIsLoading(true)\n setHasLoadingDelayElapsed(loadingDelayRef.current <= 0)\n if (hasInjectedRef.current) {\n setInjectionId((id) => id + 1)\n } else {\n hasInjectedRef.current = true\n }\n /* eslint-enable @eslint-react/set-state-in-effect */\n\n let nonReactTarget: WrapperType\n\n if (wrapper === 'svg') {\n nonReactWrapper = document.createElementNS(svgNamespace, wrapper)\n nonReactWrapper.setAttribute('xmlns', svgNamespace)\n nonReactWrapper.setAttribute('xmlns:xlink', xlinkNamespace)\n nonReactTarget = document.createElementNS(svgNamespace, wrapper)\n } else {\n nonReactWrapper = document.createElement(wrapper)\n nonReactTarget = document.createElement(wrapper)\n }\n\n nonReactWrapper.appendChild(nonReactTarget)\n nonReactTarget.dataset.src = src\n\n reactWrapper.appendChild(nonReactWrapper)\n\n const handleError = (error: unknown) => {\n removeSVG()\n if (isActive) {\n setHasError(true)\n setIsLoading(false)\n }\n callbacksRef.current.onError(error)\n }\n\n const afterEach = (error: Error | null, svg?: SVGSVGElement) => {\n if (error) {\n handleError(error)\n return\n }\n\n if (!isActive) {\n return\n }\n\n setIsLoading(false)\n\n try {\n callbacksRef.current.afterInjection(svg!)\n } catch (afterInjectionError) {\n handleError(afterInjectionError)\n }\n }\n\n // WAI best practice: SVGs need role=\"img\" plus aria-labelledby/\n // aria-describedby pointing to <title>/<desc> element IDs for screen\n // readers to announce them. svg-injector copies the HTML title\n // *attribute* (tooltip) but doesn't create SVG-namespace child\n // elements or ARIA linkage, so we handle that here.\n const beforeEach = (svg: SVGSVGElement): void => {\n svg.setAttribute('role', 'img')\n\n const ariaLabelledBy: string[] = []\n const ariaDescribedBy: string[] = []\n\n if (title) {\n const originalTitle = svg.querySelector(':scope > title')\n if (originalTitle) {\n svg.removeChild(originalTitle)\n }\n const titleId = `${idPrefix}-title-${++idCounter}`\n // createElementNS is required: createElement would produce an\n // HTML-namespace node that screen readers ignore inside SVG.\n const newTitle = document.createElementNS(svgNamespace, 'title')\n newTitle.id = titleId\n newTitle.textContent = title\n svg.prepend(newTitle)\n ariaLabelledBy.push(titleId)\n }\n\n if (desc) {\n const originalDesc = svg.querySelector(':scope > desc')\n if (originalDesc) {\n svg.removeChild(originalDesc)\n }\n const descId = `${idPrefix}-desc-${++idCounter}`\n const newDesc = document.createElementNS(svgNamespace, 'desc')\n newDesc.id = descId\n newDesc.textContent = desc\n const existingTitle = svg.querySelector(':scope > title')\n if (existingTitle) {\n existingTitle.after(newDesc)\n } else {\n svg.prepend(newDesc)\n }\n ariaDescribedBy.push(descId)\n }\n\n if (ariaLabelledBy.length > 0) {\n svg.setAttribute('aria-labelledby', ariaLabelledBy.join(' '))\n }\n\n if (ariaDescribedBy.length > 0) {\n svg.setAttribute('aria-describedby', ariaDescribedBy.join(' '))\n }\n\n try {\n callbacksRef.current.beforeInjection(svg)\n } catch (error) {\n handleError(error)\n }\n }\n\n SVGInjector(nonReactTarget, {\n afterEach,\n beforeEach,\n cacheRequests: useRequestCache,\n evalScripts,\n httpRequestWithCredentials,\n renumerateIRIElements,\n })\n\n return () => {\n isActive = false\n removeSVG()\n }\n }, [\n desc,\n evalScripts,\n httpRequestWithCredentials,\n renumerateIRIElements,\n src,\n title,\n useRequestCache,\n wrapper,\n ])\n\n // Keyed on `injectionId` rather than living in the injection effect, so\n // that changing `loadingDelay` restarts the timer without re-running the\n // injection. Only the timer lives here; a re-injection's flag is cleared by\n // the injection effect itself, which is a render earlier than this can run.\n //\n // `isLoading` is a dependency so the timer is cleared once an injection\n // finishes, and `injectionId` so it re-arms for every injection - including\n // one that starts while the previous request is still in flight, which\n // leaves `isLoading` true the whole way through.\n React.useEffect(() => {\n if (!isLoading) {\n return\n }\n\n /* eslint-disable @eslint-react/set-state-in-effect */\n if (loadingDelay <= 0) {\n setHasLoadingDelayElapsed(true)\n return\n }\n\n setHasLoadingDelayElapsed(false)\n /* eslint-enable @eslint-react/set-state-in-effect */\n\n const timeoutId = setTimeout(() => {\n setHasLoadingDelayElapsed(true)\n }, loadingDelay)\n\n return () => {\n clearTimeout(timeoutId)\n }\n }, [injectionId, isLoading, loadingDelay])\n\n const Wrapper = wrapper\n\n return (\n <Wrapper\n {...rest}\n ref={refCallback}\n {...(wrapper === 'svg'\n ? {\n xmlns: svgNamespace,\n xmlnsXlink: xlinkNamespace,\n }\n : {})}\n >\n {isLoading && hasLoadingDelayElapsed && Loading && <Loading />}\n {hasError && Fallback && <Fallback />}\n </Wrapper>\n )\n },\n)\n\nReactSVG.displayName = 'ReactSVG'\n"],"mappings":"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"}