Skip to content

feat: configure native clipboard identity for linked selections - #962

Open
juliusmarminge wants to merge 3 commits into
software-mansion:mainfrom
pingdotgg:feat/selection-clipboard
Open

juliusmarminge wants to merge 3 commits into
software-mansion:mainfrom
pingdotgg:feat/selection-clipboard

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What/Why?

Applications can render a shortened link or pill label while needing its canonical identity when pasted back. Add optional selectionClipboard configuration for exact link URLs, escaped HTML attributes and iOS custom pasteboard types. Ordinary Copy replaces configured link text, including partially selected labels. Copy as Markdown preserves the Markdown payload; metadata is applied only when the selection contains a configured link. Configuration does not alter rendering or measurement.

Android supports plain text and HTML. Custom pasteboard types are iOS only and do not overwrite existing standard formats. Web ignores the native configuration. Removing the prop restores ordinary copy behavior.

Fixes #955.

Testing

  • Four registered iOS XCTest cases executed successfully with zero failures or skips. They exercise actual UIPasteboard output for canonical text, escaped HTML attributes and custom types, unconfigured selections, partial link selections and Copy as Markdown metadata.
  • Two production Android clipboard tests and three web native-prop-filter cases passed. Library types, Bob/module/codegen, ktlint, scoped example lint/formatting and normal hooks passed.
  • The added iOS example and Maestro flow passed native Copy and Paste with configuration disabled, enabled, then disabled again on the same mounted view. Adjacent plain and link spans form one selectable word in the fixture. This avoids selection-handle coordinates while verifying replacement of only the link span.

The recording shows those three states in the current build. It retains the original playback speed; it is functional proof, not a performance comparison or a separate baseline build.

Native selection copy and paste with canonical configuration disabled, enabled, and disabled again

Local native proof uses an isolated upstream example checkout on an iPhone 17 Pro simulator running iOS 27. Xcode used two jobs, disabled signing/index/debug symbols, and task-owned XCFrameworks retained the arm64 simulator slice to fit the host. The clipboard XCTest run used an Xcode-managed isolated test worker. These are host resource settings, not workflow or production changes. No Android device, macOS runtime, VoiceOver traversal, or full Maestro-suite pass is claimed. Standalone example typecheck remains blocked by existing React Native workspace type conflicts; library types and scoped example lint passed.

Base: b80be973. Current head: c1227447. Current-head CI is pending; earlier-head results are not claimed as verification of this commit.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android, production clipboard tests passed but no device run
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • Added focused iOS E2E test is passing, full suite not run
  • Required E2E tests have been added

Model: GPT-6.1. Harness: Codex in T3 Code.

@juliusmarminge
juliusmarminge marked this pull request as ready for review October 8, 2026 19:15
@hryhoriiK97

hryhoriiK97 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

@juliusmarminge thanks for the PR. The use case is valid, but I'd like to land a smaller API for it. I've updated this comment after reading how T3 Code uses the prop, so please read it again if you saw the earlier version.

What I'd keep

A single typed prop for the plain-text form of a copied selection:

<EnrichedMarkdownText copyLinksAs="markdown" />

With 'markdown', every link is written as [label](url), so your example pastes as before[Context](ref:one). 'label' (the default) keeps today's behaviour. The other clipboard formats and Copy as Markdown stay unchanged. A 'url' option can come later if someone needs it.

This applies to every link, so ordinary links would also paste as [docs](https://…), including into plain-text apps. Labels and URLs also need escaping so the output stays valid Markdown.

What I'd drop

  • linkTextByUrl: the per-URL table forces apps to list every link up front. T3 Code's values are already in the [label](url) form that 'markdown' produces.
  • htmlAttributes and mimeTypes: application-specific formats shouldn't be part of this library's API. mimeTypes are UTIs on iOS, not MIME types, and Android can't honour them.
  • The JSON-string prop: this should be typed through codegen.
  • The example screen and Maestro flow. I'd keep a small unit test for the link rewriting, since the output has to be exact.

The context fragment

I earlier assumed the plain text carries everything a paste needs. From T3 Code's source, it only carries the reference: the records live in the app.t3.context-fragment pasteboard type on iOS and the data-t3-context-fragment HTML attribute on Android, and your native composer reads them on paste. So copyLinksAs alone wouldn't cover that flow.

Instead of adding those formats to the library, I'd add one event:

<EnrichedMarkdownText
  copyLinksAs="markdown"
  onSelectionCopy={({ text, markdown }) => {
    // add your own clipboard data here
  }}
/>

You already have writeContextClipboard(text, fragment), so you could write the fragment from this callback, and the library wouldn't need to know about UTIs or HTML attributes. The downside is that your write lands slightly after the native copy, so a very fast paste could miss it.

Question

Would copyLinksAs plus onSelectionCopy cover T3 Code, or does anything need the fragment on the clipboard in the same write as the native copy? Have I misread how the fragment is used?

Separately, I found that our own input component reads Markdown from a private pasteboard type that our text component's Copy doesn't write, so copy-paste between the two loses links today. That's our bug and I'll fix it on main regardless of this PR.

@juliusmarminge

Copy link
Copy Markdown
Contributor Author

Thanks for digging into how we use it. You read it right: the plain text only carries the t3-context:// references, and the records live in the fragment.

copyLinksAs plus onSelectionCopy covers T3 Code. Nothing needs the fragment in the same write as the native copy. Only our own composers read it, pasting is always a user action, and the composer drops records the pasted text doesn't reference. So a late write at worst pastes the references without their records. From the callback we'd merge the fragment into your pasteboard item, rather than calling writeContextClipboard, which replaces everything and would drop your RTF, HTML and Markdown.

A few requests so that merge works:

  • Fire onSelectionCopy after the native write, from every copy path: the menu's Copy, Cmd+C, Android's action mode, and Copy as Markdown.
  • Keep writing a single pasteboard item on iOS.
  • In 'markdown' mode, escape only what would break the link: \, [ and ] in labels, and whatever the URL needs. Our label sanitizer turns \ into a space, so my\_file.ts would come back as my _file.ts.

Happy to drop linkTextByUrl, the HTML attribute and UTI options, the JSON prop and the example screen. Should I rework this PR down to copyLinksAs plus the event, or would you rather take it from here?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow native copied selections to preserve application link identity

2 participants