Skip to content

Commit 6331cad

Browse files
author
Thomas Horta
authored
Merge pull request #19670 from wordpress-mobile/feature/dashboard-compose-card-toolbar
[My Site Dashboard] Compose cards reusable toolbar
2 parents eef6a89 + b7676b4 commit 6331cad

3 files changed

Lines changed: 283 additions & 122 deletions

File tree

WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/BlazeCampaignsCard.kt

Lines changed: 33 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package org.wordpress.android.ui.mysite.cards.blaze
22

3-
import androidx.compose.foundation.background
43
import androidx.compose.foundation.clickable
54
import androidx.compose.foundation.layout.Arrangement
6-
import androidx.compose.foundation.layout.Box
75
import androidx.compose.foundation.layout.Column
86
import androidx.compose.foundation.layout.Row
97
import androidx.compose.foundation.layout.Spacer
@@ -12,21 +10,9 @@ import androidx.compose.foundation.layout.padding
1210
import androidx.compose.foundation.layout.size
1311
import androidx.compose.foundation.layout.width
1412
import androidx.compose.foundation.shape.RoundedCornerShape
15-
import androidx.compose.material.ContentAlpha
1613
import androidx.compose.material.Divider
17-
import androidx.compose.material.MaterialTheme
1814
import androidx.compose.material.Text
19-
import androidx.compose.material.icons.Icons
20-
import androidx.compose.material.icons.rounded.MoreVert
21-
import androidx.compose.material3.DropdownMenu
22-
import androidx.compose.material3.DropdownMenuItem
23-
import androidx.compose.material3.Icon
24-
import androidx.compose.material3.IconButton
2515
import androidx.compose.runtime.Composable
26-
import androidx.compose.runtime.getValue
27-
import androidx.compose.runtime.mutableStateOf
28-
import androidx.compose.runtime.remember
29-
import androidx.compose.runtime.setValue
3016
import androidx.compose.ui.Alignment
3117
import androidx.compose.ui.Modifier
3218
import androidx.compose.ui.draw.clip
@@ -44,6 +30,8 @@ import org.wordpress.android.ui.compose.styles.DashboardCardTypography
4430
import org.wordpress.android.ui.compose.utils.uiStringText
4531
import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.BlazeCampaignsCardModel
4632
import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.BlazeCampaignsCardModel.BlazeCampaignsCardItem.BlazeCampaignStats
33+
import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbar
34+
import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbarContextMenuItem
4735
import org.wordpress.android.ui.utils.UiString
4836

4937
@Composable
@@ -55,21 +43,8 @@ fun BlazeCampaignsCard(
5543
UnelevatedCard(
5644
modifier = modifier.clickable { blazeCampaignCardModel.onClick.click() },
5745
content = {
58-
Column(
59-
modifier = Modifier.padding(top = 16.dp)
60-
) {
61-
Row(
62-
verticalAlignment = Alignment.CenterVertically
63-
) {
64-
Text(
65-
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
66-
text = uiStringText(uiString = blazeCampaignCardModel.title),
67-
style = DashboardCardTypography.smallTitle,
68-
textAlign = TextAlign.Start,
69-
)
70-
Spacer(modifier = Modifier.weight(1f))
71-
CardDropDownMenu(moreMenuOptions = blazeCampaignCardModel.moreMenuOptions)
72-
}
46+
Column {
47+
CardToolbar(blazeCampaignCardModel)
7348
Column(
7449
modifier = Modifier
7550
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
@@ -117,6 +92,35 @@ fun BlazeCampaignsCard(
11792
)
11893
}
11994

95+
@Composable
96+
private fun CardToolbar(
97+
blazeCampaignCardModel: BlazeCampaignsCardModel,
98+
) {
99+
MySiteCardToolbar(
100+
onContextMenuClick = { blazeCampaignCardModel.moreMenuOptions.onMoreClick.click() },
101+
contextMenuItems = listOf(
102+
MySiteCardToolbarContextMenuItem.Option(
103+
text = stringResource(id = R.string.blaze_campaigns_card_more_menu_view_all_campaigns),
104+
onClick = { blazeCampaignCardModel.moreMenuOptions.viewAllCampaignsItemClick.click() }
105+
),
106+
MySiteCardToolbarContextMenuItem.Option(
107+
text = stringResource(id = R.string.blaze_campaigns_card_more_menu_learn_more),
108+
onClick = { blazeCampaignCardModel.moreMenuOptions.learnMoreClick.click() }
109+
),
110+
MySiteCardToolbarContextMenuItem.Option(
111+
text = stringResource(id = R.string.blaze_campaigns_card_more_menu_hide_this),
112+
onClick = { blazeCampaignCardModel.moreMenuOptions.hideThisMenuItemClick.click() }
113+
),
114+
),
115+
) {
116+
Text(
117+
text = uiStringText(uiString = blazeCampaignCardModel.title),
118+
style = DashboardCardTypography.smallTitle,
119+
textAlign = TextAlign.Start,
120+
)
121+
}
122+
}
123+
120124
@Composable
121125
fun CampaignTitleThumbnail(campaignTitle: UiString, featuredImageUrl: String?, modifier: Modifier = Modifier) {
122126
Row(
@@ -192,51 +196,3 @@ private fun CampaignStat(title: String, value: UiString, modifier: Modifier = Mo
192196
)
193197
}
194198
}
195-
196-
197-
@Composable
198-
private fun CardDropDownMenu(moreMenuOptions: BlazeCampaignsCardModel.MoreMenuOptions, modifier: Modifier = Modifier) {
199-
Box(modifier = modifier) {
200-
var isExpanded by remember { mutableStateOf(false) }
201-
202-
IconButton(onClick = {
203-
isExpanded = true
204-
moreMenuOptions.onMoreClick.click()
205-
}) {
206-
Icon(
207-
imageVector = Icons.Rounded.MoreVert,
208-
contentDescription = stringResource(id = R.string.more),
209-
tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)
210-
)
211-
}
212-
213-
DropdownMenu(
214-
expanded = isExpanded,
215-
onDismissRequest = { isExpanded = false },
216-
modifier = Modifier
217-
.background(MaterialTheme.colors.surface.copy(alpha = ContentAlpha.high))
218-
) {
219-
DropdownMenuItem(
220-
text = { Text(stringResource(id = R.string.blaze_campaigns_card_more_menu_view_all_campaigns)) },
221-
onClick = {
222-
isExpanded = false
223-
moreMenuOptions.viewAllCampaignsItemClick.click()
224-
}
225-
)
226-
DropdownMenuItem(
227-
text = { Text(stringResource(id = R.string.blaze_campaigns_card_more_menu_learn_more)) },
228-
onClick = {
229-
isExpanded = false
230-
moreMenuOptions.learnMoreClick.click()
231-
}
232-
)
233-
DropdownMenuItem(
234-
text = { Text(stringResource(id = R.string.blaze_campaigns_card_more_menu_hide_this)) },
235-
onClick = {
236-
isExpanded = false
237-
moreMenuOptions.hideThisMenuItemClick.click()
238-
}
239-
)
240-
}
241-
}
242-
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package org.wordpress.android.ui.mysite.cards.compose
2+
3+
import android.content.res.Configuration.UI_MODE_NIGHT_YES
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.PaddingValues
9+
import androidx.compose.foundation.layout.Row
10+
import androidx.compose.foundation.layout.RowScope
11+
import androidx.compose.foundation.layout.Spacer
12+
import androidx.compose.foundation.layout.fillMaxWidth
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.size
15+
import androidx.compose.foundation.layout.width
16+
import androidx.compose.material.ContentAlpha
17+
import androidx.compose.material.Divider
18+
import androidx.compose.material.MaterialTheme
19+
import androidx.compose.material.Text
20+
import androidx.compose.material.icons.Icons
21+
import androidx.compose.material.icons.rounded.MoreVert
22+
import androidx.compose.material3.DropdownMenu
23+
import androidx.compose.material3.DropdownMenuItem
24+
import androidx.compose.material3.Icon
25+
import androidx.compose.material3.IconButton
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.getValue
28+
import androidx.compose.runtime.mutableStateOf
29+
import androidx.compose.runtime.remember
30+
import androidx.compose.runtime.setValue
31+
import androidx.compose.ui.Alignment
32+
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.res.stringResource
34+
import androidx.compose.ui.tooling.preview.Preview
35+
import androidx.compose.ui.unit.dp
36+
import org.wordpress.android.R
37+
import org.wordpress.android.ui.compose.components.card.UnelevatedCard
38+
import org.wordpress.android.ui.compose.styles.DashboardCardTypography
39+
import org.wordpress.android.ui.compose.theme.AppTheme
40+
41+
/**
42+
* A toolbar for MySite cards written in Compose, that tries to match behavior and positioning of cards written in XML.
43+
*
44+
* When using this component, there is no need to set top, start, and end padding on the card, as this component sets
45+
* those values internally to closely match my_site_card_toolbar.xml.
46+
*
47+
* It's important to note this component is stateful, meaning the UI for the context menu is managed internally. Use
48+
* [onContextMenuClick] and the [contextMenuItems] callbacks to handle behavior NOT related to the dropdown menu UI.
49+
*
50+
* @param modifier Modifier to be applied to the toolbar (should not be used in most cases).
51+
* @param onContextMenuClick Callback to be invoked when the context menu is clicked.
52+
* @param contextMenuItems List of [MySiteCardToolbarContextMenuItem] to be displayed in the context menu.
53+
* @param showContextMenu Whether or not to show the context menu. Default: true if [contextMenuItems] is not empty.
54+
* @param content Content to be displayed in the toolbar. Usually some sort of title that will be left aligned and
55+
* vertically aligned with the context menu. If null, the context menu will be right aligned.
56+
*/
57+
@Composable
58+
fun MySiteCardToolbar(
59+
modifier: Modifier = Modifier,
60+
onContextMenuClick: (() -> Unit)? = null,
61+
contextMenuItems: List<MySiteCardToolbarContextMenuItem> = emptyList(),
62+
showContextMenu: Boolean = contextMenuItems.isNotEmpty(),
63+
content: @Composable (RowScope.() -> Unit)? = null,
64+
) {
65+
val horizontalArrangement = Arrangement.SpaceBetween.takeIf { content != null } ?: Arrangement.End
66+
val padding = if (showContextMenu) {
67+
PaddingValues(start = 16.dp, end = 12.dp, top = 8.dp)
68+
} else {
69+
PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp)
70+
}
71+
72+
Row(
73+
verticalAlignment = Alignment.CenterVertically,
74+
horizontalArrangement = horizontalArrangement,
75+
modifier = modifier
76+
.padding(padding)
77+
.fillMaxWidth()
78+
) {
79+
content?.invoke(this)
80+
81+
if (content != null && showContextMenu) {
82+
// minimum spacing between content and context menu if both are shown
83+
Spacer(modifier = Modifier.width(16.dp))
84+
}
85+
86+
if (showContextMenu) {
87+
CardDropDownMenu(
88+
onContextMenuClick = onContextMenuClick,
89+
contextMenuItems = contextMenuItems,
90+
)
91+
}
92+
}
93+
}
94+
95+
@Composable
96+
private fun CardDropDownMenu(
97+
modifier: Modifier = Modifier,
98+
onContextMenuClick: (() -> Unit)? = null,
99+
contextMenuItems: List<MySiteCardToolbarContextMenuItem> = emptyList(),
100+
) {
101+
Box(modifier = modifier) {
102+
var isExpanded by remember { mutableStateOf(false) }
103+
104+
IconButton(
105+
modifier = Modifier.size(32.dp), // to match the icon in my_site_card_toolbar.xml
106+
onClick = {
107+
isExpanded = true
108+
onContextMenuClick?.invoke()
109+
}
110+
) {
111+
Icon(
112+
imageVector = Icons.Rounded.MoreVert,
113+
contentDescription = stringResource(id = R.string.more),
114+
tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
115+
)
116+
}
117+
118+
DropdownMenu(
119+
expanded = isExpanded,
120+
onDismissRequest = { isExpanded = false },
121+
modifier = Modifier.background(MaterialTheme.colors.surface.copy(alpha = ContentAlpha.high))
122+
) {
123+
contextMenuItems.map { item ->
124+
when (item) {
125+
is MySiteCardToolbarContextMenuItem.Option -> {
126+
DropdownMenuItem(
127+
text = { Text(item.text) },
128+
onClick = {
129+
isExpanded = false
130+
item.onClick()
131+
}
132+
)
133+
}
134+
135+
MySiteCardToolbarContextMenuItem.Divider -> Divider()
136+
}
137+
}
138+
}
139+
}
140+
}
141+
142+
sealed interface MySiteCardToolbarContextMenuItem {
143+
data class Option(
144+
val text: String,
145+
val onClick: () -> Unit,
146+
) : MySiteCardToolbarContextMenuItem
147+
148+
data object Divider : MySiteCardToolbarContextMenuItem
149+
}
150+
151+
@Preview(
152+
name = "Light Mode"
153+
)
154+
@Preview(
155+
name = "Dark Mode",
156+
showBackground = true,
157+
uiMode = UI_MODE_NIGHT_YES
158+
)
159+
@Composable
160+
private fun MySiteCardToolbarPreview() {
161+
AppTheme {
162+
MySiteCardToolbar(
163+
onContextMenuClick = {},
164+
contextMenuItems = listOf(
165+
MySiteCardToolbarContextMenuItem.Option(
166+
text = "An option",
167+
onClick = {}
168+
),
169+
MySiteCardToolbarContextMenuItem.Divider,
170+
MySiteCardToolbarContextMenuItem.Option(
171+
text = "Another option",
172+
onClick = {}
173+
),
174+
),
175+
) {
176+
Text(
177+
text = "Card Title",
178+
style = DashboardCardTypography.smallTitle,
179+
)
180+
}
181+
}
182+
}
183+
184+
@Preview(
185+
name = "Light Mode"
186+
)
187+
@Preview(
188+
name = "Dark Mode",
189+
showBackground = true,
190+
uiMode = UI_MODE_NIGHT_YES
191+
)
192+
@Composable
193+
private fun MySiteCardToolbarInCardPreview() {
194+
AppTheme {
195+
UnelevatedCard(
196+
modifier = Modifier.padding(8.dp)
197+
) {
198+
Column {
199+
MySiteCardToolbar(
200+
onContextMenuClick = {},
201+
contextMenuItems = listOf(
202+
MySiteCardToolbarContextMenuItem.Option(
203+
text = "An option",
204+
onClick = {}
205+
),
206+
MySiteCardToolbarContextMenuItem.Divider,
207+
MySiteCardToolbarContextMenuItem.Option(
208+
text = "Another option",
209+
onClick = {}
210+
),
211+
),
212+
) {
213+
Text(
214+
text = "Card Title",
215+
style = DashboardCardTypography.smallTitle,
216+
)
217+
}
218+
219+
Box(Modifier.padding(16.dp)) {
220+
Text(
221+
text = "This is my card content!"
222+
)
223+
}
224+
}
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)