Files
skills/docs/foundation/data/clipboard/src/fallback/fallback.md
2026-05-29 08:20:56 +01:00

95 lines
3.4 KiB
Markdown

# `fallback/fallback.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Legacy clipboard fallback paths used when `navigator.clipboard` is
unavailable or rejects. Provides a hidden-textarea + `execCommand("copy")`
write path and a paste-event-based read path. Both return the same
`ClipboardResult` shape as the API path so the manager's call sites do
not branch on which mechanism actually ran. The paste-event fallback is
purpose-built for environments — older Safari, embedded WebViews, and
permission-denied contexts — where `navigator.clipboard.readText`
rejects but the user can paste manually.
## Features
- `copyTextFallback(text)` — synchronous textarea + `execCommand` copy.
- `readTextFallback(timeoutMs)` — focus + paste-event capture with a
configurable timeout.
- `dispatchSyntheticPaste(text)` — test helper that fires a synthetic
`paste` event on the most recent hidden host.
- SSR-safe — both copy and read paths return
`CLIPBOARD.DOCUMENT_UNAVAILABLE` when `document` is undefined.
- Off-screen, non-interactive host element so users never see the
textarea visibly steal focus.
## Usage: Basic
```ts
import { copyTextFallback } from "@suaveplan/clipboard";
const result = copyTextFallback("hello");
if (!result.success) {
console.warn("legacy copy failed:", result.code);
}
```
## Usage: Advanced
```ts
import {
readTextFallback,
dispatchSyntheticPaste,
} from "@suaveplan/clipboard";
async function readClipboardWithPasteFallback(): Promise<string | undefined> {
const result = await readTextFallback(2000);
return result.success ? result.data : undefined;
}
// Inside a test harness:
const promise = readTextFallback(500);
queueMicrotask(() => dispatchSyntheticPaste("from-test"));
const result = await promise;
```
## API Reference
### `copyTextFallback(text: string): ClipboardResult<string>`
Stages a hidden `<textarea>` carrying `text`, selects it, runs
`document.execCommand("copy")`, and removes the textarea on cleanup.
Returns `{ success: true, data, method: "fallback" }` on success and a
structured failure with `code: CLIPBOARD.FALLBACK_FAILED` (or
`CLIPBOARD.DOCUMENT_UNAVAILABLE` outside the browser) otherwise.
### `readTextFallback(timeoutMs: number): Promise<ClipboardResult<string>>`
Creates a hidden textarea, focuses it, listens for the first `paste`
event, and resolves with the `text/plain` payload carried by the event.
Resolves with `code: CLIPBOARD.READ_FAILED` if the timeout elapses
first and `code: CLIPBOARD.DOCUMENT_UNAVAILABLE` outside the browser.
### `dispatchSyntheticPaste(text: string): boolean`
Test helper. Locates the most recent hidden host (`textarea[aria-hidden="true"]`)
and dispatches a `paste` ClipboardEvent carrying `text` as `text/plain`.
Returns `true` when a host was found, `false` otherwise.
## Implementation Notes
The hidden textarea is positioned at `-9999px` and given `opacity: 0`
plus `aria-hidden="true"` so assistive technologies skip it. The cleanup
runs in a `finally` block — even if `execCommand` throws, the textarea is
removed. The read fallback tracks a `settled` flag so a duplicate or
post-timeout `paste` event cannot resolve the promise twice. The paste
event may carry a null `clipboardData` on some browsers; the module
falls back to an empty string in that case rather than rejecting, since
"clipboard contained nothing" is a valid outcome. The synthetic-paste
helper is exported to keep tests deterministic — production code never
calls it.