From 9dba88842b93e81ea3d9fa1477fbf7734f7e6b15 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 8 Jul 2026 13:41:08 +0200 Subject: [PATCH 1/3] feat(ios): voiceover read contents of input element --- .../ios/input/EnrichedMarkdownTextInput.mm | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm index de8e41dcf..389034a2e 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm @@ -226,11 +226,35 @@ - (void)setupTextView _placeholderLabel = ENRMCreatePlaceholderLabel(_textView, _formatterStyle.baseFont); #if !TARGET_OS_OSX _placeholderLabel.adjustsFontForContentSizeCategory = YES; + _placeholderLabel.accessibilityElementsHidden = YES; #endif [self resetBaseTypingAttributes]; } +#if !TARGET_OS_OSX +#pragma mark - Accessibility + +// The custom TextKit 1 stack leaves the inner UITextView invisible to VoiceOver, so +// the host vends the content itself as one readable element. +- (BOOL)isAccessibilityElement +{ + return YES; +} + +- (NSString *)accessibilityValue +{ + NSString *text = ENRMGetPlainText(_textView); + return text.length > 0 ? text : _placeholderLabel.text; +} + +- (BOOL)accessibilityActivate +{ + ENRMFocusTextView(_textView); + return YES; +} +#endif + #pragma mark - State - (void)updateState:(const facebook::react::State::Shared &)state From 9c1797a507b6ff9e147212e961c6b23444154fad Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 8 Jul 2026 13:43:00 +0200 Subject: [PATCH 2/3] docs: update accessibility docs --- docs/ACCESSIBILITY.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md index 19388783f..3591c49a0 100644 --- a/docs/ACCESSIBILITY.md +++ b/docs/ACCESSIBILITY.md @@ -164,9 +164,21 @@ Tables expose one focusable element per row (header included), labeled with the Each math block exposes a single focusable element labeled `"Math: "`. The library does **not** convert LaTeX to natural language — the screen reader reads the raw LaTeX source. If you need spoken math, plug in a LaTeX→speech library on the consumer side and translate the labels accordingly. +## Text input (`EnrichedMarkdownTextInput`) + +The editor's accessibility model is intentionally simpler than the renderer's. The input is built on a custom TextKit stack, so its inner `UITextView` is not surfaced to VoiceOver directly — instead the component is presented as a **single accessibility element** that reads its whole content as one paragraph. + +**iOS (VoiceOver):** +- The field is one element whose spoken value is the full plain text (delimiters stripped). When the field is empty, the placeholder is announced instead. +- `accessibilityLabel` / `accessibilityHint` set on the component are forwarded and announced. +- Double-tapping activates the field: it becomes first responder and the keyboard opens for editing. + +**Android (TalkBack):** the input is a native `EditText`, announced and edited as a standard editable text field. + ## Known Limitations - **Inline formatting** (bold / italic / underline / strikethrough / inline code / spoiler) is not split into separate accessibility elements — the entire paragraph is read as one element, exactly as the surrounding text. Screen readers don't apply visual emphasis to bold/italic by default. - **macOS** screen-reader support is still pending — `MarkdownAccessibilityElementBuilder.m` ships a no-op stub for macOS. Tracked as a TODO; iOS implementation can serve as a reference. - **Android** has no rotor concept — `accessibilityLabels.rotor.*` is silently ignored on that platform. - **iOS** blockquote backgrounds may break at link boundaries instead of spanning the full line. Visual only; doesn't affect accessibility. +- **Text input** (`EnrichedMarkdownTextInput`, iOS) is exposed as a single VoiceOver element reading its full plain text as one paragraph. It is **not** announced with the native "text field" role, has no in-field cursor navigation or per-character echo (editing happens via the keyboard after a double-tap activation), and its spoken value refreshes on re-focus rather than live. Exposing the inner `UITextView` for these would be unreliable under the custom TextKit stack (duplicate/flaky announcements). Inline formatting and headings are read as plain text. From c117b7aa8b3e757366312dfa0caa55a1a1480c4f Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 8 Jul 2026 14:22:28 +0200 Subject: [PATCH 3/3] fix: apply cr --- .../ios/input/EnrichedMarkdownTextInput.mm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm index 389034a2e..638abe602 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm @@ -227,6 +227,7 @@ - (void)setupTextView #if !TARGET_OS_OSX _placeholderLabel.adjustsFontForContentSizeCategory = YES; _placeholderLabel.accessibilityElementsHidden = YES; + _placeholderLabel.isAccessibilityElement = NO; #endif [self resetBaseTypingAttributes]; @@ -245,12 +246,19 @@ - (BOOL)isAccessibilityElement - (NSString *)accessibilityValue { NSString *text = ENRMGetPlainText(_textView); - return text.length > 0 ? text : _placeholderLabel.text; + if (text.length > 0) { + return text; + } + if (_placeholderLabel.text.length > 0) { + return _placeholderLabel.text; + } + return [super accessibilityValue]; } - (BOOL)accessibilityActivate { ENRMFocusTextView(_textView); + [super accessibilityActivate]; return YES; } #endif