diff --git a/compose-material/src/debug/java/com/google/android/horologist/compose/material/CompactChipPreview.kt b/compose-material/src/debug/java/com/google/android/horologist/compose/material/CompactChipPreview.kt new file mode 100644 index 0000000000..06cb95ecc4 --- /dev/null +++ b/compose-material/src/debug/java/com/google/android/horologist/compose/material/CompactChipPreview.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.compose.material + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import com.google.android.horologist.compose.tools.WearPreview + +@WearPreview +@Composable +fun CompactChipPreview() { + CompactChip( + onClick = { }, + label = "Primary label" + ) +} + +@WearPreview +@Composable +fun CompactChipPreviewWithIcon() { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Filled.Add + ) +} + +@WearPreview +@Composable +fun CompactChipPreviewIconOnly() { + CompactChip( + onClick = { }, + icon = Icons.Filled.Add + ) +} diff --git a/compose-material/src/main/java/com/google/android/horologist/compose/material/CompactChip.kt b/compose-material/src/main/java/com/google/android/horologist/compose/material/CompactChip.kt new file mode 100644 index 0000000000..636cddb392 --- /dev/null +++ b/compose-material/src/main/java/com/google/android/horologist/compose/material/CompactChip.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.compose.material + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.wear.compose.material.ChipBorder +import androidx.wear.compose.material.ChipColors +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.CompactChip +import androidx.wear.compose.material.LocalContentAlpha +import androidx.wear.compose.material.Text +import coil.compose.rememberAsyncImagePainter +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.material.util.DECORATIVE_ELEMENT_CONTENT_DESCRIPTION + +@ExperimentalHorologistApi +@Composable +public fun CompactChip( + onClick: () -> Unit, + modifier: Modifier = Modifier, + label: String? = null, + icon: Any? = null, + iconRtlMode: IconRtlMode = IconRtlMode.Default, + placeholder: Painter? = null, + colors: ChipColors = ChipDefaults.primaryChipColors(), + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + border: ChipBorder = ChipDefaults.chipBorder() +) { + val iconParam: (@Composable BoxScope.() -> Unit)? = + icon?.let { + { + Row { + val iconModifier = Modifier + .size(ChipDefaults.SmallIconSize) + when (icon) { + is ImageVector -> + Icon( + imageVector = icon, + contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION, + modifier = iconModifier, + rtlMode = iconRtlMode + ) + + is Int -> + Icon( + id = icon, + contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION, + modifier = iconModifier + ) + + else -> + Image( + painter = rememberAsyncImagePainter( + model = icon, + placeholder = placeholder + ), + contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION, + modifier = iconModifier, + contentScale = ContentScale.Crop, + alpha = LocalContentAlpha.current + ) + } + } + } + } + val hasIcon = icon != null + + val labelParam: (@Composable RowScope.() -> Unit)? = + label?.let { + { + Text( + modifier = Modifier.fillMaxWidth(), + text = label, + textAlign = if (hasIcon) TextAlign.Start else TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + } + + CompactChip( + modifier = modifier, + onClick = onClick, + label = labelParam, + icon = iconParam, + colors = colors, + enabled = enabled, + interactionSource = interactionSource, + border = border + ) +} + +/** + * This component is an alternative to [CompactChip], providing the following: + * - a convenient way of providing a string resource label; + * - a convenient way of providing an icon and a placeholder, and choosing their size based on the + * sizes recommended by the Wear guidelines; + */ +@ExperimentalHorologistApi +@Composable +public fun CompactChip( + @StringRes labelId: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, + icon: Any? = null, + iconRtlMode: IconRtlMode = IconRtlMode.Default, + placeholder: Painter? = null, + colors: ChipColors = ChipDefaults.primaryChipColors(), + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + border: ChipBorder = ChipDefaults.chipBorder() +) { + CompactChip( + onClick = onClick, + modifier = modifier, + label = stringResource(id = labelId), + icon = icon, + iconRtlMode = iconRtlMode, + placeholder = placeholder, + colors = colors, + enabled = enabled, + interactionSource = interactionSource, + border = border + ) +} diff --git a/compose-material/src/test/java/com/google/android/horologist/compose/material/CompactChipA11yTest.kt b/compose-material/src/test/java/com/google/android/horologist/compose/material/CompactChipA11yTest.kt new file mode 100644 index 0000000000..743b60dfdc --- /dev/null +++ b/compose-material/src/test/java/com/google/android/horologist/compose/material/CompactChipA11yTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.compose.material + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.google.android.horologist.screenshots.ScreenshotBaseTest +import com.google.android.horologist.screenshots.ScreenshotTestRule +import org.junit.Test + +class CompactChipA11yTest : ScreenshotBaseTest( + ScreenshotTestRule.screenshotTestRuleParams { + enableA11y = true + screenTimeText = {} + } +) { + @Test + fun withIcon() { + screenshotTestRule.setContent(takeScreenshot = true) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Filled.Add + ) + } + } + } + + @Test + fun disabled() { + screenshotTestRule.setContent(takeScreenshot = true) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Filled.Add, + enabled = false + ) + } + } + } +} diff --git a/compose-material/src/test/java/com/google/android/horologist/compose/material/CompactChipTest.kt b/compose-material/src/test/java/com/google/android/horologist/compose/material/CompactChipTest.kt new file mode 100644 index 0000000000..67ba717fa7 --- /dev/null +++ b/compose-material/src/test/java/com/google/android/horologist/compose/material/CompactChipTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.compose.material + +import android.R +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.DirectionsBike +import androidx.compose.material.icons.filled.Image +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.unit.LayoutDirection +import androidx.wear.compose.material.MaterialTheme +import com.google.accompanist.testharness.TestHarness +import com.google.android.horologist.compose.material.util.rememberVectorPainter +import com.google.android.horologist.compose.tools.coil.FakeImageLoader +import com.google.android.horologist.screenshots.ScreenshotBaseTest +import org.junit.Test + +class CompactChipTest : ScreenshotBaseTest() { + + @Test + fun default() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + label = "Primary label" + ) + } + } + + @Test + fun withIcon() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Filled.Add + ) + } + } + + @Test + fun iconOnly() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + icon = Icons.Filled.Add + ) + } + } + + @Test + fun disabled() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Filled.Add, + enabled = false + ) + } + } + + @Test + fun withLongText() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + label = "Primary label very very very very very very very very very very very very very very very very very long text" + ) + } + } + + @Test + fun withLongTextAndLargestFontScale() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + TestHarness(fontScale = largestFontScale) { + CompactChip( + onClick = { }, + label = "Primary label very very very very very very very very very very very very very very very very very long text" + ) + } + } + } + + @Test + fun usingDrawableResAsIcon() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = R.drawable.ic_delete + ) + } + } + + @Test + fun withPlaceholderIcon() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + // In inspection mode will jump to placeholder + CompositionLocalProvider(LocalInspectionMode.provides(true)) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = "iconUri", + placeholder = rememberVectorPainter( + image = Icons.Default.Image, + tintColor = MaterialTheme.colors.primary + ) + ) + } + } + } + + @Test + fun disabledWithIconPlaceholder() { + screenshotTestRule.setContent( + isComponent = true, + takeScreenshot = true, + fakeImageLoader = FakeImageLoader.Never + ) { + // In inspection mode will jump to placeholder + CompositionLocalProvider(LocalInspectionMode.provides(true)) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = "iconUri", + placeholder = rememberVectorPainter( + image = Icons.Default.Image, + tintColor = MaterialTheme.colors.primary + ), + enabled = false + ) + } + } + } + + @Test + fun withIconRtl() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + TestHarness(layoutDirection = LayoutDirection.Rtl) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Default.DirectionsBike + ) + } + } + } + + @Test + fun mirrored() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Default.DirectionsBike, + iconRtlMode = IconRtlMode.Mirrored + ) + } + } + + @Test + fun mirroredRtl() { + screenshotTestRule.setContent(isComponent = true, takeScreenshot = true) { + TestHarness(layoutDirection = LayoutDirection.Rtl) { + CompactChip( + onClick = { }, + label = "Primary label", + icon = Icons.Default.DirectionsBike, + iconRtlMode = IconRtlMode.Mirrored + ) + } + } + } + + companion object { + private const val largestFontScale = 1.18f + } +}