Merge pull request #7182 from element-hq/feature/bma/focussedButton

[A11y] Improve rendering on focused UI items
This commit is contained in:
Benoit Marty
2026-07-16 10:47:19 +02:00
committed by GitHub
22 changed files with 127 additions and 85 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,26 @@ fun ElementTheme(
CompositionLocalProvider(
LocalCompoundColors provides currentCompoundColor,
LocalContentColor provides colorScheme.onSurface,
// Configure the keyboard focus style: Draw a blue inset ring around the focused component.
// Ref: https://www.figma.com/design/hlbsmSekQorGRN1t2R9JEy/Accessibility-checks?node-id=271-42066
// 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.secondary 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,
)
}
}
@@ -294,7 +311,7 @@ internal enum class ButtonStyle {
@Composable
fun getColors(destructive: Boolean): ButtonColors = when (this) {
Filled -> ButtonDefaults.buttonColors(
Filled -> ButtonDefaults.filledTonalButtonColors(
containerColor = getPrimaryColor(destructive),
contentColor = ElementTheme.materialColors.onPrimary,
disabledContainerColor = if (destructive) {
@@ -304,13 +321,13 @@ internal enum class ButtonStyle {
},
disabledContentColor = ElementTheme.colors.textOnSolidPrimary
)
Outlined -> ButtonDefaults.buttonColors(
Outlined -> ButtonDefaults.outlinedButtonColors(
containerColor = Color.Transparent,
contentColor = getPrimaryColor(destructive),
disabledContainerColor = Color.Transparent,
disabledContentColor = getDisabledContentColor(destructive),
)
Text -> ButtonDefaults.buttonColors(
Text -> ButtonDefaults.textButtonColors(
containerColor = Color.Transparent,
contentColor = if (destructive) {
ElementTheme.colors.textCriticalPrimary
@@ -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
}
)
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:839c24d04f7008a3b33be3624c71f19f01554a78903cc5d8e5337ab90d0fc41b
size 8268
oid sha256:76632bda4afa1bc900b1174fe68a09a3d757d6f3227b4f541e0198219875c90c
size 8275
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:032efe4da3ebda3dcc92a6027604822dabf6062073c4e556242e755adf38e717
size 11803
oid sha256:b40de4a02b67814007f201cea9383e8ab30f2b7b56fb1efcdd2348ee207f4815
size 11814
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a448f6810521acc7d85904ba71ee4a3153d2eeb2c40d132b889eda774e0a6fdf
size 7956
oid sha256:04473b9423b916f25920837e07b2b22ba495daa5d4f2de4070b70d3dd571072c
size 7975
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:effa92f7695f9643efeebf929db6fdac31bd608a449ab306511897e8b2197b82
size 10939
oid sha256:7cf8f0f420c2eb69bc6a56e9238fb720d6fada2c250ae7b2dbdb8f431dfd9668
size 10944
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5dc5c492ed3fd552e77c0be85b639d55bc7e976f15b761f927b0b80ecca601d
size 12115
oid sha256:195338046951366fd2345fdc8471e3e2b2786723c8bfffa9fa9057929c608afb
size 12113
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7ce8a8c6b4d1692e4b127b7f17f507552666c631893025082c385b93d835a73
size 11726
oid sha256:4ef1b643a16b5a94c75f8bb845c36d3cbc7f7ed96a0cac32fdb9f0b09d26b4d1
size 11747
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8b93c08531efbeacb55f84b17ac793a7b226e5a2db7bc5072e94bc040cd1e9bc
size 11386
oid sha256:ce754ce205fd864e01828659aff42ab19dccf7c921c1c733ed1757f1b2122309
size 11403
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20742175a23c7116089cec7b9886ad567db50d899dd5dca4388534d6120d73ce
size 60252
oid sha256:8f8ef2430abd11b31b3e90976f85bd4578e667bd0acc0851544ee4d16358479d
size 60237
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee1ee7ae5a0350ca808b1839d7f033cb83947d33305a5cb5ff9a9faa4a5dadbf
size 58345
oid sha256:8265b9acc3932df6f0e1eee5e85c2b16d13060c4164c3fbb5b1b0b598745770c
size 58326
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4b18fc1dffd0b62b81d9e9ec5de8588919e27b18af178bfba474771cafd9800
size 42717
oid sha256:37f8e05e435b8d531c72a180ff87183dcec2aa319473bc540e19c9fdd0e5346a
size 42673
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7813a6da740ee5a83c56ac4883b2905582482b6d5d17254b30b895a242f85b60
size 42752
oid sha256:ffe628766efb7b932c82749546d5c9a6dd271a4ffe2cf162dbc36796538fce12
size 42723
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4b18fc1dffd0b62b81d9e9ec5de8588919e27b18af178bfba474771cafd9800
size 42717
oid sha256:37f8e05e435b8d531c72a180ff87183dcec2aa319473bc540e19c9fdd0e5346a
size 42673
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c081116bbbb293635b599ae4d4d25505cf442a0feae7fbded69f7772fec7d96f
size 42850
oid sha256:ff352e5d173cc05b74d4086e5412d467b378239fa7f75b3c17c11b7f024047c1
size 42823
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fdfe5a8f3e42c503560f32835237ab0f340b0a82a9dea7945d6cf19ed1845c93
size 42868
oid sha256:8ce5baff8faa00bec873dd65a22415d9bdba0c69f68168fb893036684ce2b0dc
size 42882
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:928165a963ba3dadd278c8fee183fb5d6398b28369f28f782c9f08dbc69df52c
size 14235
oid sha256:ad303cbc4984982a5d3e6fe341c611483d76a3cd4bf15705864c5c20443f9b4d
size 14226
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5ca516e5f7bc8a2231b5fa7535a80b03eb6ff44b2d83e66163b010a1802fa7e
size 32464
oid sha256:ccc9c9c95b9a56bb6c453aea4b13be812b64ad5b7352f169a32208166ff70b9f
size 32449
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97e41908816ae4f32eeb85f00125dcd7eb28810d9157441f535a228de3ff31bd
size 13774
oid sha256:70193284306460b8f9c333849c8d5e1b5b1e95c3ce446307ccff482c90c2d09a
size 13775
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5cd30c16ff85d4140d25b738916948bf4180b49341b650886a318080c8697fd6
oid sha256:0284751ecc89b295784fab1eb4a85e4615015ef915c9492a59ad8671f2805806
size 31606