Draw the focus indicator around the whole SelectedItem

Previously, only the small remove button could get the keyboard focus, and the
focus ring was barely visible. Now the whole item is focusable, the focus ring is
drawn around it, and pressing an activation key (Enter, Space, D-pad center)
removes the item. Touch behavior is unchanged: only the remove button is clickable.

Also add a shared `activationKeys` set in the designsystem module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benoit Marty
2026-10-09 09:31:15 +02:00
parent 6cb8a85539
commit b62999d4c0
2 changed files with 52 additions and 0 deletions
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.utils
import androidx.compose.ui.input.key.Key
/**
* Keys which can be used to activate (i.e. click) the component having the keyboard focus.
*/
val activationKeys = setOf(
Key.Enter,
Key.NumPadEnter,
Key.DirectionCenter,
Key.Spacebar,
)
@@ -9,6 +9,8 @@
package io.element.android.libraries.matrix.ui.components package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -25,11 +27,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -48,6 +55,7 @@ import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.activationKeys
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@@ -62,9 +70,31 @@ fun SelectedItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val actionRemove = stringResource(id = CommonStrings.action_remove) val actionRemove = stringResource(id = CommonStrings.action_remove)
val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = modifier modifier = modifier
.width(avatarData.size.dp) .width(avatarData.size.dp)
.then(
if (canRemove) {
// Let the whole item get the keyboard focus, so that the focus indicator is drawn around it.
Modifier
.onKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key in activationKeys) {
onRemoveClick()
true
} else {
false
}
}
.indication(
interactionSource = interactionSource,
indication = ripple(),
)
.focusable(interactionSource = interactionSource)
} else {
Modifier
}
)
.clearAndSetSemantics { .clearAndSetSemantics {
contentDescription = a11yContentDescription contentDescription = a11yContentDescription
if (canRemove) { if (canRemove) {
@@ -130,6 +160,8 @@ fun SelectedItem(
.clip(CircleShape) .clip(CircleShape)
.size(20.dp) .size(20.dp)
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
// The whole item is focusable, not the remove button
.focusProperties { canFocus = false }
.clickable( .clickable(
indication = ripple(), indication = ripple(),
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },