From 8c935d8682896a4237e9121fd7973b8d37356464 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2026 12:14:55 +0200 Subject: [PATCH] Improve focus rendering: now display a blue inset ring around the focused component. Closes #6397 --- .../android/compound/theme/ElementTheme.kt | 24 ++++ .../designsystem/theme/components/Button.kt | 107 ++++++++++-------- .../theme/components/SearchField.kt | 2 +- .../theme/components/TextField.kt | 2 +- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt index ac83365658..d927ba8836 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt @@ -16,7 +16,11 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.LocalRippleThemeConfiguration import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RippleConfiguration +import androidx.compose.material3.RippleThemeConfiguration import androidx.compose.material3.Typography import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme @@ -28,6 +32,7 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.compoundTypography import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.compound.tokens.generated.TypographyTokens @@ -159,6 +164,25 @@ fun ElementTheme( CompositionLocalProvider( LocalCompoundColors provides currentCompoundColor, LocalContentColor provides colorScheme.onSurface, + // Configure the keyboard focus style: Draw a blue inset ring around the focused component. + // By default, a semi-transparent black overlay is used + LocalRippleThemeConfiguration provides RippleThemeConfiguration( + RippleThemeConfiguration.Focus.InsetRing( + outerStrokeInset = 0.dp, + outerStrokeWidth = 2.dp, + innerStrokeInset = 0.dp, + innerStrokeWidth = 0.dp, + ) + ), + // Configure the keyboard focus color. + // By default, ColorScheme.secondaryColor is used + LocalRippleConfiguration provides + RippleConfiguration( + focus = RippleConfiguration.Focus.InsetRing( + outerStrokeColor = ElementTheme.colors.borderFocused, + innerStrokeColor = Color.Transparent, + ) + ), ) { MaterialTheme( colorScheme = colorScheme, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index 000ad1e470..299744cebf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -28,13 +28,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleThemeConfiguration +import androidx.compose.material3.RippleThemeConfiguration import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector @@ -181,11 +183,8 @@ private fun ButtonInternal( ButtonSize.LargeLowPadding -> PaddingValues(horizontal = lowHorizontalPaddingValue, vertical = 13.dp) } - val shape = when (style) { - ButtonStyle.Filled, - ButtonStyle.Outlined -> RoundedCornerShape(percent = 50) - ButtonStyle.Text -> RectangleShape - } + // Apply the same shape to all buttons, so that the focus ring is consistent across styles. + val shape = RoundedCornerShape(percent = 50) val border = when (style) { ButtonStyle.Filled -> null @@ -202,48 +201,66 @@ private fun ButtonInternal( ButtonStyle.Text -> null } - androidx.compose.material3.Button( - onClick = { - if (!showProgress) { - onClick() - } - }, - modifier = modifier.heightIn(min = minHeight), - enabled = enabled, - shape = shape, - colors = colors, - elevation = null, - border = border, - contentPadding = contentPadding, - interactionSource = remember { MutableInteractionSource() }, + // Reduce outerStrokeInset on Filled buttons so that it's displayed around the button + val outerStrokeInset = when (style) { + ButtonStyle.Filled -> (-4).dp + ButtonStyle.Outlined, + ButtonStyle.Text -> 0.dp + } + + CompositionLocalProvider( + LocalRippleThemeConfiguration provides RippleThemeConfiguration( + RippleThemeConfiguration.Focus.InsetRing( + outerStrokeInset = outerStrokeInset, + outerStrokeWidth = 2.dp, + innerStrokeInset = 0.dp, + innerStrokeWidth = 0.dp, + ) + ), ) { - when { - showProgress -> { - CircularProgressIndicator( - modifier = Modifier - .progressSemantics() - .size(20.dp), - color = LocalContentColor.current, - strokeWidth = 2.dp, - ) - Spacer(modifier = Modifier.width(8.dp)) - } - leadingIcon != null -> { - Icon( - painter = leadingIcon.getPainter(), - contentDescription = null, - tint = LocalContentColor.current, - modifier = Modifier.size(20.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) + androidx.compose.material3.Button( + onClick = { + if (!showProgress) { + onClick() + } + }, + modifier = modifier.heightIn(min = minHeight), + enabled = enabled, + shape = shape, + colors = colors, + elevation = null, + border = border, + contentPadding = contentPadding, + interactionSource = remember { MutableInteractionSource() }, + ) { + when { + showProgress -> { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + color = LocalContentColor.current, + strokeWidth = 2.dp, + ) + Spacer(modifier = Modifier.width(8.dp)) + } + leadingIcon != null -> { + Icon( + painter = leadingIcon.getPainter(), + contentDescription = null, + tint = LocalContentColor.current, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + } } + Text( + text = text, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } - Text( - text = text, - style = ElementTheme.typography.fontBodyLgMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt index d677a89577..8b318ecf4a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt @@ -139,7 +139,7 @@ private fun SearchFieldContainer( border = BorderStroke( width = 1.dp, color = if (isFocused) { - ElementTheme.colors.borderInteractiveHovered + ElementTheme.colors.borderFocused } else { ElementTheme.colors.borderInteractiveSecondary } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt index d4e08eacf1..36d0255b08 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -293,8 +293,8 @@ private fun TextFieldContainer( width = if (isFocused) 2.dp else 1.dp, color = when { !enabled -> ElementTheme.colors.borderDisabled + isFocused -> ElementTheme.colors.borderFocused isError -> ElementTheme.colors.borderCriticalPrimary - isFocused -> ElementTheme.colors.borderInteractiveHovered else -> ElementTheme.colors.borderInteractiveSecondary } )