Repository navigation
feat: add a per-block line wrap toggle to code block headers - #1010
juliusmarminge wants to merge 3 commits into
Conversation
Adds an opt-in `enableCodeBlockWrapToggle` prop to the React Native package. Each top-level fenced code block gets a wrap button next to copy; tapping it wraps that block's lines at the block width, and tapping again returns to horizontal scrolling. Wrap state is a set of document-order indexes of top-level code blocks, fed to the view-free measure pass (part of the iOS measurement cache key and the Android measure hash). It resets when anything before the previous document's last line changes, so streaming and task toggles keep it. Labels come from `accessibilityLabels.codeBlock.wrap` / `.unwrap`. Closes software-mansion#1009 Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kdown toggleTaskListItemAtIndex wrote `[ ]` for checked and `[x]` for unchecked, the inverse of Android, since software-mansion#92. The checkbox looked right because the attributed string is patched separately, but the cached markdown recorded the opposite state, so anything reading it after a tap saw a stale checkbox and a prop echo of the toggled document never matched the local render. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…icon iOS reset the wrapped set when the markdown prop changed before its last line, which a task-toggle echo always does. The reset now skips a prop that equals the markdown already rendered locally, so the reader's choice stands. Android drew the wrap glyph through androidx.core.graphics.PathParser, a RestrictTo API, as a filled shape. WrapGlyph strokes the same Material outline with the copy glyph's paint, so the two header buttons match. Docs note that macOS accepts the prop without a toggle. Comments restating adjacent code are trimmed. CodeBlockContainerViewTest moves to a follow-up PR that adds native coverage on both platforms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks, this fits the library well: the boolean composes with a future What I found
What
|
eszlamczyk
left a comment
There was a problem hiding this comment.
It's hard for me to agree with "Code blocks inside blockquotes and admonitions have no toggle and keep scrolling." Code inside admonitions is common, and a button that only shows up on some blocks looks like a bug to users. I'd like nested blocks supported in this PR.
That would be easier if the wrap logic were pulled out of EnrichedMarkdown into a small per-instance class (e.g. CodeBlockWrapState). Right now it's spread across the field, the setter, sync, toggle and the reset check. The root keeps owning the instance; it just gets passed down to BlockquoteContainerView too. Not a companion object, since that would be shared across all instances. It should also be the only writer: the view emits the toggle, the helper updates the set and MeasurementStore, then pushes wrapped back to the views.
I'm gonna look into fixing this before approving and merging this today.
Heads-up: the API will change from opt-in to opt-out. @juliusmarminge, if you use this in t3code, consider setting enableCodeBlockWrapToggle explicitly.
| | Type | Default Value | Platform | | ||
| | --------- | ------------- | ------------ | | ||
| | `boolean` | `false` | iOS, Android | |
There was a problem hiding this comment.
This seems like a good enchancement for users to have out of the box, so I'd rather have this default to true instead of false
| - **Task list items** have no accessibility handling in the React Native package on either platform: a checkbox is read as plain list text, with no checked state announced and no toggle action exposed. There is no label key for it. | ||
| - **Admonitions** carry no accessibility role; they are announced as the blockquote they are built from. | ||
| - **Code blocks** are covered only partially - under `flavor="github"` the header copy button carries the resolved copy label, and iOS vends a VoiceOver copy action, but the block itself has no dedicated role. | ||
| - **Code blocks** are covered only partially - under `flavor="github"` the header copy button carries the resolved copy label and the optional wrap toggle (`enableCodeBlockWrapToggle`) speaks `codeBlock.wrap` / `codeBlock.unwrap`; iOS vends both as VoiceOver actions, but the block itself has no dedicated role. |
There was a problem hiding this comment.
Nit
| - **Code blocks** are covered only partially - under `flavor="github"` the header copy button carries the resolved copy label and the optional wrap toggle (`enableCodeBlockWrapToggle`) speaks `codeBlock.wrap` / `codeBlock.unwrap`; iOS vends both as VoiceOver actions, but the block itself has no dedicated role. | |
| - **Code blocks** are covered only partially - under `flavor="github"` the header copy button carries the resolved copy label and the optional wrap toggle (`enableCodeBlockWrapToggle`) speaks `codeBlock.wrap` / `codeBlock.unwrap`, while iOS vends both as VoiceOver actions, but the block itself has no dedicated role. |
|
Continuing in #1021. I don't have push access to |
What/Why?
Closes #1009.
In the React Native package, a top-level fenced code block scrolls horizontally. On a phone, a long shell command or a one-line JSON blob means swiping back and forth inside a small box. Wrapping every block instead makes indented code harder to read. This lets the reader choose per block.
The new opt-in
enableCodeBlockWrapToggleprop (defaultfalse) adds a wrap button to the left of copy in each top-level fenced code block header (flavor="github"):text.alignleftSF Symbol. Wrapping limits the cached TextKit container to the pane width and turns off horizontal scrolling.wrap_textglyph. Wrapping caps the codeTextView's width at the pane width inside the existingHorizontalScrollView.accessibilityLabels.codeBlock.wrap/.unwrap, with defaults "Wrap lines" / "Disable line wrap".MeasurementCacheKey. On Android it goes into the measure hash.On the API shape, see the alternatives in #1009. The standalone packages'
codeBlockLayoutdefaults to wrapping, so a port there would show the toggle only withscrollable. I'm happy to change this to a thirdcodeBlockLayout-style mode instead of a boolean if you prefer.We've shipped this in T3 Code's mobile app through our fork: pingdotgg#1, by @Abdullah1738, which this PR carries over. The only difference from the fork is that
EnrichedMarkdownText.tsxpasses the prop without the fork's media-slot code path.Testing
typecheck, Jest (103 tests) and ESLint on the touched TS files pass. clang-format shows nothing new in the touched ObjC files; the remaining hits are ones upstreammainalready has.:react-native-enriched-markdown:testDebugUnitTestandktlintCheckpass. Native unit coverage for the toggle (height parity wrapped and unwrapped, and the reset rule) follows in a separate PR on both platforms.ReactNativeEnrichedMarkdownpod builds for the simulator (xcodebuild).Screenshots
The screenshots are from T3 Code's app; the list below the code blocks comes from another change (#1005).
Maintainer follow-ups on this branch
d25182f0fixes a pre-existing iOS bug this feature surfaced:toggleTaskListItemAtIndexwrote the inverse checkbox state into the cached markdown since feat: implement task list support #92, so a task-toggle echo from JS never matched the local render.887f088ckeeps the wrap state when that echo arrives, strokes the Android wrap icon with the copy glyph's paint instead of going through the restrictedandroidx.core.graphics.PathParser, notes in the docs that macOS accepts the prop without a toggle, and trims comments. Verified on iPhone 17 / iOS 26.5 and Pixel 9 / API 36: wrap a block, tap a checkbox with the app echoing the markdown back, the block stays wrapped.PR Checklist