Improve list items: add lists to dialogs, rework ListItem customizations (#1119)
* Improve list items: - Create `ListItemContent`. - Create `ListItemStyle`. - Apply those to `ListItem` components. - Create helper list item components for checkboxes, switches, radio buttons. * Create single/multiple selection dialogs. * Create `SingleSelectionListItem` and `MultipleSelectionListItem` - Add `subtitle` to `AlertDialogContents`. - Fix paddings and margins inside dialogs. - Add `ListOption`. * Adds small delay before hiding the single selection dialog. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
b6518c595d
commit
7f50671a45
36 changed files with 1155 additions and 85 deletions
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.dialogs
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to store the visual data for a list option.
|
||||||
|
*/
|
||||||
|
data class ListOption(
|
||||||
|
val title: String,
|
||||||
|
val subtitle: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Creates an immutable list of [ListOption]s from the given [values], using them as titles. */
|
||||||
|
fun listOptionOf(vararg values: String): ImmutableList<ListOption> {
|
||||||
|
return values.map { ListOption(it) }.toImmutableList()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.dialogs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.airbnb.android.showkase.annotation.ShowkaseComposable
|
||||||
|
import io.element.android.libraries.designsystem.components.list.CheckboxListItem
|
||||||
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.DialogPreview
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MultipleSelectionDialog(
|
||||||
|
options: ImmutableList<ListOption>,
|
||||||
|
onConfirmClicked: (List<Int>) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
confirmButtonTitle: String = stringResource(CommonStrings.action_confirm),
|
||||||
|
dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
|
||||||
|
title: String? = null,
|
||||||
|
subtitle: String? = null,
|
||||||
|
initialSelection: ImmutableList<Int> = persistentListOf(),
|
||||||
|
) {
|
||||||
|
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
|
||||||
|
@Composable {
|
||||||
|
ListSupportingText(
|
||||||
|
text = it,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlertDialog(
|
||||||
|
modifier = modifier,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
MultipleSelectionDialogContent(
|
||||||
|
title = title,
|
||||||
|
subtitle = decoratedSubtitle,
|
||||||
|
options = options,
|
||||||
|
confirmButtonTitle = confirmButtonTitle,
|
||||||
|
onConfirmClicked = onConfirmClicked,
|
||||||
|
dismissButtonTitle = dismissButtonTitle,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
initialSelected = initialSelection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MultipleSelectionDialogContent(
|
||||||
|
options: ImmutableList<ListOption>,
|
||||||
|
confirmButtonTitle: String,
|
||||||
|
onConfirmClicked: (List<Int>) -> Unit,
|
||||||
|
dismissButtonTitle: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String? = null,
|
||||||
|
initialSelected: ImmutableList<Int> = persistentListOf(),
|
||||||
|
subtitle: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val selectedOptionIndexes = remember { initialSelected.toMutableStateList() }
|
||||||
|
|
||||||
|
fun isSelected(index: Int) = selectedOptionIndexes.any { it == index }
|
||||||
|
|
||||||
|
SimpleAlertDialogContent(
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
modifier = modifier,
|
||||||
|
submitText = confirmButtonTitle,
|
||||||
|
onSubmitClicked = {
|
||||||
|
onConfirmClicked(selectedOptionIndexes.toList())
|
||||||
|
},
|
||||||
|
cancelText = dismissButtonTitle,
|
||||||
|
onCancelClicked = onDismissRequest,
|
||||||
|
applyPaddingToContents = false,
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
itemsIndexed(options) { index, option ->
|
||||||
|
CheckboxListItem(
|
||||||
|
headline = option.title,
|
||||||
|
checked = isSelected(index),
|
||||||
|
onChange = {
|
||||||
|
if (isSelected(index)) {
|
||||||
|
selectedOptionIndexes.remove(index)
|
||||||
|
} else {
|
||||||
|
selectedOptionIndexes.add(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
supportingText = option.subtitle,
|
||||||
|
compactLayout = true,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@ShowkaseComposable(group = PreviewGroup.Dialogs)
|
||||||
|
@Composable
|
||||||
|
internal fun MultipleSelectionDialogContentPreview() {
|
||||||
|
ElementPreview(showBackground = false) {
|
||||||
|
DialogPreview {
|
||||||
|
val options = persistentListOf(
|
||||||
|
ListOption("Option 1", "Supporting line text lorem ipsum dolor sit amet, consectetur."),
|
||||||
|
ListOption("Option 2"),
|
||||||
|
ListOption("Option 3"),
|
||||||
|
)
|
||||||
|
MultipleSelectionDialogContent(
|
||||||
|
title = "Dialog title",
|
||||||
|
options = options,
|
||||||
|
onConfirmClicked = {},
|
||||||
|
onDismissRequest = {},
|
||||||
|
confirmButtonTitle = "Save",
|
||||||
|
dismissButtonTitle = "Cancel",
|
||||||
|
initialSelected = persistentListOf(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.dialogs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.airbnb.android.showkase.annotation.ShowkaseComposable
|
||||||
|
import io.element.android.libraries.designsystem.components.list.RadioButtonListItem
|
||||||
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.DialogPreview
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SingleSelectionDialog(
|
||||||
|
options: ImmutableList<ListOption>,
|
||||||
|
onOptionSelected: (Int) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String? = null,
|
||||||
|
subtitle: String? = null,
|
||||||
|
dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
|
||||||
|
initialSelection: Int? = null,
|
||||||
|
) {
|
||||||
|
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
|
||||||
|
@Composable {
|
||||||
|
ListSupportingText(
|
||||||
|
text = it,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlertDialog(
|
||||||
|
modifier = modifier,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
SingleSelectionDialogContent(
|
||||||
|
title = title,
|
||||||
|
subtitle = decoratedSubtitle,
|
||||||
|
options = options,
|
||||||
|
onOptionSelected = onOptionSelected,
|
||||||
|
dismissButtonTitle = dismissButtonTitle,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
initialSelection = initialSelection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionDialogContent(
|
||||||
|
options: ImmutableList<ListOption>,
|
||||||
|
onOptionSelected: (Int) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
dismissButtonTitle: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String? = null,
|
||||||
|
initialSelection: Int? = null,
|
||||||
|
subtitle: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
SimpleAlertDialogContent(
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
modifier = modifier,
|
||||||
|
cancelText = dismissButtonTitle,
|
||||||
|
onCancelClicked = onDismissRequest,
|
||||||
|
applyPaddingToContents = false,
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
itemsIndexed(options) { index, option ->
|
||||||
|
RadioButtonListItem(
|
||||||
|
headline = option.title,
|
||||||
|
supportingText = option.subtitle,
|
||||||
|
selected = index == initialSelection,
|
||||||
|
onSelected = { onOptionSelected(index) },
|
||||||
|
compactLayout = true,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@ShowkaseComposable(group = PreviewGroup.Dialogs)
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionDialogContentPreview() {
|
||||||
|
ElementPreview(showBackground = false) {
|
||||||
|
DialogPreview {
|
||||||
|
val options = persistentListOf(
|
||||||
|
ListOption("Option 1"),
|
||||||
|
ListOption("Option 2"),
|
||||||
|
ListOption("Option 3"),
|
||||||
|
)
|
||||||
|
SingleSelectionDialogContent(
|
||||||
|
title = "Dialog title",
|
||||||
|
options = options,
|
||||||
|
onOptionSelected = {},
|
||||||
|
onDismissRequest = {},
|
||||||
|
dismissButtonTitle = "Cancel",
|
||||||
|
initialSelection = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CheckboxListItem(
|
||||||
|
headline: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingText: String? = null,
|
||||||
|
trailingContent: ListItemContent? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
|
compactLayout: Boolean = false,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headlineContent = { Text(headline) },
|
||||||
|
supportingContent = supportingText?.let { @Composable { Text(it) } },
|
||||||
|
leadingContent = ListItemContent.Checkbox(checked, null, enabled, compact = compactLayout),
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
style = style,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = { onChange(!checked) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Checkbox as CheckboxComponent
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon as IconComponent
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.RadioButton as RadioButtonComponent
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Switch as SwitchComponent
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text as TextComponent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a helper to set default leading and trailing content for [ListItem]s.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Icon content for [ListItem]. Sets the Icon component to a predefined size.
|
||||||
|
* @param iconSource The icon to display, using [IconSource.getPainter].
|
||||||
|
*/
|
||||||
|
data class Icon(val iconSource: IconSource) : ListItemContent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Text content for [ListItem]. Sets the Text component to a max size and clips overflow.
|
||||||
|
* @param text The text to display.
|
||||||
|
*/
|
||||||
|
data class Text(val text: String) : ListItemContent
|
||||||
|
|
||||||
|
/** Displays any custom content. */
|
||||||
|
data class Custom(val content: @Composable () -> Unit) : ListItemContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun View() {
|
||||||
|
when (this) {
|
||||||
|
is Switch -> SwitchComponent(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onChange,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
is Checkbox -> CheckboxComponent(
|
||||||
|
modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onChange,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
is RadioButton -> RadioButtonComponent(
|
||||||
|
modifier = if (compact) Modifier.size(maxCompactSize) else Modifier,
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
is Icon -> IconComponent(
|
||||||
|
modifier = Modifier.size(maxCompactSize),
|
||||||
|
painter = iconSource.getPainter(),
|
||||||
|
contentDescription = iconSource.contentDescription
|
||||||
|
)
|
||||||
|
is Text -> TextComponent(modifier = Modifier.widthIn(max = 128.dp), text = text, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
|
is Custom -> content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val maxCompactSize = DpSize(24.dp, 24.dp)
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ListOption
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.MultipleSelectionDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.listOptionOf
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MultipleSelectionListItem(
|
||||||
|
headline: String,
|
||||||
|
options: ImmutableList<ListOption>,
|
||||||
|
onSelectionChanged: (List<Int>) -> Unit,
|
||||||
|
resultFormatter: (List<Int>) -> String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingText: String? = null,
|
||||||
|
leadingContent: ListItemContent? = null,
|
||||||
|
selected: ImmutableList<Int> = persistentListOf(),
|
||||||
|
displayResultInTrailingContent: Boolean = false,
|
||||||
|
) {
|
||||||
|
val selectedIndexes = remember(selected) { selected.toMutableStateList() }
|
||||||
|
val selectedItemsText by remember { derivedStateOf { resultFormatter(selectedIndexes) } }
|
||||||
|
|
||||||
|
val decoratedSupportedText: @Composable (() -> Unit)? = when {
|
||||||
|
!selectedItemsText.isNullOrBlank() && !displayResultInTrailingContent -> {
|
||||||
|
@Composable {
|
||||||
|
Text(selectedItemsText!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
supportingText != null -> {
|
||||||
|
@Composable {
|
||||||
|
Text(supportingText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val trailingContent: ListItemContent? = if (!selectedItemsText.isNullOrBlank() && displayResultInTrailingContent) {
|
||||||
|
ListItemContent.Text(selectedItemsText!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
var displaySelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headlineContent = { Text(text = headline) },
|
||||||
|
supportingContent = decoratedSupportedText,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
onClick = { displaySelectionDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (displaySelectionDialog) {
|
||||||
|
MultipleSelectionDialog(
|
||||||
|
title = headline,
|
||||||
|
options = options,
|
||||||
|
onConfirmClicked = { newSelectedIndexes ->
|
||||||
|
if (newSelectedIndexes != selectedIndexes.toList()) {
|
||||||
|
onSelectionChanged(newSelectedIndexes)
|
||||||
|
selectedIndexes.clear()
|
||||||
|
selectedIndexes.addAll(newSelectedIndexes)
|
||||||
|
}
|
||||||
|
displaySelectionDialog = false
|
||||||
|
},
|
||||||
|
onDismissRequest = { displaySelectionDialog = false },
|
||||||
|
initialSelection = selectedIndexes.toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Multiple selection List item - no selection", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun MutipleSelectionListItemPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
val options = listOptionOf("Option 1", "Option 2", "Option 3")
|
||||||
|
MultipleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = options,
|
||||||
|
onSelectionChanged = {},
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
resultFormatter = { result -> formatResult(result, options) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Multiple selection List item - selection in supporting text", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun MutipleSelectionListItemSelectedPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
val options = listOptionOf("Option 1", "Option 2", "Option 3")
|
||||||
|
val selected = persistentListOf<Int>(0, 2)
|
||||||
|
MultipleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = options,
|
||||||
|
onSelectionChanged = {},
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
resultFormatter = {
|
||||||
|
val selectedValues = formatResult(it, options)
|
||||||
|
"Selected: $selectedValues"
|
||||||
|
},
|
||||||
|
selected = selected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Multiple selection List item - selection in trailing content", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun MutipleSelectionListItemSelectedTrailingContentPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
val options = listOptionOf("Option 1", "Option 2", "Option 3")
|
||||||
|
val selected = persistentListOf<Int>(0, 2)
|
||||||
|
MultipleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = options,
|
||||||
|
onSelectionChanged = {},
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
resultFormatter = { selected.size.toString() },
|
||||||
|
displayResultInTrailingContent = true,
|
||||||
|
selected = selected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatResult(result: List<Int>, options: ImmutableList<ListOption>): String? {
|
||||||
|
return options.mapIndexedNotNull { index, value -> value.title.takeIf { result.contains(index) } }.joinToString(", ").takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RadioButtonListItem(
|
||||||
|
headline: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onSelected: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingText: String? = null,
|
||||||
|
trailingContent: ListItemContent? = null,
|
||||||
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
compactLayout: Boolean = false,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headlineContent = { Text(headline) },
|
||||||
|
supportingContent = supportingText?.let { @Composable { Text(it) } },
|
||||||
|
leadingContent = ListItemContent.RadioButton(selected, null, enabled, compact = compactLayout),
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
style = style,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ListOption
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.listOptionOf
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SingleSelectionListItem(
|
||||||
|
headline: String,
|
||||||
|
options: ImmutableList<ListOption>,
|
||||||
|
onSelectionChanged: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingText: String? = null,
|
||||||
|
leadingContent: ListItemContent? = null,
|
||||||
|
resultFormatter: (Int) -> String? = { options.getOrNull(it)?.title },
|
||||||
|
selected: Int? = null,
|
||||||
|
displayResultInTrailingContent: Boolean = false,
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var selectedIndex by rememberSaveable(selected) { mutableStateOf(selected) }
|
||||||
|
val selectedItem by remember { derivedStateOf { selectedIndex?.let { resultFormatter(it) } } }
|
||||||
|
val decoratedSupportedText: @Composable (() -> Unit)? = if (!selectedItem.isNullOrBlank() && !displayResultInTrailingContent) {
|
||||||
|
@Composable {
|
||||||
|
Text(selectedItem!!)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
supportingText?.let {
|
||||||
|
@Composable {
|
||||||
|
Text(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val trailingContent: ListItemContent? = if (!selectedItem.isNullOrBlank() && displayResultInTrailingContent) {
|
||||||
|
ListItemContent.Text(selectedItem!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
var displaySelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headlineContent = { Text(text = headline) },
|
||||||
|
supportingContent = decoratedSupportedText,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
onClick = { displaySelectionDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (displaySelectionDialog) {
|
||||||
|
SingleSelectionDialog(
|
||||||
|
title = headline,
|
||||||
|
options = options,
|
||||||
|
onOptionSelected = { index ->
|
||||||
|
if (index != selectedIndex) {
|
||||||
|
onSelectionChanged(index)
|
||||||
|
selectedIndex = index
|
||||||
|
}
|
||||||
|
// Delay hiding the dialog for a bit so the new state is displayed in it before being dismissed
|
||||||
|
coroutineScope.launch {
|
||||||
|
delay(0.5.seconds)
|
||||||
|
displaySelectionDialog = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = { displaySelectionDialog = false },
|
||||||
|
initialSelection = selectedIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Single selection List item - no selection", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionListItemPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
SingleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = listOptionOf("Option 1", "Option 2", "Option 3"),
|
||||||
|
onSelectionChanged = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Single selection List item - no selection, supporting text", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionListItemUnselectedWithSupportingTextPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
SingleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = listOptionOf("Option 1", "Option 2", "Option 3"),
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
onSelectionChanged = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Single selection List item - selection in supporting text", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionListItemSelectedInSupportingTextPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
SingleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = listOptionOf("Option 1", "Option 2", "Option 3"),
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
onSelectionChanged = {},
|
||||||
|
selected = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Single selection List item - selection in trailing content", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionListItemSelectedInTrailingContentPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
SingleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = listOptionOf("Option 1", "Option 2", "Option 3"),
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
onSelectionChanged = {},
|
||||||
|
selected = 1,
|
||||||
|
displayResultInTrailingContent = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview("Single selection List item - custom formatter", group = PreviewGroup.ListItems)
|
||||||
|
@Composable
|
||||||
|
internal fun SingleSelectionListItemCustomFormattertPreview() {
|
||||||
|
ElementThemedPreview {
|
||||||
|
SingleSelectionListItem(
|
||||||
|
headline = "Headline",
|
||||||
|
options = listOptionOf("Option 1", "Option 2", "Option 3"),
|
||||||
|
supportingText = "Supporting text",
|
||||||
|
onSelectionChanged = {},
|
||||||
|
resultFormatter = { "Selected index: $it"},
|
||||||
|
selected = 1,
|
||||||
|
displayResultInTrailingContent = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchListItem(
|
||||||
|
headline: String,
|
||||||
|
value: Boolean,
|
||||||
|
onChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
supportingText: String? = null,
|
||||||
|
leadingContent: ListItemContent? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier,
|
||||||
|
headlineContent = { Text(headline) },
|
||||||
|
supportingContent = supportingText?.let { @Composable { Text(it) } },
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = ListItemContent.Switch(value, null, enabled),
|
||||||
|
style = style,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = { onChange(!value) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -55,11 +56,49 @@ internal fun SimpleAlertDialogContent(
|
||||||
onCancelClicked: () -> Unit,
|
onCancelClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
|
subtitle: @Composable (() -> Unit)? = null,
|
||||||
submitText: String? = null,
|
submitText: String? = null,
|
||||||
onSubmitClicked: () -> Unit = {},
|
onSubmitClicked: () -> Unit = {},
|
||||||
thirdButtonText: String? = null,
|
thirdButtonText: String? = null,
|
||||||
onThirdButtonClicked: () -> Unit = {},
|
onThirdButtonClicked: () -> Unit = {},
|
||||||
|
applyPaddingToContents: Boolean = true,
|
||||||
icon: @Composable (() -> Unit)? = null,
|
icon: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
SimpleAlertDialogContent(
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
style = ElementTheme.materialTypography.bodyMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cancelText = cancelText,
|
||||||
|
onCancelClicked = onCancelClicked,
|
||||||
|
modifier = modifier,
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
submitText = submitText,
|
||||||
|
onSubmitClicked = onSubmitClicked,
|
||||||
|
thirdButtonText = thirdButtonText,
|
||||||
|
onThirdButtonClicked = onThirdButtonClicked,
|
||||||
|
icon = icon,
|
||||||
|
applyPaddingToContents = applyPaddingToContents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SimpleAlertDialogContent(
|
||||||
|
cancelText: String,
|
||||||
|
onCancelClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String? = null,
|
||||||
|
subtitle: @Composable (() -> Unit)? = null,
|
||||||
|
submitText: String? = null,
|
||||||
|
onSubmitClicked: () -> Unit = {},
|
||||||
|
thirdButtonText: String? = null,
|
||||||
|
onThirdButtonClicked: () -> Unit = {},
|
||||||
|
applyPaddingToContents: Boolean = true,
|
||||||
|
icon: @Composable (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialogContent(
|
AlertDialogContent(
|
||||||
buttons = {
|
buttons = {
|
||||||
|
|
@ -99,12 +138,8 @@ internal fun SimpleAlertDialogContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
subtitle = subtitle,
|
||||||
Text(
|
content = content,
|
||||||
text = content,
|
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = DialogContentDefaults.shape,
|
shape = DialogContentDefaults.shape,
|
||||||
containerColor = DialogContentDefaults.containerColor,
|
containerColor = DialogContentDefaults.containerColor,
|
||||||
iconContentColor = DialogContentDefaults.iconContentColor,
|
iconContentColor = DialogContentDefaults.iconContentColor,
|
||||||
|
|
@ -117,6 +152,7 @@ internal fun SimpleAlertDialogContent(
|
||||||
// TextButtons will not consume this provided content color value, and will used their
|
// TextButtons will not consume this provided content color value, and will used their
|
||||||
// own defined or default colors.
|
// own defined or default colors.
|
||||||
buttonContentColor = MaterialTheme.colorScheme.primary,
|
buttonContentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
applyPaddingToContents = applyPaddingToContents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +164,8 @@ internal fun AlertDialogContent(
|
||||||
buttons: @Composable () -> Unit,
|
buttons: @Composable () -> Unit,
|
||||||
icon: (@Composable () -> Unit)?,
|
icon: (@Composable () -> Unit)?,
|
||||||
title: (@Composable () -> Unit)?,
|
title: (@Composable () -> Unit)?,
|
||||||
text: @Composable (() -> Unit)?,
|
subtitle: @Composable (() -> Unit)?,
|
||||||
|
content: @Composable (() -> Unit)?,
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
containerColor: Color,
|
containerColor: Color,
|
||||||
tonalElevation: Dp,
|
tonalElevation: Dp,
|
||||||
|
|
@ -137,6 +174,7 @@ internal fun AlertDialogContent(
|
||||||
titleContentColor: Color,
|
titleContentColor: Color,
|
||||||
textContentColor: Color,
|
textContentColor: Color,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
applyPaddingToContents: Boolean = true,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -145,12 +183,21 @@ internal fun AlertDialogContent(
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(DialogContentDefaults.externalPadding)
|
modifier = Modifier.padding(
|
||||||
|
if (applyPaddingToContents) {
|
||||||
|
// We can just apply the same padding to the whole dialog contents
|
||||||
|
DialogContentDefaults.externalPadding
|
||||||
|
} else {
|
||||||
|
// We should only apply vertical padding in this case, every component will apply the horizontal content individually
|
||||||
|
DialogContentDefaults.externalVerticalPadding
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
icon?.let {
|
icon?.let {
|
||||||
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
|
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
.then(if (applyPaddingToContents) Modifier else Modifier.padding(DialogContentDefaults.externalHorizontalPadding))
|
||||||
.padding(DialogContentDefaults.iconPadding)
|
.padding(DialogContentDefaults.iconPadding)
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
|
|
@ -165,6 +212,12 @@ internal fun AlertDialogContent(
|
||||||
Box(
|
Box(
|
||||||
// Align the title to the center when an icon is present.
|
// Align the title to the center when an icon is present.
|
||||||
Modifier
|
Modifier
|
||||||
|
.then(
|
||||||
|
if (applyPaddingToContents)
|
||||||
|
Modifier
|
||||||
|
else
|
||||||
|
Modifier.padding(DialogContentDefaults.externalHorizontalPadding)
|
||||||
|
)
|
||||||
.padding(DialogContentDefaults.titlePadding)
|
.padding(DialogContentDefaults.titlePadding)
|
||||||
.align(
|
.align(
|
||||||
if (icon == null) {
|
if (icon == null) {
|
||||||
|
|
@ -179,23 +232,28 @@ internal fun AlertDialogContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text?.let {
|
subtitle?.invoke()
|
||||||
|
content?.let {
|
||||||
CompositionLocalProvider(LocalContentColor provides textContentColor) {
|
CompositionLocalProvider(LocalContentColor provides textContentColor) {
|
||||||
val textStyle =
|
val textStyle = MaterialTheme.typography.bodyMedium
|
||||||
MaterialTheme.typography.bodyMedium
|
|
||||||
ProvideTextStyle(textStyle) {
|
ProvideTextStyle(textStyle) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.weight(weight = 1f, fill = false)
|
.weight(weight = 1f, fill = false)
|
||||||
|
// We don't apply padding here if it wasn't applied to the root component, this allows us to have a full width content
|
||||||
.padding(DialogContentDefaults.textPadding)
|
.padding(DialogContentDefaults.textPadding)
|
||||||
.align(Alignment.Start)
|
.align(Alignment.Start)
|
||||||
) {
|
) {
|
||||||
text()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.align(Alignment.End)) {
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.then(if (applyPaddingToContents) Modifier else Modifier.padding(DialogContentDefaults.externalHorizontalPadding))
|
||||||
|
.align(Alignment.End)
|
||||||
|
) {
|
||||||
CompositionLocalProvider(LocalContentColor provides buttonContentColor) {
|
CompositionLocalProvider(LocalContentColor provides buttonContentColor) {
|
||||||
val textStyle =
|
val textStyle =
|
||||||
MaterialTheme.typography.labelLarge
|
MaterialTheme.typography.labelLarge
|
||||||
|
|
@ -304,6 +362,7 @@ private fun AlertDialogFlowRow(
|
||||||
internal fun DialogPreview(content: @Composable () -> Unit) {
|
internal fun DialogPreview(content: @Composable () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.background(ElementTheme.materialColors.onSurfaceVariant)
|
||||||
.sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
|
.sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
|
||||||
.padding(20.dp),
|
.padding(20.dp),
|
||||||
propagateMinConstraints = true
|
propagateMinConstraints = true
|
||||||
|
|
@ -313,8 +372,11 @@ internal fun DialogPreview(content: @Composable () -> Unit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object DialogContentDefaults {
|
internal object DialogContentDefaults {
|
||||||
|
private val externalPaddingDp = 24.dp
|
||||||
val shape = RoundedCornerShape(12.dp)
|
val shape = RoundedCornerShape(12.dp)
|
||||||
val externalPadding = PaddingValues(all = 24.dp)
|
val externalPadding = PaddingValues(all = externalPaddingDp)
|
||||||
|
val externalHorizontalPadding = PaddingValues(horizontal = externalPaddingDp)
|
||||||
|
val externalVerticalPadding = PaddingValues(vertical = externalPaddingDp)
|
||||||
val titlePadding = PaddingValues(bottom = 16.dp)
|
val titlePadding = PaddingValues(bottom = 16.dp)
|
||||||
val iconPadding = PaddingValues(bottom = 8.dp)
|
val iconPadding = PaddingValues(bottom = 8.dp)
|
||||||
val textPadding = PaddingValues(bottom = 16.dp)
|
val textPadding = PaddingValues(bottom = 16.dp)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -123,6 +124,21 @@ fun Icon(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Icon(
|
||||||
|
painter: Painter,
|
||||||
|
contentDescription: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
tint: Color = LocalContentColor.current,
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Icon(
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = modifier,
|
||||||
|
tint = tint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Icons)
|
@Preview(group = PreviewGroup.Icons)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IconImageVectorPreview() =
|
internal fun IconImageVectorPreview() =
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
|
import androidx.compose.material3.ListItemColors
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
|
@ -29,9 +30,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
@ -55,35 +58,27 @@ fun ListItem(
|
||||||
headlineContent: @Composable () -> Unit,
|
headlineContent: @Composable () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
supportingContent: @Composable (() -> Unit)? = null,
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
leadingContent: @Composable (() -> Unit)? = null,
|
leadingContent: ListItemContent? = null,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: ListItemContent? = null,
|
||||||
style: ListItemStyle = ListItemStyle.Default,
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val headlineColor = if (enabled) when (style) {
|
val colors = ListItemDefaults.colors(
|
||||||
ListItemStyle.Destructive -> ElementTheme.colors.textCriticalPrimary
|
containerColor = Color.Transparent,
|
||||||
else -> ElementTheme.colors.textPrimary
|
headlineColor = style.headlineColor(),
|
||||||
} else {
|
leadingIconColor = style.leadingIconColor(),
|
||||||
// We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
|
trailingIconColor = style.trailingIconColor(),
|
||||||
ElementTheme.colors.textDisabled
|
supportingColor = style.supportingTextColor(),
|
||||||
}
|
disabledHeadlineColor = ListItemDefaultColors.headlineDisabled,
|
||||||
|
disabledLeadingIconColor = ListItemDefaultColors.iconDisabled,
|
||||||
|
disabledTrailingIconColor = ListItemDefaultColors.iconDisabled,
|
||||||
|
)
|
||||||
|
|
||||||
val supportingContentColor = if (enabled) {
|
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
||||||
ElementTheme.materialColors.onSurfaceVariant
|
val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
|
||||||
} else {
|
val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
|
||||||
// We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
|
val trailingContentColor = if (enabled) colors.trailingIconColor else colors.disabledTrailingIconColor
|
||||||
ElementTheme.colors.textDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
val leadingTrailingContentColor = if (enabled) when (style) {
|
|
||||||
ListItemStyle.Primary -> ElementTheme.colors.iconPrimary
|
|
||||||
ListItemStyle.Destructive -> ElementTheme.colors.iconCriticalPrimary
|
|
||||||
else -> ElementTheme.colors.iconTertiary
|
|
||||||
} else {
|
|
||||||
// We cannot apply a disabled color by default: https://issuetracker.google.com/issues/280480132
|
|
||||||
ElementTheme.colors.iconDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
val decoratedHeadlineContent: @Composable () -> Unit = {
|
val decoratedHeadlineContent: @Composable () -> Unit = {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
|
@ -97,7 +92,6 @@ fun ListItem(
|
||||||
{
|
{
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
|
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
|
||||||
LocalContentColor provides supportingContentColor,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|
@ -106,31 +100,31 @@ fun ListItem(
|
||||||
val decoratedLeadingContent: (@Composable () -> Unit)? = leadingContent?.let { content ->
|
val decoratedLeadingContent: (@Composable () -> Unit)? = leadingContent?.let { content ->
|
||||||
{
|
{
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides leadingTrailingContentColor,
|
LocalContentColor provides leadingContentColor,
|
||||||
) {
|
) {
|
||||||
content()
|
content.View()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val decoratedTrailingContent: (@Composable () -> Unit)? = trailingContent?.let { content ->
|
val decoratedTrailingContent: (@Composable () -> Unit)? = trailingContent?.let { content ->
|
||||||
{
|
{
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides leadingTrailingContentColor,
|
|
||||||
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
|
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
LocalContentColor provides trailingContentColor,
|
||||||
) {
|
) {
|
||||||
content()
|
content.View()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.material3.ListItem(
|
androidx.compose.material3.ListItem(
|
||||||
headlineContent = decoratedHeadlineContent,
|
headlineContent = decoratedHeadlineContent,
|
||||||
modifier = modifier.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {}),
|
modifier = Modifier.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {}).then(modifier),
|
||||||
overlineContent = null,
|
overlineContent = null,
|
||||||
supportingContent = decoratedSupportingContent,
|
supportingContent = decoratedSupportingContent,
|
||||||
leadingContent = decoratedLeadingContent,
|
leadingContent = decoratedLeadingContent,
|
||||||
trailingContent = decoratedTrailingContent,
|
trailingContent = decoratedTrailingContent,
|
||||||
colors = ListItemDefaults.colors(), // These aren't really used since we need the workaround for the disabled state color
|
colors = colors,
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 0.dp,
|
||||||
shadowElevation = 0.dp,
|
shadowElevation = 0.dp,
|
||||||
)
|
)
|
||||||
|
|
@ -143,6 +137,47 @@ sealed interface ListItemStyle {
|
||||||
object Default : ListItemStyle
|
object Default : ListItemStyle
|
||||||
object Primary: ListItemStyle
|
object Primary: ListItemStyle
|
||||||
object Destructive : ListItemStyle
|
object Destructive : ListItemStyle
|
||||||
|
|
||||||
|
@Composable fun headlineColor() = when (this) {
|
||||||
|
Default, Primary -> ListItemDefaultColors.headline
|
||||||
|
Destructive -> ElementTheme.colors.textCriticalPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable fun supportingTextColor() = when (this) {
|
||||||
|
Default, Primary -> ListItemDefaultColors.supportingText
|
||||||
|
// FIXME once we have a defined color for this value
|
||||||
|
Destructive -> ElementTheme.colors.textCriticalPrimary.copy(alpha = 0.8f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable fun leadingIconColor() = when (this) {
|
||||||
|
Default -> ListItemDefaultColors.icon
|
||||||
|
Primary -> ElementTheme.colors.iconPrimary
|
||||||
|
Destructive -> ElementTheme.colors.iconCriticalPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable fun trailingIconColor() = when (this) {
|
||||||
|
Default -> ListItemDefaultColors.icon
|
||||||
|
Primary -> ElementTheme.colors.iconPrimary
|
||||||
|
Destructive -> ElementTheme.colors.iconCriticalPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ListItemDefaultColors {
|
||||||
|
val headline: Color @Composable get() = ElementTheme.colors.textPrimary
|
||||||
|
val headlineDisabled: Color @Composable get() = ElementTheme.colors.textDisabled
|
||||||
|
val supportingText: Color @Composable get() = ElementTheme.materialColors.onSurfaceVariant
|
||||||
|
val icon: Color @Composable get() = ElementTheme.colors.iconTertiary
|
||||||
|
val iconDisabled: Color @Composable get() = ElementTheme.colors.iconDisabled
|
||||||
|
|
||||||
|
val colors: ListItemColors @Composable get() = ListItemDefaults.colors(
|
||||||
|
headlineColor = headline,
|
||||||
|
supportingColor = supportingText,
|
||||||
|
leadingIconColor = icon,
|
||||||
|
trailingIconColor = icon,
|
||||||
|
disabledHeadlineColor = headlineDisabled,
|
||||||
|
disabledLeadingIconColor = iconDisabled,
|
||||||
|
disabledTrailingIconColor = iconDisabled,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// region: Simple list item
|
// region: Simple list item
|
||||||
|
|
@ -335,8 +370,9 @@ private object PreviewItems {
|
||||||
@Composable
|
@Composable
|
||||||
fun ThreeLinesListItemPreview(
|
fun ThreeLinesListItemPreview(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
leadingContent: @Composable (() -> Unit)? = null,
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
leadingContent: ListItemContent? = null,
|
||||||
|
trailingContent: ListItemContent? = null,
|
||||||
) {
|
) {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
@ -344,6 +380,7 @@ private object PreviewItems {
|
||||||
supportingContent = PreviewItems.text(),
|
supportingContent = PreviewItems.text(),
|
||||||
leadingContent = leadingContent,
|
leadingContent = leadingContent,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
|
style = style,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -352,8 +389,9 @@ private object PreviewItems {
|
||||||
@Composable
|
@Composable
|
||||||
fun TwoLinesListItemPreview(
|
fun TwoLinesListItemPreview(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
leadingContent: @Composable (() -> Unit)? = null,
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
leadingContent: ListItemContent? = null,
|
||||||
|
trailingContent: ListItemContent? = null,
|
||||||
) {
|
) {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
@ -361,6 +399,7 @@ private object PreviewItems {
|
||||||
supportingContent = PreviewItems.textSingleLine(),
|
supportingContent = PreviewItems.textSingleLine(),
|
||||||
leadingContent = leadingContent,
|
leadingContent = leadingContent,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
|
style = style,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -369,9 +408,9 @@ private object PreviewItems {
|
||||||
@Composable
|
@Composable
|
||||||
fun OneLineListItemPreview(
|
fun OneLineListItemPreview(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
leadingContent: @Composable (() -> Unit)? = null,
|
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
|
||||||
style: ListItemStyle = ListItemStyle.Default,
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
|
leadingContent: ListItemContent? = null,
|
||||||
|
trailingContent: ListItemContent? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
|
|
@ -402,25 +441,22 @@ private object PreviewItems {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun checkbox() = @Composable {
|
fun checkbox(): ListItemContent {
|
||||||
var checked by remember { mutableStateOf(false) }
|
var checked by remember { mutableStateOf(false) }
|
||||||
Checkbox(checked = checked, onCheckedChange = { checked = !checked })
|
return ListItemContent.Checkbox(checked = checked, onChange = { checked = !checked })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun radioButton() = @Composable {
|
fun radioButton(): ListItemContent {
|
||||||
var checked by remember { mutableStateOf(false) }
|
var checked by remember { mutableStateOf(false) }
|
||||||
RadioButton(selected = checked, onClick = { checked = !checked })
|
return ListItemContent.RadioButton(selected = checked, onClick = { checked = !checked })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun switch() = @Composable {
|
fun switch() : ListItemContent {
|
||||||
var checked by remember { mutableStateOf(false) }
|
var checked by remember { mutableStateOf(false) }
|
||||||
Switch(checked = checked, onCheckedChange = { checked = !checked })
|
return ListItemContent.Switch(checked = checked, onChange = { checked = !checked })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
fun icon() = ListItemContent.Icon(iconSource = IconSource.Vector(Icons.Outlined.Share))
|
||||||
fun icon() = @Composable {
|
|
||||||
Icon(imageVector = Icons.Outlined.Share, contentDescription = null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material.icons.outlined.Share
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -32,6 +33,7 @@ import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
@ -249,7 +251,7 @@ internal fun ListSupportingTextDefaultPaddingPreview() {
|
||||||
internal fun ListSupportingTextSmallPaddingPreview() {
|
internal fun ListSupportingTextSmallPaddingPreview() {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
Column {
|
Column {
|
||||||
ListItem(headlineContent = { Text("A title") }, leadingContent = { Icon(Icons.Default.Share, null) })
|
ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Icon(IconSource.Vector(Icons.Outlined.Share)))
|
||||||
ListSupportingText(
|
ListSupportingText(
|
||||||
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
|
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
|
||||||
contentPadding = ListSupportingTextDefaults.Padding.SmallLeadingContent,
|
contentPadding = ListSupportingTextDefaults.Padding.SmallLeadingContent,
|
||||||
|
|
@ -263,7 +265,7 @@ internal fun ListSupportingTextSmallPaddingPreview() {
|
||||||
internal fun ListSupportingTextLargePaddingPreview() {
|
internal fun ListSupportingTextLargePaddingPreview() {
|
||||||
ElementThemedPreview {
|
ElementThemedPreview {
|
||||||
Column {
|
Column {
|
||||||
ListItem(headlineContent = { Text("A title") }, leadingContent = { Switch(checked = true, onCheckedChange = null) })
|
ListItem(headlineContent = { Text("A title") }, leadingContent = ListItemContent.Switch(checked = true, onChange = {}))
|
||||||
ListSupportingText(
|
ListSupportingText(
|
||||||
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
|
text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more",
|
||||||
contentPadding = ListSupportingTextDefaults.Padding.LargeLeadingContent,
|
contentPadding = ListSupportingTextDefaults.Padding.LargeLeadingContent,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ private fun ContentToPreview() {
|
||||||
buttons = { /*TODO*/ },
|
buttons = { /*TODO*/ },
|
||||||
icon = { /*TODO*/ },
|
icon = { /*TODO*/ },
|
||||||
title = { /*TODO*/ },
|
title = { /*TODO*/ },
|
||||||
text = { DatePicker(state = state, showModeToggle = true) },
|
subtitle = null,
|
||||||
|
content = { DatePicker(state = state, showModeToggle = true) },
|
||||||
shape = AlertDialogDefaults.shape,
|
shape = AlertDialogDefaults.shape,
|
||||||
containerColor = AlertDialogDefaults.containerColor,
|
containerColor = AlertDialogDefaults.containerColor,
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ internal fun TimePickerHorizontalPreview() {
|
||||||
buttons = { /*TODO*/ },
|
buttons = { /*TODO*/ },
|
||||||
icon = { /*TODO*/ },
|
icon = { /*TODO*/ },
|
||||||
title = { /*TODO*/ },
|
title = { /*TODO*/ },
|
||||||
text = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Horizontal) },
|
subtitle = null,
|
||||||
|
content = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Horizontal) },
|
||||||
shape = AlertDialogDefaults.shape,
|
shape = AlertDialogDefaults.shape,
|
||||||
containerColor = AlertDialogDefaults.containerColor,
|
containerColor = AlertDialogDefaults.containerColor,
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||||
|
|
@ -60,7 +61,8 @@ internal fun TimePickerVerticalPreviewLight() {
|
||||||
buttons = { /*TODO*/ },
|
buttons = { /*TODO*/ },
|
||||||
icon = { /*TODO*/ },
|
icon = { /*TODO*/ },
|
||||||
title = { /*TODO*/ },
|
title = { /*TODO*/ },
|
||||||
text = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Vertical) },
|
subtitle = null,
|
||||||
|
content = { TimePicker(state = rememberTimePickerState(), layoutType = TimePickerLayoutType.Vertical) },
|
||||||
shape = AlertDialogDefaults.shape,
|
shape = AlertDialogDefaults.shape,
|
||||||
containerColor = AlertDialogDefaults.containerColor,
|
containerColor = AlertDialogDefaults.containerColor,
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||||
|
|
@ -85,7 +87,8 @@ internal fun TimePickerVerticalPreviewDark() {
|
||||||
buttons = { /*TODO*/ },
|
buttons = { /*TODO*/ },
|
||||||
icon = { /*TODO*/ },
|
icon = { /*TODO*/ },
|
||||||
title = { /*TODO*/ },
|
title = { /*TODO*/ },
|
||||||
text = { TimePicker(state = pickerState, layoutType = TimePickerLayoutType.Vertical) },
|
subtitle = null,
|
||||||
|
content = { TimePicker(state = pickerState, layoutType = TimePickerLayoutType.Vertical) },
|
||||||
shape = AlertDialogDefaults.shape,
|
shape = AlertDialogDefaults.shape,
|
||||||
containerColor = AlertDialogDefaults.containerColor,
|
containerColor = AlertDialogDefaults.containerColor,
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:461788974e3bee520d26dbec39b2721c8c2cc0b8b907a7850e8761784abc2792
|
oid sha256:1e13461ce6fe7e873fa2796520cf107e6e041559b43c8d911ba78848de34b398
|
||||||
size 23763
|
size 24585
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:449a17861383c164774ada4098e8bfd5cb364de211d25b83baeb930e0f40020d
|
oid sha256:eebe29b4f5b0aa38316bb54e439f9ab541185251deffbae07ba14342b453c5cb
|
||||||
size 17446
|
size 17528
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:76aadb1f17f02efa6b0dbbafec5e5a08a1bfdb08e540b494eeaa48825c7c130a
|
||||||
|
size 30155
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fd3224a9292b9268874f355b681f28ae8a530341fdded66c85e36db9a14818a7
|
oid sha256:a19f93d6d31a074b2e5c1d691505881e8424361f1d244366bc6ee5fbd5de15f6
|
||||||
size 23194
|
size 23225
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:18f79874b2342ffe0e6cd759f381e452767c6c824069cc04441c49914b2860d8
|
||||||
|
size 20757
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f540b6b0f761949348fbf65596103d482b0f3c29eb8c99750756dce1c40e674e
|
||||||
|
size 28692
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:aa6c275983acd5902a73fecc03a3050f1dbf70c272557bf8240c7bc5ec6c1a45
|
||||||
|
size 19809
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:eb1e859e011c8359935b0254472be653d878146a3735196f9edee324a204b1c1
|
||||||
|
size 14192
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:504265795b72916040c091b767ffd278fe273a83d4f4a1683c06d805b3e3a16d
|
||||||
|
size 17818
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3a0eb132bfaf1c90c6507c12b25150fb29e260ae3bd0a5760f5fb2e9483fda26
|
||||||
|
size 14814
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:624a00c3098b821482dcdb9529db12cfcf46dc82e136d8b255561125e932862b
|
||||||
|
size 19467
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:eb1e859e011c8359935b0254472be653d878146a3735196f9edee324a204b1c1
|
||||||
|
size 14192
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d86d982afc4add4d3d12574814ec686ad90f20f5098b965560a7a5e413b7f1dd
|
||||||
|
size 8441
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c442cb40b7c6a26d6014005587cd618d213ccb003e8d107faf8d688f069395f3
|
||||||
|
size 11844
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a2ffe9b836ee6dc753f7e6faf10814d7a52611cd1ff9d11a539cc045fdafc1db
|
||||||
|
size 17227
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6695c2d908300730424b28de599a966a3a7ea44f056ac6de745c713915a6a0a5
|
oid sha256:ee64fde90f43c95741d8f39e9bf4c770cf542dfaadaf611e98151e9419f1bf9d
|
||||||
size 21745
|
size 22069
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:033fc99c9948b786db535249490b123f9830b4c0c6b8d29d40188e917b8e1209
|
oid sha256:a38a4a04c3527e9d1bb7efc3c4b91f6cfbd4eab93c84d316e68a4fc558e137cd
|
||||||
size 51022
|
size 51533
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:61c633685a0afc23e5f896bb5576658a7444b0a0fa7a1aefc4be9fae86fcd3e7
|
oid sha256:0182810e251fe8a355d275bbe97a1f7c8f82577b7f70a769a852811f8e9ee715
|
||||||
size 57358
|
size 57958
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:92502759287057d36a3fe5532d48e911e24df080c8c1e8eab01c4e406bd95fc8
|
oid sha256:9980d0479b405e3b3215628ae92227c02423b7839c2754bb4b230da549e82cbe
|
||||||
size 55792
|
size 56305
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5ba37602f8c2519c1854776ea349fd53fb9fb6fd4b1d94a55d297b297854656b
|
oid sha256:3b37e1793e658812995411e954e060e983e48e7d450af28e1577b2b8f6f8e30f
|
||||||
size 27584
|
size 27527
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1d2a776e9724abd16b1692f0fdac43e39dedcd8c003128d1ea354f1f8df245ac
|
oid sha256:acb1adfada4482915a2b42b86c9b73c5815fe992647e67b065f569b45ae69779
|
||||||
size 26147
|
size 26304
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue