[JEWEL-857] LazySelectableList/LazyTree/Tabs/SegmentedButton do not read content from selected item

- Updated sample app semantics configuration marking containers as traversal groups
- Merging children nodes in the 'Modifier.selectable' to ensure screen reader understand it as one and read the text properly
- Merging the child nodes in the SimpleListItem to ensure screen reader understand it as one and read the text properly

closes https://github.com/JetBrains/intellij-community/pull/3227

GitOrigin-RevId: f93172c5077418180dad26362509f08c341c251e
This commit is contained in:
Gustavo Fao Valvassori
2025-09-18 15:26:58 +00:00
committed by intellij-monorepo-bot
parent c27f72b43e
commit 6e0d9a581e
6 changed files with 43 additions and 18 deletions
@@ -30,6 +30,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
@@ -232,6 +233,7 @@ private fun Modifier.selectable(
selected = itemKey in selectableState.selectedKeys
focused = selectableState.lastActiveItemIndex == allKeys.indexOfFirst { it.key == itemKey }
stateDescription = ""
isTraversalGroup = false
}
// Handle pointer input but ensure Tab keys aren't intercepted
.pointerInput(allKeys, itemKey) {
@@ -240,7 +242,6 @@ private fun Modifier.selectable(
val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Press -> {
requester?.requestFocus()
actionHandler.handlePointerEventPress(
pointerEvent = event,
keybindings = keybindings,
@@ -249,6 +250,7 @@ private fun Modifier.selectable(
allKeys = allKeys,
key = itemKey,
)
requester?.requestFocus()
}
}
}
@@ -2,7 +2,6 @@
// Apache 2.0 license.
package org.jetbrains.jewel.samples.showcase.components
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
@@ -13,8 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -48,6 +45,7 @@ import org.jetbrains.jewel.ui.component.RadioButtonChip
import org.jetbrains.jewel.ui.component.SimpleListItem
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.ToggleableChip
import org.jetbrains.jewel.ui.component.VerticalScrollbar
import org.jetbrains.jewel.ui.theme.colorPalette
@Composable
@@ -177,27 +175,27 @@ public fun SelectableLazyColumnSample(modifier: Modifier = Modifier) {
}
val state = rememberSelectableLazyListState()
Box(modifier = modifier.size(200.dp, 200.dp)) {
Box(modifier = modifier.size(200.dp)) {
if (listOfItems.isEmpty()) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
} else {
SelectableLazyColumn(modifier = Modifier.focusable(), state = state) {
items(listOfItems, key = { item -> item }) { item ->
LaunchedEffect(isSelected) {
if (isSelected) {
JewelLogger.getInstance("ChipsAndTree").info("Item $item got selected")
}
}
SimpleListItem(
text = item,
selected = isSelected,
active = isActive,
modifier =
Modifier.fillMaxWidth().selectable(isSelected) {
JewelLogger.getInstance("ChipsAndTree").info("Click on $item")
},
modifier = Modifier.fillMaxWidth(),
)
}
}
VerticalScrollbar(
rememberScrollbarAdapter(state.lazyListState),
modifier = Modifier.align(Alignment.CenterEnd),
)
VerticalScrollbar(state.lazyListState, modifier = Modifier.align(Alignment.CenterEnd))
}
}
}
@@ -18,6 +18,8 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import kotlin.time.Duration.Companion.milliseconds
import org.jetbrains.jewel.foundation.modifier.trackActivation
@@ -43,7 +45,11 @@ public fun ComponentsView(
toolbarButtonMetrics: IconButtonMetrics,
modifier: Modifier = Modifier,
) {
Row(modifier.trackActivation().fillMaxSize().background(JewelTheme.globalColors.panelBackground)) {
Row(
modifier.trackActivation().fillMaxSize().background(JewelTheme.globalColors.panelBackground).semantics {
isTraversalGroup = true
}
) {
ComponentsToolBar(
buttonMetrics = toolbarButtonMetrics,
views = viewModel.getViews(),
@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import org.jetbrains.jewel.foundation.modifier.trackActivation
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.markdown.MarkdownMode
@@ -19,7 +21,11 @@ import org.jetbrains.jewel.ui.component.Divider
@Composable
internal fun MarkdownDemo() {
Row(Modifier.trackActivation().fillMaxSize().background(JewelTheme.globalColors.panelBackground)) {
Row(
Modifier.trackActivation().fillMaxSize().background(JewelTheme.globalColors.panelBackground).semantics {
isTraversalGroup = true
}
) {
WithMarkdownMode(MarkdownMode.EditorPreview(scrollingSynchronizer = null)) {
val editorState = rememberTextFieldState(JewelReadme)
MarkdownEditor(state = editorState, modifier = Modifier.fillMaxHeight().weight(1f))
@@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.foundation.JewelFlags
import org.jetbrains.jewel.foundation.modifier.trackActivation
@@ -32,7 +34,11 @@ import org.jetbrains.jewel.ui.typography
internal fun WelcomeView() {
Column(
modifier =
Modifier.trackActivation().fillMaxSize().background(JewelTheme.globalColors.panelBackground).padding(24.dp),
Modifier.trackActivation()
.fillMaxSize()
.background(JewelTheme.globalColors.panelBackground)
.padding(24.dp)
.semantics { isTraversalGroup = true },
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Icon(key = ShowcaseIcons.jewelLogo, contentDescription = null, modifier = Modifier.size(200.dp))
@@ -13,6 +13,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
@@ -210,7 +211,10 @@ public fun SimpleListItem(
Row(
modifier =
modifier
.semantics { selected = state.isSelected }
.semantics(mergeDescendants = true) {
selected = state.isSelected
isTraversalGroup = false
}
.fillMaxWidth()
.height(height)
.padding(style.metrics.outerPadding)
@@ -337,7 +341,10 @@ public fun SimpleListItem(
Row(
modifier =
modifier
.semantics { selected = state.isSelected }
.semantics(mergeDescendants = true) {
selected = state.isSelected
isTraversalGroup = false
}
.fillMaxWidth()
.height(height)
.padding(style.metrics.outerPadding)