Skip to content

Commit b326ca2

Browse files
jmartinespElementBot
and
ElementBot
authored
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 <[email protected]>
1 parent cbeab31 commit b326ca2

File tree

36 files changed

+1155
-85
lines changed

36 files changed

+1155
-85
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.designsystem.components.dialogs
18+
19+
import kotlinx.collections.immutable.ImmutableList
20+
import kotlinx.collections.immutable.toImmutableList
21+
22+
/**
23+
* Used to store the visual data for a list option.
24+
*/
25+
data class ListOption(
26+
val title: String,
27+
val subtitle: String? = null,
28+
)
29+
30+
/** Creates an immutable list of [ListOption]s from the given [values], using them as titles. */
31+
fun listOptionOf(vararg values: String): ImmutableList<ListOption> {
32+
return values.map { ListOption(it) }.toImmutableList()
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.designsystem.components.dialogs
18+
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.foundation.lazy.LazyColumn
21+
import androidx.compose.foundation.lazy.itemsIndexed
22+
import androidx.compose.material3.AlertDialog
23+
import androidx.compose.material3.ExperimentalMaterial3Api
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.remember
26+
import androidx.compose.runtime.toMutableStateList
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.unit.dp
30+
import com.airbnb.android.showkase.annotation.ShowkaseComposable
31+
import io.element.android.libraries.designsystem.components.list.CheckboxListItem
32+
import io.element.android.libraries.designsystem.preview.DayNightPreviews
33+
import io.element.android.libraries.designsystem.preview.ElementPreview
34+
import io.element.android.libraries.designsystem.preview.PreviewGroup
35+
import io.element.android.libraries.designsystem.theme.components.DialogPreview
36+
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
37+
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
38+
import io.element.android.libraries.ui.strings.CommonStrings
39+
import kotlinx.collections.immutable.ImmutableList
40+
import kotlinx.collections.immutable.persistentListOf
41+
42+
@OptIn(ExperimentalMaterial3Api::class)
43+
@Composable
44+
fun MultipleSelectionDialog(
45+
options: ImmutableList<ListOption>,
46+
onConfirmClicked: (List<Int>) -> Unit,
47+
onDismissRequest: () -> Unit,
48+
modifier: Modifier = Modifier,
49+
confirmButtonTitle: String = stringResource(CommonStrings.action_confirm),
50+
dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
51+
title: String? = null,
52+
subtitle: String? = null,
53+
initialSelection: ImmutableList<Int> = persistentListOf(),
54+
) {
55+
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
56+
@Composable {
57+
ListSupportingText(
58+
text = it,
59+
modifier = Modifier.padding(start = 8.dp)
60+
)
61+
}
62+
}
63+
AlertDialog(
64+
modifier = modifier,
65+
onDismissRequest = onDismissRequest,
66+
) {
67+
MultipleSelectionDialogContent(
68+
title = title,
69+
subtitle = decoratedSubtitle,
70+
options = options,
71+
confirmButtonTitle = confirmButtonTitle,
72+
onConfirmClicked = onConfirmClicked,
73+
dismissButtonTitle = dismissButtonTitle,
74+
onDismissRequest = onDismissRequest,
75+
initialSelected = initialSelection,
76+
)
77+
}
78+
}
79+
80+
@Composable
81+
internal fun MultipleSelectionDialogContent(
82+
options: ImmutableList<ListOption>,
83+
confirmButtonTitle: String,
84+
onConfirmClicked: (List<Int>) -> Unit,
85+
dismissButtonTitle: String,
86+
onDismissRequest: () -> Unit,
87+
modifier: Modifier = Modifier,
88+
title: String? = null,
89+
initialSelected: ImmutableList<Int> = persistentListOf(),
90+
subtitle: @Composable (() -> Unit)? = null,
91+
) {
92+
val selectedOptionIndexes = remember { initialSelected.toMutableStateList() }
93+
94+
fun isSelected(index: Int) = selectedOptionIndexes.any { it == index }
95+
96+
SimpleAlertDialogContent(
97+
title = title,
98+
subtitle = subtitle,
99+
modifier = modifier,
100+
submitText = confirmButtonTitle,
101+
onSubmitClicked = {
102+
onConfirmClicked(selectedOptionIndexes.toList())
103+
},
104+
cancelText = dismissButtonTitle,
105+
onCancelClicked = onDismissRequest,
106+
applyPaddingToContents = false,
107+
) {
108+
LazyColumn {
109+
itemsIndexed(options) { index, option ->
110+
CheckboxListItem(
111+
headline = option.title,
112+
checked = isSelected(index),
113+
onChange = {
114+
if (isSelected(index)) {
115+
selectedOptionIndexes.remove(index)
116+
} else {
117+
selectedOptionIndexes.add(index)
118+
}
119+
},
120+
supportingText = option.subtitle,
121+
compactLayout = true,
122+
modifier = Modifier.padding(start = 8.dp)
123+
)
124+
}
125+
}
126+
}
127+
}
128+
129+
@DayNightPreviews
130+
@ShowkaseComposable(group = PreviewGroup.Dialogs)
131+
@Composable
132+
internal fun MultipleSelectionDialogContentPreview() {
133+
ElementPreview(showBackground = false) {
134+
DialogPreview {
135+
val options = persistentListOf(
136+
ListOption("Option 1", "Supporting line text lorem ipsum dolor sit amet, consectetur."),
137+
ListOption("Option 2"),
138+
ListOption("Option 3"),
139+
)
140+
MultipleSelectionDialogContent(
141+
title = "Dialog title",
142+
options = options,
143+
onConfirmClicked = {},
144+
onDismissRequest = {},
145+
confirmButtonTitle = "Save",
146+
dismissButtonTitle = "Cancel",
147+
initialSelected = persistentListOf(0),
148+
)
149+
}
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.designsystem.components.dialogs
18+
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.foundation.lazy.LazyColumn
21+
import androidx.compose.foundation.lazy.itemsIndexed
22+
import androidx.compose.material3.AlertDialog
23+
import androidx.compose.material3.ExperimentalMaterial3Api
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.unit.dp
28+
import com.airbnb.android.showkase.annotation.ShowkaseComposable
29+
import io.element.android.libraries.designsystem.components.list.RadioButtonListItem
30+
import io.element.android.libraries.designsystem.preview.DayNightPreviews
31+
import io.element.android.libraries.designsystem.preview.ElementPreview
32+
import io.element.android.libraries.designsystem.preview.PreviewGroup
33+
import io.element.android.libraries.designsystem.theme.components.DialogPreview
34+
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
35+
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
36+
import io.element.android.libraries.ui.strings.CommonStrings
37+
import kotlinx.collections.immutable.ImmutableList
38+
import kotlinx.collections.immutable.persistentListOf
39+
40+
@OptIn(ExperimentalMaterial3Api::class)
41+
@Composable
42+
fun SingleSelectionDialog(
43+
options: ImmutableList<ListOption>,
44+
onOptionSelected: (Int) -> Unit,
45+
onDismissRequest: () -> Unit,
46+
modifier: Modifier = Modifier,
47+
title: String? = null,
48+
subtitle: String? = null,
49+
dismissButtonTitle: String = stringResource(CommonStrings.action_cancel),
50+
initialSelection: Int? = null,
51+
) {
52+
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
53+
@Composable {
54+
ListSupportingText(
55+
text = it,
56+
modifier = Modifier.padding(start = 8.dp)
57+
)
58+
}
59+
}
60+
AlertDialog(
61+
modifier = modifier,
62+
onDismissRequest = onDismissRequest,
63+
) {
64+
SingleSelectionDialogContent(
65+
title = title,
66+
subtitle = decoratedSubtitle,
67+
options = options,
68+
onOptionSelected = onOptionSelected,
69+
dismissButtonTitle = dismissButtonTitle,
70+
onDismissRequest = onDismissRequest,
71+
initialSelection = initialSelection,
72+
)
73+
}
74+
}
75+
76+
@Composable
77+
internal fun SingleSelectionDialogContent(
78+
options: ImmutableList<ListOption>,
79+
onOptionSelected: (Int) -> Unit,
80+
onDismissRequest: () -> Unit,
81+
dismissButtonTitle: String,
82+
modifier: Modifier = Modifier,
83+
title: String? = null,
84+
initialSelection: Int? = null,
85+
subtitle: @Composable (() -> Unit)? = null,
86+
) {
87+
SimpleAlertDialogContent(
88+
title = title,
89+
subtitle = subtitle,
90+
modifier = modifier,
91+
cancelText = dismissButtonTitle,
92+
onCancelClicked = onDismissRequest,
93+
applyPaddingToContents = false,
94+
) {
95+
LazyColumn {
96+
itemsIndexed(options) { index, option ->
97+
RadioButtonListItem(
98+
headline = option.title,
99+
supportingText = option.subtitle,
100+
selected = index == initialSelection,
101+
onSelected = { onOptionSelected(index) },
102+
compactLayout = true,
103+
modifier = Modifier.padding(start = 8.dp)
104+
)
105+
}
106+
}
107+
}
108+
}
109+
110+
@DayNightPreviews
111+
@ShowkaseComposable(group = PreviewGroup.Dialogs)
112+
@Composable
113+
internal fun SingleSelectionDialogContentPreview() {
114+
ElementPreview(showBackground = false) {
115+
DialogPreview {
116+
val options = persistentListOf(
117+
ListOption("Option 1"),
118+
ListOption("Option 2"),
119+
ListOption("Option 3"),
120+
)
121+
SingleSelectionDialogContent(
122+
title = "Dialog title",
123+
options = options,
124+
onOptionSelected = {},
125+
onDismissRequest = {},
126+
dismissButtonTitle = "Cancel",
127+
initialSelection = 0
128+
)
129+
}
130+
}
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.designsystem.components.list
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Modifier
21+
import io.element.android.libraries.designsystem.theme.components.ListItem
22+
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
23+
import io.element.android.libraries.designsystem.theme.components.Text
24+
25+
@Composable
26+
fun CheckboxListItem(
27+
headline: String,
28+
checked: Boolean,
29+
onChange: (Boolean) -> Unit,
30+
modifier: Modifier = Modifier,
31+
supportingText: String? = null,
32+
trailingContent: ListItemContent? = null,
33+
enabled: Boolean = true,
34+
style: ListItemStyle = ListItemStyle.Default,
35+
compactLayout: Boolean = false,
36+
) {
37+
ListItem(
38+
modifier = modifier,
39+
headlineContent = { Text(headline) },
40+
supportingContent = supportingText?.let { @Composable { Text(it) } },
41+
leadingContent = ListItemContent.Checkbox(checked, null, enabled, compact = compactLayout),
42+
trailingContent = trailingContent,
43+
style = style,
44+
enabled = enabled,
45+
onClick = { onChange(!checked) },
46+
)
47+
}

0 commit comments

Comments
 (0)