diff --git a/.github/actions/setup-gradle/action.yml b/.github/actions/setup-gradle/action.yml index 111dd7880..b8b36097e 100644 --- a/.github/actions/setup-gradle/action.yml +++ b/.github/actions/setup-gradle/action.yml @@ -3,7 +3,7 @@ description: 'Gradle setup and wrapper validation' runs: using: composite steps: - - uses: gradle/actions/setup-gradle@6cec5d49d4d6d4bb982fbed7047db31ea6d38f11 # v3.3.0 + - uses: gradle/actions/setup-gradle@db19848a5fa7950289d3668fb053140cf3028d43 # v3.3.2 with: validate-wrappers: true gradle-home-cache-cleanup: true diff --git a/CHANGELOG.md b/CHANGELOG.md index ff80d24d6..2fe34e4c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,55 @@ ## [Unreleased] +## [0.9.0] + +_2024-04-23_ + ### Spark -* 🎨 Add a new intent param to the `Popover` +#### πŸ†• Chips can now be selectable and closed +> [!CAUTION] +> The `Filled` style has been removed and may break your build if used. You need to see with your ui to know which styles to use instead of this one + +> [!WARNING] +> The styles for chips have been deprecated you now need to use either the `Chip` or the `ChipSelectable` components for your need and provide the style in argument + +If you want to make your Chip closable then you will need to add a callback action in the new `onClose` parameter. + +--- + +#### πŸ†• BottomSheet now use the spark specs +> [!CAUTION] +> This change will most likely break your build since most of the api has changed. +> We now use the M3 `BottomSheet` instead of a fork from a alpha version of it we did when it was only available in M2. + +> [!WARNING] +> The `BottomSheet` currently only accept M3 snackbars, you won't be able to display a SparkSnackbar + +--- + +- πŸ†• ProgressTracker is now available! it still has a few minor visual bugs but it can be tested by squads on their scope don't hesitate to give us feedbacks! +- πŸ†• `TextLinkButton` will now use `LocalContentColor` when using the Surface intent. This will allow you to have a `onSurface` `TextLink` when needed +- πŸ†• `Popover` can now take an intent for its surface color +- πŸ†• `Image` has its `emptyIcon` and `errorIcon` parameters open now for special cases +- πŸ’¬ A11y have been translated to german +- πŸ’„ `Rating` will now have a lisible color when disabled +- πŸ’„ Badge now use surface instead of onColor for its border color +- πŸ› Filled and Contrast `Button` now have a clear disabled state when their content color is dark +- πŸ’„ New icons have been added ### Catalog App +- 🎨 Brand colors has been updated to their latest values +- πŸ”§ All Configurators are now scrollable + +### CI + +- πŸ”§ Decorrelated spotless and ktlint +- πŸ†• Added Paparazzi as a manual workflow +- πŸ†• Ran Lava Vulnerability Scanner on CI workflow +- πŸ”§ Moved code formatting tasks first in the contributing list + ## [0.8.0] _2024-02-28_ @@ -220,7 +263,9 @@ _2023-03-29_ -[Unreleased]: https://github.com/adevinta/spark-android/compare/0.8.0...HEAD +[Unreleased]: https://github.com/adevinta/spark-android/compare/0.9.0...HEAD + +[0.9.0]: https://github.com/adevinta/spark-android/releases/tag/0.9.0 [0.8.0]: https://github.com/adevinta/spark-android/releases/tag/0.8.0 diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/backdrop/Backdrop.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/backdrop/Backdrop.kt index f54fdc068..ad65a4ca4 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/backdrop/Backdrop.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/backdrop/Backdrop.kt @@ -64,10 +64,10 @@ import androidx.compose.ui.zIndex import com.adevinta.spark.SparkTheme import com.adevinta.spark.catalog.backdrop.BackdropValue.Concealed import com.adevinta.spark.catalog.backdrop.BackdropValue.Revealed -import com.adevinta.spark.components.bottomsheet.PreUpPostDownNestedScrollConnection -import com.adevinta.spark.components.bottomsheet.SwipeableDefaults -import com.adevinta.spark.components.bottomsheet.SwipeableState -import com.adevinta.spark.components.bottomsheet.swipeable +import com.adevinta.spark.components.bottomsheet.layout.PreUpPostDownNestedScrollConnection +import com.adevinta.spark.components.bottomsheet.layout.SwipeableDefaults +import com.adevinta.spark.components.bottomsheet.layout.SwipeableState +import com.adevinta.spark.components.bottomsheet.layout.swipeable import com.adevinta.spark.components.snackbars.SnackbarHost import com.adevinta.spark.components.snackbars.SnackbarHostState import com.adevinta.spark.components.surface.Surface diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/configurator/samples/bottomsheet/BottomSheetConfigurator.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/configurator/samples/bottomsheet/BottomSheetConfigurator.kt new file mode 100644 index 000000000..726073758 --- /dev/null +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/configurator/samples/bottomsheet/BottomSheetConfigurator.kt @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2023-2024 Adevinta + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.adevinta.spark.catalog.configurator.samples.bottomsheet + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.adevinta.spark.SparkTheme +import com.adevinta.spark.catalog.model.Configurator +import com.adevinta.spark.catalog.util.SampleSourceUrl +import com.adevinta.spark.components.bottomsheet.BottomSheet +import com.adevinta.spark.components.bottomsheet.DragHandle +import com.adevinta.spark.components.buttons.ButtonFilled +import com.adevinta.spark.components.buttons.ButtonIntent +import com.adevinta.spark.components.icons.Icon +import com.adevinta.spark.components.image.Illustration +import com.adevinta.spark.components.image.Image +import com.adevinta.spark.components.list.ListItem +import com.adevinta.spark.components.menu.DropdownMenuItem +import com.adevinta.spark.components.spacer.VerticalSpacer +import com.adevinta.spark.components.text.Text +import com.adevinta.spark.components.text.TextLinkButton +import com.adevinta.spark.components.textfields.SelectTextField +import com.adevinta.spark.components.toggles.SwitchLabelled +import com.adevinta.spark.icons.LikeFill +import com.adevinta.spark.icons.SparkIcons +import com.adevinta.spark.icons.Store +import com.adevinta.spark.tokens.highlight +import kotlinx.coroutines.launch + +public val BottomSheetConfigurator: Configurator = Configurator( + name = "BottomSheet", + description = "BottomSheet configuration", + sourceUrl = "$SampleSourceUrl/BottomSheetSamples.kt", +) { + BottomSheetSample() +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ColumnScope.BottomSheetSample() { + var isDragHandlerEnabled by remember { mutableStateOf(true) } + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + var skipPartiallyExpanded by remember { mutableStateOf(false) } + var bottomSheetContentExample by remember { mutableStateOf(BottomSheetContentExamples.Text) } + + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + ) + val scope = rememberCoroutineScope() + + SwitchLabelled( + checked = isDragHandlerEnabled, + onCheckedChange = { + isDragHandlerEnabled = it + }, + ) { + Text( + text = "Show Drag Handle", + modifier = Modifier.fillMaxWidth(), + ) + } + + SwitchLabelled( + checked = skipPartiallyExpanded, + onCheckedChange = { + skipPartiallyExpanded = it + }, + ) { + Text( + text = "Skip Partially Expanded", + modifier = Modifier.fillMaxWidth(), + ) + } + + val contentExamples = BottomSheetContentExamples.entries.toTypedArray() + var expanded by remember { mutableStateOf(false) } + SelectTextField( + modifier = Modifier.fillMaxWidth(), + value = bottomSheetContentExample.name, + onValueChange = {}, + readOnly = true, + label = "BottomSheet Content Example", + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + onDismissRequest = { expanded = false }, + dropdownContent = { + contentExamples.forEach { + DropdownMenuItem( + text = { Text(it.name) }, + onClick = { + bottomSheetContentExample = it + expanded = false + }, + ) + } + }, + ) + + VerticalSpacer(24.dp) + + ButtonFilled( + text = "Show BottomSheet", + onClick = { openBottomSheet = !openBottomSheet }, + ) + + ConfiguredBottomSheet( + bottomSheetContentExample = bottomSheetContentExample, + openBottomSheet = openBottomSheet, + isDragHandlerEnabled = isDragHandlerEnabled, + onDismissRequest = { openBottomSheet = false }, + onHideBottomSheetClicked = { + scope.launch { bottomSheetState.hide() }.invokeOnCompletion { + if (!bottomSheetState.isVisible) { + openBottomSheet = false + } + } + }, + bottomSheetState = bottomSheetState, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ConfiguredBottomSheet( + bottomSheetContentExample: BottomSheetContentExamples, + openBottomSheet: Boolean, + isDragHandlerEnabled: Boolean, + onDismissRequest: () -> Unit, + onHideBottomSheetClicked: () -> Unit, + bottomSheetState: SheetState, +) { + if (openBottomSheet) { + BottomSheet( + dragHandle = if (isDragHandlerEnabled) { + { DragHandle() } + } else { + null + }, + onDismissRequest = onDismissRequest, + sheetState = bottomSheetState, + ) { + when (bottomSheetContentExample) { + BottomSheetContentExamples.Text -> TextContent() + BottomSheetContentExamples.Image -> ImageContent() + BottomSheetContentExamples.Illustration -> IllustrationContent() + BottomSheetContentExamples.List -> ListContent(onHideBottomSheetClicked) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ListContent(onHideBottomSheetClicked: () -> Unit) { + LazyColumn { + stickyHeader { + Row(horizontalArrangement = Arrangement.Center) { + // Note: If you provide logic outside of onDismissRequest to remove the sheet, + // you must additionally handle intended state cleanup, if any. + ButtonFilled( + modifier = Modifier.padding(24.dp), + text = "Hide Bottom Sheet", + onClick = onHideBottomSheetClicked, + ) + } + } + + items(50) { + ListItem( + headlineContent = { Text("Item $it") }, + leadingContent = { + Icon( + SparkIcons.LikeFill, + contentDescription = "Localized description", + ) + }, + ) + } + } +} + +@Composable +private fun TextContent() { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Title", + modifier = Modifier.padding(bottom = 16.dp), + style = SparkTheme.typography.headline1.highlight, + ) + Text( + text = "Do you want to have this cookie now?", + modifier = Modifier.padding(bottom = 16.dp), + style = SparkTheme.typography.body2.highlight, + ) + TextLinkButton( + text = "Text Link", + onClick = {}, + intent = ButtonIntent.Alert, + ) + } +} + +@Composable +private fun ImageContent() { + Box( + contentAlignment = Alignment.TopCenter, + ) { + Image( + modifier = Modifier + .height(500.dp) + .fillMaxWidth(), + model = "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } +} + +@Composable +private fun IllustrationContent() { + Illustration( + sparkIcon = SparkIcons.Store, + contentDescription = null, + modifier = Modifier.size(100.dp), + ) +} + +public enum class BottomSheetContentExamples { + Text, + Image, + Illustration, + List, +} diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/example/Example.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/example/Example.kt index 6bf0b494e..b20fbea4b 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/example/Example.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/example/Example.kt @@ -31,9 +31,13 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.adevinta.spark.catalog.model.Example +import com.adevinta.spark.components.scaffold.Scaffold +import com.adevinta.spark.components.snackbars.SnackbarHost +import com.adevinta.spark.components.snackbars.SnackbarHostState import com.adevinta.spark.tokens.Layout @Composable @@ -41,18 +45,22 @@ public fun Example( example: Example, ) { val scrollState = rememberScrollState() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(top = 32.dp) - .padding(horizontal = Layout.bodyMargin) - .imePadding(), + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { - with(example) { - this@Column.content() + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(top = 32.dp) + .padding(horizontal = Layout.bodyMargin) + .imePadding(), + ) { + with(example) { + this@Column.content(snackbarHostState) + } } } } diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/bottomsheet/BottomSheetExamples.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/bottomsheet/BottomSheetExamples.kt new file mode 100644 index 000000000..f6a9d89a4 --- /dev/null +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/bottomsheet/BottomSheetExamples.kt @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2023 Adevinta + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.adevinta.spark.catalog.examples.samples.bottomsheet + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.adevinta.spark.SparkTheme +import com.adevinta.spark.catalog.configurator.samples.bottomsheet.BottomSheetContentExamples +import com.adevinta.spark.catalog.model.Example +import com.adevinta.spark.catalog.util.SparkSampleSourceUrl +import com.adevinta.spark.components.bottomsheet.BottomSheet +import com.adevinta.spark.components.bottomsheet.DragHandle +import com.adevinta.spark.components.buttons.ButtonFilled +import com.adevinta.spark.components.buttons.ButtonSize +import com.adevinta.spark.components.icons.Icon +import com.adevinta.spark.components.image.Illustration +import com.adevinta.spark.components.image.Image +import com.adevinta.spark.components.list.ListItem +import com.adevinta.spark.components.spacer.VerticalSpacer +import com.adevinta.spark.components.text.Text +import com.adevinta.spark.components.text.TextLinkButton +import com.adevinta.spark.icons.LikeFill +import com.adevinta.spark.icons.SparkIcons +import com.adevinta.spark.icons.Store +import com.adevinta.spark.tokens.highlight +import kotlinx.coroutines.launch + +private const val BottomSheetExampleSourceUrl = "$SparkSampleSourceUrl/bottomsheet/BottomSheetExamples.kt" + +@OptIn(ExperimentalMaterial3Api::class) +public val BottomSheetExamples: List = listOf( + Example( + name = "BottomSheet List Content", + description = "BottomSheet List Content", + sourceUrl = BottomSheetExampleSourceUrl, + ) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false, + ) + VerticalSpacer(24.dp) + ConfiguredBottomSheet( + bottomSheetContentExample = BottomSheetContentExamples.List, + isDragHandlerEnabled = true, + bottomSheetState = bottomSheetState, + ) + }, + Example( + name = "BottomSheet List Content", + description = "BottomSheet List Content, no drag handle", + sourceUrl = BottomSheetExampleSourceUrl, + ) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false, + ) + VerticalSpacer(24.dp) + ConfiguredBottomSheet( + bottomSheetContentExample = BottomSheetContentExamples.List, + isDragHandlerEnabled = false, + bottomSheetState = bottomSheetState, + ) + }, + Example( + name = "BottomSheet List Content", + description = "BottomSheet List Content fully expanded", + sourceUrl = BottomSheetExampleSourceUrl, + ) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + VerticalSpacer(24.dp) + ConfiguredBottomSheet( + bottomSheetContentExample = BottomSheetContentExamples.List, + isDragHandlerEnabled = true, + bottomSheetState = bottomSheetState, + ) + }, + Example( + name = "BottomSheet Text Content", + description = "BottomSheet Text Content", + sourceUrl = BottomSheetExampleSourceUrl, + ) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false, + ) + VerticalSpacer(24.dp) + ConfiguredBottomSheet( + bottomSheetContentExample = BottomSheetContentExamples.Text, + isDragHandlerEnabled = true, + bottomSheetState = bottomSheetState, + ) + }, + Example( + name = "BottomSheet Image Content", + description = "BottomSheet Image Content", + sourceUrl = BottomSheetExampleSourceUrl, + ) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false, + ) + VerticalSpacer(24.dp) + ConfiguredBottomSheet( + bottomSheetContentExample = BottomSheetContentExamples.Image, + isDragHandlerEnabled = true, + bottomSheetState = bottomSheetState, + ) + }, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ConfiguredBottomSheet( + bottomSheetContentExample: BottomSheetContentExamples, + isDragHandlerEnabled: Boolean, + bottomSheetState: SheetState, +) { + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + val onDismissRequest: () -> Unit = { openBottomSheet = false } + + val onHideBottomSheetClicked: () -> Unit = { + scope.launch { bottomSheetState.hide() }.invokeOnCompletion { + if (!bottomSheetState.isVisible) { + openBottomSheet = false + } + } + } + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + ButtonFilled( + size = ButtonSize.Large, + text = "Show BottomSheet", + onClick = { openBottomSheet = !openBottomSheet }, + ) + } + if (openBottomSheet) { + BottomSheet( + content = { + when (bottomSheetContentExample) { + BottomSheetContentExamples.Text -> TextContent() + BottomSheetContentExamples.Image -> ImageContent() + BottomSheetContentExamples.Illustration -> IllustrationContent() + BottomSheetContentExamples.List -> ListContent(onHideBottomSheetClicked) + } + }, + dragHandle = if (isDragHandlerEnabled) { + { DragHandle() } + } else { + null + }, + onDismissRequest = onDismissRequest, + sheetState = bottomSheetState, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ListContent(onHideBottomSheetClicked: () -> Unit) { + LazyColumn { + stickyHeader { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + ButtonFilled( + text = "Hide Bottom Sheet", + onClick = onHideBottomSheetClicked, + ) + } + } + + items(50) { + ListItem( + headlineContent = { Text("Item $it") }, + leadingContent = { + Icon( + SparkIcons.LikeFill, + contentDescription = "Localized description", + ) + }, + ) + } + } +} + +@Composable +private fun TextContent() { + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Text( + text = "Title", + modifier = Modifier.padding(bottom = 16.dp), + style = SparkTheme.typography.headline1.highlight, + ) + Text( + text = "Do you want to have this cookie now?", + modifier = Modifier.padding(bottom = 16.dp), + style = SparkTheme.typography.body2.highlight, + ) + TextLinkButton(text = "Text Link", onClick = {}) + } +} + +@Composable +private fun ImageContent() { + Box( + contentAlignment = Alignment.TopCenter, + ) { + Image( + modifier = Modifier + .height(500.dp) + .fillMaxWidth(), + model = "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } +} + +@Composable +private fun IllustrationContent() { + Illustration( + sparkIcon = SparkIcons.Store, + contentDescription = null, + modifier = Modifier.size(100.dp), + ) +} + +public enum class BottomSheetContentExamples { + Text, + Image, + Illustration, + List, +} diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/progresstracker/ProgressTrackerExamples.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/progresstracker/ProgressTrackerExamples.kt index 7b5403edb..eb1ad3aff 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/progresstracker/ProgressTrackerExamples.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/progresstracker/ProgressTrackerExamples.kt @@ -21,17 +21,13 @@ */ package com.adevinta.spark.catalog.examples.samples.progresstracker -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import com.adevinta.spark.catalog.model.Example @@ -235,17 +231,13 @@ private fun ColumnScope.ProgressTrackerSizes() { selectedStep = selectedStep, ) } - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - ) { + Row { for (size in ProgressSizes.entries) { ProgressTrackerColumn( items = persistentListOf( - ProgressStep("", true), - ProgressStep("", true), - ProgressStep("", false), + ProgressStep("a", true), + ProgressStep("a", true), + ProgressStep("a", false), ), size = size, selectedStep = selectedStep, @@ -270,11 +262,7 @@ private fun ColumnScope.ProgressTrackerColors() { selectedStep = selectedStep, ) } - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - ) { + Row { for (intent in ProgressTrackerIntent.entries) { ProgressTrackerColumn( items = persistentListOf( @@ -305,11 +293,7 @@ private fun ColumnScope.ProgressTrackerStyles() { selectedStep = selectedStep, ) } - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - ) { + Row { for (style in ProgressStyles.entries) { ProgressTrackerColumn( items = persistentListOf( diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/text/TextLinkExamples.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/text/TextLinkExamples.kt index 2e395c518..5a6353a0f 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/text/TextLinkExamples.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/text/TextLinkExamples.kt @@ -24,7 +24,6 @@ package com.adevinta.spark.catalog.examples.samples.text import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.SnackbarDuration -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,9 +32,7 @@ import com.adevinta.spark.SparkTheme import com.adevinta.spark.catalog.R import com.adevinta.spark.catalog.model.Example import com.adevinta.spark.catalog.util.SampleSourceUrl -import com.adevinta.spark.components.scaffold.Scaffold -import com.adevinta.spark.components.snackbars.SnackbarHost -import com.adevinta.spark.components.snackbars.SnackbarHostState +import com.adevinta.spark.components.snackbars.SnackbarColors import com.adevinta.spark.components.text.TextLink import com.adevinta.spark.components.text.TextLinkButton import com.adevinta.spark.icons.Link @@ -50,138 +47,107 @@ public val TextLinksExamples: List = listOf( name = "Link inside title", description = "Link inside title no icon", sourceUrl = TextLinksExampleSourceUrl, - ) { - val snackbarHostState = remember { SnackbarHostState() } + ) { snackbarHostState -> val scope = rememberCoroutineScope() - Scaffold( + Box( + contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), - snackbarHost = { - SnackbarHost(snackbarHostState) - }, ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - TextLink( - style = SparkTheme.typography.subhead, - text = annotatedStringResource(id = R.string.spark_text_link_short_example_), - lineHeight = 40.sp, - onClickLabel = "https://kotlinlang.org", - onClick = { - scope.launch { - snackbarHostState.showSnackbar( - message = "https://kotlinlang.org", - actionLabel = "Action", - duration = SnackbarDuration.Short, - ) - } - }, - ) - } + TextLink( + style = SparkTheme.typography.subhead, + text = annotatedStringResource(id = R.string.spark_text_link_short_example_), + lineHeight = 40.sp, + onClickLabel = "https://kotlinlang.org", + onClick = { + scope.launch { + snackbarHostState.showSnackbar( + message = "https://kotlinlang.org", + actionLabel = "Action", + duration = SnackbarDuration.Short, + colors = SnackbarColors.Default, + ) + } + }, + ) } }, Example( name = "Link inside paragraph", description = "Link inside paragraph no icon", sourceUrl = TextLinksExampleSourceUrl, - ) { - val snackbarHostState = remember { SnackbarHostState() } + ) { snackbarHostState -> val scope = rememberCoroutineScope() - Scaffold( + Box( + contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), - snackbarHost = { - SnackbarHost(snackbarHostState) - }, ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - TextLink( - style = SparkTheme.typography.subhead, - text = annotatedStringResource(id = R.string.spark_text_link_paragraph_example_), - onClickLabel = "textLink", - onClick = { - scope.launch { - snackbarHostState.showSnackbar( - message = "Link Clicked", - actionLabel = "Action", - duration = SnackbarDuration.Short, - ) - } - }, - ) - } + TextLink( + style = SparkTheme.typography.subhead, + text = annotatedStringResource(id = R.string.spark_text_link_paragraph_example_), + onClickLabel = "textLink", + onClick = { + scope.launch { + snackbarHostState.showSnackbar( + message = "Link Clicked", + actionLabel = "Action", + duration = SnackbarDuration.Short, + ) + } + }, + ) } }, Example( name = "Entire text as link no icon", description = "Entire text as link no icon using Text Link Button", sourceUrl = TextLinksExampleSourceUrl, - ) { - val snackbarHostState = remember { SnackbarHostState() } + ) { snackbarHostState -> val scope = rememberCoroutineScope() - Scaffold( + Box( + contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), - snackbarHost = { - SnackbarHost(snackbarHostState) - }, ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - TextLinkButton( - text = "Try out Android Development", - onClick = { - scope.launch { - snackbarHostState.showSnackbar( - message = "Try out Android Development Clicked", - actionLabel = "Action", - duration = SnackbarDuration.Short, - ) - } - }, - ) - } + TextLinkButton( + text = "Try out Android Development", + onClick = { + scope.launch { + snackbarHostState.showSnackbar( + message = "Try out Android Development Clicked", + actionLabel = "Action", + duration = SnackbarDuration.Short, + ) + } + }, + ) } }, Example( name = "Entire text as link with icon", description = "Entire text as link with icon using Text Link Button", sourceUrl = TextLinksExampleSourceUrl, - ) { - val snackbarHostState = remember { SnackbarHostState() } + ) { snackbarHostState -> val scope = rememberCoroutineScope() - Scaffold( + Box( + contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), - snackbarHost = { - SnackbarHost(snackbarHostState) - }, ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - TextLinkButton( - text = "Try out Android Development", - icon = SparkIcons.Link, - onClick = { - scope.launch { - snackbarHostState.showSnackbar( - message = "Try out Android Development Clicked", - actionLabel = "Action", - duration = SnackbarDuration.Short, - ) - } - }, - ) - } + TextLinkButton( + text = "Try out Android Development", + icon = SparkIcons.Link, + onClick = { + scope.launch { + snackbarHostState.showSnackbar( + message = "Try out Android Development Clicked", + actionLabel = "Action", + duration = SnackbarDuration.Short, + ) + } + }, + ) } }, ) diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt index 41b427996..f2c47613a 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt @@ -24,6 +24,7 @@ package com.adevinta.spark.catalog.model import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.adevinta.spark.catalog.R +import com.adevinta.spark.catalog.configurator.samples.bottomsheet.BottomSheetConfigurator import com.adevinta.spark.catalog.configurator.samples.buttons.ButtonsConfigurator import com.adevinta.spark.catalog.configurator.samples.buttons.IconButtonsConfigurator import com.adevinta.spark.catalog.configurator.samples.buttons.IconToggleButtonsConfigurator @@ -40,6 +41,7 @@ import com.adevinta.spark.catalog.configurator.samples.textfields.TextFieldsConf import com.adevinta.spark.catalog.configurator.samples.toggles.CheckboxConfigurator import com.adevinta.spark.catalog.configurator.samples.toggles.RadioButtonConfigurator import com.adevinta.spark.catalog.configurator.samples.toggles.SwitchConfigurator +import com.adevinta.spark.catalog.examples.samples.bottomsheet.BottomSheetExamples import com.adevinta.spark.catalog.examples.samples.buttons.ButtonsExamples import com.adevinta.spark.catalog.examples.samples.buttons.IconButtonsExamples import com.adevinta.spark.catalog.examples.samples.chips.ChipsExamples @@ -160,11 +162,24 @@ private val Popovers = Component( description = R.string.component_popovers_description, guidelinesUrl = "$ComponentGuidelinesUrl/p/88a08c-popover/b/904ceb", docsUrl = "$PackageSummaryUrl/com.adevinta.spark.popover/index.html", - sourceUrl = "$SparkSourceUrl/kotlin/com/adevinta/popover/Color.kt", + sourceUrl = "$SparkSourceUrl/kotlin/com/adevinta/spark/components/popover/Popover.kt", examples = PopoverExamples, configurator = PopoverConfigurator, ) +private val BottomSheets = Component( + id = nextId(), + name = "BottomSheets", + illustration = R.drawable.bottomsheet, + tintIcon = false, + description = R.string.component_bottomsheets_description, + guidelinesUrl = "$ComponentGuidelinesUrl/p/67d41e-bottom-sheet/b/02056b", + docsUrl = "$PackageSummaryUrl/com.adevinta.spark.bottom-sheet/index.html", + sourceUrl = "$SparkSourceUrl/kotlin/com/adevinta/spark/components/bottomsheet/modal/ModalBottomSheet.kt", + examples = BottomSheetExamples, + configurator = BottomSheetConfigurator, +) + private val RadioButtons = Component( id = nextId(), name = "Radio buttons", @@ -314,6 +329,7 @@ public val Components: List = listOf( IconButtons, IconToggleButtons, Popovers, + BottomSheets, Progressbars, ProgressTracker, RadioButtons, diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Example.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Example.kt index e48ceee14..8dd0dd310 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Example.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Example.kt @@ -25,10 +25,11 @@ package com.adevinta.spark.catalog.model import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable +import com.adevinta.spark.components.snackbars.SnackbarHostState public data class Example( val name: String, val description: String, val sourceUrl: String, - val content: @Composable ColumnScope.() -> Unit, + val content: @Composable ColumnScope.(SnackbarHostState) -> Unit, ) diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/themes/themeprovider/leboncoin/Color.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/themes/themeprovider/leboncoin/Color.kt index 0e51520e0..30687c12c 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/themes/themeprovider/leboncoin/Color.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/themes/themeprovider/leboncoin/Color.kt @@ -246,7 +246,6 @@ internal val LeboncoinColorPartLightLegacy: SparkColors = lightSparkColors( onSurface = Grey10, surfaceInverse = Grey20, onSurfaceInverse = Grey95, - surfaceTint = BrikkeOrange, outline = GreyBlue50, outlineHigh = GreyBlue80, dimContent1 = .72f, @@ -311,7 +310,6 @@ internal val LeboncoinColorProLightLegacy: SparkColors = lightSparkColors( onSurface = Grey10, surfaceInverse = Grey20, onSurfaceInverse = Grey95, - surfaceTint = BrikkeOrange, outline = GreyBlue50, outlineHigh = GreyBlue80, dimContent1 = .72f, @@ -376,7 +374,6 @@ internal val LeboncoinColorPartLight: SparkColors = lightSparkColors( onSurface = Blueberry900, surfaceInverse = NightShade800, onSurfaceInverse = Color.White, - surfaceTint = Clementin500, outline = NightShade400, outlineHigh = NightShade900, ) @@ -436,7 +433,6 @@ internal val LeboncoinColorProLight: SparkColors = lightSparkColors( onSurface = Blueberry900, surfaceInverse = NightShade800, onSurfaceInverse = Color.White, - surfaceTint = Clementin500, outline = NightShade400, outlineHigh = NightShade900, ) diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/util/Url.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/util/Url.kt index 2c8376172..e4b43bc70 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/util/Url.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/util/Url.kt @@ -40,7 +40,7 @@ public const val DocsUrl: String = "https://adevinta.github.io/spark-android" public const val SourceUrl: String = "https://github.com/adevinta/spark-android" public const val SparkSourceUrl: String = "https://github.com/adevinta/spark-android/tree/main/spark/src/main" -// Use the real sample url from spark once we have our first ones +public const val SparkSampleSourceUrl: String = "https://github.com/adevinta/spark-android/blob/main/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples" public const val SampleSourceUrl: String = "https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples" public const val PackageSummaryUrl: String = "https://adevinta.github.io/spark-android/spark" diff --git a/catalog/src/main/res/drawable/bottomsheet.xml b/catalog/src/main/res/drawable/bottomsheet.xml new file mode 100644 index 000000000..e24606672 --- /dev/null +++ b/catalog/src/main/res/drawable/bottomsheet.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index 5f1957b47..6c119e0ab 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -65,6 +65,7 @@ The input / text-field component allows users to write in the space provided for the content. The tokens are the basis of this Design System and allow each brand to choose its colors, shapes, typos and icons. Provides a descriptive message or Info for an Anchor. + A bottom sheet is a UI component commonly used in mobile applications to present additional content or options from the bottom of the screen. ⚠️ Theming don’t work on these! These are the previews that the developers have when working on components. They are not exhaustive and are only meant to give you a quick idea of what the component looks like. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 5280a28c9..dd627d3d0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -43,6 +43,10 @@ or install it directly from **Android Studio**: Here is a list of Gradle tasks commonly used in this project: +- Code formatting: run spotless formatter + ```bash + ./gradlew spotlessApply + ``` - Building: assemble all modules in release mode ```bash ./gradlew assembleRelease @@ -59,10 +63,6 @@ Here is a list of Gradle tasks commonly used in this project: ```bash ./gradlew lintRelease ``` -- Code formatting: run spotless formatter - ```bash - ./gradlew spotlessApply - ``` - Deploying: publish all Maven publications to the local Maven cache `~/.m2`. ```bash ./gradlew publishToMavenLocal diff --git a/gradle.properties b/gradle.properties index dcce4b688..5d4f801c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ # SOFTWARE. # -version=0.9.0-SNAPSHOT +version=0.10.0-SNAPSHOT group=com.adevinta.spark org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65778d621..06f8a1f5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] accompanist = "0.34.0" android-gradlePlugin = "8.3.2" -androidx-activity = "1.8.2" +androidx-activity = "1.9.0" androidx-appCompat = "1.6.1" -androidx-compose-bom = "2024.04.00" +androidx-compose-bom = "2024.04.01" androidx-compose-compiler = "1.5.13-dev-k2.0.0-RC1-50f08dfa4b4" androidx-lifecycle = "2.7.0" androidx-navigation = "2.7.7" androidx-savedstate = "1.2.1" -androidx-core = "1.12.0" +androidx-core = "1.13.0" androidx-window = "1.2.0" coil = "2.6.0" compileSdk = "34" -datastore = "1.0.0" +datastore = "1.1.0" dependencyGuard = "0.5.0" dokka = "1.9.20" junit = "4.13.2" diff --git a/spark-icons/dependencies/releaseRuntimeClasspath.txt b/spark-icons/dependencies/releaseRuntimeClasspath.txt index 404f1fc1a..afa90af26 100644 --- a/spark-icons/dependencies/releaseRuntimeClasspath.txt +++ b/spark-icons/dependencies/releaseRuntimeClasspath.txt @@ -10,23 +10,23 @@ androidx.autofill:autofill:1.0.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.runtime:runtime-android:1.6.5 -androidx.compose.runtime:runtime-saveable-android:1.6.5 -androidx.compose.runtime:runtime-saveable:1.6.5 -androidx.compose.runtime:runtime:1.6.5 -androidx.compose.ui:ui-android:1.6.5 -androidx.compose.ui:ui-geometry-android:1.6.5 -androidx.compose.ui:ui-geometry:1.6.5 -androidx.compose.ui:ui-graphics-android:1.6.5 -androidx.compose.ui:ui-graphics:1.6.5 -androidx.compose.ui:ui-text-android:1.6.5 -androidx.compose.ui:ui-text:1.6.5 -androidx.compose.ui:ui-unit-android:1.6.5 -androidx.compose.ui:ui-unit:1.6.5 -androidx.compose.ui:ui-util-android:1.6.5 -androidx.compose.ui:ui-util:1.6.5 -androidx.compose.ui:ui:1.6.5 -androidx.compose:compose-bom:2024.04.00 +androidx.compose.runtime:runtime-android:1.6.6 +androidx.compose.runtime:runtime-saveable-android:1.6.6 +androidx.compose.runtime:runtime-saveable:1.6.6 +androidx.compose.runtime:runtime:1.6.6 +androidx.compose.ui:ui-android:1.6.6 +androidx.compose.ui:ui-geometry-android:1.6.6 +androidx.compose.ui:ui-geometry:1.6.6 +androidx.compose.ui:ui-graphics-android:1.6.6 +androidx.compose.ui:ui-graphics:1.6.6 +androidx.compose.ui:ui-text-android:1.6.6 +androidx.compose.ui:ui-text:1.6.6 +androidx.compose.ui:ui-unit-android:1.6.6 +androidx.compose.ui:ui-unit:1.6.6 +androidx.compose.ui:ui-util-android:1.6.6 +androidx.compose.ui:ui-util:1.6.6 +androidx.compose.ui:ui:1.6.6 +androidx.compose:compose-bom:2024.04.01 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 androidx.core:core:1.12.0 diff --git a/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_content_behind_handle.png b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_content_behind_handle.png new file mode 100644 index 000000000..0868a9469 --- /dev/null +++ b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_content_behind_handle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d4f1930bfb7679ba7241e6059043646a9a5a2fcc1e2bf0c07ed1e5e5cab75f4 +size 2914092 diff --git a/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_dark_no_handle.png b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_dark_no_handle.png new file mode 100644 index 000000000..2b99ac732 --- /dev/null +++ b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_dark_no_handle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c944a34ab78c39f2318ea6b73b8e8cc5d0a59da870c02e6caa5d20d39c1ede7 +size 73518 diff --git a/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_dark_with_handle.png b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_dark_with_handle.png new file mode 100644 index 000000000..b59cd125f --- /dev/null +++ b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_dark_with_handle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:909d6803a93a99d661b661bf609b3b36f6fa4685054aea12b2875877fd784e50 +size 72502 diff --git a/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_light_no_handle.png b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_light_no_handle.png new file mode 100644 index 000000000..046d42733 --- /dev/null +++ b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_light_no_handle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8047547c6df1f9caf658a9fab5810b9deb3449be5730287a65fdea15596d6f56 +size 73946 diff --git a/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_light_with_handle.png b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_light_with_handle.png new file mode 100644 index 000000000..41e1e92d5 --- /dev/null +++ b/spark-screenshot-testing/src/test/snapshots/images/bottomsheet_light_with_handle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7c5fa8b90195ca0d174d994d1d4a96a1d8f5d50f3245c4b7202563df0cb865c +size 72743 diff --git a/spark-screenshot-testing/src/test/snapshots/images/bottomsheetscaffold.png b/spark-screenshot-testing/src/test/snapshots/images/bottomsheetscaffold.png new file mode 100644 index 000000000..5c6e8aca0 --- /dev/null +++ b/spark-screenshot-testing/src/test/snapshots/images/bottomsheetscaffold.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e3e283e610dc57131dbb0756e600a293eff0472c64854539bcbd6b1821df811 +size 33156 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Bath].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Bath].png index d8611fe38..247b83e4e 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Bath].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Bath].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:562208fda8d7255f26a139077b4cd6167d4ad88877031da2488e9afe53304c80 -size 1268 +oid sha256:2dc8751964c6b18a2cb2da6dda5e41901a116cc1f664b292842540e7ccdd4f0b +size 1266 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[ClockOutline].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[ClockOutline].png index a8b72544f..2093b189d 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[ClockOutline].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[ClockOutline].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8743d78de194c27fa4e2f3051c667fdc8ba56fc5926a2875ebfeaf58b4752ee8 -size 3151 +oid sha256:9c4703dce959a5916e672dab8a7c7969e828aec55a65cdf22eb895b164468ad4 +size 3148 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Crop].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Crop].png index d471a6a36..2b5e4ccd4 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Crop].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Crop].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4090b71e511892fa49a601599eb4b816a0b121bbf40d1ba833005ff476041bfe +oid sha256:3b5fe7fad1c04a59576f238111c93e3916ec3ccea9750941f6b1a5c00a6df21e size 938 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DissatisfiedFill].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DissatisfiedFill].png index b02f05ebf..e225e7e45 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DissatisfiedFill].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DissatisfiedFill].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4304bb3d482efb26dbed19043b247b9bbe9351b2d5bf4d6032755280634297d -size 1872 +oid sha256:6f063bde0fca5541a3714ec4d57b1aaff88be576b3168d7ad90540b136c88862 +size 1873 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DumpTruck].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DumpTruck].png index 7810aa275..3a2b0af3c 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DumpTruck].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[DumpTruck].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4561abb10789c13053f766d8017b140361e1da2e08fc549f222f2c93cb70c4ba -size 2584 +oid sha256:477dcaf655ad3d11917c6aee1c494e08c7b93144af974f6964bcfcae435783f8 +size 2585 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[House].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[House].png index 4d331b6ab..d6b3ee60b 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[House].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[House].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:092f9643ff32093680b26cae388d5a908aafb6a0c9acce2e0b4bfc60bf481766 -size 2272 +oid sha256:52892d93e649044ad639d10595a453c522275b13324b01ea1bfed3c02ddf0106 +size 2270 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[MoneyOutline].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[MoneyOutline].png index 2ff1f880d..a9d7e6ab9 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[MoneyOutline].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[MoneyOutline].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c742f3c309b548dbe2000f4108068976d47a0b6f514ac17f9d1e01b2095a9bc3 +oid sha256:24a193f52873d1f4fce399124ae0e7089648dc6b857070e739c3c947580a645a size 2777 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[PauseOutline].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[PauseOutline].png index 593594221..8113dc76a 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[PauseOutline].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[PauseOutline].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f999c7b4d48278161f5b6ca3375eb6600417950fcd10e20e5ce5a719e5d622d3 -size 3418 +oid sha256:4539ee592e2da1e0680385fd4d875c6fe601952f6adaeb2bfa387acb27632bc4 +size 3415 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Pinterest].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Pinterest].png index 153f25ed6..f46118325 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Pinterest].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[Pinterest].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80d8f9b6183610e321062aeaea274be8a8a5a75ae254071e9e1cb151c94dab7d -size 2658 +oid sha256:04c4ae739ad471423e5c7072056175dba566df2af4ced1ad6a97edad4558c98c +size 2657 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[SearchFill].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[SearchFill].png index 0ee8f616c..cd9595316 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[SearchFill].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[SearchFill].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a333246cf444a1d921b2e4ef9d5343685b0cadfe3c54366249afa3258e8e847c -size 3147 +oid sha256:a6fe8579ebf55ae94bfb8debede52fee8f5f323b45bb82e2e327e466fee86218 +size 3144 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[VoiceOffFill].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[VoiceOffFill].png index c88221858..b31464de1 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[VoiceOffFill].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[VoiceOffFill].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fa4b2a0202e61c4eb10d33f2873c5f215ec8be4b27c9d37c289bbc5fabd0c31 +oid sha256:430521b8f0c85171c24e34667d0c2b170d4cc93c4c74fe76ab6fa06932012ff1 size 2235 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[WheelRim].png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[WheelRim].png index 5367d95af..a98757234 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[WheelRim].png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.icons_IconsScreenshot_render[WheelRim].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fdeae50f34e42e6f8344f67da080966a2a20ff77a3636775c6c6ad53a0cd4a3 -size 3561 +oid sha256:760e5b0eb0abda14eaf9297a473b45e44bd896970cbaec6f4c8e5780c5129e18 +size 3560 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__dark.png index d57909797..048f3f0d3 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c57244903abf3c0fc6a386ef9987e549b787837e62a54859a971e6c6abc711dc -size 316396 +oid sha256:c0e714e665b4952a76ff565c2f86632db8415ce98864693eaded92482d5c7329 +size 316624 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__light.png index 985ea257f..43966dcc0 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.progress.tracker_ProgressTrackerColumnScreenshot_themesProgressTrackerColumns__light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ff6eb74665f5e2fd83b1499d030cab21e41ccb84d45d4c9ad43e70621c48bda -size 309577 +oid sha256:d4e32ab444be0ee6ef96b672184f0565293617b3e82af678089c93d39e960ecd +size 309793 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_bigValue.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_bigValue.png index 44fff85d9..b0f4e2364 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_bigValue.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_bigValue.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e073a8a15fe0984a21c43f2d2f3d2b453063f991fff8ec57fb6b8704158d3aa8 -size 170847 +oid sha256:def3f38a55eebf9a60181c7adf5542201cde60d76294aaf02dfaea27f7a3d6e8 +size 170849 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__dark.png index c18eac761..ccc398e0a 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:759ee9821eb70a899db5c869075791c9e16bfddf7290682adf65d4fedcc3ac55 +oid sha256:d64b14dd1c8d45361547db73c13c9303d6988001ceb22efb7c52cbe734f38f05 size 42140 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__light.png index 1d8da7ac1..5c592eca1 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_showcase__light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b3223b27ffbee7ac1063885e7bee685ad8570a411494f4ef04df563099fb547 -size 40035 +oid sha256:6cc6bea80ce843ee164cfa8c10378b7f21db4881a09b20174e7ac952ee4c7df1 +size 40036 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_smallValue.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_smallValue.png index 2b72f1288..b52a2c5d2 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_smallValue.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_MultilineTextFieldScreenshot_smallValue.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eea5fcf298f6b302b7eeae9a5ad0b5bc137f0bc8dadca86e90fbf847412bcd5 -size 128207 +oid sha256:572afb31ba10697f2d447992d0e69387f89c68a495018f7ad676b52f603a6247 +size 128198 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__dark.png index cf6244ef7..4c577c7b8 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7416a15e3aa5f46f6d97bc8a305c34521fbf8c3b0e57047589e45c2c910754f7 +oid sha256:a0ef1d4e1eaab18c125f5114222d67f381143778d300bb575d4f7581d1c32158 size 43771 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__light.png index c4a0c16bd..a100303e2 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark.textfields_TextFieldDocScreenshot_multilineTextFieldShowcase__light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6edd201fed8df60e26bd285230837fa64672d20ed5f5cd9530c77fc121dfcfc6 +oid sha256:3b6fd8d601ee1af12a89f384a5c53ab846a3df6bf5ae1050ebd90c31714c863a size 41679 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_dark.png index 82166e257..3a8b5268e 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d34ec1d4207781d11888727afff1c2e4d3e2de32e29eef1d1d34ff13c4cdc0d -size 117229 +oid sha256:fd6d5fe8f0d057fb50d97d1ecabc2597d43e20a94a38f31a27673744e61b4fd3 +size 110470 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_light.png index 69f2efaac..f39e67c38 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgedboxwithstroke_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85323fcf16312869aaa44d6fac1953e53ce73b4040860a0014f0478be1da448a -size 119886 +oid sha256:1a3af6f9cfbda408be2fefd7a39cf52c3de42f40326b58d6b7abfd7a2d06f4a5 +size 109637 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_dark.png index 881500ad6..99ef77a10 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26f7ee52abc18e439fb5807765c09633391b2b2665ccc2c26f681042402aeec2 -size 124212 +oid sha256:5ca03b988cf691468094cc9c25f2721733a37ff499b7b72e48bb1db8590c2a6c +size 121032 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_light.png index 9c0ad5a99..8078ac57d 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_badge_badgewithstroke_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ee33d71f16b9d14c462407c8184b0767e138455589db90fc25603252c87907f -size 134275 +oid sha256:909352065013a57a414b17a1e203105f2ed6536e7ba38386559c6076075644d7 +size 127765 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_bottomsheet_bottomsheetscaffold.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_bottomsheet_bottomsheetscaffold.png index 3ce9ab8bc..4dbcbe1bb 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_bottomsheet_bottomsheetscaffold.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_bottomsheet_bottomsheetscaffold.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:272d7b7cfc5706bdd1a575953abec6364ecd25d9846152e46e538d4a766b0785 -size 9648 +oid sha256:1a6be424afd59b7ce92eedc4f0d96500ec8f284429bb7d552f50aaeb7d0f7dee +size 32719 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_dark.png index 3b1b1467c..55d2f5d5b 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89d6021a963390fc59a59047bd547a91f10722fd7c3f974bfdf903bc515fa910 -size 18869 +oid sha256:6786f129fa1e9f41240c1759f853d3c58c9e015e63c677ba1c585e3b64330425 +size 18758 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_light.png index 97312aec1..a20ed523a 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttoncontrast_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47d08c0b5f91b3fc83fa65687d6844e9e985a08e689325d3c07b40a2ac6a4ac4 -size 16856 +oid sha256:844fa632afa4f9b93cf716d1fadb7f1ab46d07d18bcec979aaf1c54a3fae2b46 +size 16713 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttonfilled_dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttonfilled_dark.png index c48b5063c..1ff5566de 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttonfilled_dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_buttons_buttonfilled_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:567932b9c2f4ab09f0994ae19cf33013b8934fabdb15d5a90061a19f6bac8deb -size 18331 +oid sha256:855903b9768d569c3a864f08cf13a6772166a626ba4f8a31a0da808ec678b6aa +size 17866 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_dark.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_dark.png index 0f780d0fd..b98820f69 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_dark.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a15d4a181bc1cdcbf9b26708a4e54afc5891cd70ee1abf674062c4115a53006 -size 612 +oid sha256:0bca70d84a1c43fd1b15b04ebac4accf26a3f256cc3e58202e5616cc1ac527e3 +size 713 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_light.png index 4ae788c9b..9a65202e2 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_defaultgroup_previewdraghandle_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4aeb812efcfa3e8dca6019a1010310bdc683ef1c589be2b507c78336d863e29 -size 614 +oid sha256:c65dbe85557f67a3f73ce91cccfa59332bead327397b68d205a729c9ef0aa077 +size 683 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_dialog_alertdialog_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_dialog_alertdialog_light.png index 5333e1b6b..4fdb99761 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_dialog_alertdialog_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_dialog_alertdialog_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c6e0d798a583118a42c67899adacedbb2422e728a9e3e38601e5ac7b09d95ae -size 32398 +oid sha256:c494d16d2dae4333237ec306c4e7b95ee4f51eb1a4749c745cbf11ca0d6e41d8 +size 29987 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_drawer_modaldrawersheet_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_drawer_modaldrawersheet_light.png index 4ba51d32c..f72cdec2d 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_drawer_modaldrawersheet_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_drawer_modaldrawersheet_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ec503ffd98c39b91ada1c74f0f0c959e1ef06a609846005893e8ef5fe63c176 -size 12724 +oid sha256:15e819bf1e35921e9a93a9354389b4e81903c7759b159d6becab0ca427c4aaf1 +size 12721 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentdrawersheet_permanentdrawersheet_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentdrawersheet_permanentdrawersheet_light.png index aaca1d162..5794b54a6 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentdrawersheet_permanentdrawersheet_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentdrawersheet_permanentdrawersheet_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a4169cc18caadaa47963b4b68a3ed67c1f796ff96d6ca308bd9bbc081ca8a8a -size 7339 +oid sha256:a5a6d2d54744662ae1a9cee558300a2afc17c1fb056bd247a73a887145fe7eef +size 7469 diff --git a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentnavigationdrawer_permanentnavigationdrawer_light.png b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentnavigationdrawer_permanentnavigationdrawer_light.png index ee5056218..5d2e5faf3 100644 --- a/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentnavigationdrawer_permanentnavigationdrawer_light.png +++ b/spark-screenshot-testing/src/test/snapshots/images/com.adevinta.spark_PreviewScreenshotTests_preview_tests_permanentnavigationdrawer_permanentnavigationdrawer_light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e059bae3b1356a8781455cdef78b21ec5b391028a773070f28d1188b671ecda -size 14578 +oid sha256:50e22a6818fe83fa012b084d27d445a8ffc1e532b23311997618f64443776795 +size 11614 diff --git a/spark/dependencies/releaseRuntimeClasspath.txt b/spark/dependencies/releaseRuntimeClasspath.txt index 20e291ea8..8943a6f8c 100644 --- a/spark/dependencies/releaseRuntimeClasspath.txt +++ b/spark/dependencies/releaseRuntimeClasspath.txt @@ -1,6 +1,6 @@ -androidx.activity:activity-compose:1.8.2 -androidx.activity:activity-ktx:1.8.2 -androidx.activity:activity:1.8.2 +androidx.activity:activity-compose:1.9.0 +androidx.activity:activity-ktx:1.9.0 +androidx.activity:activity:1.9.0 androidx.annotation:annotation-experimental:1.4.0 androidx.annotation:annotation-jvm:1.7.1 androidx.annotation:annotation:1.7.1 @@ -11,45 +11,45 @@ androidx.autofill:autofill:1.0.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.animation:animation-android:1.6.5 -androidx.compose.animation:animation-core-android:1.6.5 -androidx.compose.animation:animation-core:1.6.5 -androidx.compose.animation:animation:1.6.5 -androidx.compose.foundation:foundation-android:1.6.5 -androidx.compose.foundation:foundation-layout-android:1.6.5 -androidx.compose.foundation:foundation-layout:1.6.5 -androidx.compose.foundation:foundation:1.6.5 +androidx.compose.animation:animation-android:1.6.6 +androidx.compose.animation:animation-core-android:1.6.6 +androidx.compose.animation:animation-core:1.6.6 +androidx.compose.animation:animation:1.6.6 +androidx.compose.foundation:foundation-android:1.6.6 +androidx.compose.foundation:foundation-layout-android:1.6.6 +androidx.compose.foundation:foundation-layout:1.6.6 +androidx.compose.foundation:foundation:1.6.6 androidx.compose.material3:material3-android:1.2.1 androidx.compose.material3:material3-window-size-class-android:1.2.1 androidx.compose.material3:material3-window-size-class:1.2.1 androidx.compose.material3:material3:1.2.1 -androidx.compose.material:material-icons-core-android:1.6.5 -androidx.compose.material:material-icons-core:1.6.5 -androidx.compose.material:material-ripple-android:1.6.5 -androidx.compose.material:material-ripple:1.6.5 -androidx.compose.runtime:runtime-android:1.6.5 -androidx.compose.runtime:runtime-saveable-android:1.6.5 -androidx.compose.runtime:runtime-saveable:1.6.5 -androidx.compose.runtime:runtime:1.6.5 -androidx.compose.ui:ui-android:1.6.5 -androidx.compose.ui:ui-geometry-android:1.6.5 -androidx.compose.ui:ui-geometry:1.6.5 -androidx.compose.ui:ui-graphics-android:1.6.5 -androidx.compose.ui:ui-graphics:1.6.5 -androidx.compose.ui:ui-text-android:1.6.5 -androidx.compose.ui:ui-text-google-fonts:1.6.5 -androidx.compose.ui:ui-text:1.6.5 -androidx.compose.ui:ui-tooling-preview-android:1.6.5 -androidx.compose.ui:ui-tooling-preview:1.6.5 -androidx.compose.ui:ui-unit-android:1.6.5 -androidx.compose.ui:ui-unit:1.6.5 -androidx.compose.ui:ui-util-android:1.6.5 -androidx.compose.ui:ui-util:1.6.5 -androidx.compose.ui:ui:1.6.5 -androidx.compose:compose-bom:2024.04.00 +androidx.compose.material:material-icons-core-android:1.6.6 +androidx.compose.material:material-icons-core:1.6.6 +androidx.compose.material:material-ripple-android:1.6.6 +androidx.compose.material:material-ripple:1.6.6 +androidx.compose.runtime:runtime-android:1.6.6 +androidx.compose.runtime:runtime-saveable-android:1.6.6 +androidx.compose.runtime:runtime-saveable:1.6.6 +androidx.compose.runtime:runtime:1.6.6 +androidx.compose.ui:ui-android:1.6.6 +androidx.compose.ui:ui-geometry-android:1.6.6 +androidx.compose.ui:ui-geometry:1.6.6 +androidx.compose.ui:ui-graphics-android:1.6.6 +androidx.compose.ui:ui-graphics:1.6.6 +androidx.compose.ui:ui-text-android:1.6.6 +androidx.compose.ui:ui-text-google-fonts:1.6.6 +androidx.compose.ui:ui-text:1.6.6 +androidx.compose.ui:ui-tooling-preview-android:1.6.6 +androidx.compose.ui:ui-tooling-preview:1.6.6 +androidx.compose.ui:ui-unit-android:1.6.6 +androidx.compose.ui:ui-unit:1.6.6 +androidx.compose.ui:ui-util-android:1.6.6 +androidx.compose.ui:ui-util:1.6.6 +androidx.compose.ui:ui:1.6.6 +androidx.compose:compose-bom:2024.04.01 androidx.concurrent:concurrent-futures:1.1.0 -androidx.core:core-ktx:1.12.0 -androidx.core:core:1.12.0 +androidx.core:core-ktx:1.13.0 +androidx.core:core:1.13.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.emoji2:emoji2:1.3.0 androidx.exifinterface:exifinterface:1.3.7 diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/badge/Badge.kt b/spark/src/main/kotlin/com/adevinta/spark/components/badge/Badge.kt index d84ada817..33a785b3e 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/badge/Badge.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/badge/Badge.kt @@ -80,7 +80,7 @@ internal fun SparkBadge( Row( modifier = modifier - .ifTrue(hasStroke) { border(2.dp, colors.onColor, shape).padding(2.dp) } + .ifTrue(hasStroke) { border(2.dp, SparkTheme.colors.surface, shape).padding(2.dp) } .defaultMinSize(minWidth = size, minHeight = size) .background( color = colors.color, diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/BottomSheet.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/BottomSheet.kt new file mode 100644 index 000000000..326d30f64 --- /dev/null +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/BottomSheet.kt @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2023-2024 Adevinta + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.adevinta.spark.components.bottomsheet + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.BottomSheetDefaults.ExpandedShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheetDefaults +import androidx.compose.material3.ModalBottomSheetProperties +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.adevinta.spark.components.bottomsheet.SheetDefaults.ContentTopPadding +import com.adevinta.spark.components.bottomsheet.SheetDefaults.ContentTopPaddingNoHandle +import com.adevinta.spark.components.buttons.ButtonFilled +import com.adevinta.spark.components.icons.Icon +import com.adevinta.spark.components.list.ListItem +import com.adevinta.spark.components.text.Text +import com.adevinta.spark.components.toggles.CheckboxLabelled +import com.adevinta.spark.icons.LikeFill +import com.adevinta.spark.icons.SparkIcons +import com.adevinta.spark.tokens.contentColorFor +import kotlinx.coroutines.launch + +/** + * Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile, + * especially when offering a long list of action items, or when items require longer descriptions + * and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other + * app functionality when they appear, and remaining on screen until confirmed, dismissed, or a + * required action has been taken. + * + * @param onDismissRequest + * @param modifier Optional [Modifier] for the bottom sheet. + * @param showHandle Optional [Boolean] to show / hide handle, if handle is hidden it will fill all screen. + * + * + * @param contentTopPadding The top padding for the content of the bottom sheet, does not apply to the handle. + * + * By default if showHandle is [Boolean.true]. contentTopPadding = [SheetDefaults.ContentTopPadding] + * else contentTopPadding = [SheetDefaults.ContentTopPaddingNoHandle] + * + * If you want to have immersive BottomSheet, you can set contentTopPadding = 0.dp, + * Beware you need to set your content top padding yourself + * to avoid content to be hidden by the handle at least [SheetDefaults.ContentTopPadding] + * + * + * @param sheetState the state of the bottom sheet. + * @param content The content to be displayed inside the bottom sheet. + */ +@Composable +@ExperimentalMaterial3Api +public fun BottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(), + dragHandle: @Composable (() -> Unit)? = { + DragHandle() + }, + content: @Composable ColumnScope.() -> Unit, +) { + SparkModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + sheetState = sheetState, + content = content, + dragHandle = dragHandle, + ) +} + +/** + * Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile, + * especially when offering a long list of action items, or when items require longer descriptions + * and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other + * app functionality when they appear, and remaining on screen until confirmed, dismissed, or a + * required action has been taken. + ** + * A simple example of a modal bottom sheet looks like this: + * @param contentTopPadding The top padding for the content of the bottom sheet, does not apply to the handle. + * @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet + * animates to [androidx.compose.material3.SheetValue.Hidden]. + * @param modifier Optional [Modifier] for the bottom sheet. + * @param showHandle Optional [Boolean] to show / hide handle, if handle is hidden it will fill all screen. + * @param sheetState The state of the bottom sheet. + * @param shape The shape of the bottom sheet. + * @param containerColor The color used for the background of this bottom sheet + * + * @param contentColor The preferred color for content inside this bottom sheet. Defaults to either + * the matching content color for [containerColor], + * or to the current [androidx.compose.material3.LocalContentColor] if [containerColor] is not a color from the theme. + * + * @param tonalElevation The tonal elevation of this bottom sheet. + * @param scrimColor Color of the scrim that obscures content when the bottom sheet is open. + * @param dragHandle Optional visual marker to swipe the bottom sheet. + * @param content The content to be displayed inside the bottom sheet. + */ + +@Composable +@ExperimentalMaterial3Api +internal fun SparkModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(), + shape: Shape = ExpandedShape, + containerColor: Color = SheetDefaults.ContainerColor, + contentColor: Color = contentColorFor(containerColor), + dragHandle: @Composable (() -> Unit)? = { + DragHandle() + }, + windowInsets: WindowInsets = BottomSheetDefaults.windowInsets, + properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(), + content: @Composable ColumnScope.() -> Unit, +) { + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + sheetState = sheetState, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + windowInsets = windowInsets, + properties = properties, + dragHandle = null, + ) { + Box { + Column( + modifier = Modifier.padding( + top = if (dragHandle != null) ContentTopPadding else ContentTopPaddingNoHandle, + ), + ) { + content() + } + if (dragHandle != null) { + Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxWidth()) { + dragHandle() + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Preview( + group = "BottomSheet", + name = "ModalBottomSheet", +) +@Composable +private fun ModalBottomSheetSample() { + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + var skipPartiallyExpanded by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + ) + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + CheckboxLabelled( + state = if (skipPartiallyExpanded) ToggleableState.On else ToggleableState.Off, + onClick = { skipPartiallyExpanded = !skipPartiallyExpanded }, + ) { + Text("Skip Partially Expanded State") + } + + ButtonFilled( + text = "Show Bottom Sheet", + onClick = { openBottomSheet = !openBottomSheet }, + ) + } + + if (openBottomSheet) { + BottomSheet( + onDismissRequest = { openBottomSheet = false }, + sheetState = bottomSheetState, + ) { + LazyColumn { + stickyHeader { + Row( + Modifier + .fillMaxWidth() + .background(Color.Green), + horizontalArrangement = Arrangement.Center, + ) { + // Note: If you provide logic outside of onDismissRequest to remove the sheet, + // you must additionally handle intended state cleanup, if any. + ButtonFilled( + modifier = Modifier.padding(24.dp), + text = "Hide Bottom Sheet", + onClick = { + scope.launch { bottomSheetState.hide() }.invokeOnCompletion { + if (!bottomSheetState.isVisible) { + openBottomSheet = false + } + } + }, + ) + } + } + + items(50) { + ListItem( + headlineContent = { Text("Item $it") }, + leadingContent = { + Icon( + SparkIcons.LikeFill, + contentDescription = "Localized description", + ) + }, + ) + } + } + } + } +} diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/BottomSheet.md b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/BottomSheet.md new file mode 100644 index 000000000..9a3889fc5 --- /dev/null +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/BottomSheet.md @@ -0,0 +1,89 @@ +# Package com.adevinta.spark.components.bottomsheet + +[BottomSheet](https://spark.adevinta.com/1186e1705/p/67d41e-bottom-sheet/b/02056b) +A bottom sheet is a UI component commonly used in mobile applications to present additional content +or options from the bottom of the screen. +It is a modal component that slides up from the bottom of the screen and covers the entire screen. + +#### BottomSheet + +```kotlin +fun BottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + showHandle: Boolean = true, + contentTopPadding: Dp, , + sheetState: SheetState = rememberModalBottomSheetState(), + content = { + Text( + text = "BottomSheet Content", + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) + } +) +``` + +# BottomSheet content with / No handle Example + +| Light | Dark | +|----------------------------------------------------|----------------------------------------------------| +| ![](../../images/bottomsheet_dark_with_handle.png) | ![](../../images/bottomsheet_dark_with_handle.png) | +| ![](../../images/bottomsheet_light_no_handle.png) | ![](../../images/bottomsheet_dark_no_handle.png) | + +# BottomSheet content behind handle Example + +```kotlin +fun BottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + showHandle: Boolean = true, + contentTopPadding = 0.dp, + sheetState: SheetState = rememberModalBottomSheetState(), + content = { + Box( + contentAlignment = Alignment.TopCenter, + ) { + Image( + modifier = Modifier + .height(500.dp) + .fillMaxWidth(), + model = "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } + } +) +``` +![](../../images/bottomsheet_content_behind_handle.png) + +#### BottomSheetScaffold + +```kotlin +fun BottomSheetScaffold( + sheetContent = { + Text( + text = "Sheet Content", + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) +}, + content = { + Text( + text = "Screen Content", + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) + }, + modifier: Modifier = Modifier, + scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), + showHandle: Boolean = true, + sheetContentTopPadding: Dp = if (showHandle) ContentTopPadding else ContentTopPaddingNoHandle, + screenContentPadding: Dp = ContentTopPadding, + sheetSwipeEnabled: Boolean = true, + topBar: @Composable (() -> Unit)? = null, + snackbarHost: @Composable (androidx.compose.material3.SnackbarHostState) -> Unit, +) +``` + +# BottomSheetScaffold Example + +![](../../images/bottomsheetscaffold.png) diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/ModalBottomSheetLayout.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/ModalBottomSheetLayout.kt index 8ccca3635..58fa130bc 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/ModalBottomSheetLayout.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/ModalBottomSheetLayout.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Adevinta + * Copyright (c) 2023-2024 Adevinta * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -63,9 +63,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.adevinta.spark.SparkTheme -import com.adevinta.spark.components.bottomsheet.ModalBottomSheetValue.Expanded -import com.adevinta.spark.components.bottomsheet.ModalBottomSheetValue.HalfExpanded -import com.adevinta.spark.components.bottomsheet.ModalBottomSheetValue.Hidden +import com.adevinta.spark.components.bottomsheet.layout.PreUpPostDownNestedScrollConnection +import com.adevinta.spark.components.bottomsheet.layout.SwipeableDefaults +import com.adevinta.spark.components.bottomsheet.layout.SwipeableState +import com.adevinta.spark.components.bottomsheet.layout.swipeable import com.adevinta.spark.components.surface.Surface import com.adevinta.spark.res.resources import com.adevinta.spark.tokens.contentColorFor @@ -79,6 +80,11 @@ import kotlin.math.roundToInt * Possible values of [ModalBottomSheetState]. */ @ExperimentalMaterial3Api +@Deprecated( + message = "Use [SheetValue] instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("SheetValue", "androidx.compose.material3.SheetValue"), +) public enum class ModalBottomSheetValue { /** * The bottom sheet is not visible. @@ -104,7 +110,7 @@ public enum class ModalBottomSheetValue { * [ModalBottomSheetValue.HalfExpanded] if [isSkipHalfExpanded] is set to true. * @param animationSpec The default animation that will be used to animate to a new state. * @param isSkipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should - * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the + * be skipped. If true, the sheet will always expand to the [ModalBottomSheetValue.Expanded] state and move to the * [Hidden] state when hiding the sheet, either programmatically or by user interaction. * Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded]. * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an @@ -112,6 +118,12 @@ public enum class ModalBottomSheetValue { * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. */ @ExperimentalMaterial3Api +@Deprecated( + message = "Use [SheetState] instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("SheetState", "androidx.compose.material3.SheetState"), +) +@Suppress("DEPRECATION") public class ModalBottomSheetState( initialValue: ModalBottomSheetValue, animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec, @@ -126,10 +138,10 @@ public class ModalBottomSheetState( * Whether the bottom sheet is visible. */ public val isVisible: Boolean - get() = currentValue != Hidden + get() = currentValue != ModalBottomSheetValue.Hidden internal val hasHalfExpandedState: Boolean - get() = anchors.values.contains(HalfExpanded) + get() = anchors.values.contains(ModalBottomSheetValue.HalfExpanded) public constructor( initialValue: ModalBottomSheetValue, @@ -139,9 +151,8 @@ public class ModalBottomSheetState( init { if (isSkipHalfExpanded) { - require(initialValue != HalfExpanded) { - "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" + - " true." + require(initialValue != ModalBottomSheetValue.HalfExpanded) { + "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" + " true." } } } @@ -155,8 +166,8 @@ public class ModalBottomSheetState( */ public suspend fun show() { val targetValue = when { - hasHalfExpandedState -> HalfExpanded - else -> Expanded + hasHalfExpandedState -> ModalBottomSheetValue.HalfExpanded + else -> ModalBottomSheetValue.Expanded } animateTo(targetValue = targetValue) } @@ -171,7 +182,7 @@ public class ModalBottomSheetState( if (!hasHalfExpandedState) { return } - animateTo(HalfExpanded) + animateTo(ModalBottomSheetValue.HalfExpanded) } /** @@ -180,7 +191,7 @@ public class ModalBottomSheetState( * * * @throws [CancellationException] if the animation is interrupted */ - internal suspend fun expand() = animateTo(Expanded) + internal suspend fun expand() = animateTo(ModalBottomSheetValue.Expanded) /** * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has @@ -188,7 +199,7 @@ public class ModalBottomSheetState( * * @throws [CancellationException] if the animation is interrupted */ - public suspend fun hide(): Unit = animateTo(Hidden) + public suspend fun hide(): Unit = animateTo(ModalBottomSheetValue.Hidden) internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection @@ -221,7 +232,7 @@ public class ModalBottomSheetState( * @param initialValue The initial value of the state. * @param animationSpec The default animation that will be used to animate to a new state. * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should - * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the + * be skipped. If true, the sheet will always expand to the [ModalBottomSheetValue.Expanded] state and move to the * [Hidden] state when hiding the sheet, either programmatically or by user interaction. * Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded]. * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an @@ -230,12 +241,21 @@ public class ModalBottomSheetState( */ @Composable @ExperimentalMaterial3Api +@Deprecated( + message = "Use one of the options: [BottomSheet] , [BottomSheetScaffold]", + replaceWith = ReplaceWith( + "rememberModalBottomSheetState(initialValue,skipHalfExpanded,confirmStateChange)", + "androidx.compose.material3.rememberModalBottomSheetState", + ), +) +@Suppress("DEPRECATION") public fun rememberModalBottomSheetState( initialValue: ModalBottomSheetValue, skipHalfExpanded: Boolean, animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec, confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }, ): ModalBottomSheetState { + @Suppress("DEPRECATION") return rememberSaveable( initialValue, animationSpec, @@ -265,6 +285,7 @@ public fun rememberModalBottomSheetState( */ @Composable @ExperimentalMaterial3Api +@Suppress("DEPRECATION") public fun rememberModalBottomSheetState( initialValue: ModalBottomSheetValue, animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec, @@ -277,14 +298,11 @@ public fun rememberModalBottomSheetState( ) /** - * Material Design modal bottom sheet. * * Modal bottom sheets present a set of choices while blocking interaction with the rest of the * screen. They are an alternative to inline menus and simple dialogs, providing * additional room for content, iconography, and actions. - * - * ![Modal bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material/modal-bottom-sheet.png) - * + ** * @param sheetContent The content of the bottom sheet. * @param modifier Optional [Modifier] for the entire component. * @param sheetState The state of the bottom sheet. @@ -303,13 +321,15 @@ public fun rememberModalBottomSheetState( @Composable @ExperimentalMaterial3Api @Deprecated( - message = "Use one of the options: ModalBottomSheet, BottomSheetScaffold", + message = "Use one of the options: [BottomSheet] , [BottomSheetScaffold]", level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("BottomSheet", "com.adevinta.spark.components.bottomsheet.BottomSheet"), ) +@Suppress("DEPRECATION") public fun ModalBottomSheetLayout( sheetContent: @Composable ColumnScope.() -> Unit, modifier: Modifier = Modifier, - sheetState: ModalBottomSheetState = rememberModalBottomSheetState(Hidden), + sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), sheetShape: Shape = SparkTheme.shapes.large.copy( bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp), @@ -329,11 +349,11 @@ public fun ModalBottomSheetLayout( Scrim( color = scrimColor, onDismiss = { - if (sheetState.confirmStateChange(Hidden)) { + if (sheetState.confirmStateChange(ModalBottomSheetValue.Hidden)) { scope.launch { sheetState.hide() } } }, - visible = sheetState.targetValue != Hidden, + visible = sheetState.targetValue != ModalBottomSheetValue.Hidden, ) } Surface( @@ -357,21 +377,21 @@ public fun ModalBottomSheetLayout( .semantics { if (sheetState.isVisible) { dismiss { - if (sheetState.confirmStateChange(Hidden)) { + if (sheetState.confirmStateChange(ModalBottomSheetValue.Hidden)) { scope.launch { sheetState.hide() } } true } - if (sheetState.currentValue == HalfExpanded) { + if (sheetState.currentValue == ModalBottomSheetValue.HalfExpanded) { expand { - if (sheetState.confirmStateChange(Expanded)) { + if (sheetState.confirmStateChange(ModalBottomSheetValue.Expanded)) { scope.launch { sheetState.expand() } } true } } else if (sheetState.hasHalfExpandedState) { collapse { - if (sheetState.confirmStateChange(HalfExpanded)) { + if (sheetState.confirmStateChange(ModalBottomSheetValue.HalfExpanded)) { scope.launch { sheetState.halfExpand() } } true @@ -398,7 +418,7 @@ public fun ModalBottomSheetLayout( } } -@Suppress("ModifierInspectorInfo") +@Suppress("ModifierInspectorInfo", "DEPRECATION") @OptIn(ExperimentalMaterial3Api::class) private fun Modifier.bottomSheetSwipeable( sheetState: ModalBottomSheetState, @@ -409,21 +429,21 @@ private fun Modifier.bottomSheetSwipeable( val modifier = if (sheetHeight != null) { val anchors = if (sheetHeight < fullHeight / 2 || sheetState.isSkipHalfExpanded) { mapOf( - fullHeight to Hidden, - fullHeight - sheetHeight to Expanded, + fullHeight to ModalBottomSheetValue.Hidden, + fullHeight - sheetHeight to ModalBottomSheetValue.Expanded, ) } else { mapOf( - fullHeight to Hidden, - fullHeight / 2 to HalfExpanded, - max(0f, fullHeight - sheetHeight) to Expanded, + fullHeight to ModalBottomSheetValue.Hidden, + fullHeight / 2 to ModalBottomSheetValue.HalfExpanded, + max(0f, fullHeight - sheetHeight) to ModalBottomSheetValue.Expanded, ) } Modifier.swipeable( state = sheetState, anchors = anchors, orientation = Orientation.Vertical, - enabled = sheetState.currentValue != Hidden, + enabled = sheetState.currentValue != ModalBottomSheetValue.Hidden, resistance = null, ) } else { @@ -485,6 +505,5 @@ public object ModalBottomSheetDefaults { * The default scrim color used by [ModalBottomSheetLayout]. */ public val scrimColor: Color - @Composable - get() = SparkTheme.colors.onSurface.copy(alpha = 0.32f) + @Composable get() = SparkTheme.colors.onSurface.copy(alpha = 0.32f) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaults.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaults.kt index 349d97603..32001dc91 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaults.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaults.kt @@ -21,371 +21,34 @@ */ package com.adevinta.spark.components.bottomsheet -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.adevinta.spark.SparkTheme -import com.adevinta.spark.components.bottomsheet.SheetValue.Expanded -import com.adevinta.spark.components.bottomsheet.SheetValue.Hidden -import com.adevinta.spark.components.bottomsheet.SheetValue.PartiallyExpanded -import kotlinx.coroutines.CancellationException /** - * State of a sheet composable, such as [ModalBottomSheet] - * - * Contains states relating to it's swipe position as well as animations between state values. - * - * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large - * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move - * to the [Hidden] state if available when hiding the sheet, either programmatically or by user - * interaction. - * @param initialValue The initial value of the state. - * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. - * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always - * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either - * programmatically or by user interaction. + * Contains the default values used by [BottomSheet] + * and [com.adevinta.spark.components.bottomsheet.scaffold.BottomSheetScaffold]. */ @Stable -@ExperimentalMaterial3Api -public class SheetState( - internal val skipPartiallyExpanded: Boolean, - initialValue: SheetValue = Hidden, - confirmValueChange: (SheetValue) -> Boolean = { true }, - internal val skipHiddenState: Boolean = false, -) { - init { - if (skipPartiallyExpanded) { - require(initialValue != PartiallyExpanded) { - "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " + - "is set to true." - } - } - if (skipHiddenState) { - require(initialValue != Hidden) { - "The initial value must not be set to Hidden if skipHiddenState is set to true." - } - } - } - - /** - * The current value of the state. - * - * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is - * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet - * was in before the swipe or animation started. - */ - - public val currentValue: SheetValue get() = swipeableState.currentValue - - /** - * The target value of the bottom sheet state. - * - * If a swipe is in progress, this is the value that the sheet would animate to if the - * swipe finishes. If an animation is running, this is the target value of that animation. - * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. - */ - public val targetValue: SheetValue get() = swipeableState.targetValue - - /** - * Whether the modal bottom sheet is visible. - */ - public val isVisible: Boolean - get() = swipeableState.currentValue != Hidden - - /** - * Require the current offset (in pixels) of the bottom sheet. - * - * The offset will be initialized during the first measurement phase of the provided sheet - * content. - * - * These are the phases: - * Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing - * - * During the first composition, an [IllegalStateException] is thrown. In subsequent - * compositions, the offset will be derived from the anchors of the previous pass. Always prefer - * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next - * frame, after layout. - * - * @throws IllegalStateException If the offset has not been initialized yet - */ - public fun requireOffset(): Float = swipeableState.requireOffset() - - /** - * Whether the sheet has an expanded state defined. - */ - - public val hasExpandedState: Boolean - get() = swipeableState.hasAnchorForValue(Expanded) - - /** - * Whether the modal bottom sheet has a partially expanded state defined. - */ - internal val hasPartiallyExpandedState: Boolean - get() = swipeableState.hasAnchorForValue(PartiallyExpanded) - - /** - * Fully expand the bottom sheet with animation and suspend until it is fully expanded or - * animation has been cancelled. - * * - * @throws [CancellationException] if the animation is interrupted - */ - public suspend fun expand() { - swipeableState.animateTo(Expanded) - } - - /** - * Animate the bottom sheet and suspend until it is partially expanded or animation has been - * cancelled. - * @throws [CancellationException] if the animation is interrupted - * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true - */ - public suspend fun partialExpand() { - check(!skipPartiallyExpanded) { - "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" + - " skipPartiallyExpanded to false to use this function." - } - animateTo(PartiallyExpanded) - } - - /** - * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined - * else [Expanded]. - * @throws [CancellationException] if the animation is interrupted - */ - public suspend fun show() { - val targetValue = when { - hasPartiallyExpandedState -> PartiallyExpanded - else -> Expanded - } - animateTo(targetValue) - } - - /** - * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has - * been cancelled. - * @throws [CancellationException] if the animation is interrupted - */ - public suspend fun hide() { - check(!skipHiddenState) { - "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" + - " to false to use this function." - } - animateTo(Hidden) - } - - /** - * Animate to a [targetValue]. - * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the - * [targetValue] without updating the offset. - * - * @throws CancellationException if the interaction interrupted by another interaction like a - * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. - * - * @param targetValue The target value of the animation - */ - internal suspend fun animateTo( - targetValue: SheetValue, - velocity: Float = swipeableState.lastVelocity, - ) { - swipeableState.animateTo(targetValue, velocity) - } - - /** - * Snap to a [targetValue] without any animation. - * - * @throws CancellationException if the interaction interrupted by another interaction like a - * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. - * - * @param targetValue The target value of the animation - */ - internal suspend fun snapTo(targetValue: SheetValue) { - swipeableState.snapTo(targetValue) - } - - /** - * Find the closest anchor taking into account the velocity and settle at it with an animation. - */ - internal suspend fun settle(velocity: Float) { - swipeableState.settle(velocity) - } - - internal var swipeableState = SwipeableV2State( - initialValue = initialValue, - animationSpec = SwipeableV2Defaults.AnimationSpec, - confirmValueChange = confirmValueChange, - ) - - internal val offset: Float? get() = swipeableState.offset - - public companion object { - /** - * The default [Saver] implementation for [SheetState]. - */ - @Suppress("ktlint:standard:function-naming") - public fun Saver( - skipPartiallyExpanded: Boolean, - confirmValueChange: (SheetValue) -> Boolean, - ): Saver = Saver( - save = { it.currentValue }, - restore = { savedValue -> - SheetState(skipPartiallyExpanded, savedValue, confirmValueChange) - }, - ) - } -} - -/** - * Possible values of [SheetState]. - */ -@ExperimentalMaterial3Api -public enum class SheetValue { - /** - * The sheet is not visible. - */ - Hidden, - - /** - * The sheet is visible at full height. - */ - Expanded, +internal object SheetDefaults { /** - * The sheet is partially visible. + * the default color of the container for the bottom sheet. */ - PartiallyExpanded, -} - -/** - * Contains the default values used by [ModalBottomSheet] and [BottomSheetScaffold]. - */ -@Stable -@ExperimentalMaterial3Api -public object BottomSheetDefaults { - /** The default shape for bottom sheets in a [Hidden] state. */ - public val HiddenShape: Shape - @Composable get() = SparkTheme.shapes.none - - /** The default shape for a bottom sheets in [PartiallyExpanded] and [Expanded] states. */ - public val ExpandedShape: Shape - @Composable get() = SparkTheme.shapes.extraLarge.copy( - bottomStart = CornerSize(0.dp), - bottomEnd = CornerSize(0.dp), - ) - - /** The default container color for a bottom sheet. */ - public val ContainerColor: Color + val ContainerColor: Color @Composable get() = SparkTheme.colors.surface - /** The default elevation for a bottom sheet. */ - public val Elevation: Dp = 1.0.dp - /** The default color of the scrim overlay for background content. */ - public val ScrimColor: Color - @Composable get() = SparkTheme.colors.onSurface.copy(alpha = 0.32f) + val ScrimColor: Color + @Composable get() = SparkTheme.colors.scrim - /** - * The default peek height used by [BottomSheetScaffold]. - */ - public val SheetPeekHeight: Dp = 56.dp + val DragHandleHeight: Dp = 4.dp + val DragHandleWidth: Dp = 32.dp + val DragHandleTopPadding: Dp = 8.dp - /** - * The optional visual marker placed on top of a bottom sheet to indicate it may be dragged. - */ - @Composable - public fun DragHandle(modifier: Modifier = Modifier) { - com.adevinta.spark.components.bottomsheet.DragHandle(modifier) - } + val ContentTopPadding: Dp = 24.dp + val ContentTopPaddingNoHandle: Dp = 16.dp } - -@OptIn(ExperimentalMaterial3Api::class) -@Suppress("ktlint:standard:function-naming") -internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( - sheetState: SheetState, - orientation: Orientation, - onFling: (velocity: Float) -> Unit, -): NestedScrollConnection = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.Drag) { - sheetState.swipeableState.dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (source == NestedScrollSource.Drag) { - sheetState.swipeableState.dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero - } - } - - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - val currentOffset = sheetState.requireOffset() - return if (toFling < 0 && currentOffset > sheetState.swipeableState.minOffset) { - onFling(toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero - } - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - onFling(available.toFloat()) - return available - } - - private fun Float.toOffset(): Offset = Offset( - x = if (orientation == Orientation.Horizontal) this else 0f, - y = if (orientation == Orientation.Vertical) this else 0f, - ) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y -} - -@Composable -@ExperimentalMaterial3Api -internal fun rememberSheetState( - skipPartiallyExpanded: Boolean = false, - confirmValueChange: (SheetValue) -> Boolean = { true }, - initialValue: SheetValue = Hidden, - skipHiddenState: Boolean = false, -): SheetState { - return rememberSaveable( - skipPartiallyExpanded, - confirmValueChange, - saver = SheetState.Saver( - skipPartiallyExpanded = skipPartiallyExpanded, - confirmValueChange = confirmValueChange, - ), - ) { - SheetState(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) - } -} - -private val DragHandleVerticalPadding = 22.dp -internal val BottomSheetMaxWidth = 640.dp diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaultsDeprecated.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaultsDeprecated.kt new file mode 100644 index 000000000..583ce6910 --- /dev/null +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SheetDefaultsDeprecated.kt @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2023-2024 Adevinta + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.adevinta.spark.components.bottomsheet + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.adevinta.spark.SparkTheme +import com.adevinta.spark.components.bottomsheet.SheetValue.Expanded +import com.adevinta.spark.components.bottomsheet.SheetValue.Hidden +import com.adevinta.spark.components.bottomsheet.SheetValue.PartiallyExpanded +import com.adevinta.spark.components.bottomsheet.layout.SwipeableV2Defaults +import com.adevinta.spark.components.bottomsheet.layout.SwipeableV2State +import kotlinx.coroutines.CancellationException + +/** + * State of a sheet composable, such as [ModalBottomSheet] + * + * Contains states relating to it's swipe position as well as animations between state values. + * + * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large + * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move + * to the [Hidden] state if available when hiding the sheet, either programmatically or by user + * interaction. + * @param initialValue The initial value of the state. + * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. + * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always + * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either + * programmatically or by user interaction. + */ +@Stable +@ExperimentalMaterial3Api +public class SheetState( + internal val skipPartiallyExpanded: Boolean, + initialValue: SheetValue = Hidden, + confirmValueChange: (SheetValue) -> Boolean = { true }, + internal val skipHiddenState: Boolean = false, +) { + init { + if (skipPartiallyExpanded) { + require(initialValue != PartiallyExpanded) { + "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " + + "is set to true." + } + } + if (skipHiddenState) { + require(initialValue != Hidden) { + "The initial value must not be set to Hidden if skipHiddenState is set to true." + } + } + } + + /** + * The current value of the state. + * + * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is + * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet + * was in before the swipe or animation started. + */ + + public val currentValue: SheetValue get() = swipeableState.currentValue + + /** + * The target value of the bottom sheet state. + * + * If a swipe is in progress, this is the value that the sheet would animate to if the + * swipe finishes. If an animation is running, this is the target value of that animation. + * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. + */ + public val targetValue: SheetValue get() = swipeableState.targetValue + + /** + * Whether the modal bottom sheet is visible. + */ + public val isVisible: Boolean + get() = swipeableState.currentValue != Hidden + + /** + * Require the current offset (in pixels) of the bottom sheet. + * + * The offset will be initialized during the first measurement phase of the provided sheet + * content. + * + * These are the phases: + * Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing + * + * During the first composition, an [IllegalStateException] is thrown. In subsequent + * compositions, the offset will be derived from the anchors of the previous pass. Always prefer + * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next + * frame, after layout. + * + * @throws IllegalStateException If the offset has not been initialized yet + */ + public fun requireOffset(): Float = swipeableState.requireOffset() + + /** + * Whether the sheet has an expanded state defined. + */ + + public val hasExpandedState: Boolean + get() = swipeableState.hasAnchorForValue(Expanded) + + /** + * Whether the modal bottom sheet has a partially expanded state defined. + */ + internal val hasPartiallyExpandedState: Boolean + get() = swipeableState.hasAnchorForValue(PartiallyExpanded) + + /** + * Fully expand the bottom sheet with animation and suspend until it is fully expanded or + * animation has been cancelled. + * * + * @throws [CancellationException] if the animation is interrupted + */ + public suspend fun expand() { + swipeableState.animateTo(Expanded) + } + + /** + * Animate the bottom sheet and suspend until it is partially expanded or animation has been + * cancelled. + * @throws [CancellationException] if the animation is interrupted + * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true + */ + public suspend fun partialExpand() { + check(!skipPartiallyExpanded) { + "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" + + " skipPartiallyExpanded to false to use this function." + } + animateTo(PartiallyExpanded) + } + + /** + * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined + * else [Expanded]. + * @throws [CancellationException] if the animation is interrupted + */ + public suspend fun show() { + val targetValue = when { + hasPartiallyExpandedState -> PartiallyExpanded + else -> Expanded + } + animateTo(targetValue) + } + + /** + * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has + * been cancelled. + * @throws [CancellationException] if the animation is interrupted + */ + public suspend fun hide() { + check(!skipHiddenState) { + "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" + + " to false to use this function." + } + animateTo(Hidden) + } + + /** + * Animate to a [targetValue]. + * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the + * [targetValue] without updating the offset. + * + * @throws CancellationException if the interaction interrupted by another interaction like a + * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. + * + * @param targetValue The target value of the animation + */ + internal suspend fun animateTo( + targetValue: SheetValue, + velocity: Float = swipeableState.lastVelocity, + ) { + swipeableState.animateTo(targetValue, velocity) + } + + /** + * Snap to a [targetValue] without any animation. + * + * @throws CancellationException if the interaction interrupted by another interaction like a + * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. + * + * @param targetValue The target value of the animation + */ + internal suspend fun snapTo(targetValue: SheetValue) { + swipeableState.snapTo(targetValue) + } + + /** + * Find the closest anchor taking into account the velocity and settle at it with an animation. + */ + internal suspend fun settle(velocity: Float) { + swipeableState.settle(velocity) + } + + internal var swipeableState = SwipeableV2State( + initialValue = initialValue, + animationSpec = SwipeableV2Defaults.AnimationSpec, + confirmValueChange = confirmValueChange, + ) + + internal val offset: Float? get() = swipeableState.offset + + public companion object { + /** + * The default [Saver] implementation for [SheetState]. + */ + @Suppress("ktlint:standard:function-naming") + public fun Saver( + skipPartiallyExpanded: Boolean, + confirmValueChange: (SheetValue) -> Boolean, + ): Saver = Saver( + save = { it.currentValue }, + restore = { savedValue -> + SheetState(skipPartiallyExpanded, savedValue, confirmValueChange) + }, + ) + } +} + +/** + * Possible values of [SheetState]. + */ +@ExperimentalMaterial3Api +public enum class SheetValue { + /** + * The sheet is not visible. + */ + Hidden, + + /** + * The sheet is visible at full height. + */ + Expanded, + + /** + * The sheet is partially visible. + */ + PartiallyExpanded, +} + +/** + * Contains the default values used by [ModalBottomSheet] and [BottomSheetScaffold]. + */ +@Stable +@ExperimentalMaterial3Api +public object BottomSheetDefaults { + /** The default shape for bottom sheets in a [Hidden] state. */ + public val HiddenShape: Shape + @Composable get() = SparkTheme.shapes.none + + /** The default shape for a bottom sheets in [PartiallyExpanded] and [Expanded] states. */ + public val ExpandedShape: Shape + @Composable get() = SparkTheme.shapes.extraLarge.copy( + bottomStart = CornerSize(0.dp), + bottomEnd = CornerSize(0.dp), + ) + + /** The default container color for a bottom sheet. */ + public val ContainerColor: Color + @Composable get() = SparkTheme.colors.surface + + /** The default elevation for a bottom sheet. */ + public val Elevation: Dp = 1.0.dp + + /** The default color of the scrim overlay for background content. */ + public val ScrimColor: Color + @Composable get() = SparkTheme.colors.onSurface.copy(alpha = 0.32f) + + /** + * The default peek height used by [BottomSheetScaffold]. + */ + public val SheetPeekHeight: Dp = 56.dp + + /** + * The optional visual marker placed on top of a bottom sheet to indicate it may be dragged. + */ + @Composable + @Deprecated( + message = "Use DragHandle instead", + replaceWith = ReplaceWith( + "com.adevinta.spark.components.bottomsheet.handle.DragHandle()", + ), + ) + public fun DragHandle(modifier: Modifier = Modifier) { + com.adevinta.spark.components.bottomsheet.DragHandle(modifier) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Suppress("ktlint:standard:function-naming") +internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection( + sheetState: SheetState, + orientation: Orientation, + onFling: (velocity: Float) -> Unit, +): NestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + sheetState.swipeableState.dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return if (source == NestedScrollSource.Drag) { + sheetState.swipeableState.dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + val currentOffset = sheetState.requireOffset() + return if (toFling < 0 && currentOffset > sheetState.swipeableState.minOffset) { + onFling(toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + onFling(available.toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset( + x = if (orientation == Orientation.Horizontal) this else 0f, + y = if (orientation == Orientation.Vertical) this else 0f, + ) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y +} + +@Composable +@ExperimentalMaterial3Api +internal fun rememberSheetState( + skipPartiallyExpanded: Boolean = false, + confirmValueChange: (SheetValue) -> Boolean = { true }, + initialValue: SheetValue = Hidden, + skipHiddenState: Boolean = false, +): SheetState { + return rememberSaveable( + skipPartiallyExpanded, + confirmValueChange, + saver = SheetState.Saver( + skipPartiallyExpanded = skipPartiallyExpanded, + confirmValueChange = confirmValueChange, + ), + ) { + SheetState(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) + } +} + +private val DragHandleVerticalPadding = 22.dp +internal val BottomSheetMaxWidth = 640.dp diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/DragHandle.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/handle/DragHandle.kt similarity index 53% rename from spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/DragHandle.kt rename to spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/handle/DragHandle.kt index 68ff99141..629ff4dfc 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/DragHandle.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/handle/DragHandle.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Adevinta + * Copyright (c) 2023-2024 Adevinta * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,46 +21,59 @@ */ package com.adevinta.spark.components.bottomsheet -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import com.adevinta.spark.PreviewTheme +import com.adevinta.spark.R import com.adevinta.spark.SparkTheme +import com.adevinta.spark.components.bottomsheet.SheetDefaults.DragHandleHeight +import com.adevinta.spark.components.bottomsheet.SheetDefaults.DragHandleTopPadding +import com.adevinta.spark.components.bottomsheet.SheetDefaults.DragHandleWidth import com.adevinta.spark.components.surface.Surface +import com.adevinta.spark.tokens.dim1 import com.adevinta.spark.tools.modifiers.sparkUsageOverlay -import com.adevinta.spark.tools.preview.ThemeProvider -import com.adevinta.spark.tools.preview.ThemeVariant +/** + * The optional visual marker placed on top of a bottom sheet to indicate it may be dragged. + */ @Composable -public fun DragHandle(modifier: Modifier = Modifier) { - Box( +public fun DragHandle( + modifier: Modifier = Modifier, + color: Color = SparkTheme.colors.outline.dim1, +) { + val dragHandleDescription = stringResource(id = R.string.spark_drag_handle_a11y) + Surface( modifier = modifier - .width(32.dp) - .height(4.dp) - .clip(SparkTheme.shapes.large) - .background(SparkTheme.colors.neutral.copy(0.4f)) + .padding(vertical = DragHandleTopPadding) + .semantics { contentDescription = dragHandleDescription } .sparkUsageOverlay(), - ) + color = color, + shape = SparkTheme.shapes.full, + ) { + Box( + Modifier.size( + width = DragHandleWidth, + height = DragHandleHeight, + ), + ) + } } -@Preview +@PreviewLightDark @Composable -internal fun PreviewDragHandle( - @PreviewParameter(ThemeProvider::class) theme: ThemeVariant, -) { - PreviewTheme(theme) { - Surface(color = SparkTheme.colors.background) { - Box(modifier = Modifier.padding(4.dp)) { - DragHandle() - } +private fun PreviewDragHandle() { + PreviewTheme { + Box(modifier = Modifier.padding(4.dp)) { + DragHandle() } } } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/InternalMutatorMutex.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/InternalMutatorMutex.kt similarity index 98% rename from spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/InternalMutatorMutex.kt rename to spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/InternalMutatorMutex.kt index e27d4dd3e..722f23273 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/InternalMutatorMutex.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/InternalMutatorMutex.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Adevinta + * Copyright (c) 2023-2024 Adevinta * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -19,7 +19,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package com.adevinta.spark.components.bottomsheet +package com.adevinta.spark.components.bottomsheet.layout import androidx.compose.foundation.MutatePriority import androidx.compose.runtime.Stable diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/Swipeable.kt similarity index 98% rename from spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt rename to spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/Swipeable.kt index e96a65746..014b472c6 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/Swipeable.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/Swipeable.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Adevinta + * Copyright (c) 2023-2024 Adevinta * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -19,7 +19,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package com.adevinta.spark.components.bottomsheet +package com.adevinta.spark.components.bottomsheet.layout /* * Copyright 2021 The Android Open Source Project @@ -72,10 +72,10 @@ import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import com.adevinta.spark.InternalSparkApi -import com.adevinta.spark.components.bottomsheet.SwipeableDefaults.AnimationSpec -import com.adevinta.spark.components.bottomsheet.SwipeableDefaults.StandardResistanceFactor -import com.adevinta.spark.components.bottomsheet.SwipeableDefaults.VelocityThreshold -import com.adevinta.spark.components.bottomsheet.SwipeableDefaults.resistanceConfig +import com.adevinta.spark.components.bottomsheet.layout.SwipeableDefaults.AnimationSpec +import com.adevinta.spark.components.bottomsheet.layout.SwipeableDefaults.StandardResistanceFactor +import com.adevinta.spark.components.bottomsheet.layout.SwipeableDefaults.VelocityThreshold +import com.adevinta.spark.components.bottomsheet.layout.SwipeableDefaults.resistanceConfig import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SwipeableV2.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/SwipeableV2.kt similarity index 99% rename from spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SwipeableV2.kt rename to spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/SwipeableV2.kt index d6a147016..01b18d368 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/SwipeableV2.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/layout/SwipeableV2.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Adevinta + * Copyright (c) 2023-2024 Adevinta * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -19,7 +19,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package com.adevinta.spark.components.bottomsheet +package com.adevinta.spark.components.bottomsheet.layout import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.SpringSpec diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/modal/ModalBottomSheet.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/modal/ModalBottomSheet.kt index fa50d356b..cee314c2d 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/modal/ModalBottomSheet.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/modal/ModalBottomSheet.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Adevinta + * Copyright (c) 2023-2024 Adevinta * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,6 +21,7 @@ */ package com.adevinta.spark.components.bottomsheet.modal +import android.annotation.SuppressLint import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas @@ -38,7 +39,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn @@ -68,20 +68,20 @@ import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties -import com.adevinta.spark.components.bottomsheet.AnchorChangeHandler import com.adevinta.spark.components.bottomsheet.BottomSheetDefaults import com.adevinta.spark.components.bottomsheet.BottomSheetMaxWidth -import com.adevinta.spark.components.bottomsheet.ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection +import com.adevinta.spark.components.bottomsheet.DragHandle import com.adevinta.spark.components.bottomsheet.SheetState import com.adevinta.spark.components.bottomsheet.SheetValue import com.adevinta.spark.components.bottomsheet.SheetValue.Expanded import com.adevinta.spark.components.bottomsheet.SheetValue.Hidden import com.adevinta.spark.components.bottomsheet.SheetValue.PartiallyExpanded +import com.adevinta.spark.components.bottomsheet.consumeSwipeWithinBottomSheetBoundsNestedScrollConnection +import com.adevinta.spark.components.bottomsheet.layout.AnchorChangeHandler +import com.adevinta.spark.components.bottomsheet.layout.swipeAnchors import com.adevinta.spark.components.bottomsheet.rememberSheetState -import com.adevinta.spark.components.bottomsheet.swipeAnchors import com.adevinta.spark.components.buttons.ButtonFilled import com.adevinta.spark.components.icons.Icon import com.adevinta.spark.components.list.ListItem @@ -108,8 +108,6 @@ import kotlin.math.max * * A simple example of a modal bottom sheet looks like this: * - * @sample androidx.compose.material3.samples.ModalBottomSheetSample - * * @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet * animates to [Hidden]. * @param modifier Optional [Modifier] for the bottom sheet. @@ -124,8 +122,14 @@ import kotlin.math.max * @param dragHandle Optional visual marker to swipe the bottom sheet. * @param content The content to be displayed inside the bottom sheet. */ + @Composable @ExperimentalMaterial3Api +@Suppress("deprecation") +@Deprecated( + message = "ModalBottomSheet has new specs and has been renamed to BottomSheet", + replaceWith = ReplaceWith("BottomSheet(onDismissRequest,modifier,sheetState,dragHandle,content)"), +) public fun ModalBottomSheet( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, @@ -136,7 +140,7 @@ public fun ModalBottomSheet( tonalElevation: Dp = BottomSheetDefaults.Elevation, scrimColor: Color = BottomSheetDefaults.ScrimColor, dragHandle: @Composable (() -> Unit)? = { - BottomSheetDefaults.DragHandle(Modifier.padding(vertical = 8.dp)) + DragHandle() }, content: @Composable ColumnScope.() -> Unit, ) { @@ -158,7 +162,7 @@ public fun ModalBottomSheet( // Callback that is invoked when the anchors have changed. val anchorChangeHandler = remember(sheetState, scope) { - ModalBottomSheetAnchorChangeHandler( + modalBottomSheetAnchorChangeHandler( state = sheetState, animateTo = { target, velocity -> scope.launch { sheetState.animateTo(target, velocity = velocity) } @@ -199,7 +203,7 @@ public fun ModalBottomSheet( } .nestedScroll( remember(sheetState) { - ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( + consumeSwipeWithinBottomSheetBoundsNestedScrollConnection( sheetState = sheetState, orientation = Orientation.Vertical, onFling = settleToDismiss, @@ -220,20 +224,23 @@ public fun ModalBottomSheet( contentColor = contentColor, elevation = tonalElevation, ) { - Column(Modifier.fillMaxWidth()) { + Column( + Modifier + .fillMaxWidth(), + ) { if (dragHandle != null) { Box( Modifier .align(Alignment.CenterHorizontally) .semantics(mergeDescendants = true) { - // Provides semantics to interact with the bottomsheet based on its + // Provides semantics to interact with the BottomSheet based on its // current value. with(sheetState) { dismiss { animateToDismiss() true } - if (currentValue == SheetValue.PartiallyExpanded) { + if (currentValue == PartiallyExpanded) { expand { if (swipeableState.confirmValueChange(Expanded)) { scope.launch { sheetState.expand() } @@ -242,8 +249,8 @@ public fun ModalBottomSheet( } } else if (hasPartiallyExpandedState) { collapse { - val confirmPartial = swipeableState - .confirmValueChange(SheetValue.PartiallyExpanded) + val confirmPartial = + swipeableState.confirmValueChange(PartiallyExpanded) if (confirmPartial) { scope.launch { partialExpand() } } @@ -278,6 +285,13 @@ public fun ModalBottomSheet( */ @Composable @ExperimentalMaterial3Api +@Deprecated( + message = "Use Material SheetState instead", + replaceWith = ReplaceWith( + "rememberModalBottomSheetState(skipPartiallyExpanded,confirmValueChange)", + "androidx.compose.material3.rememberModalBottomSheetState", + ), +) public fun rememberModalBottomSheetState( skipPartiallyExpanded: Boolean = false, initialValue: SheetValue = Hidden, @@ -294,6 +308,7 @@ private fun Scrim( val alpha by animateFloatAsState( targetValue = if (visible) 1f else 0f, animationSpec = TweenSpec(), + label = "scrimAlpha", ) val dismissSheet = if (visible) { Modifier @@ -316,6 +331,7 @@ private fun Scrim( } } +@SuppressLint("ModifierFactoryUnreferencedReceiver") @ExperimentalMaterial3Api private fun Modifier.modalBottomSheetSwipeable( sheetState: SheetState, @@ -329,31 +345,30 @@ private fun Modifier.modalBottomSheetSwipeable( enabled = sheetState.isVisible, startDragImmediately = sheetState.swipeableState.isAnimationRunning, onDragStopped = onDragStopped, -) - .swipeAnchors( - state = sheetState.swipeableState, - anchorChangeHandler = anchorChangeHandler, - possibleValues = setOf(Hidden, SheetValue.PartiallyExpanded, Expanded), - ) { value, sheetSize -> - when (value) { - Hidden -> screenHeight + bottomPadding - SheetValue.PartiallyExpanded -> when { - sheetSize.height < screenHeight / 2 -> null - sheetState.skipPartiallyExpanded -> null - else -> screenHeight / 2f - } +).swipeAnchors( + state = sheetState.swipeableState, + anchorChangeHandler = anchorChangeHandler, + possibleValues = setOf(Hidden, PartiallyExpanded, Expanded), +) { value, sheetSize -> + when (value) { + Hidden -> screenHeight + bottomPadding + PartiallyExpanded -> when { + sheetSize.height < screenHeight / 2 -> null + sheetState.skipPartiallyExpanded -> null + else -> screenHeight / 2f + } - Expanded -> if (sheetSize.height != 0) { - max(0f, screenHeight - sheetSize.height) - } else { - null - } + Expanded -> if (sheetSize.height != 0) { + max(0f, screenHeight - sheetSize.height) + } else { + null } } +} @ExperimentalMaterial3Api -@Suppress("ktlint:standard:function-naming") -private fun ModalBottomSheetAnchorChangeHandler( +@Suppress("KtLint:standard:function-naming") +private fun modalBottomSheetAnchorChangeHandler( state: SheetState, animateTo: (target: SheetValue, velocity: Float) -> Unit, snapTo: (target: SheetValue) -> Unit, @@ -361,10 +376,10 @@ private fun ModalBottomSheetAnchorChangeHandler( val previousTargetOffset = previousAnchors[previousTarget] val newTarget = when (previousTarget) { Hidden -> Hidden - SheetValue.PartiallyExpanded, Expanded -> { - val hasPartiallyExpandedState = newAnchors.containsKey(SheetValue.PartiallyExpanded) + PartiallyExpanded, Expanded -> { + val hasPartiallyExpandedState = newAnchors.containsKey(PartiallyExpanded) val newTarget = if (hasPartiallyExpandedState) { - SheetValue.PartiallyExpanded + PartiallyExpanded } else if (newAnchors.containsKey(Expanded)) { Expanded } else { @@ -399,6 +414,7 @@ internal fun ModalBottomSheetPopup( content = content, ) +@Suppress("DEPRECATION") @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Preview( group = "BottomSheet", @@ -431,6 +447,18 @@ private fun ModalBottomSheetSample() { text = "Show Bottom Sheet", onClick = { openBottomSheet = !openBottomSheet }, ) + + repeat(10) { + ListItem( + headlineContent = { Text("Item $it") }, + leadingContent = { + Icon( + SparkIcons.LikeFill, + contentDescription = "Localized description", + ) + }, + ) + } } // Sheet content diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/scaffold/BottomSheetScaffold.kt b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/scaffold/BottomSheetScaffold.kt index 944a60b0f..846d9cad4 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/scaffold/BottomSheetScaffold.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/bottomsheet/scaffold/BottomSheetScaffold.kt @@ -21,414 +21,100 @@ */ package com.adevinta.spark.components.bottomsheet.scaffold +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeightIn -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.semantics.collapse -import androidx.compose.ui.semantics.dismiss -import androidx.compose.ui.semantics.expand -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.adevinta.spark.PreviewTheme -import com.adevinta.spark.components.bottomsheet.AnchorChangeHandler -import com.adevinta.spark.components.bottomsheet.BottomSheetDefaults -import com.adevinta.spark.components.bottomsheet.BottomSheetMaxWidth -import com.adevinta.spark.components.bottomsheet.ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection -import com.adevinta.spark.components.bottomsheet.SheetState -import com.adevinta.spark.components.bottomsheet.SheetValue -import com.adevinta.spark.components.bottomsheet.SheetValue.Expanded -import com.adevinta.spark.components.bottomsheet.SheetValue.Hidden -import com.adevinta.spark.components.bottomsheet.SheetValue.PartiallyExpanded -import com.adevinta.spark.components.bottomsheet.rememberSheetState -import com.adevinta.spark.components.bottomsheet.swipeAnchors -import com.adevinta.spark.components.bottomsheet.swipeableV2 -import com.adevinta.spark.components.snackbars.SnackbarHost -import com.adevinta.spark.components.snackbars.SnackbarHostState -import com.adevinta.spark.components.surface.Surface +import com.adevinta.spark.components.bottomsheet.DragHandle +import com.adevinta.spark.components.bottomsheet.SheetDefaults.ContentTopPadding +import com.adevinta.spark.components.bottomsheet.SheetDefaults.ContentTopPaddingNoHandle +import com.adevinta.spark.components.icons.Icon +import com.adevinta.spark.components.list.ListItem import com.adevinta.spark.components.text.Text -import com.adevinta.spark.tokens.contentColorFor -import kotlinx.coroutines.launch -import java.lang.Float.max -import kotlin.math.roundToInt +import com.adevinta.spark.icons.LikeFill +import com.adevinta.spark.icons.SparkIcons /** - * Material Design standard bottom sheet scaffold. + * BottomSheetScaffold is a composable that implements a scaffold with a bottom sheet. + * It is a wrapper around [androidx.compose.material3.BottomSheetScaffold]. + * It provides a way to display a bottom sheet that can be swiped up and down. + * The scaffold can have a top bar, a screen content, a bottom sheet content, and a drag handle. + * The scaffold can also have a snackbar host. + * @param sheetContent The content of the bottom sheet. + * @param modifier The modifier to apply to this layout. + * @param scaffoldState The state of the scaffold. + * @param showHandle Whether to show the drag handle. + * @param sheetContentTopPadding The top padding of the sheet content. + * @param screenContentPadding The padding of the screen content. + * @param sheetSwipeEnabled Whether the sheet can be swiped. + * @param topBar The top bar composable. + * @param snackbarHost The snackbar host composable. + * @param content The content of the screen. * - * Standard bottom sheets co-exist with the screen’s main UI region and allow for simultaneously - * viewing and interacting with both regions. They are commonly used to keep a feature or - * support content visible on screen when content in main UI region is frequently scrolled or - * panned. - * - * ![Bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png) - * - * This component provides API to put together several material components to construct your - * screen, by ensuring proper layout strategy for them and collecting necessary data so these - * components will work together correctly. - * - * A simple example of a standard bottom sheet looks like this: - * - * @sample androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample - * - * @param sheetContent the content of the bottom sheet - * @param modifier the [Modifier] to be applied to this scaffold - * @param scaffoldState the state of the bottom sheet scaffold - * @param sheetPeekHeight the height of the bottom sheet when it is collapsed - * @param sheetShape the shape of the bottom sheet - * @param sheetContainerColor the background color of the bottom sheet - * @param sheetContentColor the preferred content color provided by the bottom sheet to its - * children. Defaults to the matching content color for [sheetContainerColor], or if that is - * not a color from the theme, this will keep the same content color set above the bottom sheet. - * @param sheetTonalElevation the tonal elevation of the bottom sheet - * @param sheetShadowElevation the shadow elevation of the bottom sheet - * @param sheetDragHandle optional visual marker to pull the scaffold's bottom sheet - * @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's - * input - * @param topBar top app bar of the screen, typically a [SmallTopAppBar] - * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via - * [SnackbarHostState.showSnackbar], typically a [SnackbarHost] - * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent] - * to have no color. - * @param contentColor the preferred color for content inside this scaffold. Defaults to either the - * matching content color for [containerColor], or to the current [LocalContentColor] if - * [containerColor] is not a color from the theme. - * @param content content of the screen. The lambda receives a [PaddingValues] that should be - * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to - * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to - * the child of the scroll, and not on the scroll itself. + * @sample com.adevinta.spark.components.bottomsheet.scaffold.BottomSheetPreview */ @Composable @ExperimentalMaterial3Api public fun BottomSheetScaffold( - sheetContent: @Composable ColumnScope.() -> Unit, + sheetContent: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), - sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight, - sheetShape: Shape = BottomSheetDefaults.ExpandedShape, - sheetContainerColor: Color = BottomSheetDefaults.ContainerColor, - sheetContentColor: Color = contentColorFor(sheetContainerColor), - sheetTonalElevation: Dp = BottomSheetDefaults.Elevation, - sheetDragHandle: @Composable (() -> Unit)? = { - BottomSheetDefaults.DragHandle(Modifier.padding(vertical = 8.dp)) - }, sheetSwipeEnabled: Boolean = true, + sheetDragHandle: @Composable (() -> Unit)? = { DragHandle() }, topBar: @Composable (() -> Unit)? = null, - snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, - containerColor: Color = MaterialTheme.colorScheme.surface, - contentColor: Color = contentColorFor(containerColor), + snackbarHost: @Composable (androidx.compose.material3.SnackbarHostState) -> Unit = { + androidx.compose.material3.SnackbarHost(it) + }, content: @Composable (PaddingValues) -> Unit, ) { - BottomSheetScaffoldLayout( - modifier = modifier, - topBar = topBar, - body = content, - snackbarHost = { - snackbarHost(scaffoldState.snackbarHostState) - }, - sheetPeekHeight = sheetPeekHeight, - sheetOffset = { scaffoldState.bottomSheetState.requireOffset() }, - sheetState = scaffoldState.bottomSheetState, - containerColor = containerColor, - contentColor = contentColor, - bottomSheet = { layoutHeight -> - StandardBottomSheet( - state = scaffoldState.bottomSheetState, - peekHeight = sheetPeekHeight, - sheetSwipeEnabled = sheetSwipeEnabled, - layoutHeight = layoutHeight.toFloat(), - shape = sheetShape, - containerColor = sheetContainerColor, - contentColor = sheetContentColor, - tonalElevation = sheetTonalElevation, - dragHandle = sheetDragHandle, - content = sheetContent, - ) - }, - ) -} - -/** - * State of the [BottomSheetScaffold] composable. - * - * @param bottomSheetState the state of the persistent bottom sheet - * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold - */ -@ExperimentalMaterial3Api -@Stable -public class BottomSheetScaffoldState( - public val bottomSheetState: SheetState, - public val snackbarHostState: SnackbarHostState, -) - -/** - * Create and [remember] a [BottomSheetScaffoldState]. - * - * @param bottomSheetState the state of the standard bottom sheet. See - * [rememberStandardBottomSheetState] - * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold - */ -@Composable -@ExperimentalMaterial3Api -public fun rememberBottomSheetScaffoldState( - bottomSheetState: SheetState = rememberStandardBottomSheetState(), - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, -): BottomSheetScaffoldState { - return remember(bottomSheetState, snackbarHostState) { - BottomSheetScaffoldState( - bottomSheetState = bottomSheetState, - snackbarHostState = snackbarHostState, - ) - } -} - -/** - * Create and [remember] a [SheetState] for [BottomSheetScaffold]. - * - * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or - * [Expanded] if [skipHiddenState] is true - * @param confirmValueChange optional callback invoked to confirm or veto a pending state change - * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold] - */ -@Composable -@ExperimentalMaterial3Api -public fun rememberStandardBottomSheetState( - initialValue: SheetValue = PartiallyExpanded, - confirmValueChange: (SheetValue) -> Boolean = { true }, - skipHiddenState: Boolean = true, -): SheetState = rememberSheetState(false, confirmValueChange, initialValue, skipHiddenState) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun StandardBottomSheet( - state: SheetState, - peekHeight: Dp, - sheetSwipeEnabled: Boolean, - layoutHeight: Float, - shape: Shape, - containerColor: Color, - contentColor: Color, - tonalElevation: Dp, - dragHandle: @Composable (() -> Unit)?, - content: @Composable ColumnScope.() -> Unit, -) { - val scope = rememberCoroutineScope() - val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() } - val orientation = Orientation.Vertical - - // Callback that is invoked when the anchors have changed. - val anchorChangeHandler = remember(state, scope) { - BottomSheetScaffoldAnchorChangeHandler( - state = state, - animateTo = { target, velocity -> - scope.launch { - state.swipeableState.animateTo( - target, - velocity = velocity, - ) - } - }, - snapTo = { target -> - scope.launch { state.swipeableState.snapTo(target) } - }, - ) - } - Surface( - modifier = Modifier - .widthIn(max = BottomSheetMaxWidth) - .fillMaxWidth() - .requiredHeightIn(min = peekHeight) - .nestedScroll( - remember(state.swipeableState) { - ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( - sheetState = state, - orientation = orientation, - onFling = { scope.launch { state.settle(it) } }, - ) - }, - ) - .swipeableV2( - state = state.swipeableState, - orientation = orientation, - enabled = sheetSwipeEnabled, - ) - .swipeAnchors( - state.swipeableState, - possibleValues = setOf(Hidden, PartiallyExpanded, Expanded), - anchorChangeHandler = anchorChangeHandler, - ) { value, sheetSize -> - when (value) { - PartiallyExpanded -> if (state.skipPartiallyExpanded) { - null - } else { - layoutHeight - peekHeightPx - } - - Expanded -> if (sheetSize.height == peekHeightPx.roundToInt()) { - null - } else { - max(0f, layoutHeight - sheetSize.height) - } - - Hidden -> if (state.skipHiddenState) null else layoutHeight - } - }, - shape = shape, - color = containerColor, - contentColor = contentColor, - elevation = tonalElevation, - ) { - Column(Modifier.fillMaxWidth()) { - if (dragHandle != null) { + androidx.compose.material3.BottomSheetScaffold( + sheetContent = { + Box { Box( - Modifier - .align(CenterHorizontally) - .semantics(mergeDescendants = true) { - with(state) { - // Provides semantics to interact with the bottomsheet if there is more - // than one anchor to swipe to and swiping is enabled. - if (swipeableState.anchors.size > 1 && sheetSwipeEnabled) { - if (currentValue == PartiallyExpanded) { - if (swipeableState.confirmValueChange(Expanded)) { - expand { - scope.launch { expand() } - true - } - } - } else { - if (swipeableState.confirmValueChange(PartiallyExpanded)) { - collapse { - scope.launch { partialExpand() } - true - } - } - } - if (!state.skipHiddenState) { - dismiss { - scope.launch { hide() } - true - } - } - } - } - }, + modifier = Modifier.padding( + top = if (sheetDragHandle != null) ContentTopPadding else ContentTopPaddingNoHandle, + ), ) { - dragHandle() + sheetContent() + } + if (sheetDragHandle != null) { + Box(contentAlignment = TopCenter, modifier = Modifier.fillMaxWidth()) { + DragHandle() + } } } - content() - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun BottomSheetScaffoldLayout( - topBar: @Composable (() -> Unit)?, - body: @Composable (innerPadding: PaddingValues) -> Unit, - bottomSheet: @Composable (layoutHeight: Int) -> Unit, - snackbarHost: @Composable () -> Unit, - sheetPeekHeight: Dp, - sheetOffset: () -> Float, - sheetState: SheetState, - containerColor: Color, - contentColor: Color, - modifier: Modifier = Modifier, -) { - SubcomposeLayout { constraints -> - val layoutWidth = constraints.maxWidth - val layoutHeight = constraints.maxHeight - val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) { - bottomSheet(layoutHeight) - }[0].measure(looseConstraints) - val sheetOffsetY = sheetOffset().roundToInt() - val sheetOffsetX = Integer.max(0, (layoutWidth - sheetPlaceable.width) / 2) - - val topBarPlaceable = topBar?.let { - subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0] - .measure(looseConstraints) - } - val topBarHeight = topBarPlaceable?.height ?: 0 - - val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight) - val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) { - Surface( - modifier = modifier, - color = containerColor, - contentColor = contentColor, - ) { body(PaddingValues(bottom = sheetPeekHeight)) } - }[0].measure(bodyConstraints) - - val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0] - .measure(looseConstraints) - val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2 - val snackbarOffsetY = when (sheetState.currentValue) { - PartiallyExpanded -> sheetOffsetY - snackbarPlaceable.height - Expanded, Hidden -> layoutHeight - snackbarPlaceable.height - } - - layout(layoutWidth, layoutHeight) { - // Placement order is important for elevation - bodyPlaceable.placeRelative(0, topBarHeight) - topBarPlaceable?.placeRelative(0, 0) - sheetPlaceable.placeRelative(sheetOffsetX, sheetOffsetY) - snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY) - } - } -} - -@ExperimentalMaterial3Api -@Suppress("ktlint:standard:function-naming") -private fun BottomSheetScaffoldAnchorChangeHandler( - state: SheetState, - animateTo: (target: SheetValue, velocity: Float) -> Unit, - snapTo: (target: SheetValue) -> Unit, -) = AnchorChangeHandler { previousTarget, previousAnchors, newAnchors -> - val previousTargetOffset = previousAnchors[previousTarget] - val newTarget = when (previousTarget) { - Hidden, PartiallyExpanded -> PartiallyExpanded - Expanded -> if (newAnchors.containsKey(Expanded)) Expanded else PartiallyExpanded - } - val newTargetOffset = newAnchors.getValue(newTarget) - if (newTargetOffset != previousTargetOffset) { - if (state.swipeableState.isAnimationRunning) { - // Re-target the animation to the new offset if it changed - animateTo(newTarget, state.swipeableState.lastVelocity) - } else { - // Snap to the new offset value of the target if no animation was running - snapTo(newTarget) - } + }, + modifier = modifier, + scaffoldState = scaffoldState, + sheetDragHandle = null, + sheetSwipeEnabled = sheetSwipeEnabled, + topBar = topBar, + snackbarHost = snackbarHost, + ) { + content(it) } } -private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Snackbar } - -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Preview( group = "BottomSheet", name = "BottomSheetScaffold", @@ -451,18 +137,33 @@ internal fun BottomSheetPreview() { }, modifier = Modifier.fillMaxSize(), scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(PartiallyExpanded), + bottomSheetState = rememberStandardBottomSheetState(), ), ) { - Box( - modifier = Modifier - .background(Color.Red) - .fillMaxSize(), - ) { - Text( - modifier = Modifier.align(TopCenter), - text = "Screen Content", - ) + LazyColumn { + stickyHeader { + Row( + Modifier + .fillMaxWidth() + .background(Color.Green), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Screen Content", + ) + } + } + items(50) { + ListItem( + headlineContent = { Text("Item $it") }, + leadingContent = { + Icon( + SparkIcons.LikeFill, + contentDescription = "Localized description", + ) + }, + ) + } } } } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt index b8f3df0a1..bb40ba1fa 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt @@ -41,6 +41,7 @@ import com.adevinta.spark.icons.IdentityOutline import com.adevinta.spark.icons.Link import com.adevinta.spark.icons.SparkIcon import com.adevinta.spark.icons.SparkIcons +import com.adevinta.spark.tokens.dim3 import com.adevinta.spark.tokens.disabled import com.adevinta.spark.tools.preview.ThemeProvider import com.adevinta.spark.tools.preview.ThemeVariant @@ -90,7 +91,7 @@ public fun ButtonContrast( val buttonColors = ButtonDefaults.buttonColors( containerColor = containerColor, contentColor = contentColor, - disabledContainerColor = containerColor.disabled, + disabledContainerColor = containerColor.dim3, disabledContentColor = contentColor.disabled, ) BaseSparkButton( @@ -155,7 +156,7 @@ public fun ButtonContrast( val buttonColors = ButtonDefaults.buttonColors( containerColor = containerColor, contentColor = contentColor, - disabledContainerColor = containerColor.disabled, + disabledContainerColor = containerColor.dim3, disabledContentColor = contentColor.disabled, ) SparkButton( @@ -209,17 +210,18 @@ public fun ButtonContrast( isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { - val backgroundColor by animateColorAsState( - targetValue = intent.colors().color, - label = "background color", - ) + val containerColor = SparkTheme.colors.surface + val colors = intent.colors() val contentColor by animateColorAsState( - targetValue = intent.colors().onColor, + targetValue = if (colors.color == containerColor) colors.onColor else colors.color, label = "content color", ) - val colors = ButtonDefaults.buttonColors( - containerColor = backgroundColor, + + val buttonColors = ButtonDefaults.buttonColors( + containerColor = containerColor, contentColor = contentColor, + disabledContainerColor = containerColor.dim3, + disabledContentColor = contentColor.disabled, ) SparkButton( onClick = onClick, @@ -228,7 +230,7 @@ public fun ButtonContrast( size = size, enabled = enabled, elevation = ButtonDefaults.buttonElevation(), - colors = colors, + colors = buttonColors, icon = icon, iconSide = iconSide, isLoading = isLoading, diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt index aa062969f..0b75c2e54 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt @@ -40,6 +40,7 @@ import com.adevinta.spark.SparkTheme import com.adevinta.spark.icons.IdentityOutline import com.adevinta.spark.icons.SparkIcon import com.adevinta.spark.icons.SparkIcons +import com.adevinta.spark.tokens.dim3 import com.adevinta.spark.tokens.disabled import com.adevinta.spark.tools.preview.ThemeProvider import com.adevinta.spark.tools.preview.ThemeVariant @@ -93,7 +94,7 @@ public fun ButtonFilled( containerColor = backgroundColor, contentColor = contentColor, disabledContainerColor = backgroundColor.disabled, - disabledContentColor = contentColor, + disabledContentColor = contentColor.dim3, ) BaseSparkButton( onClick = onClick, @@ -161,7 +162,7 @@ public fun ButtonFilled( containerColor = backgroundColor, contentColor = contentColor, disabledContainerColor = backgroundColor.disabled, - disabledContentColor = contentColor, + disabledContentColor = contentColor.disabled, ) SparkButton( onClick = onClick, diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/image/Image.kt b/spark/src/main/kotlin/com/adevinta/spark/components/image/Image.kt index ba37ec136..9564d95b3 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/image/Image.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/image/Image.kt @@ -138,6 +138,8 @@ internal fun SparkImage( * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. * background) * @param onState A callback function that is called when the state of the image changes. + * @param emptyIcon Placeholder used when the image loading has not started yet. + * @param errorIcon Placeholder used when the image loading failed. * @param alignment Optional alignment parameter used to place the image in the given * bounds defined by the width and height * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used @@ -157,6 +159,8 @@ public fun Image( contentDescription: String?, modifier: Modifier = Modifier, onState: ((State) -> Unit)? = null, + emptyIcon: @Composable () -> Unit = { ImageIconState(SparkIcons.NoPhoto) }, + errorIcon: @Composable () -> Unit = { ImageIconState(SparkIcons.ErrorPhoto) }, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, @@ -169,6 +173,8 @@ public fun Image( contentDescription = contentDescription, modifier = modifier, onState = onState, + emptyIcon = emptyIcon, + errorIcon = errorIcon, alignment = alignment, contentScale = contentScale, alpha = alpha, diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTracker.kt b/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTracker.kt index 639131021..645af9f07 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTracker.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTracker.kt @@ -78,7 +78,7 @@ import com.adevinta.spark.icons.SparkIcons import com.adevinta.spark.tokens.dim1 import com.adevinta.spark.tokens.disabled import com.adevinta.spark.tokens.highlight -import com.adevinta.spark.tools.modifiers.ifTrue +import com.adevinta.spark.tokens.transparent import com.adevinta.spark.tools.modifiers.sparkUsageOverlay import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -160,6 +160,11 @@ public fun ProgressTrackerColumn( onStepClick: ((index: Int) -> Unit)? = null, selectedStep: Int = 0, ) { + require(items.all { it.label.isNotBlank() }) { + "All steps in the vertical layout should have a label otherwise it'll have render issues." + + "This is a known bug, if you need to make this layout please report it there" + + " https://github.com/adevinta/spark-android/issues/1080" + } ProgressTracker( items = items, orientation = Vertical, @@ -218,7 +223,6 @@ private fun ProgressTracker( label = progressStep.label, enabled = progressStep.enabled, orientation = orientation, - size = size, selected = index == selectedStep, interactionSource = interactionSources[index], onClick = { onStepClick?.invoke(index) }, @@ -238,7 +242,7 @@ private fun ProgressTracker( done = isDone, hasIndicatorContent = hasIndicatorContent, onClick = onStepClick?.let { - { onStepClick.invoke(index) } + { onStepClick(index) } }, interactionSource = interactionSources[index], ) @@ -298,7 +302,6 @@ internal fun progressTrackerMeasurePolicy( private fun StepLabel( onClick: () -> Unit, orientation: LayoutOrientation, - size: ProgressSizes, modifier: Modifier = Modifier, label: CharSequence = "", selected: Boolean = false, @@ -314,15 +317,13 @@ private fun StepLabel( val labelModifier = modifier .layoutId(LabelId) .paddingFromBaseline(top = 16.dp) - .ifTrue(size == ProgressSizes.Large) { - selectable( - selected = selected, - interactionSource = interactionSource, - indication = LocalIndication.current, - enabled = enabled, - ) { - onClick() - } + .selectable( + selected = selected, + interactionSource = interactionSource, + indication = LocalIndication.current, + enabled = enabled, + ) { + onClick() } val textAlign = if (orientation == Horizontal) TextAlign.Center else TextAlign.Start when (label) { @@ -379,7 +380,16 @@ private fun StepIndicator( targetValue = if (selected) { if (isOutlined) colors.containerColor else colors.color } else { - if (isOutlined) Color.Transparent else colors.containerColor + if (isOutlined) colors.containerColor.transparent else colors.containerColor + }, + label = "Indicator color", + ) + + val indicatorContentColor by animateColorAsState( + targetValue = if (selected) { + if (isOutlined) colors.onContainerColor else colors.onColor + } else { + if (isOutlined) SparkTheme.colors.onSurface else colors.onContainerColor }, label = "Indicator color", ) @@ -400,16 +410,18 @@ private fun StepIndicator( } .size(size.size), color = indicatorColor, + contentColor = indicatorContentColor, border = if (isOutlined) { BorderStroke(borderSize, colors.color) } else { null }, elevation = elevation, - enabled = enabled && size == ProgressSizes.Large, + enabled = enabled, onClick = { onClick?.invoke() }, interactionSource = interactionSource, ) { + onClick?.invoke() AnimatedVisibility(visible = size != ProgressSizes.Small && hasIndicatorContent) { Box( contentAlignment = Alignment.Center, @@ -557,3 +569,66 @@ private fun PreviewProgressStyles() { ) } } + +@Composable +@Preview +private fun PreviewProgressWithNoLabel() { + PreviewTheme(padding = PaddingValues(0.dp), contentPadding = 0.dp) { + val selectedStep by remember { mutableIntStateOf(1) } + val items = persistentListOf( + ProgressStep("", true), + ProgressStep("", true), + ProgressStep("", false), + ) + for (size in ProgressSizes.entries) { + ProgressTrackerRow( + items = items, + size = size, + selectedStep = selectedStep, + ) + } + Row { + for (size in ProgressSizes.entries) { + ProgressTrackerColumn( + items = persistentListOf( + ProgressStep("", false), + ProgressStep("", false), + ProgressStep("", false), + ), + size = size, + selectedStep = selectedStep, + ) + } + } + } +} + +@Composable +@Preview( + group = "ProgressIndicator", +) +private fun PreviewProgressIndicator() { + PreviewTheme { + val selectedStep by remember { mutableIntStateOf(1) } + val items = persistentListOf( + ProgressStep("", true), + ProgressStep("", true), + ProgressStep("", false), + ) + items.forEachIndexed { index, progressStep -> + val isDone = index < selectedStep + StepIndicator( + colors = ProgressTrackerIntent.Basic.colors(), + size = ProgressSizes.Large, + style = ProgressStyles.Outlined, + index = index, + enabled = progressStep.enabled, + selected = index == selectedStep, + done = isDone, + hasIndicatorContent = true, + onClick = {}, + interactionSource = MutableInteractionSource(), + ) + } + } +} diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTrackerMeasurePolicy.kt b/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTrackerMeasurePolicy.kt index 578cbeaa0..1bfc7440b 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTrackerMeasurePolicy.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/progress/tracker/ProgressTrackerMeasurePolicy.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMapIndexed import androidx.compose.ui.util.fastMaxOfOrNull import androidx.compose.ui.util.fastSumBy +import com.adevinta.spark.components.textfields.hasWidthThenDefault import com.adevinta.spark.components.textfields.heightOrZero internal data class ProgressTrackerMeasurePolicy( @@ -103,16 +104,23 @@ internal data class ProgressTrackerMeasurePolicy( layoutWidth = labelPlaceables.fastSumBy(Placeable::width) // Calculate height of the layout by taking into account the height maximum height of all labels, indicator // and padding - layoutHeight = (labelPlaceables.fastMaxOfOrNull(Placeable::height) ?: 0) + - arrangementSpacing.roundToPx() + - indicatorPlaceables.first().width + val maxLabelHeight = labelPlaceables.fastMaxOfOrNull(Placeable::height) ?: 0 + val maxLabelHeightWithPadding = maxLabelHeight.takeIf { + it > 0 // Don't add the padding if there are no labels + }?.let { + it + arrangementSpacing.roundToPx() + } ?: 0 + layoutHeight = indicatorPlaceables.first().height + maxLabelHeightWithPadding } else { - layoutWidth = ( - labelPlaceables.minByOrNull(Placeable::width)?.width - ?: 0 - ) + arrangementSpacing.roundToPx() + indicatorPlaceables.first().width + val maxLabelWidth = labelPlaceables.maxByOrNull(Placeable::width)?.width ?: 0 + val maxLabelWidthWithPadding = maxLabelWidth.takeIf { + it > 0 // Don't add the padding if there are no labels + }?.let { + it + arrangementSpacing.roundToPx() + } ?: 0 + layoutWidth = indicatorPlaceables.first().width + maxLabelWidthWithPadding layoutHeight = labelPlaceables.fastSumBy { - maxOf(it.height, indicatorPlaceables.first().width) + arrangementSpacing.roundToPx() + maxOf(it.height, indicatorPlaceables.first().height) + arrangementSpacing.roundToPx() } } @@ -179,29 +187,29 @@ internal data class ProgressTrackerMeasurePolicy( with(measureScope) { var previousLabelY = 0 - labelPlaceables.fastForEachIndexed { index, labelPlaceable -> - val indicatorPlaceable = indicatorPlaceables[index] + indicatorPlaceables.fastForEachIndexed { index, indicatorPlaceable -> + val labelPlaceable = labelPlaceables[index] val labelFirstBaseline = labelPlaceable[FirstBaseline] labelPlaceable.placeRelative( - x = indicatorPlaceable.width + arrangementSpacing.roundToPx(), + x = indicatorPlaceable.width + labelPlaceable.hasWidthThenDefault(arrangementSpacing.roundToPx()), y = previousLabelY + ( indicatorPlaceable.height / 2 - labelFirstBaseline + 5.sp.roundToPx() // Magic number to center the text to the indicator text baseline ), ) - indicatorPlaceable.placeRelative( - x = 0, - y = previousLabelY, - ) + indicatorPlaceable.placeRelative(x = 0, y = previousLabelY) if (index < indicatorPlaceables.size - 1) { trackPlaceables[index].placeRelative( x = indicatorPlaceable.width / 2, y = previousLabelY + indicatorPlaceable.height + arrangementSpacing.roundToPx() / 2, ) } - previousLabelY += labelPlaceable.height + arrangementSpacing.roundToPx() + previousLabelY += maxOf( + labelPlaceable.height, + indicatorSize.roundToPx(), + ) + arrangementSpacing.roundToPx() } } } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/progressbar/Progressbar.kt b/spark/src/main/kotlin/com/adevinta/spark/components/progressbar/Progressbar.kt index e481a6af6..f426b8f46 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/progressbar/Progressbar.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/progressbar/Progressbar.kt @@ -74,17 +74,14 @@ internal fun SparkProgressbar( * represents full progress. Values outside of this range are coerced into the range. * @param intent The intent color for the Progressbar. * @param modifier Modifier to be applied to the Progressbar - * @param isRounded Controls the border shape of the progressbar. When `true`, this progressbar will have rounded border shape, & the default is rounded + * @param isRounded Controls the border shape of the progressbar. When `true`, + * this progressbar will have rounded border shape, & the default is rounded */ @Deprecated( message = "Use the overload that takes `progress` as a lambda", replaceWith = ReplaceWith( - "Progressbar(\n" + - "progress = { progress },\n" + - "modifier = modifier,\n" + - "intent = intent,\n" + - "isRounded = isRounded,\n" + - ")", + "Progressbar(\n" + "progress = { progress },\n" + "modifier = modifier,\n" + + "intent = intent,\n" + "isRounded = isRounded,\n" + ")", ), ) @Composable @@ -112,7 +109,8 @@ public fun Progressbar( * represents full progress. Values outside of this range are coerced into the range. * @param intent The intent color for the Progressbar. * @param modifier Modifier to be applied to the Progressbar - * @param isRounded Controls the border shape of the progressbar. When `true`, this progressbar will have rounded border shape, & the default is rounded + * @param isRounded Controls the border shape of the progressbar. When `true`, + * this progressbar will have rounded border shape, & the default is rounded */ @Composable @@ -138,7 +136,8 @@ public fun Progressbar( * * @param intent The intent color for the Progressbar. * @param modifier Modifier to be applied to the Progressbar - * @param isRounded Controls the border shape of the progressbar. When `true`, this progressbar will have rounded border shape, & the default is rounded + * @param isRounded Controls the border shape of the progressbar. When `true`, + * this progressbar will have rounded border shape, & the default is rounded */ @Composable diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/text/TextLink.kt b/spark/src/main/kotlin/com/adevinta/spark/components/text/TextLink.kt index a258097c1..32a5b68f6 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/text/TextLink.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/text/TextLink.kt @@ -175,7 +175,6 @@ public fun TextLink( * 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. */ -@SuppressLint("VisibleForTests") @Composable public fun TextLinkButton( text: String, diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/MultilineTextField.kt b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/MultilineTextField.kt index b648ea764..710510182 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/MultilineTextField.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/MultilineTextField.kt @@ -43,7 +43,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter import com.adevinta.spark.PreviewTheme import com.adevinta.spark.R import com.adevinta.spark.components.icons.Icon @@ -54,8 +53,6 @@ import com.adevinta.spark.icons.DeleteOutline import com.adevinta.spark.icons.LikeFill import com.adevinta.spark.icons.SparkIcons import com.adevinta.spark.tokens.SparkTypography -import com.adevinta.spark.tools.preview.ThemeProvider -import com.adevinta.spark.tools.preview.ThemeVariant import kotlinx.coroutines.flow.flowOf /** @@ -304,10 +301,8 @@ public fun MultilineTextField( name = "MultilineTextField intents", ) @Composable -private fun MultilineTextFieldIntentPreview( - @PreviewParameter(ThemeProvider::class) theme: ThemeVariant, -) { - PreviewTheme(theme) { +private fun MultilineTextFieldIntentPreview() { + PreviewTheme { PreviewTextFields( state = null, stateMessage = "Helper text", @@ -340,9 +335,9 @@ private fun ColumnScope.PreviewTextFields( required = true, label = "Label", placeholder = "Placeholder", - counter = TextFieldCharacterCounter(12, 24), + counter = TextFieldCharacterCounter(242424242, 242424242), maxLines = 3, - helper = "Helper text", + helper = "Placeholder Placeholder Placeholder Placeholder", leadingContent = icon, ) diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldImpl.kt b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldImpl.kt index d69bf5f7d..50f488505 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldImpl.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldImpl.kt @@ -379,7 +379,9 @@ internal fun SparkTextFieldLayout( } internal fun widthOrZero(placeable: Placeable?) = placeable?.width ?: 0 +internal fun Placeable?.hasWidthThenDefault(default: Int) = this?.width?.takeIf { it > 0 }?.let { default } ?: 0 internal fun heightOrZero(placeable: Placeable?) = placeable?.height ?: 0 +internal fun Placeable?.hasHeightThenDefault(default: Int) = this?.height?.takeIf { it > 0 }?.let { default } ?: 0 internal fun Modifier.outlineCutout(labelSize: Size, leftPadding: Dp) = this.drawWithContent { diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldMeasurePolicy.kt b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldMeasurePolicy.kt index eea9afa81..157d50366 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldMeasurePolicy.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/textfields/SparkTextFieldMeasurePolicy.kt @@ -136,7 +136,7 @@ internal class SparkTextFieldMeasurePolicy( it.width + CounterPadding.roundToPx() + paddingValues.calculateEndPadding(layoutDirection).roundToPx() } ?: 0 val supportingMaxWidth = - width - paddingValues.calculateLeftPadding(layoutDirection).roundToPx() - counterOccupiedWidth + maxOf(width - paddingValues.calculateLeftPadding(layoutDirection).roundToPx() - counterOccupiedWidth, 0) // measure supporting text val supportingConstraints = relaxedConstraints.offset( diff --git a/spark/src/main/kotlin/com/adevinta/spark/tokens/Color.kt b/spark/src/main/kotlin/com/adevinta/spark/tokens/Color.kt index ccb53ce91..ea2d976ff 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/tokens/Color.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/tokens/Color.kt @@ -162,7 +162,6 @@ public fun lightSparkColors( onSurface: Color = BlackAdevinta900, surfaceInverse: Color = BlackAdevinta800, onSurfaceInverse: Color = White, - surfaceTint: Color = main, inversePrimary: Color = Color.Magenta, outline: Color = BlackAdevinta100, outlineHigh: Color = BlackAdevinta900, @@ -233,7 +232,7 @@ public fun lightSparkColors( onSurfaceInverse = onSurfaceInverse, surfaceVariant = backgroundVariant, onSurfaceVariant = onBackgroundVariant, - surfaceTint = surfaceTint, + surfaceTint = surface, outline = outline, outlineHigh = outlineHigh, outlineVariant = outlineHigh, diff --git a/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/Drawings.kt b/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/Drawings.kt index d8cb1d60f..99773a243 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/Drawings.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/tools/modifiers/Drawings.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache @@ -69,6 +70,22 @@ public fun Modifier.dashedBorder(border: BorderStroke, shape: Shape = RectangleS public fun Modifier.dashedBorder(width: Dp, color: Color, shape: Shape = RectangleShape): Modifier = dashedBorder(width = width, brush = SolidColor(color), shape = shape) +/** + * Returns a [Modifier] that adds border with appearance specified with [width], [color] and a + * [radius], pads the content by the [width] and clips it. + * + * @param width width of the border. Use [Dp.Hairline] for a hairline border. + * @param color color to paint the border with + * @param radius shape of the border + */ +@Deprecated( + message = "Use dashedBorder(width: Dp, color: Color, shape: Shape) instead since it know allow you to specify " + + "a shape from Spark", + replaceWith = ReplaceWith("dashedBorder(width, color, shape)"), +) +public fun Modifier.dashedBorder(width: Dp, radius: Dp, color: Color): Modifier = + dashedBorder(width = width, brush = SolidColor(color), shape = RoundedCornerShape(radius)) + /** * Add a dashed border on a Composable */ diff --git a/spark/src/main/res/values-fr/strings.xml b/spark/src/main/res/values-fr/strings.xml index 2f272700e..871a06f56 100644 --- a/spark/src/main/res/values-fr/strings.xml +++ b/spark/src/main/res/values-fr/strings.xml @@ -61,6 +61,7 @@ Obligatoire Tout supprimer + PoignΓ©e de dΓ©placement diff --git a/spark/src/main/res/values/strings.xml b/spark/src/main/res/values/strings.xml index 1896cf835..3257b46f6 100644 --- a/spark/src/main/res/values/strings.xml +++ b/spark/src/main/res/values/strings.xml @@ -58,4 +58,7 @@ Cancel + + Drag handle + \ No newline at end of file