diff --git a/android/sample/src/main/kotlin/com/gu/source/MainActivity.kt b/android/sample/src/main/kotlin/com/gu/source/MainActivity.kt index d81941bc0..2b7e39c22 100644 --- a/android/sample/src/main/kotlin/com/gu/source/MainActivity.kt +++ b/android/sample/src/main/kotlin/com/gu/source/MainActivity.kt @@ -41,6 +41,7 @@ import com.gu.source.previews.IconsPreview import com.gu.source.previews.ImagePagerWithProgressIndicator import com.gu.source.previews.Palette import com.gu.source.previews.RatingPreview +import com.gu.source.previews.TextButtonPreview import com.gu.source.utils.PreviewPhoneBothMode import com.gu.source.utils.plus import kotlinx.coroutines.launch @@ -61,6 +62,7 @@ private enum class SheetContentType { Palette, PagerProgressBar, Buttons, + TextButtons, CoreIcons, AlertBanner, Chips, @@ -85,6 +87,7 @@ private fun Greeting(modifier: Modifier = Modifier) { SheetContentType.Palette -> Palette(sheetModifier) SheetContentType.PagerProgressBar -> ImagePagerWithProgressIndicator(sheetModifier) SheetContentType.Buttons -> ButtonPreview(sheetModifier) + SheetContentType.TextButtons -> TextButtonPreview(sheetModifier) SheetContentType.CoreIcons -> IconsPreview(sheetModifier) SheetContentType.AlertBanner -> AlertBannerPreview(sheetModifier) SheetContentType.Chips -> ChipsPreview(sheetModifier) @@ -168,6 +171,17 @@ private fun Greeting(modifier: Modifier = Modifier) { }, ) + SourceButton( + text = "Open text buttons preview", + priority = SourceButton.Priority.TertiaryOnWhite, + onClick = { + sheetContentType = SheetContentType.TextButtons + coroutineScope.launch { + scaffoldState.bottomSheetState.expand() + } + }, + ) + SourceButton( text = "Open icons preview", priority = SourceButton.Priority.TertiaryOnWhite, diff --git a/android/sample/src/main/kotlin/com/gu/source/previews/TextButtonPreview.kt b/android/sample/src/main/kotlin/com/gu/source/previews/TextButtonPreview.kt new file mode 100644 index 000000000..650773fe3 --- /dev/null +++ b/android/sample/src/main/kotlin/com/gu/source/previews/TextButtonPreview.kt @@ -0,0 +1,92 @@ +package com.gu.source.previews + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.gu.source.Source +import com.gu.source.components.buttons.SourceTextButton +import com.gu.source.components.buttons.SourceTextButton.Priority.ON_BLUE_BACKGROUND +import com.gu.source.components.buttons.SourceTextButton.Priority.ON_WHITE_BACKGROUND +import com.gu.source.components.buttons.SourceTextButton.Priority.ON_YELLOW_BACKGROUND +import com.gu.source.components.theme.ReaderRevenueTheme +import com.gu.source.foundation.palette.Brand400 +import com.gu.source.foundation.palette.BrandAlt400 +import com.gu.source.foundation.palette.Neutral100 +import com.gu.source.foundation.typography.TextSansBold17 + +@Composable +internal fun TextButtonPreview(modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = "Core Theme text button - no underline - variants", + style = Source.Typography.TextSansBold17, + ) + TextButtonRows() + Text( + text = "Reader Revenue theme text button - no underline - variant", + style = Source.Typography.TextSansBold17, + ) + ReaderRevenueTheme { + TextButtonRows() + } + Text( + text = "Core Theme text button - underline - variants", + style = Source.Typography.TextSansBold17, + ) + TextButtonRows(textButtonHasUnderline = true) + Text( + text = "Reader Revenue theme text button - underline - variant", + style = Source.Typography.TextSansBold17, + ) + ReaderRevenueTheme { + TextButtonRows(textButtonHasUnderline = true) + } + } +} + +@Composable +private fun TextButtonRows(textButtonHasUnderline: Boolean = false) { + Column(horizontalAlignment = CenterHorizontally) { + SourceTextButton.Priority.entries.forEach { priority -> + val backgroundColor = when (priority) { + ON_BLUE_BACKGROUND -> Source.Palette.Brand400 + ON_WHITE_BACKGROUND -> Source.Palette.Neutral100 + ON_YELLOW_BACKGROUND -> Source.Palette.BrandAlt400 + } + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .background(color = backgroundColor) + .fillMaxWidth(), + ) { + SourceTextButton.Size.entries.forEach { size -> + SourceTextButton( + text = "text button - ${size.name.lowercase()}", + priority = priority, + size = size, + hasUnderline = textButtonHasUnderline, + onClick = {}, + ) + } + } + } + } +} + +@Preview +@Composable +private fun TextButtonsPreview() { + TextButtonPreview() +} \ No newline at end of file diff --git a/android/source/src/main/kotlin/com/gu/source/components/buttons/SourceTextButton.kt b/android/source/src/main/kotlin/com/gu/source/components/buttons/SourceTextButton.kt new file mode 100644 index 000000000..64ed8180c --- /dev/null +++ b/android/source/src/main/kotlin/com/gu/source/components/buttons/SourceTextButton.kt @@ -0,0 +1,259 @@ +package com.gu.source.components.buttons + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.gu.source.Source +import com.gu.source.components.theme.LocalSourceTheme +import com.gu.source.components.theme.ReaderRevenueTheme +import com.gu.source.foundation.palette.Brand400 +import com.gu.source.foundation.palette.BrandAlt400 +import com.gu.source.foundation.palette.Neutral0 +import com.gu.source.foundation.palette.Neutral100 +import com.gu.source.foundation.typography.TextSansBold17 +import com.gu.source.utils.PreviewPhoneBothMode +import com.gu.source.utils.PreviewTabletBothMode + +/** + * SourceTextButton is a text button component that can be used in the Source design system. + * It is a simple button that contains text and has no background. + * The color of the text is determined by the priority of the button, + * which should be chosen based on the background color of the button. + */ +object SourceTextButton { + + /** + * The size of the button, which determines + * the typography style of the text and the minimum height of the button. + * + * @property textStyle The typography style of the text inside the button. + * @property minButtonHeight The minimum height of the button. + */ + enum class Size( + val textStyle: TextStyle, + val minButtonHeight: Dp, + ) { + /** + * The small size of the button. + */ + SMALL( + textStyle = Source.Typography.TextSansBold17.copy( + fontSize = 15.sp, + lineHeight = 20.25.sp, + ), + minButtonHeight = 20.dp, + ), + + /** + * The medium size of the button. + */ + MEDIUM( + textStyle = Source.Typography.TextSansBold17.copy( + lineHeight = 22.95.sp, + ), + minButtonHeight = 23.dp, + ), + } + + /** + * The priority of the button, which determines the color of the text. + * This should be chosen based on the background color of the button. + */ + enum class Priority { + /** + * Use this priority when the button is on a blue background, such as the brand color. + */ + ON_BLUE_BACKGROUND, + + /** + * Use this priority when the button is on a white background. + */ + ON_WHITE_BACKGROUND, + + /** + * Use this priority when the button is on a yellow background, such as the brand alt color. + */ + ON_YELLOW_BACKGROUND, + ; + + internal fun textColor(theme: Source.Theme): Color = when (this) { + ON_BLUE_BACKGROUND -> if (theme == Source.Theme.ReaderRevenue) { + Source.Palette.BrandAlt400 + } else { + Source.Palette.Neutral100 + } + + ON_WHITE_BACKGROUND -> Source.Palette.Brand400 + ON_YELLOW_BACKGROUND -> Source.Palette.Neutral0 + } + + internal fun demoBackgroundColor(): Color = when (this) { + ON_BLUE_BACKGROUND -> Source.Palette.Brand400 + ON_WHITE_BACKGROUND -> Source.Palette.Neutral100 + ON_YELLOW_BACKGROUND -> Source.Palette.BrandAlt400 + } + } +} + +/** + * A text button is a button that contains text and has no background. + * It can be used in places where a less prominent action is needed. + * + * @param text The text to display inside the button. + * @param priority The priority of the button, which determines the color of the text. + * This should be chosen based on the background color of the button. + * @param size The size of the button, which determines the typography style of the text. + * @param onClick The callback to be invoked when this button is clicked. + * @param modifier The [Modifier] to be applied to this button. + * @param hasUnderline Whether the text inside the button should have an underline. + */ +@Composable +fun SourceTextButton( + text: String, + priority: SourceTextButton.Priority, + size: SourceTextButton.Size, + onClick: () -> Unit, + modifier: Modifier = Modifier, + hasUnderline: Boolean = false, +) { + val currentTheme = LocalSourceTheme.current + + TextButton( + shape = CircleShape, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + focusedElevation = 0.dp, + hoveredElevation = 0.dp, + disabledElevation = 0.dp, + ), + onClick = onClick, + modifier = modifier.defaultMinSize(minHeight = size.minButtonHeight), + ) { + Text( + text = text, + style = size.textStyle.copy( + color = priority.textColor(theme = currentTheme), + textDecoration = if (hasUnderline) { + TextDecoration.Underline + } else { + null + }, + ), + ) + } +} + +@Composable +@PreviewPhoneBothMode +@PreviewTabletBothMode +internal fun SourceTextButtonNoUnderlinePreview() { + Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SourceTextButton.Priority.entries.forEach { priority -> + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .background(priority.demoBackgroundColor()) + .fillMaxWidth(), + ) { + SourceTextButton.Size.entries.forEach { size -> + SourceTextButton( + text = size.name.lowercase(), + priority = priority, + size = size, + onClick = {}, + ) + } + } + } + } + ReaderRevenueTheme { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SourceTextButton.Priority.entries.forEach { priority -> + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .background(priority.demoBackgroundColor()) + .fillMaxWidth(), + ) { + SourceTextButton.Size.entries.forEach { size -> + SourceTextButton( + text = size.name.lowercase(), + priority = priority, + size = size, + onClick = {}, + ) + } + } + } + } + } + } +} + +@Composable +@PreviewPhoneBothMode +@PreviewTabletBothMode +internal fun SourceTextButtonUnderlinePreview() { + Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SourceTextButton.Priority.entries.forEach { priority -> + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .background(priority.demoBackgroundColor()) + .fillMaxWidth(), + ) { + SourceTextButton.Size.entries.forEach { size -> + SourceTextButton( + text = size.name.lowercase(), + priority = priority, + size = size, + hasUnderline = true, + onClick = {}, + ) + } + } + } + } + ReaderRevenueTheme { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + SourceTextButton.Priority.entries.forEach { priority -> + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .background(priority.demoBackgroundColor()) + .fillMaxWidth(), + ) { + SourceTextButton.Size.entries.forEach { size -> + SourceTextButton( + text = size.name.lowercase(), + priority = priority, + size = size, + hasUnderline = true, + onClick = {}, + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/android/source/src/test/kotlin/com/gu/source/components/buttons/SourceButtonTest.kt b/android/source/src/test/kotlin/com/gu/source/components/buttons/SourceButtonTest.kt index 5a24dab87..586704ba1 100644 --- a/android/source/src/test/kotlin/com/gu/source/components/buttons/SourceButtonTest.kt +++ b/android/source/src/test/kotlin/com/gu/source/components/buttons/SourceButtonTest.kt @@ -54,4 +54,18 @@ class SourceButtonTest(@TestParameter private val nightMode: NightMode) { RrButtonIconAfterPreview() } } + + @Test + fun sourceTextButtonNoUnderline() { + paparazzi.snapshot { + SourceTextButtonNoUnderlinePreview() + } + } + + @Test + fun sourceTextButtonUnderline() { + paparazzi.snapshot { + SourceTextButtonUnderlinePreview() + } + } } \ No newline at end of file diff --git a/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonNoUnderline[NIGHT].png b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonNoUnderline[NIGHT].png new file mode 100644 index 000000000..8e97b5de7 Binary files /dev/null and b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonNoUnderline[NIGHT].png differ diff --git a/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonNoUnderline[NOTNIGHT].png b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonNoUnderline[NOTNIGHT].png new file mode 100644 index 000000000..8e97b5de7 Binary files /dev/null and b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonNoUnderline[NOTNIGHT].png differ diff --git a/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonUnderline[NIGHT].png b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonUnderline[NIGHT].png new file mode 100644 index 000000000..b9492d2a6 Binary files /dev/null and b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonUnderline[NIGHT].png differ diff --git a/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonUnderline[NOTNIGHT].png b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonUnderline[NOTNIGHT].png new file mode 100644 index 000000000..b9492d2a6 Binary files /dev/null and b/android/source/src/test/snapshots/images/com.gu.source.components.buttons_SourceButtonTest_sourceTextButtonUnderline[NOTNIGHT].png differ