Fork `InteractiveComponentSize` from compose.material3
Add ExperimentalMaterial3CommonApi annotation.
Bug: b/295367813
Test: Ran ./gradlew :compileReleaseKotlin locally
Relnote: N/A
Change-Id: If8438aa56aa5e1d5d31c6faf882f2e4cbcc6da27
diff --git a/compose/material3/material3-common/api/current.txt b/compose/material3/material3-common/api/current.txt
index e6f50d0..3538cd0 100644
--- a/compose/material3/material3-common/api/current.txt
+++ b/compose/material3/material3-common/api/current.txt
@@ -1 +1,16 @@
// Signature format: 4.0
+package androidx.compose.material3.common {
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-common API is experimental and is likely to change or to " + "be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3CommonApi {
+ }
+
+ public final class InteractiveComponentSizeKt {
+ method @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumInteractiveComponentEnforcement();
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumTouchTargetEnforcement();
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
+ property @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
+ property @Deprecated @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
+ }
+
+}
+
diff --git a/compose/material3/material3-common/api/restricted_current.txt b/compose/material3/material3-common/api/restricted_current.txt
index e6f50d0..3538cd0 100644
--- a/compose/material3/material3-common/api/restricted_current.txt
+++ b/compose/material3/material3-common/api/restricted_current.txt
@@ -1 +1,16 @@
// Signature format: 4.0
+package androidx.compose.material3.common {
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-common API is experimental and is likely to change or to " + "be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3CommonApi {
+ }
+
+ public final class InteractiveComponentSizeKt {
+ method @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumInteractiveComponentEnforcement();
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumTouchTargetEnforcement();
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
+ property @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
+ property @Deprecated @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
+ }
+
+}
+
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index 23b5dba..19fc1a7 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -35,6 +35,14 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
+
+ api(project(":compose:foundation:foundation"))
+ api(project(":compose:foundation:foundation-layout"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui-graphics"))
+ api(project(":compose:ui:ui-text"))
+
+ implementation(project(":compose:ui:ui-util"))
}
}
androidMain.dependencies {
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/ExperimentalMaterial3CommonApi.kt b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/ExperimentalMaterial3CommonApi.kt
new file mode 100644
index 0000000..09e229a
--- /dev/null
+++ b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/ExperimentalMaterial3CommonApi.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.compose.material3.common
+
+@RequiresOptIn(
+ "This material3-common API is experimental and is likely to change or to " +
+ "be removed in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalMaterial3CommonApi
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/InteractiveComponentSize.kt b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/InteractiveComponentSize.kt
new file mode 100644
index 0000000..6cb3de9
--- /dev/null
+++ b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/InteractiveComponentSize.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.compose.material3.common
+
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.currentValueOf
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * Reserves at least 48.dp in size to disambiguate touch interactions if the element would measure
+ * smaller.
+ *
+ * https://m3.material.io/foundations/accessible-design/accessibility-basics
+ *
+ * This uses the Material recommended minimum size of 48.dp x 48.dp, which may not the same as the
+ * system enforced minimum size. The minimum clickable / touch target size (48.dp by default) is
+ * controlled by the system via ViewConfiguration and automatically expanded at the touch input
+ * layer.
+ *
+ * This modifier is not needed for touch target expansion to happen. It only affects layout, to make
+ * sure there is adequate space for touch target expansion.
+ */
+@Stable
+fun Modifier.minimumInteractiveComponentSize(): Modifier = this then MinimumInteractiveModifier
+
+internal object MinimumInteractiveModifier :
+ ModifierNodeElement<MinimumInteractiveModifierNode>() {
+
+ override fun create(): MinimumInteractiveModifierNode = MinimumInteractiveModifierNode()
+
+ override fun update(node: MinimumInteractiveModifierNode) {}
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "minimumInteractiveComponentSize"
+ // TODO: b/214589635 - surface this information through the layout inspector in a better way
+ // - for now just add some information to help developers debug what this size represents.
+ properties["README"] = "Reserves at least 48.dp in size to disambiguate touch " +
+ "interactions if the element would measure smaller"
+ }
+
+ override fun hashCode(): Int = System.identityHashCode(this)
+ override fun equals(other: Any?) = (other === this)
+}
+
+internal class MinimumInteractiveModifierNode :
+ Modifier.Node(),
+ CompositionLocalConsumerModifierNode,
+ LayoutModifierNode {
+
+ @OptIn(ExperimentalMaterial3CommonApi::class)
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val size = minimumInteractiveComponentSize
+ val placeable = measurable.measure(constraints)
+ val enforcement = isAttached && currentValueOf(LocalMinimumInteractiveComponentEnforcement)
+
+ // Be at least as big as the minimum dimension in both dimensions
+ val width = if (enforcement) {
+ maxOf(placeable.width, size.width.roundToPx())
+ } else {
+ placeable.width
+ }
+ val height = if (enforcement) {
+ maxOf(placeable.height, size.height.roundToPx())
+ } else {
+ placeable.height
+ }
+
+ return layout(width, height) {
+ val centerX = ((width - placeable.width) / 2f).roundToInt()
+ val centerY = ((height - placeable.height) / 2f).roundToInt()
+ placeable.place(centerX, centerY)
+ }
+ }
+}
+
+/**
+ * CompositionLocal that configures whether Material components that have a visual size that is
+ * lower than the minimum touch target size for accessibility (such as Button) will include
+ * extra space outside the component to ensure that they are accessible. If set to false there
+ * will be no extra space, and so it is possible that if the component is placed near the edge of
+ * a layout / near to another component without any padding, there will not be enough space for
+ * an accessible touch target.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalMaterial3CommonApi
+@ExperimentalMaterial3CommonApi
+val LocalMinimumInteractiveComponentEnforcement: ProvidableCompositionLocal<Boolean> =
+ staticCompositionLocalOf { true }
+
+/**
+ * CompositionLocal that configures whether Material components that have a visual size that is
+ * lower than the minimum touch target size for accessibility (such as [Button]) will include
+ * extra space outside the component to ensure that they are accessible. If set to false there
+ * will be no extra space, and so it is possible that if the component is placed near the edge of
+ * a layout / near to another component without any padding, there will not be enough space for
+ * an accessible touch target.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalMaterial3CommonApi
+@ExperimentalMaterial3CommonApi
+@Deprecated(
+ message = "Use LocalMinimumInteractiveComponentEnforcement instead.",
+ replaceWith = ReplaceWith(
+ "LocalMinimumInteractiveComponentEnforcement"
+ ),
+ level = DeprecationLevel.WARNING
+)
+val LocalMinimumTouchTargetEnforcement: ProvidableCompositionLocal<Boolean> =
+ LocalMinimumInteractiveComponentEnforcement
+
+private class MinimumInteractiveComponentSizeModifier(val size: DpSize) : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+
+ val placeable = measurable.measure(constraints)
+
+ // Be at least as big as the minimum dimension in both dimensions
+ val width = maxOf(placeable.width, size.width.roundToPx())
+ val height = maxOf(placeable.height, size.height.roundToPx())
+
+ return layout(width, height) {
+ val centerX = ((width - placeable.width) / 2f).roundToInt()
+ val centerY = ((height - placeable.height) / 2f).roundToInt()
+ placeable.place(centerX, centerY)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ val otherModifier = other as? MinimumInteractiveComponentSizeModifier ?: return false
+ return size == otherModifier.size
+ }
+
+ override fun hashCode(): Int {
+ return size.hashCode()
+ }
+}
+
+private val minimumInteractiveComponentSize: DpSize = DpSize(48.dp, 48.dp)
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/androidx-compose-material3-material3-common-documentation.md b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/androidx-compose-material3-material3-common-documentation.md
similarity index 100%
rename from compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/androidx-compose-material3-material3-common-documentation.md
rename to compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/androidx-compose-material3-material3-common-documentation.md