Improve focus rendering: now display a blue inset ring around the focused component.

Closes #6397
This commit is contained in:
Benoit Marty
2026-07-10 12:14:55 +02:00
parent a2f1e9eeec
commit 8c935d8682
4 changed files with 88 additions and 47 deletions
@@ -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,
@@ -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,
)
}
}
@@ -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
}
@@ -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
}
)