From 05be0e1c063114843e2417ef2c800af490330790 Mon Sep 17 00:00:00 2001 From: Wavesonics Date: Tue, 30 Jun 2026 08:59:17 -0700 Subject: [PATCH] Expose text-editing accessibility semantics on the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editable Box was focusable and wired to the platform IME, but published no text-editing semantics. As a result accessibility services (VoiceOver / TalkBack) couldn't read or edit its content, and on iOS the platform never exposed the focused editor as a keyboard-focused text element — so UI-test frameworks like XCUITest couldn't type into it (no element reports `hasKeyboardFocus`). Add a `Modifier.semantics` block, wired to the existing TextEditorState API, exposing `editableText`, `textSelectionRange`, `setText`, `insertTextAtCursor`, `setSelection`, and `onClick` (request focus). This makes the node a first-class editable text field for accessibility, and lets XCUITest drive text entry (verified end to end against a downstream app on the iOS simulator). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../texteditor/BasicTextEditor.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/BasicTextEditor.kt b/ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/BasicTextEditor.kt index af760fd1..54b6eb25 100644 --- a/ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/BasicTextEditor.kt +++ b/ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/BasicTextEditor.kt @@ -30,6 +30,13 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.semantics.editableText +import androidx.compose.ui.semantics.insertTextAtCursor +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.setSelection +import androidx.compose.ui.semantics.setText +import androidx.compose.ui.semantics.textSelectionRange import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import com.darkrockstudios.texteditor.contextmenu.ContextMenuActions @@ -39,6 +46,7 @@ import com.darkrockstudios.texteditor.contextmenu.TextEditorContextMenuState import com.darkrockstudios.texteditor.cursor.DrawCursor import com.darkrockstudios.texteditor.input.CaptureViewForIme import com.darkrockstudios.texteditor.input.TextEditorInputModifierElement +import com.darkrockstudios.texteditor.input.selectionAsTextRange import com.darkrockstudios.texteditor.richstyle.BlockSpanStyle import com.darkrockstudios.texteditor.richstyle.RichSpan import com.darkrockstudios.texteditor.scrollbar.TextEditorScrollbar @@ -193,6 +201,40 @@ fun BasicTextEditor( .requestFocusOnPress(focusRequester) .then(inputModifierElement) .focusable(enabled = true, interactionSource = interactionSource) + // Publish text-editing semantics so the node is recognized as an editable + // text field. This drives accessibility services (VoiceOver/TalkBack read and + // edit the content) and, on iOS, lets the platform expose the focused editor as + // a keyboard-focused text element — without which XCUITest can't type into it. + .semantics { + editableText = state.getAllText() + textSelectionRange = state.selectionAsTextRange() + setText { newText -> + state.setText(newText) + true + } + insertTextAtCursor { newText -> + if (state.selector.hasSelection()) state.selector.deleteSelection() + state.insertStringAtCursor(newText) + true + } + setSelection { start, end, _ -> + val length = state.getTextLength() + val from = start.coerceIn(0, length) + val to = end.coerceIn(0, length) + if (from == to) { + state.cursor.updatePosition(state.getOffsetAtCharacter(from)) + state.selector.clearSelection() + } else { + state.selector.updateSelection( + state.getOffsetAtCharacter(from), + state.getOffsetAtCharacter(to), + ) + state.cursor.updatePosition(state.getOffsetAtCharacter(to)) + } + true + } + onClick { focusRequester.requestFocus(); true } + } .background(style.backgroundColor) .onSizeChanged { size -> state.onViewportSizeChange(