feat: introduce icon-button composables in tv material
Test: added instrumentation and snapshot tests
Relnote: "Introduce IconButton and OutlinedIconButton in Tv Material"
Change-Id: Ib504cefcdd22dc50fd43026efcf976ab8d1d43ad
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index beb7c7f..72025d1 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -274,6 +274,32 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.IndicationInstance rememberUpdatedInstance(androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class IconButtonDefaults {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method public float getLargeButtonSize();
+ method public float getLargeIconSize();
+ method public float getMediumButtonSize();
+ method public float getMediumIconSize();
+ method public float getSmallButtonSize();
+ method public float getSmallIconSize();
+ method public androidx.tv.material3.ButtonGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow);
+ method public androidx.tv.material3.ButtonScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedDisabledScale);
+ method public androidx.tv.material3.ButtonShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape);
+ property public final float LargeButtonSize;
+ property public final float LargeIconSize;
+ property public final float MediumButtonSize;
+ property public final float MediumIconSize;
+ property public final float SmallButtonSize;
+ property public final float SmallIconSize;
+ field public static final androidx.tv.material3.IconButtonDefaults INSTANCE;
+ }
+
+ public final class IconButtonKt {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ }
+
public final class IconKt {
method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Icon(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Icon(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
@@ -342,6 +368,27 @@
field public static final androidx.tv.material3.OutlinedButtonDefaults INSTANCE;
}
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedIconButtonDefaults {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method public float getLargeButtonSize();
+ method public float getLargeIconSize();
+ method public float getMediumButtonSize();
+ method public float getMediumIconSize();
+ method public float getSmallButtonSize();
+ method public float getSmallIconSize();
+ method public androidx.tv.material3.ButtonGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow);
+ method public androidx.tv.material3.ButtonScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedDisabledScale);
+ method public androidx.tv.material3.ButtonShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape);
+ property public final float LargeButtonSize;
+ property public final float LargeIconSize;
+ property public final float MediumButtonSize;
+ property public final float MediumIconSize;
+ property public final float SmallButtonSize;
+ property public final float SmallIconSize;
+ field public static final androidx.tv.material3.OutlinedIconButtonDefaults INSTANCE;
+ }
+
@androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ScaleIndication implements androidx.compose.foundation.Indication {
ctor public ScaleIndication(float scale);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.IndicationInstance rememberUpdatedInstance(androidx.compose.foundation.interaction.InteractionSource interactionSource);
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconButtonScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconButtonScreenshotTest.kt
new file mode 100644
index 0000000..9e6d21a
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconButtonScreenshotTest.kt
@@ -0,0 +1,361 @@
+/*
+ * 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
+ *
+ * http://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 androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.InputMode
+import androidx.compose.ui.input.InputModeManager
+import androidx.compose.ui.platform.LocalInputModeManager
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTestApi::class, ExperimentalTvMaterial3Api::class)
+class IconButtonScreenshotTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+ private val wrap = Modifier.wrapContentSize(Alignment.TopStart)
+ private val wrapperTestTag = "iconButtonWrapper"
+
+ @Test
+ fun iconButton_lightTheme() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_lightTheme")
+ }
+
+ @Test
+ fun iconButton_darkTheme() {
+ rule.setContent {
+ DarkMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_darkTheme")
+ }
+
+ @Test
+ fun iconButton_lightTheme_disabled() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }, enabled = false) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_lightTheme_disabled")
+ }
+
+ @Test
+ fun iconButton_darkTheme_disabled() {
+ rule.setContent {
+ DarkMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }, enabled = false) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_darkTheme_disabled")
+ }
+
+ @Test
+ fun iconButton_lightTheme_pressed() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+
+ rule.mainClock.autoAdvance = false
+ rule.onNode(hasClickAction())
+ .performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ assertAgainstGolden("iconButton_lightTheme_pressed")
+ }
+
+ @Test
+ fun iconButton_darkTheme_pressed() {
+ rule.setContent {
+ DarkMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+
+ rule.mainClock.autoAdvance = false
+ rule.onNode(hasClickAction())
+ .performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ assertAgainstGolden("iconButton_darkTheme_pressed")
+ }
+
+ @Test
+ fun iconButton_lightTheme_hovered() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+ rule.onNodeWithTag(wrapperTestTag).performMouseInput {
+ enter(center)
+ }
+
+ assertAgainstGolden("iconButton_lightTheme_hovered")
+ }
+
+ @Test
+ fun iconButton_darkTheme_hovered() {
+ rule.setContent {
+ DarkMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+ rule.onNodeWithTag(wrapperTestTag).performMouseInput {
+ enter(center)
+ }
+
+ assertAgainstGolden("iconButton_darkTheme_hovered")
+ }
+
+ @Test
+ fun iconButton_lightTheme_focused() {
+ val focusRequester = FocusRequester()
+ var localInputModeManager: InputModeManager? = null
+
+ rule.setContent {
+ LightMaterialTheme {
+ localInputModeManager = LocalInputModeManager.current
+ Box(Modifier.sizeIn(minWidth = 50.dp, minHeight = 50.dp).testTag(wrapperTestTag)) {
+ IconButton(
+ onClick = { /* doSomething() */ },
+ modifier = Modifier
+ .align(Alignment.Center)
+ .focusRequester(focusRequester)
+ ) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ @OptIn(ExperimentalComposeUiApi::class)
+ localInputModeManager!!.requestInputMode(InputMode.Keyboard)
+ focusRequester.requestFocus()
+ }
+
+ assertAgainstGolden("iconButton_lightTheme_focused")
+ }
+
+ @Test
+ fun iconButton_darkTheme_focused() {
+ val focusRequester = FocusRequester()
+ var localInputModeManager: InputModeManager? = null
+
+ rule.setContent {
+ DarkMaterialTheme {
+ localInputModeManager = LocalInputModeManager.current
+ Box(Modifier.sizeIn(minWidth = 50.dp, minHeight = 50.dp).testTag(wrapperTestTag)) {
+ IconButton(
+ onClick = { /* doSomething() */ },
+ modifier = Modifier
+ .align(Alignment.Center)
+ .focusRequester(focusRequester)
+ ) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ @OptIn(ExperimentalComposeUiApi::class)
+ localInputModeManager!!.requestInputMode(InputMode.Keyboard)
+ focusRequester.requestFocus()
+ }
+
+ assertAgainstGolden("iconButton_darkTheme_focused")
+ }
+
+ @Test
+ fun iconButton_largeContentClipped() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .background(Color.Blue))
+ }
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_largeContentClipped")
+ }
+
+ @Test
+ fun outlinedIconButton_lightTheme() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ OutlinedIconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ Icons.Outlined.FavoriteBorder,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ }
+ }
+ assertAgainstGolden("outlinedIconButton_lightTheme")
+ }
+
+ @Test
+ fun outlinedIconButton_darkTheme() {
+ rule.setContent {
+ DarkMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ OutlinedIconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ Icons.Outlined.FavoriteBorder,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ }
+ }
+ assertAgainstGolden("outlinedIconButton_darkTheme")
+ }
+
+ @Test
+ fun outlinedIconButton_lightTheme_disabled() {
+ rule.setContent {
+ LightMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ OutlinedIconButton(onClick = { /* doSomething() */ }, enabled = false) {
+ Icon(
+ Icons.Outlined.FavoriteBorder,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ }
+ }
+ assertAgainstGolden("outlinedIconButton_lightTheme_disabled")
+ }
+
+ @Test
+ fun outlinedIconButton_darkTheme_disabled() {
+ rule.setContent {
+ DarkMaterialTheme {
+ Box(wrap.testTag(wrapperTestTag)) {
+ OutlinedIconButton(onClick = { /* doSomething() */ }, enabled = false) {
+ Icon(
+ Icons.Outlined.FavoriteBorder,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ }
+ }
+ assertAgainstGolden("outlinedIconButton_darkTheme_disabled")
+ }
+
+ private fun assertAgainstGolden(goldenName: String) {
+ rule.onNodeWithTag(wrapperTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenName)
+ }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconButtonTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconButtonTest.kt
new file mode 100644
index 0000000..8e1fdc1
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconButtonTest.kt
@@ -0,0 +1,982 @@
+/*
+ * 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
+ *
+ * http://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 androidx.tv.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+ ExperimentalTestApi::class,
+ ExperimentalComposeUiApi::class,
+ ExperimentalTvMaterial3Api::class
+)
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class IconButtonTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun filledIconButton_DefaultSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(FilledIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assertWidthIsEqualTo(40.dp)
+ .assertHeightIsEqualTo(40.dp)
+ }
+
+ @Test
+ fun filledIconButton_SmallSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .size(IconButtonDefaults.SmallButtonSize)
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(FilledIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assertWidthIsEqualTo(28.dp)
+ .assertHeightIsEqualTo(28.dp)
+ }
+
+ @Test
+ fun filledIconButton_MediumSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumButtonSize)
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(FilledIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assertWidthIsEqualTo(40.dp)
+ .assertHeightIsEqualTo(40.dp)
+ }
+
+ @Test
+ fun filledIconButton_LargeSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .size(IconButtonDefaults.LargeButtonSize)
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(FilledIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assertWidthIsEqualTo(56.dp)
+ .assertHeightIsEqualTo(56.dp)
+ }
+
+ @Test
+ fun filledIconButton_CustomSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .size(64.dp)
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(FilledIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assertWidthIsEqualTo(64.dp)
+ .assertHeightIsEqualTo(64.dp)
+ }
+
+ @Test
+ fun filledIconButton_size_withoutMinimumTouchTarget() {
+ val width = 24.dp
+ val height = 24.dp
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .testTag(FilledIconButtonTag)
+ .size(width, height),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(FilledIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag, useUnmergedTree = true)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun filledIconButton_defaultSemantics() {
+ rule.setContent {
+ Box {
+ IconButton(modifier = Modifier.testTag(FilledIconButtonTag), onClick = {}) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+ .assertIsEnabled()
+ }
+
+ @Test
+ fun filledIconButton_disabledSemantics() {
+ rule.setContent {
+ Box {
+ IconButton(
+ modifier = Modifier.testTag(FilledIconButtonTag),
+ onClick = {},
+ enabled = false
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun filledIconButton_findByTagAndClick() {
+ var counter = 0
+ val onClick: () -> Unit = { ++counter }
+
+ rule.setContent {
+ Box {
+ IconButton(
+ modifier = Modifier.testTag(FilledIconButtonTag),
+ onClick = onClick
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+ rule.onNodeWithTag(FilledIconButtonTag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ rule.runOnIdle {
+ Truth.assertThat(counter).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun filledIconButton_canBeDisabled() {
+ rule.setContent {
+ var enabled by remember { mutableStateOf(true) }
+ Box {
+ IconButton(
+ modifier = Modifier.testTag(FilledIconButtonTag),
+ onClick = { enabled = false },
+ enabled = enabled
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+ rule.onNodeWithTag(FilledIconButtonTag)
+ // Confirm the button starts off enabled, with a click action
+ .assertHasClickAction()
+ .assertIsEnabled()
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ // Then confirm it's disabled with click action after clicking it
+ .assertHasClickAction()
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun filledIconButton_clickIsIndependentBetweenButtons() {
+ var addButtonCounter = 0
+ val addButtonOnClick: () -> Unit = { ++addButtonCounter }
+ val addButtonTag = "AddButton"
+
+ var phoneButtonCounter = 0
+ val phoneButtonOnClick: () -> Unit = { ++phoneButtonCounter }
+ val phoneButtonTag = "PhoneButton"
+
+ rule.setContent {
+ Column {
+ IconButton(
+ modifier = Modifier.testTag(addButtonTag),
+ onClick = addButtonOnClick
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ IconButton(
+ modifier = Modifier.testTag(phoneButtonTag),
+ onClick = phoneButtonOnClick
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(addButtonTag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(addButtonCounter).isEqualTo(1)
+ Truth.assertThat(phoneButtonCounter).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag(phoneButtonTag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(addButtonCounter).isEqualTo(1)
+ Truth.assertThat(phoneButtonCounter).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun filledIconButton_buttonPositioningSmallSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .size(IconButtonDefaults.SmallButtonSize)
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.SmallIconSize)
+ .testTag(FilledIconButtonIconTag)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ val buttonBounds = rule.onNodeWithTag(FilledIconButtonTag).getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag(FilledIconButtonIconTag).getUnclippedBoundsInRoot()
+
+ (iconBounds.left - buttonBounds.left).assertIsEqualTo(
+ 6.dp,
+ "padding between the start of the button and the start of the icon."
+ )
+
+ (iconBounds.top - buttonBounds.top).assertIsEqualTo(
+ 6.dp,
+ "padding between the top of the button and the top of the icon."
+ )
+
+ (buttonBounds.right - iconBounds.right).assertIsEqualTo(
+ 6.dp,
+ "padding between the end of the icon and the end of the button."
+ )
+
+ (buttonBounds.bottom - iconBounds.bottom).assertIsEqualTo(
+ 6.dp,
+ "padding between the bottom of the button and the bottom of the icon."
+ )
+ }
+
+ @Test
+ fun filledIconButton_buttonPositioningDefaultOrMediumSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier.testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.MediumIconSize)
+ .testTag(FilledIconButtonIconTag)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ val buttonBounds = rule.onNodeWithTag(FilledIconButtonTag).getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag(FilledIconButtonIconTag).getUnclippedBoundsInRoot()
+
+ (iconBounds.left - buttonBounds.left).assertIsEqualTo(
+ 10.dp,
+ "padding between the start of the button and the start of the icon."
+ )
+
+ (iconBounds.top - buttonBounds.top).assertIsEqualTo(
+ 10.dp,
+ "padding between the top of the button and the top of the icon."
+ )
+
+ (buttonBounds.right - iconBounds.right).assertIsEqualTo(
+ 10.dp,
+ "padding between the end of the icon and the end of the button."
+ )
+
+ (buttonBounds.bottom - iconBounds.bottom).assertIsEqualTo(
+ 10.dp,
+ "padding between the bottom of the button and the bottom of the icon."
+ )
+ }
+
+ @Test
+ fun filledIconButton_buttonPositioningLargeSize() {
+ rule.setContent {
+ IconButton(
+ modifier = Modifier
+ .size(IconButtonDefaults.LargeButtonSize)
+ .testTag(FilledIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(IconButtonDefaults.LargeIconSize)
+ .testTag(FilledIconButtonIconTag)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ val buttonBounds = rule.onNodeWithTag(FilledIconButtonTag).getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag(FilledIconButtonIconTag).getUnclippedBoundsInRoot()
+
+ (iconBounds.left - buttonBounds.left).assertIsEqualTo(
+ 14.dp,
+ "padding between the start of the button and the start of the icon."
+ )
+
+ (iconBounds.top - buttonBounds.top).assertIsEqualTo(
+ 14.dp,
+ "padding between the top of the button and the top of the icon."
+ )
+
+ (buttonBounds.right - iconBounds.right).assertIsEqualTo(
+ 14.dp,
+ "padding between the end of the icon and the end of the button."
+ )
+
+ (buttonBounds.bottom - iconBounds.bottom).assertIsEqualTo(
+ 14.dp,
+ "padding between the bottom of the button and the bottom of the icon."
+ )
+ }
+
+ @Test
+ fun filledIconButton_defaultSize_materialIconSize_iconPositioning() {
+ val diameter = IconButtonDefaults.MediumIconSize
+ rule.setContent {
+ Box {
+ IconButton(onClick = {}) {
+ Box(
+ Modifier
+ .size(diameter)
+ .testTag(FilledIconButtonIconTag)
+ )
+ }
+ }
+ }
+
+ // Icon should be centered inside the FilledIconButton
+ rule.onNodeWithTag(FilledIconButtonIconTag, useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(20.dp / 2)
+ .assertTopPositionInRootIsEqualTo(20.dp / 2)
+ }
+
+ @Test
+ fun filledIconButton_defaultSize_customIconSize_iconPositioning() {
+ val diameter = 20.dp
+ rule.setContent {
+ Box {
+ IconButton(onClick = {}) {
+ Box(
+ Modifier
+ .size(diameter)
+ .testTag(FilledIconButtonIconTag)
+ )
+ }
+ }
+ }
+
+ // Icon should be centered inside the FilledIconButton
+ rule.onNodeWithTag(FilledIconButtonIconTag, useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(10.dp)
+ .assertTopPositionInRootIsEqualTo(10.dp)
+ }
+
+ @Test
+ fun outlinedIconButton_DefaultSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assertWidthIsEqualTo(40.dp)
+ .assertHeightIsEqualTo(40.dp)
+ }
+
+ @Test
+ fun outlinedIconButton_SmallSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.SmallButtonSize)
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assertWidthIsEqualTo(28.dp)
+ .assertHeightIsEqualTo(28.dp)
+ }
+
+ @Test
+ fun outlinedIconButton_MediumSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumButtonSize)
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assertWidthIsEqualTo(40.dp)
+ .assertHeightIsEqualTo(40.dp)
+ }
+
+ @Test
+ fun outlinedIconButton_LargeSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.LargeButtonSize)
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assertWidthIsEqualTo(56.dp)
+ .assertHeightIsEqualTo(56.dp)
+ }
+
+ @Test
+ fun outlinedIconButton_CustomSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .size(64.dp)
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assertWidthIsEqualTo(64.dp)
+ .assertHeightIsEqualTo(64.dp)
+ }
+
+ @Test
+ fun outlinedIconButton__size_withoutMinimumTouchTarget() {
+ val width = 24.dp
+ val height = 24.dp
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .testTag(OutlinedIconButtonTag)
+ .size(width, height),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag, useUnmergedTree = true)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun outlinedIconButton_defaultSemantics() {
+ rule.setContent {
+ Box {
+ OutlinedIconButton(
+ modifier = Modifier.testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+ .assertIsEnabled()
+ }
+
+ @Test
+ fun outlinedIconButton_disabledSemantics() {
+ rule.setContent {
+ Box {
+ OutlinedIconButton(
+ modifier = Modifier.testTag(OutlinedIconButtonTag),
+ onClick = {},
+ enabled = false
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun outlinedIconButton_findByTagAndClick() {
+ var counter = 0
+ val onClick: () -> Unit = { ++counter }
+
+ rule.setContent {
+ Box {
+ OutlinedIconButton(
+ modifier = Modifier.testTag(OutlinedIconButtonTag),
+ onClick = onClick
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ rule.runOnIdle {
+ Truth.assertThat(counter).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun outlinedIconButton_canBeDisabled() {
+ rule.setContent {
+ var enabled by remember { mutableStateOf(true) }
+ Box {
+ OutlinedIconButton(
+ modifier = Modifier.testTag(OutlinedIconButtonTag),
+ onClick = { enabled = false },
+ enabled = enabled
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+ rule.onNodeWithTag(OutlinedIconButtonTag)
+ // Confirm the button starts off enabled, with a click action
+ .assertHasClickAction()
+ .assertIsEnabled()
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ // Then confirm it's disabled with click action after clicking it
+ .assertHasClickAction()
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun outlinedIconButton_clickIsIndependentBetweenButtons() {
+ var addButtonCounter = 0
+ val addButtonOnClick: () -> Unit = { ++addButtonCounter }
+ val addButtonTag = "AddButton"
+
+ var phoneButtonCounter = 0
+ val phoneButtonOnClick: () -> Unit = { ++phoneButtonCounter }
+ val phoneButtonTag = "PhoneButton"
+
+ rule.setContent {
+ Column {
+ OutlinedIconButton(
+ modifier = Modifier.testTag(addButtonTag),
+ onClick = addButtonOnClick
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ OutlinedIconButton(
+ modifier = Modifier.testTag(phoneButtonTag),
+ onClick = phoneButtonOnClick
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(addButtonTag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(addButtonCounter).isEqualTo(1)
+ Truth.assertThat(phoneButtonCounter).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag(phoneButtonTag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(addButtonCounter).isEqualTo(1)
+ Truth.assertThat(phoneButtonCounter).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun outlinedIconButton_PositioningSmallSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.SmallButtonSize)
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.SmallIconSize)
+ .testTag(OutlinedIconButtonIconTag)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ val buttonBounds = rule.onNodeWithTag(OutlinedIconButtonTag).getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag(OutlinedIconButtonIconTag).getUnclippedBoundsInRoot()
+
+ (iconBounds.left - buttonBounds.left).assertIsEqualTo(
+ 6.dp,
+ "padding between the start of the button and the start of the icon."
+ )
+
+ (iconBounds.top - buttonBounds.top).assertIsEqualTo(
+ 6.dp,
+ "padding between the top of the button and the top of the icon."
+ )
+
+ (buttonBounds.right - iconBounds.right).assertIsEqualTo(
+ 6.dp,
+ "padding between the end of the icon and the end of the button."
+ )
+
+ (buttonBounds.bottom - iconBounds.bottom).assertIsEqualTo(
+ 6.dp,
+ "padding between the bottom of the button and the bottom of the icon."
+ )
+ }
+
+ @Test
+ fun outlinedIconButton_PositioningDefaultOrMediumSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier.testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.MediumIconSize)
+ .testTag(OutlinedIconButtonIconTag)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ val buttonBounds = rule.onNodeWithTag(OutlinedIconButtonTag).getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag(OutlinedIconButtonIconTag).getUnclippedBoundsInRoot()
+
+ (iconBounds.left - buttonBounds.left).assertIsEqualTo(
+ 10.dp,
+ "padding between the start of the button and the start of the icon."
+ )
+
+ (iconBounds.top - buttonBounds.top).assertIsEqualTo(
+ 10.dp,
+ "padding between the top of the button and the top of the icon."
+ )
+
+ (buttonBounds.right - iconBounds.right).assertIsEqualTo(
+ 10.dp,
+ "padding between the end of the icon and the end of the button."
+ )
+
+ (buttonBounds.bottom - iconBounds.bottom).assertIsEqualTo(
+ 10.dp,
+ "padding between the bottom of the button and the bottom of the icon."
+ )
+ }
+
+ @Test
+ fun outlinedIconButton_PositioningLargeSize() {
+ rule.setContent {
+ OutlinedIconButton(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.LargeButtonSize)
+ .testTag(OutlinedIconButtonTag),
+ onClick = {}
+ ) {
+ Box(
+ modifier = Modifier
+ .size(OutlinedIconButtonDefaults.LargeIconSize)
+ .testTag(OutlinedIconButtonIconTag)
+ .semantics(mergeDescendants = true) {}
+ )
+ }
+ }
+
+ val buttonBounds = rule.onNodeWithTag(OutlinedIconButtonTag).getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag(OutlinedIconButtonIconTag).getUnclippedBoundsInRoot()
+
+ (iconBounds.left - buttonBounds.left).assertIsEqualTo(
+ 14.dp,
+ "padding between the start of the button and the start of the icon."
+ )
+
+ (iconBounds.top - buttonBounds.top).assertIsEqualTo(
+ 14.dp,
+ "padding between the top of the button and the top of the icon."
+ )
+
+ (buttonBounds.right - iconBounds.right).assertIsEqualTo(
+ 14.dp,
+ "padding between the end of the icon and the end of the button."
+ )
+
+ (buttonBounds.bottom - iconBounds.bottom).assertIsEqualTo(
+ 14.dp,
+ "padding between the bottom of the button and the bottom of the icon."
+ )
+ }
+
+ @Test
+ fun outlinedIconButton_defaultSize_materialIconSize_iconPositioning() {
+ val diameter = OutlinedIconButtonDefaults.MediumIconSize
+ rule.setContent {
+ Box {
+ OutlinedIconButton(onClick = {}) {
+ Box(
+ Modifier
+ .size(diameter)
+ .testTag(OutlinedIconButtonIconTag)
+ )
+ }
+ }
+ }
+
+ // Icon should be centered inside the OutlinedIconButton
+ rule.onNodeWithTag(OutlinedIconButtonIconTag, useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(20.dp / 2)
+ .assertTopPositionInRootIsEqualTo(20.dp / 2)
+ }
+
+ @Test
+ fun outlinedIconButton_defaultSize_customIconSize_iconPositioning() {
+ val diameter = 20.dp
+ rule.setContent {
+ Box {
+ OutlinedIconButton(onClick = {}) {
+ Box(
+ Modifier
+ .size(diameter)
+ .testTag(OutlinedIconButtonIconTag)
+ )
+ }
+ }
+ }
+
+ // Icon should be centered inside the OutlinedIconButton
+ rule.onNodeWithTag(OutlinedIconButtonIconTag, useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(10.dp)
+ .assertTopPositionInRootIsEqualTo(10.dp)
+ }
+}
+
+private const val FilledIconButtonTag = "FilledIconButton"
+private const val FilledIconButtonIconTag = "FilledIconButtonIcon"
+private val FilledIconButtonIconSize = 18.0.dp
+
+private const val OutlinedIconButtonTag = "OutlinedIconButton"
+private const val OutlinedIconButtonIconTag = "OutlinedIconButtonIcon"
+private val OutlinedIconButtonIconSize = 18.0.dp
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
index cf59b79..b547d9d 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
@@ -211,55 +211,3 @@
}
}
}
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun ButtonShape.toClickableSurfaceShape(): ClickableSurfaceShape = ClickableSurfaceShape(
- shape = shape,
- focusedShape = focusedShape,
- pressedShape = pressedShape,
- disabledShape = disabledShape,
- focusedDisabledShape = focusedDisabledShape
-)
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun ButtonColors.toClickableSurfaceContainerColor(): ClickableSurfaceColor =
- ClickableSurfaceColor(
- color = containerColor,
- focusedColor = focusedContainerColor,
- pressedColor = pressedContainerColor,
- disabledColor = disabledContainerColor,
- )
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun ButtonColors.toClickableSurfaceContentColor(): ClickableSurfaceColor =
- ClickableSurfaceColor(
- color = contentColor,
- focusedColor = focusedContentColor,
- pressedColor = pressedContentColor,
- disabledColor = disabledContentColor,
- )
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun ButtonScale.toClickableSurfaceScale() = ClickableSurfaceScale(
- scale = scale,
- focusedScale = focusedScale,
- pressedScale = pressedScale,
- disabledScale = disabledScale,
- focusedDisabledScale = focusedDisabledScale
-)
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun ButtonBorder.toClickableSurfaceBorder() = ClickableSurfaceBorder(
- border = border,
- focusedBorder = focusedBorder,
- pressedBorder = pressedBorder,
- disabledBorder = disabledBorder,
- focusedDisabledBorder = focusedDisabledBorder
-)
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun ButtonGlow.toClickableSurfaceGlow() = ClickableSurfaceGlow(
- glow = glow,
- focusedGlow = focusedGlow,
- pressedGlow = pressedGlow
-)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
index 89fe0a7..75e56dd 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
@@ -243,3 +243,55 @@
return "ButtonGlow(glow=$glow, focusedGlow=$focusedGlow, pressedGlow=$pressedGlow)"
}
}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ButtonShape.toClickableSurfaceShape(): ClickableSurfaceShape = ClickableSurfaceShape(
+ shape = shape,
+ focusedShape = focusedShape,
+ pressedShape = pressedShape,
+ disabledShape = disabledShape,
+ focusedDisabledShape = focusedDisabledShape
+)
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ButtonColors.toClickableSurfaceContainerColor(): ClickableSurfaceColor =
+ ClickableSurfaceColor(
+ color = containerColor,
+ focusedColor = focusedContainerColor,
+ pressedColor = pressedContainerColor,
+ disabledColor = disabledContainerColor,
+ )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ButtonColors.toClickableSurfaceContentColor(): ClickableSurfaceColor =
+ ClickableSurfaceColor(
+ color = contentColor,
+ focusedColor = focusedContentColor,
+ pressedColor = pressedContentColor,
+ disabledColor = disabledContentColor,
+ )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ButtonScale.toClickableSurfaceScale() = ClickableSurfaceScale(
+ scale = scale,
+ focusedScale = focusedScale,
+ pressedScale = pressedScale,
+ disabledScale = disabledScale,
+ focusedDisabledScale = focusedDisabledScale
+)
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ButtonBorder.toClickableSurfaceBorder() = ClickableSurfaceBorder(
+ border = border,
+ focusedBorder = focusedBorder,
+ pressedBorder = pressedBorder,
+ disabledBorder = disabledBorder,
+ focusedDisabledBorder = focusedDisabledBorder
+)
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+internal fun ButtonGlow.toClickableSurfaceGlow() = ClickableSurfaceGlow(
+ glow = glow,
+ focusedGlow = focusedGlow,
+ pressedGlow = pressedGlow
+)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
new file mode 100644
index 0000000..1c66a17
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
@@ -0,0 +1,160 @@
+/*
+ * 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
+ *
+ * http://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 androidx.tv.material3
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+
+/**
+ * Material Design standard icon button for TV.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * [content] should typically be an [Icon]. If using a custom icon, note that the typical size for
+ * the internal icon is 24 x 24 dp.
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param scale Defines size of the Button relative to its original size.
+ * @param glow Shadow to be shown behind the Button.
+ * @param shape Defines the Button's shape.
+ * @param colors Color to be used for background and content of the Button
+ * @param border Defines a border around the Button.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this button. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this button in different states.
+ * @param content the content of the button, typically an [Icon]
+ */
+@ExperimentalTvMaterial3Api
+@NonRestartableComposable
+@Composable
+fun IconButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ scale: ButtonScale = IconButtonDefaults.scale(),
+ glow: ButtonGlow = IconButtonDefaults.glow(),
+ shape: ButtonShape = IconButtonDefaults.shape(),
+ colors: ButtonColors = IconButtonDefaults.colors(),
+ border: ButtonBorder = IconButtonDefaults.border(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable BoxScope.() -> Unit
+) {
+ Surface(
+ modifier = modifier
+ .semantics { role = Role.Button }
+ .size(IconButtonDefaults.MediumButtonSize),
+ onClick = onClick,
+ enabled = enabled,
+ shape = shape.toClickableSurfaceShape(),
+ color = colors.toClickableSurfaceContainerColor(),
+ contentColor = colors.toClickableSurfaceContentColor(),
+ scale = scale.toClickableSurfaceScale(),
+ border = border.toClickableSurfaceBorder(),
+ glow = glow.toClickableSurfaceGlow(),
+ interactionSource = interactionSource
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ content = content
+ )
+ }
+}
+
+/**
+ * Material Design standard icon button for TV.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * [content] should typically be an [Icon]. If using a custom icon, note that the typical size for
+ * the internal icon is 24 x 24 dp.
+ * This icon button has an overall minimum touch target size of 48 x 48dp, to meet accessibility
+ * guidelines.
+ *
+ * The default text style for internal [Text] components will be set to [Typography.labelLarge].
+ *
+ * @param onClick called when this button is clicked
+ * @param modifier the [Modifier] to be applied to this button
+ * @param enabled controls the enabled state of this button. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param scale Defines size of the Button relative to its original size
+ * @param glow Shadow to be shown behind the Button.
+ * @param shape Defines the Button's shape.
+ * @param colors Color to be used for background and content of the Button
+ * @param border Defines a border around the Button.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this button. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this button in different states.
+ * @param content the content of the button, typically an [Icon]
+ */
+@ExperimentalTvMaterial3Api
+@NonRestartableComposable
+@Composable
+fun OutlinedIconButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ scale: ButtonScale = OutlinedIconButtonDefaults.scale(),
+ glow: ButtonGlow = OutlinedIconButtonDefaults.glow(),
+ shape: ButtonShape = OutlinedIconButtonDefaults.shape(),
+ colors: ButtonColors = OutlinedIconButtonDefaults.colors(),
+ border: ButtonBorder = OutlinedIconButtonDefaults.border(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable BoxScope.() -> Unit
+) {
+ Surface(
+ modifier = modifier
+ .semantics { role = Role.Button }
+ .size(OutlinedIconButtonDefaults.MediumButtonSize),
+ onClick = onClick,
+ enabled = enabled,
+ shape = shape.toClickableSurfaceShape(),
+ color = colors.toClickableSurfaceContainerColor(),
+ contentColor = colors.toClickableSurfaceContentColor(),
+ scale = scale.toClickableSurfaceScale(),
+ border = border.toClickableSurfaceBorder(),
+ glow = glow.toClickableSurfaceGlow(),
+ interactionSource = interactionSource
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ content = content
+ )
+ }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
new file mode 100644
index 0000000..c56fd82
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
@@ -0,0 +1,351 @@
+/*
+ * 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
+ *
+ * http://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 androidx.tv.material3
+
+import androidx.compose.foundation.interaction.Interaction
+import androidx.annotation.FloatRange
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.dp
+
+@ExperimentalTvMaterial3Api
+object IconButtonDefaults {
+ private val ContainerShape = CircleShape
+
+ /** The size of a small icon inside [IconButton] */
+ val SmallIconSize = 16.dp
+
+ /** The size of a medium icon inside [IconButton] */
+ val MediumIconSize = 20.dp
+
+ /** The size of a large icon inside [IconButton] */
+ val LargeIconSize = 28.dp
+
+ /** The size of a small [IconButton] */
+ val SmallButtonSize = 28.dp
+
+ /** The size of a medium [IconButton]. */
+ val MediumButtonSize = 40.dp
+
+ /** The size of a large [IconButton]. */
+ val LargeButtonSize = 56.dp
+
+ /**
+ * Creates a [ButtonShape] that represents the default container shapes used in a
+ * IconButton.
+ *
+ * @param shape the shape used when the Button is enabled, and has no other [Interaction]s.
+ * @param focusedShape the shape used when the Button is enabled and focused.
+ * @param pressedShape the shape used when the Button is enabled pressed.
+ * @param disabledShape the shape used when the Button is not enabled.
+ * @param focusedDisabledShape the shape used when the Button is not enabled and focused.
+ */
+ fun shape(
+ shape: Shape = ContainerShape,
+ focusedShape: Shape = shape,
+ pressedShape: Shape = shape,
+ disabledShape: Shape = shape,
+ focusedDisabledShape: Shape = disabledShape
+ ) = ButtonShape(
+ shape = shape,
+ focusedShape = focusedShape,
+ pressedShape = pressedShape,
+ disabledShape = disabledShape,
+ focusedDisabledShape = focusedDisabledShape
+ )
+
+ /**
+ * Creates a [ButtonColors] that represents the default colors used in a IconButton.
+ *
+ * @param containerColor the container color of this Button when enabled
+ * @param contentColor the content color of this Button when enabled
+ * @param focusedContainerColor the container color of this Button when enabled and focused
+ * @param focusedContentColor the content color of this Button when enabled and focused
+ * @param pressedContainerColor the container color of this Button when enabled and pressed
+ * @param pressedContentColor the content color of this Button when enabled and pressed
+ * @param disabledContainerColor the container color of this Button when not enabled
+ * @param disabledContentColor the content color of this Button when not enabled
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun colors(
+ containerColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
+ contentColor: Color = MaterialTheme.colorScheme.onSurface,
+ focusedContainerColor: Color = MaterialTheme.colorScheme.onSurface,
+ focusedContentColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ pressedContainerColor: Color = focusedContainerColor,
+ pressedContentColor: Color = focusedContentColor,
+ disabledContainerColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
+ disabledContentColor: Color = contentColor,
+ ) = ButtonColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor,
+ )
+
+ /**
+ * Creates a [ButtonScale] that represents the default scales used in a IconButton.
+ * scales are used to modify the size of a composable in different [Interaction]
+ * states e.g. 1f (original) in default state, 1.2f (scaled up) in focused state,
+ * 0.8f (scaled down) in pressed state, etc.
+ *
+ * @param scale the scale to be used for this Button when enabled
+ * @param focusedScale the scale to be used for this Button when focused
+ * @param pressedScale the scale to be used for this Button when pressed
+ * @param disabledScale the scale to be used for this Button when disabled
+ * @param focusedDisabledScale the scale to be used for this Button when disabled and
+ * focused
+ */
+ fun scale(
+ @FloatRange(from = 0.0) scale: Float = 1f,
+ @FloatRange(from = 0.0) focusedScale: Float = 1.1f,
+ @FloatRange(from = 0.0) pressedScale: Float = scale,
+ @FloatRange(from = 0.0) disabledScale: Float = scale,
+ @FloatRange(from = 0.0) focusedDisabledScale: Float = disabledScale
+ ) = ButtonScale(
+ scale = scale,
+ focusedScale = focusedScale,
+ pressedScale = pressedScale,
+ disabledScale = disabledScale,
+ focusedDisabledScale = focusedDisabledScale
+ )
+
+ /**
+ * Creates a [ButtonBorder] that represents the default [Border]s applied on a
+ * IconButton in different [Interaction] states.
+ *
+ * @param border the [Border] to be used for this Button when enabled
+ * @param focusedBorder the [Border] to be used for this Button when focused
+ * @param pressedBorder the [Border] to be used for this Button when pressed
+ * @param disabledBorder the [Border] to be used for this Button when disabled
+ * @param focusedDisabledBorder the [Border] to be used for this Button when disabled and
+ * focused
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun border(
+ border: Border = Border.None,
+ focusedBorder: Border = border,
+ pressedBorder: Border = focusedBorder,
+ disabledBorder: Border = border,
+ focusedDisabledBorder: Border = Border(
+ border = BorderStroke(
+ width = 2.dp,
+ color = MaterialTheme.colorScheme.border.copy(alpha = 0.2f)
+ ),
+ shape = ContainerShape
+ )
+ ) = ButtonBorder(
+ border = border,
+ focusedBorder = focusedBorder,
+ pressedBorder = pressedBorder,
+ disabledBorder = disabledBorder,
+ focusedDisabledBorder = focusedDisabledBorder
+ )
+
+ /**
+ * Creates a [ButtonGlow] that represents the default [Glow]s used in a IconButton.
+ *
+ * @param glow the Glow behind this Button when enabled
+ * @param focusedGlow the Glow behind this Button when focused
+ * @param pressedGlow the Glow behind this Button when pressed
+ */
+ fun glow(
+ glow: Glow = Glow.None,
+ focusedGlow: Glow = glow,
+ pressedGlow: Glow = glow
+ ) = ButtonGlow(
+ glow = glow,
+ focusedGlow = focusedGlow,
+ pressedGlow = pressedGlow
+ )
+}
+
+@ExperimentalTvMaterial3Api
+object OutlinedIconButtonDefaults {
+ private val ContainerShape = CircleShape
+
+ val SmallIconSize = 16.dp
+ val MediumIconSize = 20.dp
+ val LargeIconSize = 28.dp
+
+ /** The size of a small [OutlinedIconButton] */
+ val SmallButtonSize = 28.dp
+
+ /** The size of a medium [OutlinedIconButton]. */
+ val MediumButtonSize = 40.dp
+
+ /** The size of a large [OutlinedIconButton]. */
+ val LargeButtonSize = 56.dp
+
+ /**
+ * Creates a [ButtonShape] that represents the default container shapes used in an
+ * OutlinedIconButton.
+ *
+ * @param shape the shape used when the Button is enabled, and has no other [Interaction]s.
+ * @param focusedShape the shape used when the Button is enabled and focused.
+ * @param pressedShape the shape used when the Button is enabled pressed.
+ * @param disabledShape the shape used when the Button is not enabled.
+ * @param focusedDisabledShape the shape used when the Button is not enabled and focused.
+ */
+ fun shape(
+ shape: Shape = ContainerShape,
+ focusedShape: Shape = shape,
+ pressedShape: Shape = shape,
+ disabledShape: Shape = shape,
+ focusedDisabledShape: Shape = disabledShape
+ ) = ButtonShape(
+ shape = shape,
+ focusedShape = focusedShape,
+ pressedShape = pressedShape,
+ disabledShape = disabledShape,
+ focusedDisabledShape = focusedDisabledShape
+ )
+
+ /**
+ * Creates a [ButtonColors] that represents the default colors used in a OutlinedIconButton.
+ *
+ * @param containerColor the container color of this Button when enabled
+ * @param contentColor the content color of this Button when enabled
+ * @param focusedContainerColor the container color of this Button when enabled and focused
+ * @param focusedContentColor the content color of this Button when enabled and focused
+ * @param pressedContainerColor the container color of this Button when enabled and pressed
+ * @param pressedContentColor the content color of this Button when enabled and pressed
+ * @param disabledContainerColor the container color of this Button when not enabled
+ * @param disabledContentColor the content color of this Button when not enabled
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun colors(
+ containerColor: Color = Color.Transparent,
+ contentColor: Color = MaterialTheme.colorScheme.onSurface,
+ focusedContainerColor: Color = MaterialTheme.colorScheme.onSurface,
+ focusedContentColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ pressedContainerColor: Color = focusedContainerColor,
+ pressedContentColor: Color = focusedContentColor,
+ disabledContainerColor: Color = containerColor,
+ disabledContentColor: Color = contentColor,
+ ) = ButtonColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor,
+ )
+
+ /**
+ * Creates a [ButtonScale] that represents the default scales used in an
+ * OutlinedIconButton.
+ * scales are used to modify the size of a composable in different [Interaction]
+ * states e.g. 1f (original) in default state, 1.2f (scaled up) in focused state,
+ * 0.8f (scaled down) in pressed state, etc.
+ *
+ * @param scale the scale to be used for this Button when enabled
+ * @param focusedScale the scale to be used for this Button when focused
+ * @param pressedScale the scale to be used for this Button when pressed
+ * @param disabledScale the scale to be used for this Button when disabled
+ * @param focusedDisabledScale the scale to be used for this Button when disabled and
+ * focused
+ */
+ fun scale(
+ @FloatRange(from = 0.0) scale: Float = 1f,
+ @FloatRange(from = 0.0) focusedScale: Float = 1.1f,
+ @FloatRange(from = 0.0) pressedScale: Float = scale,
+ @FloatRange(from = 0.0) disabledScale: Float = scale,
+ @FloatRange(from = 0.0) focusedDisabledScale: Float = disabledScale
+ ) = ButtonScale(
+ scale = scale,
+ focusedScale = focusedScale,
+ pressedScale = pressedScale,
+ disabledScale = disabledScale,
+ focusedDisabledScale = focusedDisabledScale
+ )
+
+ /**
+ * Creates a [ButtonBorder] that represents the default [Border]s applied on an
+ * OutlinedIconButton in different [Interaction] states.
+ *
+ * @param border the [Border] to be used for this Button when enabled
+ * @param focusedBorder the [Border] to be used for this Button when focused
+ * @param pressedBorder the [Border] to be used for this Button when pressed
+ * @param disabledBorder the [Border] to be used for this Button when disabled
+ * @param focusedDisabledBorder the [Border] to be used for this Button when disabled and
+ * focused
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun border(
+ border: Border = Border(
+ border = BorderStroke(
+ width = 2.dp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
+ ),
+ shape = ContainerShape
+ ),
+ focusedBorder: Border = Border(
+ border = BorderStroke(width = 2.dp, color = MaterialTheme.colorScheme.onSurface),
+ shape = ContainerShape
+ ),
+ pressedBorder: Border = Border(
+ border = BorderStroke(width = 2.dp, color = MaterialTheme.colorScheme.onSurface),
+ shape = ContainerShape
+ ),
+ disabledBorder: Border = Border(
+ border = BorderStroke(
+ width = 2.dp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)
+ ),
+ shape = ContainerShape
+ ),
+ focusedDisabledBorder: Border = disabledBorder
+ ) = ButtonBorder(
+ border = border,
+ focusedBorder = focusedBorder,
+ pressedBorder = pressedBorder,
+ disabledBorder = disabledBorder,
+ focusedDisabledBorder = focusedDisabledBorder
+ )
+
+ /**
+ * Creates a [ButtonGlow] that represents the default [Glow]s used in an OutlinedIconButton.
+ *
+ * @param glow the Glow behind this Button when enabled
+ * @param focusedGlow the Glow behind this Button when focused
+ * @param pressedGlow the Glow behind this Button when pressed
+ */
+ fun glow(
+ glow: Glow = Glow.None,
+ focusedGlow: Glow = glow,
+ pressedGlow: Glow = glow
+ ) = ButtonGlow(
+ glow = glow,
+ focusedGlow = focusedGlow,
+ pressedGlow = pressedGlow
+ )
+}