diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c4f18d8..5ccb6d7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ + diff --git a/app/src/main/java/co/yml/coreui/MainActivity.kt b/app/src/main/java/co/yml/coreui/MainActivity.kt index e87d1b1..0139de4 100644 --- a/app/src/main/java/co/yml/coreui/MainActivity.kt +++ b/app/src/main/java/co/yml/coreui/MainActivity.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier import co.yml.coreui.core.ui.templates.AppBar import co.yml.coreui.core.ui.theme.CoreUICatalogTheme +import co.yml.coreui.feature.ytag.ui.YStepperActivity import co.yml.coreui.feature.ytag.ui.YTagActivity import co.yml.coreui.ui.R import co.yml.coreui.ui.presentation.CoreUIComponents @@ -49,6 +50,15 @@ class MainActivity : ComponentActivity() { ) ) }) + + CoreUIComponents(title = getString(R.string.title_y_stepper), onClick = { + startActivity( + Intent( + this@MainActivity, + YStepperActivity::class.java + ) + ) + }) } } } diff --git a/core/ui/src/androidTest/java/co/yml/coreui/ui/ystepper/StepperViewTest.kt b/core/ui/src/androidTest/java/co/yml/coreui/ui/ystepper/StepperViewTest.kt new file mode 100644 index 0000000..2becc00 --- /dev/null +++ b/core/ui/src/androidTest/java/co/yml/coreui/ui/ystepper/StepperViewTest.kt @@ -0,0 +1,268 @@ +package co.yml.coreui.ui.ystepper + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import co.yml.coreui.core.ui.ystepper.StepperView +import co.yml.coreui.core.ui.ystepper.model.StepperIcon +import co.yml.coreui.core.ui.ystepper.model.StepperModifiers +import co.yml.coreui.ui.R +import org.junit.Rule +import org.junit.Test + +class StepperViewTest { + @get:Rule + val composeTestRule = createComposeRule() + + private fun launchStepper( + textView: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + deleteIcon: @Composable (() -> Unit)? = null, + stepperModifiers: StepperModifiers = StepperModifiers.Builder().build(), + visible: Boolean = true + ) { + composeTestRule.setContent { + StepperView( + textView = textView, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + deleteIcon = deleteIcon, + stepperModifier = stepperModifiers, + visible = visible + ) + } + } + + @Test + fun stepper_view_shown() { + launchStepper() + + composeTestRule.onNodeWithTag("stepper_view").assertIsDisplayed() + } + + @Test + fun default_text_view_shown() { + launchStepper() + + composeTestRule.onNodeWithTag("text_view_default", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun custom_text_view_shown() { + launchStepper( + textView = { Text(text = "1") } + ) + + composeTestRule.onNodeWithTag("text_view_custom", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun text_view_text_shown(){ + val text = "1" + val stepperModifiers = StepperModifiers.Builder().text(text).build() + + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithText(text, useUnmergedTree = true) + .assertIsDisplayed() + } + + + @Test + fun default_leading_icon_shown() { + launchStepper() + + composeTestRule.onNodeWithTag("leading_icon_default", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun custom_leading_icon_shown() { + launchStepper( + leadingIcon = { + IconButton(enabled = true, onClick = {}) { + Icon( + painter = painterResource(id = R.drawable.ic_remove_20px), + contentDescription = null + ) + } + } + ) + + composeTestRule.onNodeWithTag("leading_icon_custom", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun default_trailing_icon_shown() { + launchStepper() + + composeTestRule.onNodeWithTag("trailing_icon_default", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun custom_trailing_icon_shown() { + launchStepper( + trailingIcon = { + IconButton(onClick = {}) { + Icon( + painter = painterResource(id = R.drawable.ic_add_20px), + contentDescription = null + ) + } + } + ) + + composeTestRule.onNodeWithTag("trailing_icon_custom", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun default_delete_icon_shown() { + val stepperModifiers = StepperModifiers.Builder().showDeleteIcon(true).build() + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithTag("delete_icon_default", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun custom_delete_icon_shown() { + val stepperModifiers = StepperModifiers.Builder().showDeleteIcon(true).build() + + launchStepper( + stepperModifiers = stepperModifiers, + deleteIcon = { + IconButton(onClick = {}) { + Icon( + painter = painterResource(id = R.drawable.ic_delete_20px), + contentDescription = null + ) + } + } + ) + + composeTestRule.onNodeWithTag("delete_icon_custom", useUnmergedTree = true) + .assertIsDisplayed() + } + + @Test + fun leading_icon_semantics_added() { + val semantics = "leading_icon" + val stepperModifiers = StepperModifiers.Builder().leadingIcon( + StepperIcon( + icon = R.drawable.ic_remove_20px, + iconTint = Color.Black, + semantics = semantics + ) + ).build() + + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithContentDescription(semantics, useUnmergedTree = true) + } + + @Test + fun trailing_icon_semantics_added() { + val semantics = "trailing_icon" + val stepperModifiers = StepperModifiers.Builder().trailingIcon( + StepperIcon( + icon = R.drawable.ic_add_20px, + iconTint = Color.Black, + semantics = semantics + ) + ).build() + + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithContentDescription(semantics, useUnmergedTree = true) + } + + @Test + fun delete_icon_semantics_added() { + val semantics = "delete_icon" + val stepperModifiers = StepperModifiers.Builder().deleteIcon( + StepperIcon( + icon = R.drawable.ic_add_20px, + iconTint = Color.Black, + semantics = semantics + ) + ).build() + + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithContentDescription(semantics, useUnmergedTree = true) + } + + @Test + fun text_view_semantics_added(){ + val semantics = "count" + + val stepperModifiers = StepperModifiers.Builder().textViewSemantics(semantics).build() + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithContentDescription(semantics, useUnmergedTree = true) + } + + @Test + fun is_min_value_limit() { + val minValue = 1 + val count = 1 + val enableLeadingIcon = count > minValue + + val stepperModifiers = StepperModifiers.Builder() + .minValue(minValue) + .leadingIcon( + StepperIcon( + R.drawable.ic_remove_20px, + iconTint = Color.Black, + enable = enableLeadingIcon + ) + ) + .build() + + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithTag("leading_icon_button_default", useUnmergedTree = true) + .assertIsNotEnabled() + } + + @Test + fun is_max_value_limit() { + val maxValue = 10 + val count = 10 + val enableTrailingIcon = count < maxValue + + val stepperModifiers = StepperModifiers.Builder() + .minValue(maxValue) + .trailingIcon( + StepperIcon( + R.drawable.ic_remove_20px, + iconTint = Color.Black, + enable = enableTrailingIcon + ) + ) + .build() + + launchStepper(stepperModifiers = stepperModifiers) + + composeTestRule.onNodeWithTag("trailing_icon_button_default", useUnmergedTree = true) + .assertIsNotEnabled() + } + + @Test + fun is_stepper_ui_removed_from_ui_tree() { + launchStepper(visible = false) + + composeTestRule.onNodeWithTag("stepper_view").assertDoesNotExist() + } +} diff --git a/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewTest.kt b/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewTest.kt index cc9cac4..b8b1199 100644 --- a/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewTest.kt +++ b/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewTest.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import co.yml.coreui.core.ui.ytag.TagView +import co.yml.coreui.core.ui.ytag.model.TagViewData import co.yml.coreui.core.ui.ytag.model.TagViewModifiers import org.junit.Rule import org.junit.Test @@ -32,8 +33,8 @@ class TagViewTest { private fun launchYTag( text: String, - leadingIcon: @Composable ((Boolean) -> Unit)? = null, - trailingIcon: @Composable ((Boolean) -> Unit)? = null, + leadingIcon: @Composable ((TagViewData) -> Unit)? = null, + trailingIcon: @Composable ((TagViewData) -> Unit)? = null, tagViewModifiers: TagViewModifiers = TagViewModifiers.Builder().build(), enabled: Boolean = true ) { diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/StepperView.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/StepperView.kt new file mode 100644 index 0000000..2c4f57e --- /dev/null +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/StepperView.kt @@ -0,0 +1,347 @@ +package co.yml.coreui.core.ui.ystepper + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import co.yml.coreui.core.ui.ystepper.model.StepperIcon +import co.yml.coreui.core.ui.ystepper.model.StepperModifiers +import co.yml.coreui.ui.R + +@Composable +fun StepperView( + textView: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + deleteIcon: @Composable (() -> Unit)? = null, + visible: Boolean = true, + stepperModifier: StepperModifiers = StepperModifiers.Builder().build(), +) { + val context = LocalContext.current + with(stepperModifier) { + var modifiers = if (width == Dp.Unspecified) { + Modifier.fillMaxWidth() + } else { + Modifier.width(width) + } + + modifiers = if (height == Dp.Unspecified) { + modifiers.then(Modifier.fillMaxWidth()) + } else { + modifiers.then(Modifier.height(height)) + } + + modifiers = Modifier + .width(width) + .height(height) + .run { + if (enableBorder) { + border( + width = borderWidth, + color = borderColor, + shape = shape + ) + } else { + background(color = backgroundColor, shape = shape) + } + } + .clickable { + } + .defaultMinSize(minWidth = minWidth, minHeight = minHeight) + .padding(containerPaddingValues) + .background( + color = backgroundColor, + shape = shape + ) + + if (visible) { + Surface( + shadowElevation = shadowElevation, + tonalElevation = tonalElevation, + shape = shape, + modifier = Modifier + .testTag("stepper_view") + .semantics { + this.contentDescription = stepperViewSemantics ?: context.getString(R.string.stepper_view_accessibility) + } + ) { + ConstraintLayout( + modifier = modifiers + ) { + val (leading_icon, text_view, trailing_icon) = createRefs() + + //Leading Icon + if (showDeleteIcon) { + //Delete Icon + deleteIcon?.let { + Box( + modifier = Modifier + .testTag("delete_icon_custom") + .constrainAs(leading_icon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + deleteIcon.invoke() + } + } ?: kotlin.run { + Box( + modifier = Modifier + .testTag("delete_icon_default") + .constrainAs(leading_icon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + IconButton(enabled = stepperModifier.deleteIcon.enable, onClick = { + stepperModifier.deleteIcon.onClickListener.invoke() + }) { + Icon( + painter = painterResource(id = stepperModifier.deleteIcon.icon ?: R.drawable.ic_delete_20px), + tint = stepperModifier.deleteIcon.iconTint, + contentDescription = stepperModifier.deleteIcon.semantics ?: stringResource( + id = R.string.ic_delete_accessibility + ) + ) + } + } + } + } else { + leadingIcon?.let { + Box( + modifier = Modifier + .testTag("leading_icon_custom") + .constrainAs(leading_icon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + leadingIcon.invoke() + } + } ?: kotlin.run { + Box( + modifier = Modifier + .testTag("leading_icon_default") + .constrainAs(leading_icon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + IconButton(enabled = stepperModifier.leadingIcon.enable, onClick = { + stepperModifier.leadingIcon.onClickListener.invoke() + }, modifier = Modifier.testTag("leading_icon_button_default")) { + Icon( + painter = painterResource(id = stepperModifier.leadingIcon.icon ?: R.drawable.ic_remove_20px), + tint = stepperModifier.leadingIcon.iconTint, + contentDescription = stepperModifier.leadingIcon.semantics ?: stringResource( + id = R.string.ic_remove_accessibility + ) + ) + } + } + } + } + + //Text view + textView?.let { + Box(modifier = Modifier + .testTag("text_view_custom") + .constrainAs(text_view) { + start.linkTo(leading_icon.end) + end.linkTo(trailing_icon.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) +// width = Dimension.fillToConstraints + }) { + textView.invoke() + } + } ?: kotlin.run { + Box(modifier = Modifier + .testTag("text_view_default") + .constrainAs(text_view) { + start.linkTo(leading_icon.end) + end.linkTo(trailing_icon.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) +// width = Dimension.fillToConstraints + }) { + Text( + text = text, + color = textColor, + fontSize = fontSize, + fontWeight = fontWeight, + fontFamily = fontFamily, + fontStyle = fontStyle, + letterSpacing = letterSpacing, + modifier = Modifier + .padding( + textPadding + ) + .semantics { + this.contentDescription = textViewSemantics + }, + style = style, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + onTextLayout = onTextLayout + ) + } + } + + //Trailing Icon + trailingIcon?.let { + Box( + modifier = Modifier + .testTag("trailing_icon_custom") + .constrainAs(trailing_icon) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + trailingIcon.invoke() + } + } ?: kotlin.run { + Box( + modifier = Modifier + .testTag("trailing_icon_default") + .constrainAs(trailing_icon) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + IconButton(enabled = stepperModifier.trailingIcon.enable, onClick = { + stepperModifier.trailingIcon.onClickListener.invoke() + },modifier = Modifier.testTag("trailing_icon_button_default")) { + Icon( + painter = painterResource(id = stepperModifier.trailingIcon.icon ?: R.drawable.ic_add_20px), + tint = stepperModifier.trailingIcon.iconTint, + contentDescription = stepperModifier.trailingIcon.semantics ?: stringResource( + id = R.string.ic_add_accessibility + ) + ) + } + } + } + } + } + } + } +} + +@Preview(name = "Default Stepper") +@Composable +fun DefaultStepper() { + val stepperModifiers = StepperModifiers.Builder() + .width(120.dp) + .height(36.dp) + .text("1") + .textColor(Color.Black) + .build() + + StepperView(stepperModifier = stepperModifiers) +} + +@Preview(name = "Capsule Stepper") +@Composable +fun CapsuleStepper() { + val stepperModifiers = StepperModifiers.Builder() + .width(120.dp) + .height(36.dp) + .text("1") + .textColor(Color.Black) + .shape(CircleShape) + .build() + + StepperView(stepperModifier = stepperModifiers) +} + +@Preview(name = "Capsule Stepper with border") +@Composable +fun BorderStepper() { + val stepperModifiers = StepperModifiers.Builder() + .width(120.dp) + .height(36.dp) + .text("1") + .textColor(Color.Black) + .shape(CircleShape) + .enableBorder(true) + .borderColor(Color.Red) + .build() + + StepperView(stepperModifier = stepperModifiers) +} + +@Preview(name = "Capsule Stepper with background") +@Composable +fun BackgroundStepper() { + val stepperModifiers = StepperModifiers.Builder() + .width(120.dp) + .height(36.dp) + .text("1") + .textColor(Color.Black) + .shape(CircleShape) + .backgroundColor(Color.Yellow) + .build() + + StepperView(stepperModifier = stepperModifiers) +} + +@Preview(name = "Capsule Stepper with delete icon") +@Composable +fun DeleteIconStepper() { + val stepperModifiers = StepperModifiers.Builder() + .width(120.dp) + .height(36.dp) + .text("1") + .textColor(Color.Black) + .shape(CircleShape) + .showDeleteIcon(true) + .build() + + StepperView(stepperModifier = stepperModifiers) +} + +@Preview(name = "Capsule Stepper with custom icons") +@Composable +fun CustomIconStepper() { + val stepperModifiers = StepperModifiers.Builder() + .width(120.dp) + .height(36.dp) + .text("5") + .textColor(Color.Black) + .shape(CircleShape) + .leadingIcon( + leadingIcon = StepperIcon(icon = android.R.drawable.star_on, iconTint = Color.Black) + ) + .trailingIcon(trailingIcon = StepperIcon(icon = android.R.drawable.star_off, iconTint = Color.Black)) + .build() + + StepperView(stepperModifier = stepperModifiers) +} diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/model/StepperIcon.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/model/StepperIcon.kt new file mode 100644 index 0000000..1918ece --- /dev/null +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/model/StepperIcon.kt @@ -0,0 +1,12 @@ +package co.yml.coreui.core.ui.ystepper.model + +import androidx.compose.ui.graphics.Color +import co.yml.coreui.core.ui.theme.CoreUICatalogTheme + +class StepperIcon( + val icon: Int ?=null, + val iconTint: Color, + val onClickListener: () -> Unit = {}, + val enable: Boolean = true, + val semantics: String? =null +) diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/model/StepperModifiers.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/model/StepperModifiers.kt new file mode 100644 index 0000000..8d39663 --- /dev/null +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ystepper/model/StepperModifiers.kt @@ -0,0 +1,251 @@ +package co.yml.coreui.core.ui.ystepper.model + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import co.yml.coreui.ui.R + +/** + * [StepperModifiers] Represents immutable collection of modifier elements that decorate or add behavior to Stepper elements. + * - If a parameter is explicitly set here then that parameter will always be used. + * - If a parameter is not set then the corresponding default value will be used + * + * @param minWidth define a default min width of Stepper + * @param minHeight define a default min height of Stepper + * @param width define width of Stepper + * @param height define height of Stepper + * @param textColor apply color to the text + * @param fontSize define fontSize of the text element + * @param fontStyle define fontStyle of the text element + * @param fontFamily define fontStyle of the text element + * @param letterSpacing the amount of space to add between each letter in the text element + * @param textDecoration the decorations to paint on the text element + * @param textAlign the alignment of the text within the lines of the paragraph + * @param lineHeight line height for the Paragraph in TextUnit uni + * @param overflow how visual overflow should be handled. + * @param softWrap whether the text should break at soft line breaks. + * @param maxLines an optional maximum number of lines for the text to span, wrapping if necessary. + * @param onTextLayout callback that is executed when a new text layout is calculated. + * @param style style configuration for the text such as color, font, line height etc. + * @param enableBorder enable border for Stepper + * @param borderWidth define borderWidth of the Stepper + * @param borderColor define borderColor of the Stepper + * @param backgroundColor define backgroundColor of the Stepper + * @param textPadding define padding for Stepper text component + * @param shape defines the shape of the Stepper + * @param tonalElevation When color is ColorScheme.surface, a higher the elevation will result in a darker color in light theme and lighter color in dark theme. + * @param shadowElevation The size of the shadow below the surface. + * @param containerPaddingValues define padding for Stepper + * @param onClick perform click event + * @param textViewSemantics add content description for Stepper view + */ +data class StepperModifiers ( + val minWidth: Dp, + val minHeight: Dp, + val width: Dp, + val height: Dp, + val text: String, + val textColor: Color, + val fontSize: TextUnit, + val fontStyle: FontStyle, + val fontWeight: FontWeight, + val fontFamily: FontFamily, + val letterSpacing: TextUnit, + val textDecoration: TextDecoration?, + val textAlign: TextAlign, + val lineHeight: TextUnit, + val overflow: TextOverflow, + val softWrap: Boolean, + val maxLines: Int, + val onTextLayout: (TextLayoutResult) -> Unit, + val style: TextStyle, + val enableBorder: Boolean, + val borderWidth: Dp, + val borderColor: Color, + val backgroundColor: Color, + val textPadding: PaddingValues, + val shape: Shape, + val tonalElevation: Dp, + val shadowElevation: Dp, + val containerPaddingValues: PaddingValues, + val onClick: () -> Unit, + val leadingIcon: StepperIcon, + val trailingIcon: StepperIcon, + val deleteIcon: StepperIcon, + val minValue: Int, + val maxValue: Int, + val stepValue: Int, + val showDeleteIcon: Boolean, + val textViewSemantics: String, + val stepperViewSemantics: String?){ + + class Builder { + private var minWidth: Dp = 80.dp + private var minHeight: Dp = 32.dp + private var width: Dp = Dp.Unspecified + private var height: Dp = Dp.Unspecified + private var text: String = "" + private var textColor: Color = Color.Black + private var fontSize: TextUnit = 12.sp + private var fontStyle: FontStyle = FontStyle.Normal + private var fontWeight: FontWeight = FontWeight.Normal + private var fontFamily: FontFamily = FontFamily.Default + private var letterSpacing: TextUnit = TextUnit.Unspecified + private var textDecoration: TextDecoration? = null + private var textAlign: TextAlign = TextAlign.Center + private var lineHeight: TextUnit = TextUnit.Unspecified + private var overflow: TextOverflow = TextOverflow.Clip + private var softWrap: Boolean = true + private var onTextLayout: (TextLayoutResult) -> Unit = {} + private var maxLines: Int = Int.MAX_VALUE + private var style = TextStyle() + private var enableBorder: Boolean = false + private var borderWidth: Dp = 1.dp + private var borderColor: Color = Color.Black + private var backgroundColor: Color = Color.White + private var textPadding: PaddingValues = PaddingValues(horizontal = 8.dp) + private var shape: Shape = RectangleShape + private var tonalElevation: Dp = 0.dp + private var shadowElevation: Dp = 0.dp + private var containerPaddingValues: PaddingValues = PaddingValues(horizontal = 4.dp) + private var onClick: () -> Unit = {} + private var leadingIcon: StepperIcon = StepperIcon(icon = R.drawable.ic_remove_20px, iconTint = Color.Black, onClickListener = {}) + private var trailingIcon: StepperIcon = StepperIcon(icon = R.drawable.ic_add_20px, iconTint = Color.Black, onClickListener = {}) + private var deleteIcon: StepperIcon = StepperIcon(icon = R.drawable.ic_delete_20px, iconTint = Color.Black, onClickListener = {}) + private var minValue: Int = 1 + private var maxValue: Int = Int.MAX_VALUE + private var stepValue: Int = 1 + private var showDeleteIcon = false + private var textViewSemantics: String = text + private var stepperViewSemantics: String? = null + fun minWidth(minWidth: Dp) = apply { this.minWidth = minWidth } + + fun minHeight(minHeight: Dp) = apply { this.minHeight = minHeight } + + fun width(width: Dp) = apply { this.width = width } + fun height(height: Dp) = apply { this.height = height } + + fun text(text: String) = apply { this.text = text } + + fun textColor(textColor: Color) = apply { this.textColor = textColor } + + fun fontSize(fontSize: TextUnit) = apply { this.fontSize = fontSize } + + fun fontStyle(fontStyle: FontStyle) = apply { this.fontStyle = fontStyle } + + fun fontWeight(fontWeight: FontWeight) = apply { this.fontWeight = fontWeight } + + fun fontFamily(fontFamily: FontFamily) = apply { this.fontFamily = fontFamily } + fun letterSpacing(letterSpacing: TextUnit) = apply { this.letterSpacing = letterSpacing } + fun textDecoration(textDecoration: TextDecoration) = + apply { this.textDecoration = textDecoration } + + fun textAlign(textAlign: TextAlign) = apply { this.textAlign = textAlign } + + fun lineHeight(lineHeight: TextUnit) = apply { this.lineHeight = lineHeight } + + fun overFlow(overflow: TextOverflow) = apply { this.overflow = overflow } + + fun softWrap(softWrap: Boolean) = apply { this.softWrap = softWrap } + + fun maxLines(maxLines: Int) = apply { this.maxLines } + + fun onTextLayout(onTextLayout: (TextLayoutResult) -> Unit) = + apply { this.onTextLayout = onTextLayout } + + fun style(style: TextStyle) = apply { this.style = style } + + fun enableBorder(enableBorder: Boolean) = apply { this.enableBorder = enableBorder } + + fun borderWidth(borderWidth: Dp) = apply { this.borderWidth = borderWidth } + + fun borderColor(borderColor: Color) = apply { this.borderColor = borderColor } + + fun backgroundColor(backgroundColor: Color) = + apply { this.backgroundColor = backgroundColor } + + fun textPadding(textPadding: PaddingValues) = apply { this.textPadding = textPadding } + + fun shape(shape: Shape) = apply { this.shape = shape } + + fun tonalElevation(tonalElevation: Dp) = apply { this.tonalElevation = tonalElevation } + + fun shadowElevation(shadowElevation: Dp) = apply { this.shadowElevation = shadowElevation } + fun containerPaddingValues(paddingValues: PaddingValues) = + apply { this.containerPaddingValues = paddingValues } + + fun onCLick(onClick: () -> Unit) = apply { this.onClick = onClick } + + fun leadingIcon(leadingIcon: StepperIcon) = apply { this.leadingIcon = leadingIcon } + + fun trailingIcon(trailingIcon: StepperIcon) = apply { this.trailingIcon = trailingIcon } + + fun deleteIcon(deleteIcon: StepperIcon) = apply { this.deleteIcon = deleteIcon } + + fun minValue(minValue: Int) = apply { this.minValue = minValue } + + fun maxValue(maxValue: Int) = apply { this.maxValue = maxValue } + + fun stepValue(stepValue: Int) = apply { this.stepValue = stepValue } + + fun showDeleteIcon(showDeleteIcon: Boolean) = apply { this.showDeleteIcon = showDeleteIcon } + + fun textViewSemantics(textViewSemantics: String) = apply { this.textViewSemantics = textViewSemantics } + + fun stepperViewSemantics(stepperViewSemantics: String) = apply { this.stepperViewSemantics = stepperViewSemantics } + + fun build() = StepperModifiers( + minWidth, + minHeight, + width, + height, + text, + textColor, + fontSize, + fontStyle, + fontWeight, + fontFamily, + letterSpacing, + textDecoration, + textAlign, + lineHeight, + overflow, + softWrap, + maxLines, + onTextLayout, + style, + enableBorder, + borderWidth, + borderColor, + backgroundColor, + textPadding, + shape, + tonalElevation, + shadowElevation, + containerPaddingValues, + onClick, + leadingIcon, + trailingIcon, + deleteIcon, + minValue, + maxValue, + stepValue, + showDeleteIcon, + textViewSemantics, + stepperViewSemantics + ) + } +} diff --git a/core/ui/src/main/res/drawable/ic_add_20px.xml b/core/ui/src/main/res/drawable/ic_add_20px.xml new file mode 100644 index 0000000..2211570 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_add_20px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_delete_20px.xml b/core/ui/src/main/res/drawable/ic_delete_20px.xml new file mode 100644 index 0000000..f900c92 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_delete_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_remove_20px.xml b/core/ui/src/main/res/drawable/ic_remove_20px.xml new file mode 100644 index 0000000..7fc4a6f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_remove_20px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 5c92fc8..8191ec5 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -4,4 +4,10 @@ Tags more Tag view container + Stepper + Add + Remove + Remove + Stepper View + diff --git a/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YStepperActivity.kt b/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YStepperActivity.kt new file mode 100644 index 0000000..c333657 --- /dev/null +++ b/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YStepperActivity.kt @@ -0,0 +1,331 @@ +package co.yml.coreui.feature.ytag.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import co.yml.coreui.core.ui.templates.AppBarWithBackButton +import co.yml.coreui.core.ui.theme.CoreUICatalogTheme +import co.yml.coreui.core.ui.ystepper.StepperView +import co.yml.coreui.core.ui.ystepper.model.StepperIcon +import co.yml.coreui.core.ui.ystepper.model.StepperModifiers +import co.yml.coreui.ui.R +import dagger.hilt.android.AndroidEntryPoint + +@ExperimentalMaterial3Api +@AndroidEntryPoint +class YStepperActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + CoreUICatalogTheme { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = CoreUICatalogTheme.colors.background, + topBar = { + AppBarWithBackButton( + stringResource(id = R.string.title_y_stepper), + onBackPressed = { + onBackPressed() + }) + }) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(it) + .padding(PaddingValues(horizontal = dimensionResource(id = R.dimen.padding_normal))) + ) { + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.padding_normal_medium))) + + DefaultStepper() + + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.padding_normal))) + + CustomStepper() + + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.padding_normal))) + + ModifiedBuilderStepper() + } + } + } + } + } +} + +@Composable +fun DefaultStepper() { + val minValue = 1 + val maxValue = 10 + + var count by remember { + mutableStateOf(minValue) + } + + var enableLeadingIcon by remember { + mutableStateOf(true) + } + + var enableTrailingIcon by remember { + mutableStateOf(true) + } + + var showDeleteIcon by remember { + mutableStateOf(true) + } + + var stepperVisibility by remember { + mutableStateOf(true) + } + + enableLeadingIcon = count > minValue + showDeleteIcon = count <= minValue + enableTrailingIcon = count < maxValue + + val stepperModifiers = StepperModifiers.Builder() + .width(140.dp) + .height(48.dp) + .enableBorder(true) + .borderColor(Color.Black) + .borderWidth(1.dp) + .text(count.toString()) + .textColor(Color.Black) + .shape(CircleShape) + .showDeleteIcon(showDeleteIcon) + .leadingIcon( + StepperIcon( + enable = enableLeadingIcon, + iconTint = Color.Black, + onClickListener = { + count -= 1 + }, + ) + ) + + .trailingIcon( + StepperIcon( + enable = enableTrailingIcon, + iconTint = Color.Black, + onClickListener = { + count += 1 + }) + ) + .deleteIcon( + StepperIcon( + iconTint = Color.Black, + onClickListener = { + stepperVisibility = false + } + ) + ) + .build() + + StepperView( + visible = stepperVisibility, + stepperModifier = stepperModifiers + ) +} + +@Composable +fun ModifiedBuilderStepper() { + val minValue = 1 + val maxValue = 10 + + var count by remember { + mutableStateOf(minValue) + } + + var enableLeadingIcon by remember { + mutableStateOf(true) + } + + var enableTrailingIcon by remember { + mutableStateOf(true) + } + + var stepperVisibility by remember { + mutableStateOf(true) + } + + enableLeadingIcon = count > minValue + enableTrailingIcon = count < maxValue + + val stepperModifiers = StepperModifiers.Builder() + .width(140.dp) + .height(48.dp) + .enableBorder(true) + .borderColor(Color.Gray) + .borderWidth(1.dp) + .text(count.toString()) + .textColor(Color.Black) + .shape(CircleShape) + .leadingIcon( + StepperIcon( + enable = enableLeadingIcon, + icon = R.drawable.ic_remove_20px, + iconTint = Color.Black, + onClickListener = { + count -= 1 + }, + semantics = "modified leading icon" + ) + ) + + .trailingIcon( + StepperIcon( + enable = enableTrailingIcon, + icon = R.drawable.ic_add_20px, + iconTint = Color.Black, + onClickListener = { + count += 1 + }, + semantics = "modified trailing icon" + ) + ) + .deleteIcon( + StepperIcon( + icon = R.drawable.ic_delete_20px, + iconTint = Color.Black, + onClickListener = { + stepperVisibility = false + }, + semantics = "modified delete icon" + ) + ) + .textViewSemantics("modified text view count $count") + .stepperViewSemantics("modified stepper view") + .build() + + StepperView( + visible = stepperVisibility, + stepperModifier = stepperModifiers + ) +} + +@Composable +fun CustomStepper() { + val minValue = 1 + val maxValue = 10 + val stepValue = 2 + + var count by remember { + mutableStateOf(minValue) + } + + var enableLeadingIcon by remember { + mutableStateOf(true) + } + + var enableTrailingIcon by remember { + mutableStateOf(true) + } + + var showDeleteIcon by remember { + mutableStateOf(true) + } + + var stepperVisibility by remember { + mutableStateOf(true) + } + + enableLeadingIcon = count > minValue + showDeleteIcon = count <= minValue + enableTrailingIcon = count < maxValue + + val stepperModifiers = StepperModifiers.Builder() + .width(140.dp) + .height(48.dp) + .backgroundColor(Color.Green) + .text(count.toString()) + .textColor(Color.Black) + .shape(CircleShape) + .showDeleteIcon(showDeleteIcon) + .build() + + StepperView( + visible = stepperVisibility, + stepperModifier = stepperModifiers, + textView = { + Text( + text = "$count", + modifier = Modifier.semantics { this.contentDescription = "Item count: $count" }) + }, + leadingIcon = { + IconButton(enabled = enableLeadingIcon, onClick = { + if (count - stepValue < minValue){ + count = minValue + }else{ + count -= stepValue + } + }) { + Icon( + painter = painterResource(id = R.drawable.ic_remove_20px), + contentDescription = "Leading" + ) + } + }, + trailingIcon = { + IconButton(enabled = enableTrailingIcon, onClick = { + if (count + stepValue > maxValue){ + count = maxValue + }else{ + count += stepValue + } + }) { + Icon( + painter = painterResource(id = R.drawable.ic_add_20px), + contentDescription = "Trailing" + ) + } + }, + deleteIcon = { + IconButton(enabled = enableTrailingIcon, onClick = { + stepperVisibility = false + }) { + Icon( + painter = painterResource(id = R.drawable.ic_delete_20px), + contentDescription = "Delete Item" + ) + } + } + ) +} + + +@Preview +@Composable +fun DefaultStepperPreview(){ + Column( + modifier = Modifier + .background(Color.White) + .padding(PaddingValues(horizontal = 16.dp)) + ) { + Spacer(modifier = Modifier.height(24.dp)) + + DefaultStepper() + + Spacer(modifier = Modifier.height(16.dp)) + + CustomStepper() + + Spacer(modifier = Modifier.height(16.dp)) + + ModifiedBuilderStepper() + + Spacer(modifier = Modifier.height(16.dp)) + } +}