From 3db179edcfb3663793fe048edb83357aa48550c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jun 2025 16:56:59 +0200 Subject: [PATCH] a11y: improve accessibility on rich text editor options. --- .../components/FormattingOption.kt | 30 ++++++++++++++++--- .../textcomposer/components/TextFormatting.kt | 12 ++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt index c90203b6ae..48ad19f812 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -21,6 +22,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -32,9 +35,10 @@ import io.element.android.libraries.designsystem.theme.iconSuccessPrimaryBackgro @Composable internal fun FormattingOption( state: FormattingOptionState, + toggleable: Boolean, onClick: () -> Unit, imageVector: ImageVector, - contentDescription: String?, + contentDescription: String, modifier: Modifier = Modifier, ) { val backgroundColor = when (state) { @@ -52,6 +56,7 @@ internal fun FormattingOption( modifier = modifier .clickable( onClick = onClick, + enabled = state != FormattingOptionState.Disabled, interactionSource = remember { MutableInteractionSource() }, indication = ripple( bounded = false, @@ -59,6 +64,20 @@ internal fun FormattingOption( ), ) .size(48.dp) + .then( + if (toggleable) { + Modifier.toggleable( + value = state == FormattingOptionState.Selected, + enabled = state != FormattingOptionState.Disabled, + onValueChange = { onClick() }, + ) + } else { + Modifier + } + ) + .clearAndSetSemantics { + this.contentDescription = contentDescription + } ) { Box( modifier = Modifier @@ -84,21 +103,24 @@ internal fun FormattingOptionPreview() = ElementPreview { Row { FormattingOption( state = FormattingOptionState.Default, + toggleable = false, onClick = { }, imageVector = CompoundIcons.Bold(), - contentDescription = null, + contentDescription = "", ) FormattingOption( state = FormattingOptionState.Selected, + toggleable = true, onClick = { }, imageVector = CompoundIcons.Italic(), - contentDescription = null, + contentDescription = "", ) FormattingOption( state = FormattingOptionState.Disabled, + toggleable = false, onClick = { }, imageVector = CompoundIcons.Underline(), - contentDescription = null, + contentDescription = "", ) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt index 911707022c..08c1bec23a 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt @@ -104,24 +104,28 @@ internal fun TextFormatting( ) { FormattingOption( state = state.actions[ComposerAction.BOLD].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.Bold) }, imageVector = CompoundIcons.Bold(), contentDescription = stringResource(R.string.rich_text_editor_format_bold) ) FormattingOption( state = state.actions[ComposerAction.ITALIC].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.Italic) }, imageVector = CompoundIcons.Italic(), contentDescription = stringResource(R.string.rich_text_editor_format_italic) ) FormattingOption( state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.Underline) }, imageVector = CompoundIcons.Underline(), contentDescription = stringResource(R.string.rich_text_editor_format_underline) ) FormattingOption( state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.StrikeThrough) }, imageVector = CompoundIcons.Strikethrough(), contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) @@ -141,6 +145,7 @@ internal fun TextFormatting( FormattingOption( state = state.actions[ComposerAction.LINK].toButtonState(), + toggleable = true, onClick = { linkDialogAction = state.linkAction }, imageVector = CompoundIcons.Link(), contentDescription = stringResource(R.string.rich_text_editor_link) @@ -148,42 +153,49 @@ internal fun TextFormatting( FormattingOption( state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + toggleable = true, onClick = { onToggleListClick(ordered = false) }, imageVector = CompoundIcons.ListBulleted(), contentDescription = stringResource(R.string.rich_text_editor_bullet_list) ) FormattingOption( state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + toggleable = true, onClick = { onToggleListClick(ordered = true) }, imageVector = CompoundIcons.ListNumbered(), contentDescription = stringResource(R.string.rich_text_editor_numbered_list) ) FormattingOption( state = state.actions[ComposerAction.INDENT].toButtonState(), + toggleable = false, onClick = { onIndentClick() }, imageVector = CompoundIcons.IndentIncrease(), contentDescription = stringResource(R.string.rich_text_editor_indent) ) FormattingOption( state = state.actions[ComposerAction.UNINDENT].toButtonState(), + toggleable = false, onClick = { onUnindentClick() }, imageVector = CompoundIcons.IndentDecrease(), contentDescription = stringResource(R.string.rich_text_editor_unindent) ) FormattingOption( state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + toggleable = true, onClick = { onInlineFormatClick(InlineFormat.InlineCode) }, imageVector = CompoundIcons.InlineCode(), contentDescription = stringResource(R.string.rich_text_editor_inline_code) ) FormattingOption( state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + toggleable = true, onClick = { onCodeBlockClick() }, imageVector = CompoundIcons.Code(), contentDescription = stringResource(R.string.rich_text_editor_code_block) ) FormattingOption( state = state.actions[ComposerAction.QUOTE].toButtonState(), + toggleable = true, onClick = { onQuoteClick() }, imageVector = CompoundIcons.Quote(), contentDescription = stringResource(R.string.rich_text_editor_quote)