diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index 5dae9cf7..4144c8d7 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -68,6 +68,32 @@ + + + + + + + + + + + + + + @@ -146,6 +172,19 @@ + + + + + + + @@ -250,6 +289,19 @@ + + + + + + + @@ -434,6 +486,19 @@ + + + + + + + diff --git a/example/src/main/java/com/tarkalabs/ui/UIComponentListActivity.kt b/example/src/main/java/com/tarkalabs/ui/UIComponentListActivity.kt index 1a04930e..4aacf5ab 100644 --- a/example/src/main/java/com/tarkalabs/ui/UIComponentListActivity.kt +++ b/example/src/main/java/com/tarkalabs/ui/UIComponentListActivity.kt @@ -37,7 +37,7 @@ class UIComponentListActivity : ComponentActivity() { TUICheckBoxRow( checked = status.value, enabled = true, - icon = TarkaIcons.CheckMark, + icon = TarkaIcons.CheckMark20Filled, title = "TUICheckBoxRow", style = TitleWithDescription("Description") ) { diff --git a/tarkaui/src/androidTest/java/com/tarkalabs/uicomponents/TUIChipTest.kt b/tarkaui/src/androidTest/java/com/tarkalabs/uicomponents/TUIChipTest.kt new file mode 100644 index 00000000..58aa470b --- /dev/null +++ b/tarkaui/src/androidTest/java/com/tarkalabs/uicomponents/TUIChipTest.kt @@ -0,0 +1,96 @@ +package com.tarkalabs.uicomponents + +import android.graphics.BitmapFactory +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.platform.app.InstrumentationRegistry +import com.tarkalabs.uicomponents.components.ChipLeadingContent +import com.tarkalabs.uicomponents.components.ChipType +import com.tarkalabs.uicomponents.components.TUIChip +import com.tarkalabs.uicomponents.components.TUIChipTags +import com.tarkalabs.uicomponents.models.TarkaIcons +import com.tarkalabs.uicomponents.theme.TUITheme +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class TUIChipTest { + + @get:Rule + val composable = createComposeRule() + val chipTags = TUIChipTags() + + val context = InstrumentationRegistry.getInstrumentation().context + val assetManager = context.assets + + @Test + fun display_assist_chip_avatar() { + val onClick: () -> Unit = mock() + val bitmap = BitmapFactory.decodeStream(assetManager.open("avatarTest.webp")) + composable.setContent { + TUIChip( + type = ChipType.Assist(ChipLeadingContent.Image(bitmap.asImageBitmap())), + label = "Assist chip", + onClick = onClick, + tags = chipTags.copy(parentTag = "Assist") + ) + } + + composable.onNodeWithTag("Assist").assertIsDisplayed() + composable.onNodeWithText("Assist chip").assertIsDisplayed() + composable.onNodeWithTag(Tags.TAG_AVATAR, useUnmergedTree = true).assertIsDisplayed() + composable.onNodeWithText("Assist chip").performClick() + verify(onClick).invoke() + } + + @Test + fun display_assist_chip_icon() { + composable.setContent { + TUIChip(type = ChipType.Assist(ChipLeadingContent.Icon(TarkaIcons.Calendar24Regular)), + label = "Assist chip", + onClick = {}) + } + + composable.onNodeWithContentDescription( + TarkaIcons.Calendar24Regular.contentDescription, useUnmergedTree = true + ).assertIsDisplayed() + } + + @Test + fun display_input_chip() { + val onClick: () -> Unit = mock() + composable.setContent { + TUIChip( + type = ChipType.Input(null, true, TUITheme.colors.surface), + label = "Input chip", + onClick = onClick, + tags = chipTags.copy(parentTag = "Input"), + ) + } + + composable.onNodeWithTag("Input").assertIsDisplayed() + composable.onNodeWithText("Input chip").assertIsDisplayed() + composable.onNodeWithText("Input chip").performClick() + verify(onClick).invoke() + } + + @Test + fun display_input_avatar() { + val bitmap = BitmapFactory.decodeStream(assetManager.open("avatarTest.webp")) + composable.setContent { + TUIChip( + type = ChipType.Input(content = ChipLeadingContent.Image(bitmap.asImageBitmap()), showTrailingDismiss = true, containerColor = TUITheme.colors.surface), + label = "Input chip", + onClick = { }, + ) + } + + composable.onNodeWithTag(Tags.TAG_AVATAR, useUnmergedTree = true).assertIsDisplayed() + } +} \ No newline at end of file diff --git a/tarkaui/src/androidTest/java/com/tarkalabs/uicomponents/screenshots/TUIChipScreenshotTest.kt b/tarkaui/src/androidTest/java/com/tarkalabs/uicomponents/screenshots/TUIChipScreenshotTest.kt new file mode 100644 index 00000000..d2d770ba --- /dev/null +++ b/tarkaui/src/androidTest/java/com/tarkalabs/uicomponents/screenshots/TUIChipScreenshotTest.kt @@ -0,0 +1,104 @@ +package com.tarkalabs.uicomponents.screenshots + +import android.graphics.BitmapFactory +import androidx.compose.ui.graphics.asImageBitmap +import androidx.test.platform.app.InstrumentationRegistry +import com.tarkalabs.uicomponents.components.ChipLeadingContent +import com.tarkalabs.uicomponents.components.ChipSize +import com.tarkalabs.uicomponents.components.ChipType +import com.tarkalabs.uicomponents.components.TUIChip +import com.tarkalabs.uicomponents.models.TarkaIcons +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class TUIChipScreenshotTest( + private val size: ChipSize, + private val type: ChipType, + private val darkTheme: Boolean, + private val testName: String +) : ComposeScreenshotComparator() { + + companion object { + @JvmStatic + @Parameterized.Parameters + fun data(): Collection> { + val context = InstrumentationRegistry.getInstrumentation().context + val assetManager = context.assets + val bitmap = BitmapFactory.decodeStream(assetManager.open("avatarTest.webp")) + val leadingContent = listOf( + null, + ChipLeadingContent.Image(bitmap.asImageBitmap()), + ChipLeadingContent.Icon(TarkaIcons.CheckMark16Filled) + ) + + val types = + leadingContent.map { ChipType.Assist(it) } + filterChipTypes() + leadingContent.map { + ChipType.Input( + it, false + ) + } + leadingContent.map { ChipType.Input(it, true) } + listOf( + ChipType.Suggestion(null), + ChipType.Suggestion(TarkaIcons.Calendar24Regular), + ) + + val chipSizes = ChipSize.values() + val darkThemeValues = listOf(true, false) + val testData = ArrayList>() + for (type in types) { + for (chipSize in chipSizes) { + for (darkTheme in darkThemeValues) { + val testName = when (type) { + is ChipType.Assist -> "ChipSize_${chipSize}_chipType_${type.javaClass.simpleName}_chipType_${type.content?.javaClass?.simpleName.toString()}_darkTheme_${darkTheme}" + is ChipType.Filter -> "ChipSize_${chipSize}_chipType_${type.javaClass.simpleName}_chipType_${type.showLeadingCheck}_chipType_${type.showTrailingCaret}_chipType_${type.showTrailingDismiss}_chipType_${type.selected}_darkTheme_${darkTheme}" + is ChipType.Input -> "ChipSize_${chipSize}_chipType_${type.javaClass.simpleName}_chipType_${type.showTrailingDismiss}_chipType_${type.content?.javaClass?.simpleName}_darkTheme_${darkTheme}" + is ChipType.Suggestion -> "ChipSize_${chipSize}_chipType_${type.javaClass.simpleName}_chipType_${type.image.toString()}_darkTheme_${darkTheme}" + } + testData.add( + arrayOf( + chipSize, type, darkTheme, testName, + ) + ) + } + } + } + return testData + } + } + + @Test + fun test_tui_chip() { + compareScreenshotFor(darkTheme, testName) { + TUIChip( + type = type, + label = type.javaClass.simpleName, + chipSize = size, + onClick = { }, + ) + } + } +} + +private fun filterChipTypes(): List { + val filterVariations = mutableListOf() + val booleans = listOf(true, false) + booleans.forEach { selected -> + booleans.forEach { showLeadingCheck -> + booleans.forEach { showTrailingDismiss -> + booleans.forEach { showTrailingCaret -> + filterVariations.add( + ChipType.Filter( + selected = selected, + showLeadingCheck = showLeadingCheck, + showTrailingDismiss = showTrailingDismiss, + showTrailingCaret = showTrailingCaret, + badgeCount = null + ) + ) + } + } + } + } + return filterVariations +} \ No newline at end of file diff --git a/tarkaui/src/main/java/com/tarkalabs/uicomponents/Tags.kt b/tarkaui/src/main/java/com/tarkalabs/uicomponents/Tags.kt index 16daa5d9..7e3b88e2 100644 --- a/tarkaui/src/main/java/com/tarkalabs/uicomponents/Tags.kt +++ b/tarkaui/src/main/java/com/tarkalabs/uicomponents/Tags.kt @@ -28,5 +28,5 @@ object Tags { const val TAG_CHECK_BOX = "check_box" const val TAG_CHECK_BOX_ROW = "check_box_row" const val TAG_SEARCH_BAR = "search_bar" - + const val TAG_CHIP_TAG = "tui_chip" } \ No newline at end of file diff --git a/tarkaui/src/main/java/com/tarkalabs/uicomponents/components/TUIChip.kt b/tarkaui/src/main/java/com/tarkalabs/uicomponents/components/TUIChip.kt new file mode 100644 index 00000000..dd171774 --- /dev/null +++ b/tarkaui/src/main/java/com/tarkalabs/uicomponents/components/TUIChip.kt @@ -0,0 +1,253 @@ +package com.tarkalabs.uicomponents.components + +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AssistChip +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +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.graphics.ImageBitmap +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tarkalabs.uicomponents.Tags +import com.tarkalabs.uicomponents.components.AvatarSize.XS +import com.tarkalabs.uicomponents.components.ChipLeadingContent.Icon +import com.tarkalabs.uicomponents.components.ChipLeadingContent.Image +import com.tarkalabs.uicomponents.components.ChipType.Filter +import com.tarkalabs.uicomponents.components.IconButtonSize.M +import com.tarkalabs.uicomponents.components.IconButtonStyle.GHOST +import com.tarkalabs.uicomponents.models.TarkaIcon +import com.tarkalabs.uicomponents.models.TarkaIcons +import com.tarkalabs.uicomponents.theme.TUITheme + + +/** + * Represents a generic chip type. The ChipType superclass serves as a generic base for the different chip types in the sealed class hierarchy. + */ +sealed class ChipType { + /** + * Represents an assist chip type for TUIChip. + * + * @param content The optional leading content for the chip. + */ + data class Assist(val content: ChipLeadingContent? = null) : ChipType() + + /** + * Represents an input chip type for TUIChip. + * + * @param content The optional leading content for the chip. + * @param showTrailingDismiss Whether to show a dismiss icon as a trailing icon. + * @param containerColor The color of the chip's container. If null, the default color from the theme will be used. + */ + data class Input( + val content: ChipLeadingContent? = null, + val showTrailingDismiss: Boolean = false, + val containerColor : Color? = null + ) : ChipType() + + /** + * Represents a suggestion chip type for TUIChip. + * + * @param image The optional image for the chip. + */ + data class Suggestion(val image: TarkaIcon? = null) : ChipType() + + /** + * Represents a filter chip type for TUIChip. + * + * @param selected Whether the filter is selected. + * @param showLeadingCheck Whether to show a check icon as a leading icon. + * @param showTrailingDismiss Whether to show a dismiss icon as a trailing icon. + * @param showTrailingCaret Whether to show a caret icon as a trailing icon. + * @param badgeCount The badge count to display on the chip. + */ + data class Filter( + val selected: Boolean = false, + val showLeadingCheck: Boolean = false, + val showTrailingDismiss: Boolean = false, + val showTrailingCaret: Boolean = false, + val badgeCount: Int? = null + ) : ChipType() +} + +sealed class ChipLeadingContent { + data class Image(val imageBitmap: ImageBitmap) : ChipLeadingContent() + data class Icon(val icon: TarkaIcon) : ChipLeadingContent() +} + +enum class ChipSize(val size: Dp) { + SMALL(32.dp), + BIG(40.dp), +} + +/** + * A customizable chip composable that can be used to represent different types of chips. + * + * @param modifier The modifier to be applied to the chip. + * @param type The type of chip to be rendered (Assist, Input, Filter, Suggestion). + * @param label The label text to be displayed on the chip. + * @param onClick The callback function to be invoked when the chip is clicked. + * @param chipSize The size of the chip (default is ChipSize.SMALL). + * @param tags The tags to be applied to the chip for testing purposes. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun TUIChip( + modifier: Modifier = Modifier, + type: ChipType, + label: String, + onClick: () -> Unit, + onDismissClick: (() -> Unit)? = null, + chipSize: ChipSize = ChipSize.SMALL, + tags: TUIChipTags = TUIChipTags() +) { + + val commonModifier = modifier + .testTag(tags.parentTag) + .height(chipSize.size) + val commonLabel = getCommonLabel(label) + val leadingIcon = getLeadingIcon() + + + when (type) { + is ChipType.Assist -> { + AssistChip(modifier = commonModifier, + label = commonLabel, + onClick = onClick, + leadingIcon = { leadingIcon(type.content) }) + } + + is ChipType.Input -> InputChip(modifier = commonModifier, + selected = false, + onClick = onClick, + label = commonLabel, + colors = InputChipDefaults.inputChipColors( + containerColor = type.containerColor ?: TUITheme.colors.onSurface + ), + leadingIcon = { leadingIcon(type.content) }, + trailingIcon = if (type.showTrailingDismiss) { + { + TUIIconButton(icon = TarkaIcons.Dismiss20Filled, + iconButtonStyle = GHOST, + onIconClick = { + onDismissClick?.invoke() + }, + buttonSize = M + ) + } + } else null) + + is Filter -> { + FilterChip(type, onClick, commonLabel, commonModifier) + } + + is ChipType.Suggestion -> { + SuggestionChip(onClick = onClick, + label = commonLabel, + modifier = commonModifier, + icon = if (type.image != null) { + { + Icon( + painter = painterResource(id = type.image.iconRes), + contentDescription = type.image.contentDescription + ) + } + } else null) + } + } +} + +@Composable @OptIn(ExperimentalMaterial3Api::class) private fun FilterChip( + type: Filter, onClick: () -> Unit, commonLabel: @Composable () -> Unit, modifier: Modifier +) { + Box(modifier = Modifier.wrapContentWidth()) { + FilterChip(selected = type.selected, + onClick = onClick, + label = commonLabel, + modifier = modifier, + leadingIcon = if (type.showLeadingCheck) { + { + Icon( + painter = painterResource(id = TarkaIcons.CheckMark20Filled.iconRes), + contentDescription = TarkaIcons.CheckMark20Filled.contentDescription + ) + } + } else null, + trailingIcon = if (type.showTrailingDismiss) { + { + TUIIconButton( + icon = TarkaIcons.Dismiss20Filled, iconButtonStyle = GHOST + ) + } + } else if (type.showTrailingCaret) { + { + Icon( + painter = painterResource(id = TarkaIcons.CaretDown20Filled.iconRes), + contentDescription = TarkaIcons.CaretDown20Filled.contentDescription + ) + } + } else null) + if (type.badgeCount != null) { + TUIBadge(count = type.badgeCount, modifier = Modifier.align(Alignment.TopEnd)) + } + } +} + +@Composable private fun getCommonLabel(label: String) = (@Composable { + Text( + text = label, style = TUITheme.typography.button7, color = TUITheme.colors.onSurface + ) +}) + +@Composable private fun getLeadingIcon(): @Composable (ChipLeadingContent?) -> Unit { + val leadingIcon: @Composable (ChipLeadingContent?) -> Unit = @Composable { + when (it) { + is Icon -> Icon( + painter = painterResource(id = it.icon.iconRes), + contentDescription = it.icon.contentDescription, + tint = TUITheme.colors.onSurface + ) + + is Image -> TUIAvatar( + avatarType = AvatarType.Image(it.imageBitmap), avatarSize = XS + ) + + null -> {} + } + } + return leadingIcon +} + +data class TUIChipTags( + val parentTag: String = Tags.TAG_CHIP_TAG +) + +@Preview @Composable fun TUIChipPreview() { + + Column { + TUIChip( + type = ChipType.Input(showTrailingDismiss = true, containerColor = TUITheme.colors.surfaceVariant), + label = "Something", + onClick = { Log.e("TAG_CHIP", "TUIChipPreview: TAG_CLICKED") } + ) + + TUIChip( + type = ChipType.Assist(), + label = "Something", + onClick = { Log.e("TAG_CHIP", "TUIChipPreview: TAG_CLICKED") } + ) + } + +} \ No newline at end of file diff --git a/tarkaui/src/main/java/com/tarkalabs/uicomponents/models/TarkaIcon.kt b/tarkaui/src/main/java/com/tarkalabs/uicomponents/models/TarkaIcon.kt index b63e98d3..641d9021 100644 --- a/tarkaui/src/main/java/com/tarkalabs/uicomponents/models/TarkaIcon.kt +++ b/tarkaui/src/main/java/com/tarkalabs/uicomponents/models/TarkaIcon.kt @@ -9,9 +9,11 @@ import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_arrow_sync_20_regu import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_arrow_sync_circle_24_regular import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_barcode_scanner_24_regular import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_calendar_ltr_24_regular +import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_caret_down_20_filled import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_chat_bubbles_question_24_regular import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_chat_help_20_filled import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_checkmark_16_filled +import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_checkmark_20_filled import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_checkmark_circle_16_regular import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_checkmark_starburst_24_regular import com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_chevron_down_20_regular @@ -85,12 +87,13 @@ object TarkaIcons { val ReOrderDotsVertical24Regular = TarkaIcon(ic_fluent_re_order_dots_vertical_24_regular, "Re order dots") val CheckMark16Filled = TarkaIcon(ic_fluent_checkmark_16_filled, "Check Mark") + val CheckMark20Filled = TarkaIcon(ic_fluent_checkmark_20_filled, "Check Mark") val QuestionCircle24Regular = TarkaIcon(ic_fluent_question_circle_24_regular, "Question Circle") val DocumentText24Regular = TarkaIcon(ic_fluent_document_text_24_regular, "Document Text") val ShieldTask24Regular = TarkaIcon(ic_fluent_shield_task_24_regular, "Document Text") val LocalLanguage24Regular = TarkaIcon(ic_fluent_local_language_24_regular, "Language") val BarCodeScanner24Regular = TarkaIcon(ic_fluent_barcode_scanner_24_regular, "BarCode Scanner") val Info20Filled = TarkaIcon(ic_fluent_chat_help_20_filled, "Information") - val ChatBubblesQuestion24Regular = - TarkaIcon(ic_fluent_chat_bubbles_question_24_regular, "FAQ") + val ChatBubblesQuestion24Regular = TarkaIcon(ic_fluent_chat_bubbles_question_24_regular, "FAQ") + val CaretDown20Filled = TarkaIcon(ic_fluent_caret_down_20_filled, "Caret Down") } \ No newline at end of file