diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt index a8d9fd9d0..e96a65746 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt @@ -595,6 +595,7 @@ public fun rememberSwipeableStateFor( */ @ExperimentalMaterial3Api @InternalSparkApi +@Suppress("ComposeModifierComposed") // Fork from M3 swipeable that will be removed once the api is stable // This is a copy from Material 3 Swipeable until b/163132293 is closed // Until this is fixed use it when you really cannot use a BottomSheetDialogFragment public fun Modifier.swipeable( @@ -607,7 +608,7 @@ public fun Modifier.swipeable( thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) }, resistance: ResistanceConfig? = resistanceConfig(anchors.keys), velocityThreshold: Dp = VelocityThreshold, -): Modifier = composed( +): Modifier = this then composed( inspectorInfo = debugInspectorInfo { name = "swipeable" properties["state"] = state diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/placeholder/Placeholder.kt b/spark/src/main/kotlin/com/adevinta/spark/components/placeholder/Placeholder.kt index 413d66158..751f34e12 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/placeholder/Placeholder.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/placeholder/Placeholder.kt @@ -19,6 +19,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +@file:Suppress("ComposeModifierComposed") // These modifiers will be forked so the +// work will be done when it happens + package com.adevinta.spark.components.placeholder import androidx.compose.foundation.layout.Arrangement.spacedBy diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/popover/newapi/Material3Tooltip.kt b/spark/src/main/kotlin/com/adevinta/spark/components/popover/newapi/Material3Tooltip.kt index dae45b127..a3f75397f 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/popover/newapi/Material3Tooltip.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/popover/newapi/Material3Tooltip.kt @@ -445,6 +445,7 @@ public interface TooltipState : BasicTooltipState { public val transition: MutableTransitionState } +@Suppress("ComposeModifierComposed") // Fork from M3 Tooltip that will be removed once the api is stable private fun Modifier.textVerticalPadding( subheadExists: Boolean, actionExists: Boolean, @@ -458,6 +459,7 @@ private fun Modifier.textVerticalPadding( } } +@Suppress("ComposeModifierComposed") // Fork from M3 Tooltip that will be removed once the api is stable private fun Modifier.animateTooltip( transition: Transition, ): Modifier = composed( diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/surface/ElevationOverlay.kt b/spark/src/main/kotlin/com/adevinta/spark/components/surface/ElevationOverlay.kt index cb5cae45f..481dd892e 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/surface/ElevationOverlay.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/surface/ElevationOverlay.kt @@ -24,6 +24,7 @@ package com.adevinta.spark.components.surface import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver @@ -56,6 +57,7 @@ public val LocalElevationOverlay: ProvidableCompositionLocal * See [LocalElevationOverlay] to provide your own [ElevationOverlay]. You can provide `null` * to have no ElevationOverlay applied. */ +@Stable public fun interface ElevationOverlay { /** * Returns the new background [Color] to use, representing the original background [color] diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/tab/TabGroupDefaults.kt b/spark/src/main/kotlin/com/adevinta/spark/components/tab/TabGroupDefaults.kt index 0464bc985..11ae36ebb 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/tab/TabGroupDefaults.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/tab/TabGroupDefaults.kt @@ -86,9 +86,10 @@ internal object TabGroupDefaults { * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to * calculate the offset of the indicator this modifier is applied to, as well as its width. */ + @Suppress("ComposeModifierComposed") // Fork from M3 Tab that will be removed once the api is stable internal fun Modifier.tabIndicatorOffset( currentTabPosition: TabPosition, - ): Modifier = composed( + ): Modifier = this then composed( inspectorInfo = debugInspectorInfo { name = "tabIndicatorOffset" value = currentTabPosition diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/AddonScope.kt b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/AddonScope.kt index 1ae3b2fc4..458f0f158 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/AddonScope.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/AddonScope.kt @@ -43,6 +43,7 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconToggleButtonColors import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -79,6 +80,7 @@ import androidx.compose.material3.FilledTonalButton as MaterialButton /** * Scope that provide pre-made addons for leading and trailing contents of [TextField]. */ +@Stable public abstract class AddonScope { /** diff --git a/spark/src/main/kotlin/com/adevinta/spark/tokens/Font.kt b/spark/src/main/kotlin/com/adevinta/spark/tokens/Font.kt index 714523846..6eceff903 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/tokens/Font.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/tokens/Font.kt @@ -22,6 +22,7 @@ package com.adevinta.spark.tokens import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.text.font.Font @@ -52,6 +53,7 @@ public fun sparkFontFamily( /** * Utility class to handle the change of font family for the SparkTheme. */ +@Immutable public class SparkFontFamily( private val isLegacy: Boolean, private val useSparkTokensHighlighter: Boolean, diff --git a/spark/src/main/kotlin/com/adevinta/spark/tokens/Layout.kt b/spark/src/main/kotlin/com/adevinta/spark/tokens/Layout.kt index 25ba1809d..992e7347a 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/tokens/Layout.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/tokens/Layout.kt @@ -82,6 +82,8 @@ public object Layout { /** * Support wide screen by making the content width max 840dp, centered horizontally. */ +@Suppress("ComposeModifierComposed") // WindowInsets.systemBars is internally a LocalComposition but we +// can't access it to use it in the Node API. public fun Modifier.bodyWidth(): Modifier = fillMaxWidth() .composed { windowInsetsPadding( diff --git a/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/TouchTarget.kt b/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/TouchTarget.kt index 3f81b1c72..8cb6cbab2 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/TouchTarget.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/TouchTarget.kt @@ -34,40 +34,57 @@ import androidx.compose.ui.unit.dp import kotlin.math.roundToInt /** - * Ensure that the composable has a minimum touch target size + * Reserves at least 44.dp in size to disambiguate touch interactions if the element would measure + * smaller. * - * Duplicate of Material minimumTouchTargetSize() since it's internal on their side + * https://m3.material.io/foundations/accessible-design/accessibility-basics#28032e45-c598-450c-b355-f9fe737b1cd8 + * + * This uses the Spark recommended minimum size of 44.dp x 44.dp, which may not be the same as the + * system enforced minimum size. The minimum clickable / touch target size (44.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. */ public fun Modifier.minimumTouchTargetSize(): Modifier = this then MinimumTouchTargetModifier() private class MinimumTouchTargetModifier : ModifierNodeElement() { override fun create() = MinimumTouchTargetModifierNode() - override fun equals(other: Any?): Boolean = true + override fun equals(other: Any?) = (other === this) - override fun hashCode(): Int = 0 + override fun hashCode(): Int = System.identityHashCode(this) override fun update(node: MinimumTouchTargetModifierNode) = Unit override fun InspectorInfo.inspectableProperties() { name = "minimumTouchTargetSize" - properties["README"] = "Adds outer padding to measure at least 48.dp (default) in " + + properties["README"] = "Adds outer padding to measure at least 44.dp (default) in " + "size to disambiguate touch interactions if the element would measure smaller" } } private class MinimumTouchTargetModifierNode : Modifier.Node(), LayoutModifierNode { - private val minimumTouchTargetSize = DpSize(44.dp, 44.dp) override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { + val size = minimumTouchTargetSize val placeable = measurable.measure(constraints) + val enforcement = isAttached // Be at least as big as the minimum dimension in both dimensions - val width = maxOf(placeable.width, minimumTouchTargetSize.width.roundToPx()) - val height = maxOf(placeable.height, minimumTouchTargetSize.height.roundToPx()) + 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() @@ -76,3 +93,5 @@ private class MinimumTouchTargetModifierNode : Modifier.Node(), LayoutModifierNo } } } + +private val minimumTouchTargetSize = DpSize(44.dp, 44.dp) diff --git a/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/UsageOverlay.kt b/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/UsageOverlay.kt index ae891a8e0..84a145b24 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/UsageOverlay.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/UsageOverlay.kt @@ -26,9 +26,14 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.adevinta.spark.LocalHighlightComponents @@ -37,14 +42,31 @@ import com.adevinta.spark.LocalHighlightComponents * A [Modifier.Element] that adds a draw layer to identify spark components easily. */ @Stable -internal fun Modifier.sparkUsageOverlay(overlayColor: Color = Color.Red): Modifier = composed { - this then if (LocalHighlightComponents.current) { - Modifier.drawWithContent { - drawContent() +internal fun Modifier.sparkUsageOverlay(overlayColor: Color = Color.Red): Modifier = + this then SparkUsageOverlayElement(overlayColor) + +private class SparkUsageOverlay( + var overlayColor: Color = Color.Red, +) : Modifier.Node(), + DrawModifierNode, + CompositionLocalConsumerModifierNode { + override fun ContentDrawScope.draw() { + drawContent() + if (currentValueOf(LocalHighlightComponents)) { drawRect(color = overlayColor, alpha = 0.5f) } - } else { - Modifier + } +} + +private class SparkUsageOverlayElement(val overlayColor: Color) : ModifierNodeElement() { + override fun create() = SparkUsageOverlay(overlayColor) + override fun update(node: SparkUsageOverlay) { + node.overlayColor = overlayColor + } + override fun hashCode() = System.identityHashCode(this) + override fun equals(other: Any?) = (other === this) + override fun InspectorInfo.inspectableProperties() { + name = "Spark Component" } } diff --git a/spark/src/main/res/values-fr/strings.xml b/spark/src/main/res/values-fr/strings.xml index 0af4839c5..9106992b1 100644 --- a/spark/src/main/res/values-fr/strings.xml +++ b/spark/src/main/res/values-fr/strings.xml @@ -33,12 +33,12 @@ Note de %1$.1f pour %2$d avis Note de %1$.1f pour %2$d avis - Note de %1$.1f pour %2$d avis + Note de %1$.1f pour %2$d d’avis %d avis %d avis - %d avis + %d d’avis Note de %1$.1f @@ -50,7 +50,7 @@ %d nouvelle notification %d nouvelles notifications - %d nouvelles notifications + %d de nouvelles notifications Les Meilleures pratiques pour les textes sur Android diff --git a/spark/src/main/res/values/strings.xml b/spark/src/main/res/values/strings.xml index 0dd2ef31f..5f8f466ff 100644 --- a/spark/src/main/res/values/strings.xml +++ b/spark/src/main/res/values/strings.xml @@ -41,8 +41,7 @@ Profile photo - More than %1$d new notifications + More than %1$d new notifications New notification %d new notification