Skip to content

Commit

Permalink
UI: Build Button component for TAJ (#545)
Browse files Browse the repository at this point in the history
### TL;DR
Introduced a new reusable Button component and added hex color code utilities to the design system.

### What changed?
- Created a new `Button` component in the Taj design system with support for theming, animations, and disabled states
- Added utility functions to convert hex color codes to Compose Color objects
- Refactored the theme selection screen to use the new Button component
- Improved color transition animations with FastOutSlowInEasing
- Added proper content alpha handling for enabled/disabled states

### How to test?
1. Navigate to the theme selection screen
2. Verify the "Let's #KRAIL" button appears with proper styling
3. Test button interactions and color transitions when selecting different themes
4. Verify button responds appropriately to enabled/disabled states
5. Test with various hex color codes to ensure proper color conversion

### Why make this change?
This change establishes a consistent button implementation across the app, reducing code duplication and ensuring a uniform user experience. The new button component handles common use cases like theming, animations, and accessibility, making it easier to maintain and modify button behavior app-wide.
  • Loading branch information
ksharma-xyz authored Jan 16, 2025
1 parent a0a39bc commit b0930e5
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xyz.ksharma.krail.trip.planner.ui.themeselection

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
Expand Down Expand Up @@ -36,11 +37,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import xyz.ksharma.krail.taj.components.Button
import xyz.ksharma.krail.taj.components.Text
import xyz.ksharma.krail.taj.components.TitleBar
import xyz.ksharma.krail.taj.theme.KrailTheme
Expand Down Expand Up @@ -88,11 +89,6 @@ fun ThemeSelectionScreen(
label = "buttonBackgroundColor",
animationSpec = tween(durationMillis = 300, easing = LinearEasing),
)
val buttonTextColor by animateColorAsState(
targetValue = getForegroundColor(buttonBackgroundColor),
label = "buttonTextColor",
animationSpec = tween(durationMillis = 300, easing = LinearEasing),
)

Column {
TitleBar(
Expand Down Expand Up @@ -152,29 +148,15 @@ fun ThemeSelectionScreen(
.navigationBarsPadding()
.padding(bottom = 10.dp)
) {
Text(
text = "Let's #KRAIL",
textAlign = TextAlign.Center,
color = buttonTextColor,
style = if (selectedProductClass != null) {
KrailTheme.typography.titleMedium
} else {
KrailTheme.typography.bodyLarge
},
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 24.dp)
.clip(RoundedCornerShape(50))
.background(color = buttonBackgroundColor)
.clickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
selectedProductClass?.let { productClass ->
transportModeSelected(productClass)
}
Button(
label = "Let's #KRAIL",
themeColor = buttonBackgroundColor,
onClick = {
selectedProductClass?.let { productClass ->
transportModeSelected(productClass)
}
.padding(vertical = 10.dp),
},
modifier = Modifier.padding(vertical = 10.dp),
)
}
}
Expand Down
47 changes: 47 additions & 0 deletions taj/src/commonMain/kotlin/xyz/ksharma/krail/taj/ColorsExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package xyz.ksharma.krail.taj

import androidx.compose.ui.graphics.Color

fun String.hexToComposeColor(): Color {
require(isValidHexColorCode()) {
"Invalid hex color code: $this. Hex color codes must be in the format #RRGGBB or #AARRGGBB."
}

// Remove the leading '#' if present
val hex = removePrefix("#")

// Parse the hex value
return when (hex.length) {
6 -> {
// If the string is in the format RRGGBB, add full opacity (FF) at the start
val r = hex.substring(0, 2).toInt(16)
val g = hex.substring(2, 4).toInt(16)
val b = hex.substring(4, 6).toInt(16)
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
}
8 -> {
// If the string is in the format AARRGGBB
val a = hex.substring(0, 2).toInt(16)
val r = hex.substring(2, 4).toInt(16)
val g = hex.substring(4, 6).toInt(16)
val b = hex.substring(6, 8).toInt(16)
Color(alpha = a / 255f, red = r / 255f, green = g / 255f, blue = b / 255f)
}
else -> throw IllegalArgumentException("Invalid hex color format. Use #RRGGBB or #AARRGGBB.")
}
}

/**
* Checks if a string is a valid hexadecimal color code.
*
* This function uses a regular expression to validate the format of the
* provided string. A valid hex color code must start with "#" followed by
* either 6 or 8 hexadecimal digits (0-9, A-F, a-f).
*
* @return true if the string is a valid hex color code, false otherwise.
*/
private fun String.isValidHexColorCode(): Boolean {
// Regular expression to match #RRGGBB or #RRGGBBAA hex color codes
val hexColorRegex = Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})\$")
return hexColorRegex.matches(this)
}
105 changes: 105 additions & 0 deletions taj/src/commonMain/kotlin/xyz/ksharma/krail/taj/components/Button.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package xyz.ksharma.krail.taj.components

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import xyz.ksharma.krail.taj.LocalContentAlpha
import xyz.ksharma.krail.taj.LocalContentColor
import xyz.ksharma.krail.taj.LocalOnContentColor
import xyz.ksharma.krail.taj.LocalTextStyle
import xyz.ksharma.krail.taj.LocalThemeColor
import xyz.ksharma.krail.taj.hexToComposeColor
import xyz.ksharma.krail.taj.theme.KrailTheme
import xyz.ksharma.krail.taj.theme.getForegroundColor
import xyz.ksharma.krail.taj.tokens.ContentAlphaTokens.DisabledContentAlpha
import xyz.ksharma.krail.taj.tokens.ContentAlphaTokens.EnabledContentAlpha

/**
* Button component for Taj
*
* @param label The text to be displayed on the button.
* @param modifier The modifier to be applied to the button.
* @param themeColor The background color of the button. If null, a default color is used.
* @param enabled Whether the button is enabled or not. Defaults to true.
* @param onClick The callback to be invoked when the button is clicked.
*/
@Composable
fun Button(
label: String,
modifier: Modifier = Modifier,
themeColor: Color? = null, // TODO - concrete type for theme
enabled: Boolean = true,
onClick: () -> Unit = {},
) {
val contentAlphaProvider =
rememberSaveable(enabled) { if (enabled) EnabledContentAlpha else DisabledContentAlpha }

val hexContentColor by LocalThemeColor.current
val contentColor by remember(themeColor, hexContentColor) {
mutableStateOf(themeColor ?: hexContentColor.hexToComposeColor())
}
val onContentColor by animateColorAsState(
targetValue = getForegroundColor(contentColor),
label = "buttonTextColor",
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
)

CompositionLocalProvider(
LocalContentAlpha provides contentAlphaProvider,
LocalTextStyle provides KrailTheme.typography.titleMedium,
LocalContentColor provides contentColor,
LocalOnContentColor provides onContentColor,
) {
val contentAlpha = LocalContentAlpha.current

Text(
text = label,
textAlign = TextAlign.Center,
color = LocalOnContentColor.current.copy(alpha = contentAlpha),
style = LocalTextStyle.current,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp) // TODO - tokens for padding
.clip(RoundedCornerShape(50))
.background(color = LocalContentColor.current.copy(alpha = contentAlpha))
.clickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
enabled = enabled,
indication = null,
onClick = onClick,
)
.padding(10.dp), // TODO - tokens for padding
)
}
}

/** TODO
* Button
* Kind
* - Cta button - text only
* - Button - text with background color
*
* Sizing
* - Compact - a small button with minimal padding horizontally
* - Default - a button with default padding horizontally
* - Large - a full width button.
*/

0 comments on commit b0930e5

Please sign in to comment.