Skip to content

Accessibility: improve behavior of list items #4626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,14 @@
supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) },
trailingContent = ListItemContent.Switch(
checked = state.pollKind == PollKind.Undisclosed,
onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) },
),
onClick = {
state.eventSink(
CreatePollEvents.SetPollKind(

Check warning on line 184 in features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt

View check run for this annotation

Codecov / codecov/patch

features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt#L183-L184

Added lines #L183 - L184 were not covered by tests
if (state.pollKind == PollKind.Disclosed) PollKind.Undisclosed else PollKind.Disclosed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to let the Presenter handle this, but as it's not new, feel free to ignore.

)
)
},
)
if (state.canDelete) {
ListItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,7 @@ private fun RoomAddressSection(
Text(text = stringResource(R.string.screen_security_and_privacy_room_directory_visibility_section_footer, homeserverName))
},
onClick = if (isVisibleInRoomDirectory.isSuccess()) onVisibilityChange else null,
trailingContent =
when (isVisibleInRoomDirectory) {
trailingContent = when (isVisibleInRoomDirectory) {
is AsyncData.Uninitialized, is AsyncData.Loading -> {
ListItemContent.Custom {
CircularProgressIndicator(
Expand All @@ -288,7 +287,6 @@ private fun RoomAddressSection(
is AsyncData.Success -> {
ListItemContent.Switch(
checked = isVisibleInRoomDirectory.data,
onChange = { onVisibilityChange() },
)
}
}
Expand Down Expand Up @@ -316,7 +314,6 @@ private fun EncryptionSection(
trailingContent = ListItemContent.Switch(
checked = isRoomEncrypted,
enabled = canToggleEncryption,
onChange = { onToggleEncryption() },
),
onClick = if (canToggleEncryption) onToggleEncryption else null
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,10 @@ private fun RoomListModalBottomSheetContent(
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Favourite(),
contentDescription = stringResource(id = CommonStrings.common_favourite),
)
),
trailingContent = ListItemContent.Switch(
checked = contextMenu.isFavorite,
onChange = { isFavorite ->
onFavoriteChange(isFavorite)
},
),
onClick = {
onFavoriteChange(!contextMenu.isFavorite)
Expand All @@ -157,7 +153,6 @@ private fun RoomListModalBottomSheetContent(
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Settings(),
contentDescription = stringResource(id = CommonStrings.common_settings)
)
),
style = ListItemStyle.Primary,
Expand All @@ -177,7 +172,6 @@ private fun RoomListModalBottomSheetContent(
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Leave(),
contentDescription = stringResource(id = CommonStrings.action_leave_room)
)
),
style = ListItemStyle.Destructive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ fun CheckboxListItem(
modifier = modifier,
headlineContent = { Text(headline) },
supportingContent = supportingText?.let { @Composable { Text(it) } },
leadingContent = ListItemContent.Checkbox(checked, null, enabled, compact = compactLayout),
leadingContent = ListItemContent.Checkbox(
checked = checked,
enabled = enabled,
compact = compactLayout,
),
trailingContent = trailingContent,
style = style,
enabled = enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,35 @@ sealed interface ListItemContent {
/**
* Default Switch content for [ListItem].
* @param checked The current state of the switch.
* @param onChange Callback when the switch is toggled: it should only be set to override the default click behaviour in the [ListItem].
* @param enabled Whether the switch is enabled or not.
*/
data class Switch(
val checked: Boolean,
val onChange: ((Boolean) -> Unit)? = null,
val enabled: Boolean = true
) : ListItemContent

/**
* Default Checkbox content for [ListItem].
* @param checked The current state of the checkbox.
* @param onChange Callback when the checkbox is toggled: it should only be set to override the default click behaviour in the [ListItem].
* @param enabled Whether the checkbox is enabled or not.
* @param compact Reduces the size of the component to make the wrapping [ListItem] smaller.
* This is especially useful when the [ListItem] is used inside a Dialog. `false` by default.
*/
data class Checkbox(
val checked: Boolean,
val onChange: ((Boolean) -> Unit)? = null,
val enabled: Boolean = true,
val compact: Boolean = false
) : ListItemContent

/**
* Default RadioButton content for [ListItem].
* @param selected The current state of the radio button.
* @param onClick Callback when the radio button is toggled: it should only be set to override the default click behaviour in the [ListItem].
* @param enabled Whether the radio button is enabled or not.
* @param compact Reduces the size of the component to make the wrapping [ListItem] smaller.
* This is especially useful when the [ListItem] is used inside a Dialog. `false` by default.
*/
data class RadioButton(
val selected: Boolean,
val onClick: (() -> Unit)? = null,
val enabled: Boolean = true,
val compact: Boolean = false
) : ListItemContent
Expand Down Expand Up @@ -99,24 +93,24 @@ sealed interface ListItemContent {
data class Counter(val count: Int) : ListItemContent

@Composable
fun View() {
fun View(isItemEnabled: Boolean) {
when (this) {
is Switch -> SwitchComponent(
checked = checked,
onCheckedChange = onChange,
enabled = enabled
onCheckedChange = null,
enabled = enabled && isItemEnabled,
)
is Checkbox -> CheckboxComponent(
modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
checked = checked,
onCheckedChange = onChange,
enabled = enabled
onCheckedChange = null,
enabled = enabled && isItemEnabled,
)
is RadioButton -> RadioButtonComponent(
modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
selected = selected,
onClick = onClick,
enabled = enabled
onClick = null,
enabled = enabled && isItemEnabled,
)
is Icon -> {
IconComponent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ fun RadioButtonListItem(
modifier = modifier,
headlineContent = { Text(headline) },
supportingContent = supportingText?.let { @Composable { Text(it) } },
leadingContent = ListItemContent.RadioButton(selected, null, enabled, compact = compactLayout),
leadingContent = ListItemContent.RadioButton(
selected = selected,
enabled = enabled,
compact = compactLayout,
),
trailingContent = trailingContent,
style = style,
enabled = enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ fun SwitchListItem(
headlineContent = { Text(headline) },
supportingContent = supportingText?.let { @Composable { Text(it) } },
leadingContent = leadingContent,
trailingContent = ListItemContent.Switch(value, null, enabled),
trailingContent = ListItemContent.Switch(
checked = value,
enabled = enabled,
),
style = style,
enabled = enabled,
onClick = { onChange(!value) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ fun PreferenceCheckbox(
checked = isChecked,
enabled = enabled,
),
enabled = enabled,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@ package io.element.android.libraries.designsystem.theme.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -135,7 +134,7 @@ fun ListItem(
CompositionLocalProvider(
LocalContentColor provides leadingContentColor,
) {
content.View()
content.View(isItemEnabled = enabled)
}
}
}
Expand All @@ -145,7 +144,7 @@ fun ListItem(
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
LocalContentColor provides trailingContentColor,
) {
content.View()
content.View(isItemEnabled = enabled)
}
}
}
Expand All @@ -158,7 +157,12 @@ fun ListItem(
.then(modifier)
} else {
modifier
},
}
.withAccessibilityModifier(
content = trailingContent ?: leadingContent,
enabled = enabled || alwaysClickable,
onClick = onClick,
),
overlineContent = null,
supportingContent = decoratedSupportingContent,
leadingContent = decoratedLeadingContent,
Expand All @@ -169,6 +173,45 @@ fun ListItem(
)
}

private fun Modifier.withAccessibilityModifier(
content: ListItemContent?,
enabled: Boolean,
onClick: (() -> Unit)?,
): Modifier = then(
when (content) {
is ListItemContent.Checkbox -> {
Modifier.toggleable(
value = content.checked,
role = Role.Checkbox,
enabled = content.enabled && enabled,
onValueChange = { onClick?.invoke() }
)
}
is ListItemContent.Switch -> {
Modifier.toggleable(
value = content.checked,
role = Role.Switch,
enabled = content.enabled && enabled,
onValueChange = { onClick?.invoke() }
)
}
is ListItemContent.RadioButton -> {
Modifier.selectable(
selected = content.selected,
role = Role.RadioButton,
enabled = content.enabled && enabled,
onClick = { onClick?.invoke() }
)
}
ListItemContent.Badge,
is ListItemContent.Custom,
is ListItemContent.Icon,
is ListItemContent.Text,
is ListItemContent.Counter,
null -> Modifier
}
)

/**
* The style to use for a [ListItem].
*/
Expand Down Expand Up @@ -546,20 +589,17 @@ private object PreviewItems {

@Composable
fun checkbox(): ListItemContent {
var checked by remember { mutableStateOf(false) }
return ListItemContent.Checkbox(checked = checked, onChange = { checked = !checked })
return ListItemContent.Checkbox(checked = false)
}

@Composable
fun radioButton(): ListItemContent {
var checked by remember { mutableStateOf(false) }
return ListItemContent.RadioButton(selected = checked, onClick = { checked = !checked })
return ListItemContent.RadioButton(selected = false)
}

@Composable
fun switch(): ListItemContent {
var checked by remember { mutableStateOf(false) }
return ListItemContent.Switch(checked = checked, onChange = { checked = !checked })
return ListItemContent.Switch(checked = false)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ internal fun ListSupportingTextSmallPaddingPreview() {
internal fun ListSupportingTextLargePaddingPreview() {
ElementThemedPreview {
Column {
ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true, onChange = {}))
ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true))
ListSupportingText(
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
contentPadding = ListSupportingTextDefaults.Padding.LargeLeadingContent,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading