From 9b8ddfff04b3cecc8b9d9a52bb2e42320dc06e65 Mon Sep 17 00:00:00 2001 From: Tane Morgan <464864+tanem@users.noreply.github.com> Date: Fri, 7 Aug 2026 05:37:44 +1200 Subject: [PATCH 1/4] Reduce the example READMEs to what each example shows Co-Authored-By: Claude Opus 5 --- examples/accessibility/README.md | 10 +++------- examples/api-usage/README.md | 11 ++++------- examples/basic-usage/README.md | 9 ++------- examples/before-injection/README.md | 10 +++------- examples/css-animation/README.md | 10 +++------- examples/css-in-js/README.md | 10 +++------- examples/data-url/README.md | 10 +++------- examples/external-stylesheet/README.md | 10 +++------- examples/fallbacks/README.md | 11 ++++------- examples/iframe/README.md | 10 +++------- examples/loading/README.md | 10 +++------- examples/no-extension/README.md | 13 +++---------- examples/sprite-usage/README.md | 10 +++------- examples/ssr/README.md | 20 ++++---------------- examples/styled-components/README.md | 11 ++++------- examples/svg-wrapper/README.md | 10 +++------- examples/typescript/README.md | 9 ++------- 17 files changed, 53 insertions(+), 131 deletions(-) diff --git a/examples/accessibility/README.md b/examples/accessibility/README.md index b694661c2..3c873203a 100644 --- a/examples/accessibility/README.md +++ b/examples/accessibility/README.md @@ -1,10 +1,6 @@ # ReactSVG Accessibility Example -## Available Scripts +Sets `title` and `desc`, so the injected SVG carries `` and `<desc>` +elements wired to it through `aria-labelledby` and `aria-describedby`. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/api-usage/README.md b/examples/api-usage/README.md index a58e213bd..b7a67b2af 100644 --- a/examples/api-usage/README.md +++ b/examples/api-usage/README.md @@ -1,10 +1,7 @@ # ReactSVG API Usage Example -## Available Scripts +Every prop at once, including the ones left at their defaults elsewhere - +`evalScripts`, `httpRequestWithCredentials`, `renumerateIRIElements`, +`useRequestCache` and `wrapper`. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/basic-usage/README.md b/examples/basic-usage/README.md index 488a4b57f..03763c3cd 100644 --- a/examples/basic-usage/README.md +++ b/examples/basic-usage/README.md @@ -1,10 +1,5 @@ # ReactSVG Basic Usage Example -## Available Scripts +The smallest thing that works: a single `src`, no other props. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/before-injection/README.md b/examples/before-injection/README.md index c8a806f2f..db212b7ae 100644 --- a/examples/before-injection/README.md +++ b/examples/before-injection/README.md @@ -1,10 +1,6 @@ # ReactSVG Before Injection Example -## Available Scripts +Uses `beforeInjection` to class, style and rewrite the SVG element in the window +between the fetch landing and the element reaching the DOM. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/css-animation/README.md b/examples/css-animation/README.md index 09d576ec7..629ea41a2 100644 --- a/examples/css-animation/README.md +++ b/examples/css-animation/README.md @@ -1,10 +1,6 @@ # ReactSVG CSS Animation Example -## Available Scripts +Animates the injected SVG by adding a class in `afterInjection`, which is the +first point at which the element is in the DOM and animatable. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/css-in-js/README.md b/examples/css-in-js/README.md index 0c31bea5b..5ccd0e4e5 100644 --- a/examples/css-in-js/README.md +++ b/examples/css-in-js/README.md @@ -1,10 +1,6 @@ # ReactSVG CSS-in-JS Example -## Available Scripts +Styles the injected SVG with glamor, relying on unrecognised props being spread +onto the outer wrapper so the generated class reaches the DOM. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/data-url/README.md b/examples/data-url/README.md index bb17500ab..6ae6d5a57 100644 --- a/examples/data-url/README.md +++ b/examples/data-url/README.md @@ -1,10 +1,6 @@ # ReactSVG Data URL Example -## Available Scripts +Injects from `data:image/svg+xml` URLs, both URL-encoded and base64, as a +bundler like Vite produces for small SVGs. No network request is made. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/external-stylesheet/README.md b/examples/external-stylesheet/README.md index 131798c70..7020385c4 100644 --- a/examples/external-stylesheet/README.md +++ b/examples/external-stylesheet/README.md @@ -1,10 +1,6 @@ # ReactSVG External Stylesheet Example -## Available Scripts +Styles the injected SVG from a plain stylesheet, reaching it through a class on +the wrapper. Injected markup is part of the page, so ordinary CSS applies. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/fallbacks/README.md b/examples/fallbacks/README.md index bc7ad1322..2d98f4e32 100644 --- a/examples/fallbacks/README.md +++ b/examples/fallbacks/README.md @@ -1,10 +1,7 @@ # ReactSVG Fallbacks Example -## Available Scripts +`fallback` as a class, function and string component, and the two cases that +aren't a failed fetch: an error thrown from `beforeInjection` or +`afterInjection`. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/iframe/README.md b/examples/iframe/README.md index 6d3f4eb41..c2f52aecf 100644 --- a/examples/iframe/README.md +++ b/examples/iframe/README.md @@ -1,10 +1,6 @@ # ReactSVG Iframe Example -## Available Scripts +Injects inside an iframe, where the wrapper belongs to a different document than +the one the app was loaded into. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/loading/README.md b/examples/loading/README.md index 00d80712e..600c26a62 100644 --- a/examples/loading/README.md +++ b/examples/loading/README.md @@ -1,10 +1,6 @@ # ReactSVG Loading Example -## Available Scripts +Shows a spinner through `loading` while the SVG is fetched, once on its own and +once alongside a `fallback` for a URL that fails. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/no-extension/README.md b/examples/no-extension/README.md index 61f8d822b..4edf6e36c 100644 --- a/examples/no-extension/README.md +++ b/examples/no-extension/README.md @@ -1,13 +1,6 @@ # ReactSVG No Extension Example -Demonstrates that react-svg handles SVG files served without a file extension, -provided the server responds with an appropriate content type. +An SVG served without a file extension, which works provided the server responds +with an appropriate content type. -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Builds the app with Vite, starts an Express server on port 8080, and opens the -browser automatically. +`npm start` builds the app, serves it on port 8080 and opens the browser. diff --git a/examples/sprite-usage/README.md b/examples/sprite-usage/README.md index c8a2bd021..650a2cc8c 100644 --- a/examples/sprite-usage/README.md +++ b/examples/sprite-usage/README.md @@ -1,10 +1,6 @@ # ReactSVG Sprite Usage Example -## Available Scripts +Pulls individual icons out of one `sprite.svg` through fragment identifiers +(`sprite.svg#icon-star`), sizing each one in `beforeInjection`. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/ssr/README.md b/examples/ssr/README.md index 6d86c1bd1..f4aebb753 100644 --- a/examples/ssr/README.md +++ b/examples/ssr/README.md @@ -1,20 +1,8 @@ # ReactSVG SSR Example -This project was bootstrapped with [Next.js](https://nextjs.org/). - `ReactSVG` is published with a `"use client"` directive, so it can be imported -directly into a Server Component - see `app/page.js`. Props that take functions -(`afterInjection`, `beforeInjection`, `fallback`, `loading`) can't cross the -server/client boundary, so pass those from your own Client Component. - -## Available Scripts - -In the project directory, you can run: - -### `npm run dev` - -Runs the app in the development mode.<br> -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +directly into a Next.js Server Component - see `app/page.js`. Props that take +functions (`afterInjection`, `beforeInjection`, `fallback`, `loading`) can't +cross the server/client boundary, so pass those from your own Client Component. -The page will reload if you make edits.<br> -You will also see any lint errors in the console. \ No newline at end of file +`npm run dev`, then open <http://localhost:3000>. diff --git a/examples/styled-components/README.md b/examples/styled-components/README.md index 9ed3e2a87..7db272b58 100644 --- a/examples/styled-components/README.md +++ b/examples/styled-components/README.md @@ -1,10 +1,7 @@ # ReactSVG Styled Components Example -## Available Scripts +Wraps `ReactSVG` in `styled()` and reaches the injected SVG through a descendant +selector, since the styled wrapper sits two elements above it. From +[#1911](https://github.com/tanem/react-svg/issues/1911). -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/svg-wrapper/README.md b/examples/svg-wrapper/README.md index e90d2198c..5a3bb68d0 100644 --- a/examples/svg-wrapper/README.md +++ b/examples/svg-wrapper/README.md @@ -1,10 +1,6 @@ # ReactSVG SVG Wrapper Example -## Available Scripts +`wrapper="svg"`, mounted twice: once inside an existing SVG document and once +inside plain HTML. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 20a6f9c9f..66487898e 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -1,10 +1,5 @@ # ReactSVG Typescript Example -## Available Scripts +Basic usage from TypeScript, against the bundled type declarations. -In the project directory, you can run: - -### `npm start` - -Runs the app in development mode.\ -Open [http://localhost:5173](http://localhost:5173) to view it in the browser. +`npm start`, then open <http://localhost:5173>. From 29016f9f12fffd30bff1a3df1fbb30a54e7079c8 Mon Sep 17 00:00:00 2001 From: Tane Morgan <464864+tanem@users.noreply.github.com> Date: Fri, 7 Aug 2026 05:39:30 +1200 Subject: [PATCH 2/4] Lead the manual harness README with the procedure Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- test/manual/README.md | 225 ++++++++++++++++++------------------------ 1 file changed, 95 insertions(+), 130 deletions(-) diff --git a/test/manual/README.md b/test/manual/README.md index bee00677e..9a2552244 100644 --- a/test/manual/README.md +++ b/test/manual/README.md @@ -1,39 +1,12 @@ # Manual screen-reader checks -The suite runs in jsdom, which has no accessibility layer and no paint. Every -accessibility claim this package makes is therefore pinned as markup: that -`role="img"` is set, that the `<title>` and `<desc>` nodes exist, that -`aria-labelledby` points at their IDs. Nothing in the suite observes what -assistive technology does with any of it. +A node server and a page driven by hand under VoiceOver. It answers the +accessibility questions the jsdom suite structurally cannot, and it is +deliberately not wired into CI: the instrument is a real screen reader, and +automating it would mean simulating the thing it exists to escape. -That gap matters here more than in most repos. svg-injector ships no ARIA -behaviour of its own, so the wiring in `beforeEach` and the `loading` element -this package mounts and unmounts are the accessibility contract, and this repo -owns all of it. - -This harness is how that contract gets checked. It is deliberately not wired -into CI: the instrument is a real screen reader, and automating it would mean -simulating the thing it exists to escape. - -It currently covers one question, the one it was built for. Extending it to the -`role="img"` / `<title>` / `<desc>` / `aria-labelledby` path is the obvious next -use and has not been done. - -## The question it answers - -Does a briefly-mounted `loading` element announce? - -svg-injector defers its callbacks, so on a second mount of the same `src` -react-svg commits the `loading` element to the DOM and removes it a couple of -milliseconds later. `should render the specified loader for a cached src` in -`test/browser.spec.tsx` pins that DOM behaviour. Whether a screen reader queues an announcement for an element with -that lifetime is what the suite cannot answer, since assistive technology -observes the DOM rather than the screen and paint timing has nothing to do with -it. - -A cold load mounts and unmounts `loading` in every version, and always has. What -changed is how often: every cached mount now does what previously only a genuine -load did. Report any finding as a frequency change, not as a new defect. +Run it, and update the recorded runs below, when you change the ARIA wiring or +the `loading` element's lifecycle. ## Running it @@ -52,17 +25,6 @@ Then open <http://localhost:4191>, with: Safari is the representative pairing for VoiceOver. Worth a second run in Chrome if the two disagree, since the AT-to-browser bridge differs. -Everything the page loads comes from the working tree: react-svg from `dist/`, -svg-injector and React from `node_modules/`. So it tests the build in front of -you, it needs no network, and there is no second React version to keep in step -with `package.json`. - -React ships no ES module build, which is why `server.mjs` wraps the CJS files it -does ship and `index.html` loads them as classic scripts before anything else. -The import map then points `react` and `react-dom/client` at generated shims -over those globals, so `dist/react-svg.mjs` resolves `react` to the same -instance react-dom is using. - ## The steps Run them in order. Each prints a DOM log; the caption panel is what you are @@ -75,10 +37,9 @@ actually reading. panel and nothing else here can be read. - **0b — insert a populated live region.** Inserts a `role="status"` element that arrives with its text already in it, again with no react-svg involved. - Read against step 0 it says whether any silence is about insertion versus - mutation. That is structurally what react-svg does with a `loading` component, - minus React and svg-injector, so a silent 0b puts the finding on the platform - rather than on this package. + Structurally that is what react-svg does with a `loading` component, minus + React and svg-injector, so read against step 0 it puts any silence on the + platform rather than on this package. - **1 — warm the cache.** Mounts eight icons with no `loading` component at all, waits for injection, unmounts. Nothing here can announce; it exists only to leave svg-injector's cache holding all eight. @@ -90,7 +51,11 @@ actually reading. semantics. - **4 — slow cold load, `role="status"`.** A cold load held open for ~2.5 seconds, so the loading element is mounted for a human-scale stretch rather - than a couple of milliseconds. + than a couple of milliseconds. It is **not** the instrument check, though an + earlier version of this harness treated it as one: it inserts an element that + already carries its text, which is the open question the cached cases turn on, + so a silent step 4 cannot tell a broken setup apart from a real finding. Step + 0 is the only step that can. - **5 — mid-flight re-injection probe.** The odd one out, and the only step that swaps `src` on a live component rather than remounting a fresh tree. Sets `loadingDelay`, re-injects while the first request is still in flight, and @@ -99,32 +64,55 @@ actually reading. second injection long enough that the loader has to come back, `suppress` gives it less than the delay so the loader must stay down. Needs no screen reader, only a foregrounded tab — rAF stops in a background one. Takes about a - minute. - -Step 5 answers a question the rest of this harness cannot, and one jsdom cannot -either: `loadingDelay` exists to stop a loader reaching the screen, and only a -real browser paints. It is also the only step whose result does not depend on -who is running it, so it is the one worth re-running on any change to the delay -logic. Its result is recorded separately below. - -Step 4 is **not** the instrument check, though an earlier version of this -harness treated it as one. It inserts an element that already carries -`role="status"` and its text, and whether assistive technology announces a -*newly inserted* live region is an open question in its own right — the same one -the cached cases turn on. A silent step 4 therefore cannot tell a broken setup -apart from a real finding. Step 0 exists to do that job, and it is the only step -that can. - -## What the DOM log is and is not - -It records each loading element entering and leaving the DOM, and how long it -stayed. That proves the elements existed, which is what tells an *absence of -announcements* apart from an *absence of loading elements*. It does not prove + minute, and is recorded separately below. + +## Recording a run + +| Case | Loading elements in DOM | Median lifetime | Caption panel | +| ------------------------------ | ----------------------- | --------------- | ------------- | +| 0 — instrument check | n/a | n/a | | +| 0b — inserted populated region | n/a | n/a | | +| A — cached, `role="status"` | | | | +| B — cached, plain span | | | | +| 4 — slow cold, `role="status"` | | | | + +Browser and version: +VoiceOver / macOS version: + +## Why the harness looks the way it does + +**The question it answers.** Does a briefly-mounted `loading` element announce? +svg-injector defers its callbacks, so a second mount of the same `src` commits +the `loading` element and removes it a couple of milliseconds later — +`should render the specified loader for a cached src` in `test/browser.spec.tsx` +pins that DOM behaviour. Whether a screen reader queues an announcement for an +element with that lifetime is what the suite cannot answer, since assistive +technology observes the DOM rather than the screen. A cold load has always +mounted and unmounted `loading`; what changed is how often, so report any +finding as a frequency change rather than a new defect. + +**Why it can't be automated.** jsdom has no accessibility layer and no paint, so +every accessibility claim this package makes is pinned as markup: `role="img"` +set, `<title>` and `<desc>` present, `aria-labelledby` pointing at their IDs. +Nothing in the suite observes what assistive technology does with any of it. +That gap matters here more than in most repos, because svg-injector ships no +ARIA behaviour of its own — the wiring and the `loading` element are the whole +contract, and this repo owns all of it. + +**What the DOM log is and is not.** It records each loading element entering and +leaving the DOM, and how long it stayed, which tells an _absence of +announcements_ apart from an _absence of loading elements_. It does not prove anything was announced. Do not report a finding from the log alone. -## The pieces that look incidental +**Where the page's code comes from.** The working tree: react-svg from `dist/`, +svg-injector and React from `node_modules/`. So it tests the build in front of +you, needs no network, and has no second React version to keep in step with +`package.json`. React ships no ES module build, so `server.mjs` wraps the CJS +files it does ship, `index.html` loads them as classic scripts first, and the +import map points `react` and `react-dom/client` at shims over those globals — +which is how `dist/react-svg.mjs` resolves `react` to react-dom's instance. -Each one is a wrong turn already taken once. +**The pieces that look incidental.** Each one is a wrong turn already taken once. | Piece | Covers | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -134,26 +122,27 @@ Each one is a wrong turn already taken once. | Port 4191, not 4190 | 4190 is ManageSieve, on the Fetch standard's blocked port list. WebKit refuses to connect and shows `about:blank`; Chromium loads it and hides the problem. | | Visually hidden live regions | A visible region gets read as an announcement. This produced a contradictory reading before the region was hidden. | -## Recording a run +## What it doesn't cover -| Case | Loading elements in DOM | Median lifetime | Caption panel | -| ------------------------------ | ----------------------- | --------------- | ------------- | -| 0 — instrument check | n/a | n/a | | -| 0b — inserted populated region | n/a | n/a | | -| A — cached, `role="status"` | | | | -| B — cached, plain span | | | | -| 4 — slow cold, `role="status"` | | | | +Only the probe sets `loadingDelay`, so steps 0 through 4 are a no-regression +check on the default path rather than coverage of the prop — a delay long enough +to suppress the mount leaves no element to announce, which the DOM log settles +without a screen reader. -Browser and version: -VoiceOver / macOS version: +Step 5 covers only the mid-flight re-injection path, and only the paint +question. A re-injection starting after the previous one has finished is +exercised nowhere, since A and B remount a fresh tree rather than swapping `src` +on a live component. The probe's frame count is a floor, not a measurement: rAF +samples at about 60Hz, so a mount shorter than a frame can be real and go +uncounted. Mounts are the sensitive figure. -## Last run +Extending the harness to the `role="img"` / `<title>` / `<desc>` / +`aria-labelledby` path is the obvious next use, and has not been done. -Recorded so a later run has something to compare against. +## Last run -19.0.0 plus the `loadingDelay` work and the mid-flight re-injection fix in this -commit, 2026-08-06, Safari 26.5 on macOS 15.7.7, VoiceOver with the caption -panel open: +19.0.0 plus the `loadingDelay` work and the mid-flight re-injection fix, +2026-08-06, Safari 26.5 on macOS 15.7.7, VoiceOver with the caption panel open: | Case | Caption panel | DOM | | ------------------------------------------------------- | ------------- | --------------------------------------------------- | @@ -164,39 +153,28 @@ panel open: | 4 — `loading`, `role="status"`, ~2.5s mounted | silent | 1 element, 2514.0ms | **No announcement, and lifetime is not the variable.** VoiceOver announces a -live region whose content changes and ignores one that arrives with its content -already in it. Step 0b establishes that with neither React nor svg-injector in -the picture, so it is platform behaviour react-svg inherits. React mounts a -`loading` component as a complete element, which is always the second shape, and -a `role="status"` element mounted for a full 2.5 seconds was as silent as the -millisecond-scale ones. Live-region semantics made no difference either: A and B -were equally silent. Unchanged across all three recorded runs. - -What this run does and does not cover. `loadingDelay` defaults to 0, so the -default path mounts `loading` exactly as before, and that is the path every step -here exercises - the harness never sets the prop. So this is a no-regression -check, not coverage of the prop: a delay long enough to suppress the mount -leaves no element to announce, which the DOM log settles without a screen -reader. No step changes `src` on a mounted component either, so neither -re-injection path is exercised - not the one that starts after the previous -injection finished, and not the mid-flight one this commit fixes. A and B remount -a fresh tree instead. Covering those would need a step that swaps `src` on a live -component, which the harness does not have. - -Lifetimes drift by about a millisecond a run - B's median has gone 2.0ms, -7.0ms, 8.0ms across the three - while the shape never changes: every cached -remount mounts and unmounts the element. At this scale the figure tracks the -machine and browser build rather than anything in the package, so it is recorded +live region whose content changes and ignores one that arrives already +populated. Step 0b establishes that with neither React nor svg-injector in the +picture, so it is platform behaviour react-svg inherits — React always mounts +`loading` as a complete element, the second shape. 2.5 seconds was as silent as +7ms, and live-region semantics made no difference. Unchanged across all three +recorded runs. + +Lifetimes drift about a millisecond a run — B's median has gone 2.0ms, 7.0ms, +8.0ms across the three — while the shape never changes. At this scale that +tracks the machine and browser build rather than the package, so it is recorded rather than read as a change. -Re-run this against a different browser or screen reader, or if the mounting -behaviour changes. +The mechanics were re-checked in Chrome 151 on 2026-08-04, after the move here +and to React from `node_modules`: all six steps ran, both cached remounts served +0 requests, the control held its loading element for 2509ms, and the console was +clean apart from React's DevTools notice. No screen reader was running, so that +run says only that the harness works. ## Last probe run -Step 5 is recorded separately because it is a different instrument. It needs no -screen reader, only a foregrounded tab, so unlike the steps above it reads the -same however it is driven and can be re-run by anyone. +Recorded separately because it is a different instrument: needing no screen +reader, it reads the same however it is driven and can be re-run by anyone. Chrome 151 on macOS, 30 runs per phase, against `dist/` built from this commit: @@ -209,8 +187,8 @@ Chrome 151 on macOS, 30 runs per phase, against `dist/` built from this commit: means something because `rearm` returned thirty in the same sitting: together they say the probe could see a loader and still saw none where none belonged. Run against `dist/` built from b06a2cb4, the commit before the fix, `rearm` -reads 0/30 instead - the loader never comes back for the second injection - -which is the regression the fix closes and the reason this step exists. +reads 0/30 instead — the loader never comes back for the second injection, which +is the regression the fix closes and the reason this step exists. Two figures were wrong before they were right, and both were the probe rather than the package. Counting every frame after the swap reported `suppress` as @@ -220,16 +198,3 @@ element that did not arrive after the swap. And checking what was still on screen the moment `.injected-svg` appeared reported `rearm` as 1/30 lingering, because svg-injector inserts the SVG before it calls back, so React has not yet committed `isLoading` false; the check now settles first. - -What this does not cover. Only the mid-flight re-injection path, and only the -paint question - whether assistive technology reacts to any of it is what steps -0 through 4 are for, and they still never set `loadingDelay`. The frame count -is a floor rather than a measurement: rAF samples at about 60Hz, so a mount -shorter than a frame can be real and go uncounted. Mounts are the sensitive -figure; frames only say whether one reached the screen. - -The mechanics were re-checked in Chrome 151 on 2026-08-04, after the move here -and to React from `node_modules`: all six steps ran, both cached remounts served -0 requests, the control held its loading element for 2509ms, and the console was -clean apart from React's DevTools notice. No screen reader was running, so that -run says nothing about announcements — it only says the harness works. From fb2bd6c800979f9826e74c565fbe658670e34696 Mon Sep 17 00:00:00 2001 From: Tane Morgan <464864+tanem@users.noreply.github.com> Date: Fri, 7 Aug 2026 05:41:21 +1200 Subject: [PATCH 3/4] Move the manual harness rationale out of AGENTS.md and add a length rule Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- AGENTS.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fded2f239..ff4ca4ba9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,8 @@ constrains belongs there, not here. ## Writing - NZ English everywhere ("colour", "behaviour", "initialise"). +- Match a document's length to what it needs. Cover the substance, then + stop: no filler sections, restated summaries or boilerplate. - Single-line commit messages, `git log --oneline` style. Add a body only to explain why, and only for behaviour or type changes. - No conventional-commit prefixes (`feat:`, `fix:`, `chore(deps):`) in commit @@ -44,17 +46,11 @@ through `await waitFor(...)`. The warm-cache loading test is the one deliberate exception: it needs the cache to hit, so it uses a fixed URL no other test touches. -jsdom has no accessibility layer and no paint, so the suite pins the ARIA -wiring as markup and can go no further: whether a screen reader does anything -with that markup is outside what any test here can answer. That is this repo's -blind spot rather than a shared one, because the wiring lives here — -svg-injector ships no ARIA behaviour of its own. `test/manual/` is the check for -those questions: a node server and a page driven by hand under VoiceOver. It is -not run by `npm test` and not wired into CI, deliberately, since the instrument -is a real screen reader and automating it would mean simulating the thing it -exists to escape. Run it and record the result in the PR when you change the +`test/manual/` is a hand-driven screen-reader harness, deliberately outside +`npm test` and CI. Run it and record the result in the PR when you change the ARIA wiring or the `loading` element's lifecycle, and update its recorded run in -the same commit as any deliberate change to either. +the same commit as any deliberate change to either. `test/manual/README.md` +covers why it exists and what it can and cannot answer. Raising a `size-limit` budget in `package.json` is a decision, not a fix. Find what grew first, and say why in the commit message. From 021225c697b7d1fd81afd12f62277b8c2f8f88f0 Mon Sep 17 00:00:00 2001 From: Tane Morgan <464864+tanem@users.noreply.github.com> Date: Fri, 7 Aug 2026 05:41:54 +1200 Subject: [PATCH 4/4] Merge the README background into when to use this and trim the FAQ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 94e7e0d48..e01d85688 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,12 @@ > A React component that injects SVG into the DOM. -[Background](#background) | [When To Use This](#when-to-use-this) | [Basic Usage](#basic-usage) | [API](#api) | [Live Examples](#live-examples) | [Installation](#installation) | [Security](#security) | [FAQ](#faq) | [Contributing](#contributing) | [License](#license) +[When To Use This](#when-to-use-this) | [Basic Usage](#basic-usage) | [API](#api) | [Live Examples](#live-examples) | [Installation](#installation) | [Security](#security) | [FAQ](#faq) | [Contributing](#contributing) | [License](#license) -## Background +## When To Use This This component uses [@tanem/svg-injector](https://github.com/tanem/svg-injector) to fetch an SVG from a given URL and inject its markup into the DOM ([why?](https://github.com/tanem/svg-injector#background)). Fetched SVGs are cached, so multiple uses of the same SVG only require a single request. -## When To Use This - Injection costs a network request and two wrapper elements, and it earns that cost in one case: the SVG's URL isn't known until the app runs, and the markup has to be reachable by CSS. An `<img>` tag renders an SVG but its contents can't be styled, animated or scripted from the page. - **SVGs live in your repo and are known at build time.** Reach for a build-time transform - [SVGR](https://react-svgr.com), [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr), or your bundler's SVG loader. They compile each file to a React component, so there's no runtime fetch, and unused icons are tree-shaken out. @@ -249,11 +247,9 @@ Related issues and PRs: Can I use data URIs or inline SVG strings? </summary> -`data:image/svg+xml` URLs are supported (both URL-encoded and base64-encoded). The underlying library parses the SVG content directly from the data URL using `DOMParser`, without making a network request. This is useful when bundlers like Vite inline small SVGs as data URIs. See the [data URL example](https://github.com/tanem/react-svg/tree/master/examples/data-url) for details. - -Inline SVG strings (raw markup passed directly as the `src` prop) are **not** supported. If you already have the SVG markup as a string (for example, a dynamically generated chart), consider parsing it with `DOMParser` and appending the result yourself, or rendering it with `dangerouslySetInnerHTML`. These approaches avoid the fetch step entirely and will also avoid the brief flash that occurs when `react-svg` re-injects on `src` change. +Data URIs yes, inline strings no. `data:image/svg+xml` URLs are parsed directly with `DOMParser` and make no network request - see [`src`](#src) and the [data URL example](https://github.com/tanem/react-svg/tree/master/examples/data-url). -**Security note:** inserting SVG strings into the DOM bypasses React's built-in escaping and can expose your application to XSS if the content is not trusted. If the SVG originates from user input or a third party, sanitise it first with a library like [DOMPurify](https://github.com/cure53/DOMPurify) before inserting it into the page. The same applies to fetched SVGs - see [Security](#security). +Raw markup passed as `src` is **not** supported. If you already hold the SVG as a string - a generated chart, say - parse it with `DOMParser` and append the result yourself, or render it with `dangerouslySetInnerHTML`. Both skip the fetch, and the brief flash when `react-svg` re-injects on a `src` change. Either way you're inserting markup outside React's escaping, so [Security](#security) applies. </details>