a11y: let Markdown/Rich text input render the placeholder to help for accessibility.

This commit is contained in:
Benoit Marty 2025-06-19 09:54:50 +02:00
parent 47ec84f6b3
commit 3ee5ee3f45
4 changed files with 20 additions and 22 deletions

View file

@ -31,6 +31,7 @@ object ElementRichTextEditorStyle {
} else { } else {
ElementTheme.colors.textSecondary ElementTheme.colors.textSecondary
}, },
placeholderColor = ElementTheme.colors.textSecondary,
lineHeight = TextUnit.Unspecified, lineHeight = TextUnit.Unspecified,
includeFontPadding = true, includeFontPadding = true,
) )

View file

@ -39,7 +39,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -176,12 +175,12 @@ fun TextComposer(
TextInputBox( TextInputBox(
composerMode = composerMode, composerMode = composerMode,
onResetComposerMode = onResetComposerMode, onResetComposerMode = onResetComposerMode,
placeholder = placeholder, isTextEmpty = state.richTextEditorState.messageHtml.isEmpty(),
showPlaceholder = state.richTextEditorState.messageHtml.isEmpty(),
subcomposing = subcomposing, subcomposing = subcomposing,
) { ) {
RichTextEditor( RichTextEditor(
state = state.richTextEditorState, state = state.richTextEditorState,
placeholder = placeholder,
// Disable most of the editor functionality if it's just being measured for a subcomposition. // Disable most of the editor functionality if it's just being measured for a subcomposition.
// This prevents it gaining focus and mutating the state. // This prevents it gaining focus and mutating the state.
registerStateUpdates = !subcomposing, registerStateUpdates = !subcomposing,
@ -205,12 +204,13 @@ fun TextComposer(
TextInputBox( TextInputBox(
composerMode = composerMode, composerMode = composerMode,
onResetComposerMode = onResetComposerMode, onResetComposerMode = onResetComposerMode,
placeholder = placeholder, isTextEmpty = state.state.text.value().isEmpty(),
showPlaceholder = state.state.text.value().isEmpty(),
subcomposing = subcomposing, subcomposing = subcomposing,
) { ) {
MarkdownTextInput( MarkdownTextInput(
state = state.state, state = state.state,
placeholder = placeholder,
placeholderColor = ElementTheme.colors.textSecondary,
subcomposing = subcomposing, subcomposing = subcomposing,
onTyping = onTyping, onTyping = onTyping,
onReceiveSuggestion = onReceiveSuggestion, onReceiveSuggestion = onReceiveSuggestion,
@ -492,8 +492,7 @@ private fun TextFormattingLayout(
private fun TextInputBox( private fun TextInputBox(
composerMode: MessageComposerMode, composerMode: MessageComposerMode,
onResetComposerMode: () -> Unit, onResetComposerMode: () -> Unit,
placeholder: String, isTextEmpty: Boolean,
showPlaceholder: Boolean,
subcomposing: Boolean, subcomposing: Boolean,
textInput: @Composable () -> Unit, textInput: @Composable () -> Unit,
) { ) {
@ -515,7 +514,6 @@ private fun TextInputBox(
onResetComposerMode = onResetComposerMode, onResetComposerMode = onResetComposerMode,
) )
} }
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
Box( Box(
modifier = Modifier modifier = Modifier
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
@ -523,21 +521,8 @@ private fun TextInputBox(
.then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier), .then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier),
contentAlignment = Alignment.CenterStart, contentAlignment = Alignment.CenterStart,
) { ) {
// Placeholder
if (showPlaceholder) {
Text(
text = placeholder,
style = defaultTypography.copy(
color = ElementTheme.colors.textSecondary,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
textInput() textInput()
if (isTextEmpty && composerMode.showCaptionCompatibilityWarning()) {
if (showPlaceholder && composerMode.showCaptionCompatibilityWarning()) {
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
Icon( Icon(
modifier = Modifier modifier = Modifier

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.textcomposer.components.markdown package io.element.android.libraries.textcomposer.components.markdown
import android.content.ClipData import android.content.ClipData
import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.text.Editable import android.text.Editable
@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.getSpans import androidx.core.text.getSpans
@ -26,6 +28,7 @@ import androidx.core.view.OnReceiveContentListener
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -43,6 +46,8 @@ import io.element.android.wysiwyg.compose.internal.applyStyleInCompose
@Composable @Composable
fun MarkdownTextInput( fun MarkdownTextInput(
state: MarkdownTextEditorState, state: MarkdownTextEditorState,
placeholder: String,
placeholderColor: androidx.compose.ui.graphics.Color,
subcomposing: Boolean, subcomposing: Boolean,
onTyping: (Boolean) -> Unit, onTyping: (Boolean) -> Unit,
onReceiveSuggestion: (Suggestion?) -> Unit, onReceiveSuggestion: (Suggestion?) -> Unit,
@ -87,6 +92,8 @@ fun MarkdownTextInput(
setBackgroundColor(Color.TRANSPARENT) setBackgroundColor(Color.TRANSPARENT)
val text = state.text.value() val text = state.text.value()
setText(text) setText(text)
setHint(placeholder)
setHintTextColor(ColorStateList.valueOf(placeholderColor.toArgb()))
inputType = InputType.TYPE_CLASS_TEXT or inputType = InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_MULTI_LINE or
@ -189,6 +196,8 @@ internal fun MarkdownTextInputPreview() {
val style = ElementRichTextEditorStyle.composerStyle(hasFocus = true) val style = ElementRichTextEditorStyle.composerStyle(hasFocus = true)
MarkdownTextInput( MarkdownTextInput(
state = aMarkdownTextEditorState(initialText = "Hello, World!"), state = aMarkdownTextEditorState(initialText = "Hello, World!"),
placeholder = "Placeholder",
placeholderColor = ElementTheme.colors.textSecondary,
subcomposing = false, subcomposing = false,
onTyping = {}, onTyping = {},
onReceiveSuggestion = {}, onReceiveSuggestion = {},

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
@ -175,6 +176,8 @@ class MarkdownTextInputTest {
val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus) val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus)
MarkdownTextInput( MarkdownTextInput(
state = state, state = state,
placeholder = "Placeholder",
placeholderColor = ElementTheme.colors.textSecondary,
subcomposing = subcomposing, subcomposing = subcomposing,
onTyping = onTyping, onTyping = onTyping,
onReceiveSuggestion = onSuggestionReceived, onReceiveSuggestion = onSuggestionReceived,