diff --git a/NOTICE.txt b/NOTICE.txt index f289914a..67b6476b 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -19,6 +19,7 @@ app/src/main/res/drawable/ic_copy.xml app/src/main/res/drawable/ic_design_token_figma.xml app/src/main/res/drawable/ic_dimension.xml app/src/main/res/drawable/ic_filter_effects.xml +app/src/main/res/drawable/ic_heart.xml app/src/main/res/drawable/ic_info.xml app/src/main/res/drawable/ic_layers.xml app/src/main/res/drawable/ic_menu_grid.xml @@ -28,18 +29,23 @@ app/src/main/res/drawable/ic_typography.xml app/src/main/res/drawable/ic_ui_dark_mode.xml app/src/main/res/drawable/ic_ui_light_mode.xml app/src/main/res/drawable/il_opacity_union.xml +app/src/main/res/drawable-hdpi/il_components_button.png app/src/main/res/drawable-hdpi/il_tokens_grid_column_margin.png app/src/main/res/drawable-hdpi/il_tokens_grid_max_width.png app/src/main/res/drawable-hdpi/il_tokens_grid_min_width.png +app/src/main/res/drawable-mdpi/il_components_button.png app/src/main/res/drawable-mdpi/il_tokens_grid_column_margin.png app/src/main/res/drawable-mdpi/il_tokens_grid_max_width.png app/src/main/res/drawable-mdpi/il_tokens_grid_min_width.png +app/src/main/res/drawable-xhdpi/il_components_button.png app/src/main/res/drawable-xhdpi/il_tokens_grid_column_margin.png app/src/main/res/drawable-xhdpi/il_tokens_grid_max_width.png app/src/main/res/drawable-xhdpi/il_tokens_grid_min_width.png +app/src/main/res/drawable-xxhdpi/il_components_button.png app/src/main/res/drawable-xxhdpi/il_tokens_grid_column_margin.png app/src/main/res/drawable-xxhdpi/il_tokens_grid_max_width.png app/src/main/res/drawable-xxhdpi/il_tokens_grid_min_width.png +app/src/main/res/drawable-xxxhdpi/il_components_button.png app/src/main/res/drawable-xxxhdpi/il_tokens_grid_column_margin.png app/src/main/res/drawable-xxxhdpi/il_tokens_grid_max_width.png app/src/main/res/drawable-xxxhdpi/il_tokens_grid_min_width.png diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d11a926f..f79ffb02 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,6 +19,7 @@ plugins { id(libs.plugins.android.application.get().pluginId) // https://github.com/gradle/gradle/issues/20084#issuecomment-1060822638 id(libs.plugins.kotlin.android.get().pluginId) id(libs.plugins.kotlin.kapt.get().pluginId) + id(libs.plugins.kotlin.parcelize.get().pluginId) alias(libs.plugins.compose.compiler) alias(libs.plugins.firebase.appdistribution) alias(libs.plugins.firebase.crashlytics) diff --git a/app/src/main/java/com/orange/ouds/app/ui/Screen.kt b/app/src/main/java/com/orange/ouds/app/ui/Screen.kt index 80b20987..f6b3761f 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/Screen.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/Screen.kt @@ -19,6 +19,8 @@ import com.orange.ouds.app.R import com.orange.ouds.app.ui.about.AboutDestinations import com.orange.ouds.app.ui.about.AboutMenuItem import com.orange.ouds.app.ui.about.AboutNavigationKey +import com.orange.ouds.app.ui.components.Component +import com.orange.ouds.app.ui.components.ComponentsNavigation import com.orange.ouds.app.ui.tokens.TokenCategory import com.orange.ouds.app.ui.tokens.TokensNavigation import com.orange.ouds.foundation.UiString @@ -33,14 +35,9 @@ fun getScreen(route: String, args: Bundle?): Screen? { // Specific element route -> get element id val (routeRoot) = matchElementRouteResult.destructured when (routeRoot) { - TokensNavigation.TokenCategoryDetailRoute -> { - args?.getLong(TokensNavigation.TokenCategoryIdKey)?.let { Screen.TokenCategoryDetail(it) } - } - - AboutDestinations.FileRoute -> { - args?.getLong(AboutNavigationKey.MenuItemIdKey)?.let { Screen.AboutFile(it) } - } - + TokensNavigation.TokenCategoryDetailRoute -> args?.getLong(TokensNavigation.TokenCategoryIdKey)?.let { Screen.TokenCategoryDetail(it) } + ComponentsNavigation.ComponentDetailRoute -> args?.getLong(ComponentsNavigation.ComponentIdKey)?.let { Screen.ComponentDetail(it) } + AboutDestinations.FileRoute -> args?.getLong(AboutNavigationKey.MenuItemIdKey)?.let { Screen.AboutFile(it) } else -> null } } else { @@ -94,6 +91,13 @@ sealed class Screen( title = TokenCategory.fromId(tokenCategoryId)?.nameRes?.let { UiString.StringResource(it) } ) + // Components screens + + data class ComponentDetail(val componentId: Long) : Screen( + route = ComponentsNavigation.ComponentDetailRoute, + title = Component.fromId(componentId)?.nameRes?.let { UiString.StringResource(it) } + ) + // About screens data class AboutFile(val menuItemId: Long) : Screen( diff --git a/app/src/main/java/com/orange/ouds/app/ui/components/Component.kt b/app/src/main/java/com/orange/ouds/app/ui/components/Component.kt new file mode 100644 index 00000000..d3b85189 --- /dev/null +++ b/app/src/main/java/com/orange/ouds/app/ui/components/Component.kt @@ -0,0 +1,44 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.app.ui.components + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import com.orange.ouds.app.R +import com.orange.ouds.app.ui.components.button.ButtonDemoScreen + +val components = Component::class.sealedSubclasses.mapNotNull { it.objectInstance } + +@Immutable +sealed class Component( + @StringRes val nameRes: Int, + @DrawableRes val imageRes: Int, + @StringRes val descriptionRes: Int, + val demoScreen: @Composable () -> Unit +) { + + companion object { + fun fromId(componentId: Long) = components.firstOrNull { component -> component.id == componentId } + } + + val id: Long = Component::class.sealedSubclasses.indexOf(this::class).toLong() + + data object Button : Component( + R.string.app_components_button_label, + R.drawable.il_components_button, + R.string.app_components_button_description_text, + { ButtonDemoScreen() } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsNavGraph.kt b/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsNavGraph.kt new file mode 100644 index 00000000..c23ae725 --- /dev/null +++ b/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsNavGraph.kt @@ -0,0 +1,39 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.app.ui.components + +import androidx.compose.runtime.remember +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument + +object ComponentsNavigation { + const val ComponentDetailRoute = "component" + const val ComponentIdKey = "componentId" +} + +fun NavGraphBuilder.addComponentsNavGraph() { + composable( + "${ComponentsNavigation.ComponentDetailRoute}/{${ComponentsNavigation.ComponentIdKey}}", + arguments = listOf(navArgument(ComponentsNavigation.ComponentIdKey) { type = NavType.LongType }) + ) { from -> + val arguments = requireNotNull(from.arguments) + val routeComponentId = arguments.getLong(ComponentsNavigation.ComponentIdKey) + + val component = remember(routeComponentId) { Component.fromId(routeComponentId) } + component?.let { + component.demoScreen() + } + } +} diff --git a/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsScreen.kt b/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsScreen.kt index 822633bd..2e863fec 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsScreen.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/components/ComponentsScreen.kt @@ -12,47 +12,47 @@ package com.orange.ouds.app.ui.components -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.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Text +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import com.orange.ouds.app.ui.utilities.composable.LargeCard import com.orange.ouds.app.ui.utilities.composable.Screen -import com.orange.ouds.core.component.button.OudsButton import com.orange.ouds.core.theme.value import com.orange.ouds.core.utilities.OudsPreview import com.orange.ouds.foundation.utilities.UiModePreviews -import com.orange.ouds.theme.tokens.OudsColorKeyToken -import com.orange.ouds.theme.tokens.OudsGridKeyToken import com.orange.ouds.theme.tokens.OudsSpaceKeyToken @Composable -fun ComponentsScreen() { +fun ComponentsScreen(onComponentClick: (Long) -> Unit) { + ComponentsScreen( + components = components, + onComponentClick = onComponentClick + ) +} + +@Composable +private fun ComponentsScreen(components: List, onComponentClick: (Long) -> Unit) { Screen { Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(OudsSpaceKeyToken.Fixed.Medium.value), + verticalArrangement = Arrangement.spacedBy(OudsSpaceKeyToken.Fixed.Medium.value) ) { - Text(modifier = Modifier.padding(bottom = 8.dp), text = "Components screen") - - OudsButton(text = "OUDS button", onClick = { }) - - Box( - modifier = Modifier - .padding(top = OudsSpaceKeyToken.Fixed.Medium.value) - .width(OudsGridKeyToken.Margin.value) - .height(OudsGridKeyToken.ColumnGap.value) - .background(OudsColorKeyToken.Content.BrandPrimary.value) - ) + components.forEach { component -> + LargeCard( + title = stringResource(id = component.nameRes), + imageRes = component.imageRes, + onClick = { onComponentClick(component.id) } + ) + } } } } @@ -60,5 +60,7 @@ fun ComponentsScreen() { @UiModePreviews.Default @Composable private fun PreviewComponentsScreen() = OudsPreview { - ComponentsScreen() + ComponentsScreen( + components = listOf(Component.Button) + ) {} } diff --git a/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt b/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt new file mode 100644 index 00000000..86d4b420 --- /dev/null +++ b/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt @@ -0,0 +1,149 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.app.ui.components.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.orange.ouds.app.R +import com.orange.ouds.app.ui.components.Component +import com.orange.ouds.app.ui.utilities.composable.CustomizationBottomSheetScaffold +import com.orange.ouds.app.ui.utilities.composable.CustomizationChoiceChipsColumn +import com.orange.ouds.app.ui.utilities.composable.CustomizationSwitchListItem +import com.orange.ouds.app.ui.utilities.composable.DemoScreen +import com.orange.ouds.app.ui.utilities.composable.DetailScreenDescription +import com.orange.ouds.core.component.button.OudsButton +import com.orange.ouds.core.theme.OudsTheme +import com.orange.ouds.core.theme.OudsThemeTweak +import com.orange.ouds.core.theme.value +import com.orange.ouds.core.utilities.OudsPreview +import com.orange.ouds.foundation.utilities.UiModePreviews +import com.orange.ouds.theme.tokens.OudsSpaceKeyToken + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ButtonDemoScreen() = DemoScreen(rememberButtonDemoState()) { + CustomizationBottomSheetScaffold( + bottomSheetScaffoldState = rememberBottomSheetScaffoldState(), + bottomSheetContent = { + CustomizationSwitchListItem( + label = stringResource(R.string.app_common_enabled_label), + checked = enabled, + onCheckedChange = { enabled = it }, + enabled = style == OudsButton.Style.Default + ) + CustomizationChoiceChipsColumn( + modifier = Modifier.padding(top = OudsSpaceKeyToken.Fixed.Medium.value), + label = stringResource(R.string.app_components_button_hierarchy_label), + chipsLabels = OudsButton.Hierarchy.entries.map { it.name }, + selectedChipIndex = OudsButton.Hierarchy.entries.indexOf(hierarchy), + onSelectionChange = { id -> hierarchy = OudsButton.Hierarchy.entries[id] } + ) + val styles = remember { + listOf( + OudsButton.Style.Default, + OudsButton.Style.Loading(progress = null), + OudsButton.Style.Skeleton + ) + } + CustomizationChoiceChipsColumn( + modifier = Modifier.padding(top = OudsSpaceKeyToken.Fixed.Medium.value), + label = stringResource(R.string.app_components_button_style_label), + chipsLabels = styles.map { it::class.simpleName.orEmpty() }, + selectedChipIndex = styles.indexOf(style), + onSelectionChange = { id -> style = styles[id] } + ) + CustomizationChoiceChipsColumn( + modifier = Modifier.padding(top = OudsSpaceKeyToken.Fixed.Medium.value), + label = stringResource(R.string.app_components_button_layout_label), + chipsLabels = ButtonDemoState.Layout.entries.map { stringResource(it.labelRes) }, + selectedChipIndex = ButtonDemoState.Layout.entries.indexOf(layout), + onSelectionChange = { id -> layout = ButtonDemoState.Layout.entries[id] } + ) + } + ) { + Column(modifier = Modifier.fillMaxWidth()) { + DetailScreenDescription( + modifier = Modifier.padding(all = OudsSpaceKeyToken.Fixed.Medium.value), + descriptionRes = Component.Button.descriptionRes + ) + ButtonDemo(state = this@DemoScreen) + OudsThemeTweak(OudsTheme.Tweak.Invert) { + ButtonDemo(state = this@DemoScreen) + } + } + } +} + +@Composable +private fun ButtonDemo(state: ButtonDemoState) { + Box( + modifier = Modifier + .background(OudsTheme.colorScheme.backgroundColors.primary) + .padding(all = OudsSpaceKeyToken.Fixed.Medium.value) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + val text = stringResource(id = R.string.app_components_button_label) + val icon = OudsButton.Icon(painterResource(id = R.drawable.ic_heart), stringResource(id = R.string.app_components_button_icon_a11y)) + with(state) { + when (layout) { + ButtonDemoState.Layout.TextOnly -> { + OudsButton( + text = text, + onClick = {}, + enabled = enabled, + style = style, + hierarchy = hierarchy + ) + } + ButtonDemoState.Layout.IconAndText -> { + OudsButton( + icon = icon, + text = text, + onClick = {}, + enabled = enabled, + style = style, + hierarchy = hierarchy + ) + } + ButtonDemoState.Layout.IconOnly -> { + OudsButton( + icon = icon, + onClick = {}, + enabled = enabled, + style = style, + hierarchy = hierarchy + ) + } + } + } + } +} + +@UiModePreviews.Default +@Composable +private fun PreviewButtonDemoScreen() = OudsPreview { + ButtonDemoScreen() +} \ No newline at end of file diff --git a/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoState.kt b/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoState.kt new file mode 100644 index 00000000..32f040d5 --- /dev/null +++ b/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoState.kt @@ -0,0 +1,84 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.app.ui.components.button + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.mapSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.orange.ouds.app.R +import com.orange.ouds.core.component.button.OudsButton +import com.orange.ouds.core.component.button.OudsButtonDefaults + +@Composable +fun rememberButtonDemoState( + enabled: Boolean = true, + style: OudsButton.Style = OudsButtonDefaults.Style, + hierarchy: OudsButton.Hierarchy = OudsButtonDefaults.Hierarchy, + layout: ButtonDemoState.Layout = ButtonDemoState.Layout.TextOnly +) = rememberSaveable(enabled, style, hierarchy, layout, saver = ButtonDemoState.Saver) { + ButtonDemoState(enabled, style, hierarchy, layout) +} + +class ButtonDemoState( + enabled: Boolean, + style: OudsButton.Style, + hierarchy: OudsButton.Hierarchy, + layout: Layout +) { + + companion object { + + val Saver = run { + val enabledKey = "enabled" + val styleKey = "style" + val hierarchyKey = "hierarchy" + val layoutKey = "layout" + mapSaver( + save = { state -> + mapOf( + enabledKey to state.enabled, + styleKey to state.style, + hierarchyKey to state.hierarchy, + layoutKey to state.layout + ) + }, + restore = { map -> + ButtonDemoState( + map[enabledKey] as Boolean, + map[styleKey] as OudsButton.Style, + map[hierarchyKey] as OudsButton.Hierarchy, + map[layoutKey] as Layout + ) + } + ) + } + } + + var enabled: Boolean by mutableStateOf(enabled) + + var style: OudsButton.Style by mutableStateOf(style) + + var hierarchy: OudsButton.Hierarchy by mutableStateOf(hierarchy) + + var layout: Layout by mutableStateOf(layout) + + enum class Layout(@StringRes val labelRes: Int) { + TextOnly(R.string.app_components_button_textOnlyLayout_label), + IconAndText(R.string.app_components_button_iconAndTextLayout_label), + IconOnly(R.string.app_components_button_iconOnlyLayout_label) + } +} diff --git a/app/src/main/java/com/orange/ouds/app/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/orange/ouds/app/ui/navigation/AppNavGraph.kt index 1b99d06b..ab9cc715 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/navigation/AppNavGraph.kt @@ -19,7 +19,9 @@ import com.orange.ouds.app.ui.BottomBarItem import com.orange.ouds.app.ui.about.AboutDestinations import com.orange.ouds.app.ui.about.AboutScreen import com.orange.ouds.app.ui.about.addAboutNavGraph +import com.orange.ouds.app.ui.components.ComponentsNavigation import com.orange.ouds.app.ui.components.ComponentsScreen +import com.orange.ouds.app.ui.components.addComponentsNavGraph import com.orange.ouds.app.ui.tokens.TokensNavigation import com.orange.ouds.app.ui.tokens.TokensScreen import com.orange.ouds.app.ui.tokens.addTokensNavGraph @@ -29,6 +31,7 @@ import com.orange.ouds.app.ui.tokens.addTokensNavGraph */ fun NavGraphBuilder.appNavGraph(navController: NavController) { addTokensNavGraph(navController) + addComponentsNavGraph() addAboutNavGraph() addBottomBarNavGraph(navController) } @@ -38,10 +41,10 @@ fun NavGraphBuilder.appNavGraph(navController: NavController) { */ private fun NavGraphBuilder.addBottomBarNavGraph(navController: NavController) { composable(BottomBarItem.Tokens.route) { from -> - TokensScreen(onTokenCategoryClick = { id -> navController.navigateToElement(TokensNavigation.TokenCategoryDetailRoute, id, from) }) + TokensScreen { id -> navController.navigateToElement(TokensNavigation.TokenCategoryDetailRoute, id, from) } } - composable(BottomBarItem.Components.route) { _ -> - ComponentsScreen() + composable(BottomBarItem.Components.route) { from -> + ComponentsScreen { id -> navController.navigateToElement(ComponentsNavigation.ComponentDetailRoute, id, from) } } composable(BottomBarItem.About.route) { _ -> AboutScreen { id -> navController.navigate("${AboutDestinations.FileRoute}/$id") } diff --git a/app/src/main/java/com/orange/ouds/app/ui/tokens/TokensScreen.kt b/app/src/main/java/com/orange/ouds/app/ui/tokens/TokensScreen.kt index 7e2d27a2..0fc505ec 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/tokens/TokensScreen.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/tokens/TokensScreen.kt @@ -26,6 +26,7 @@ import com.orange.ouds.app.ui.utilities.composable.Screen import com.orange.ouds.core.theme.value import com.orange.ouds.core.utilities.OudsPreview import com.orange.ouds.foundation.utilities.UiModePreviews +import com.orange.ouds.theme.tokens.OudsColorKeyToken import com.orange.ouds.theme.tokens.OudsSpaceKeyToken @Composable @@ -50,7 +51,8 @@ private fun TokensScreen(tokenCategories: List>, onTokenCategor LargeCard( title = stringResource(id = token.nameRes), imageRes = token.imageRes, - onClick = { onTokenCategoryClick(token.id) } + onClick = { onTokenCategoryClick(token.id) }, + imageTint = OudsColorKeyToken.Always.White.value ) } } diff --git a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/CustomizationBottomSheet.kt b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/CustomizationBottomSheet.kt new file mode 100644 index 00000000..40284ead --- /dev/null +++ b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/CustomizationBottomSheet.kt @@ -0,0 +1,147 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.app.ui.utilities.composable + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.lifecycle.compose.LifecycleResumeEffect +import com.orange.ouds.app.R +import com.orange.ouds.core.theme.value +import com.orange.ouds.theme.tokens.OudsColorKeyToken +import com.orange.ouds.theme.tokens.OudsSpaceKeyToken +import com.orange.ouds.theme.tokens.OudsTypographyKeyToken +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomizationBottomSheetScaffold( + bottomSheetScaffoldState: BottomSheetScaffoldState, + titleResId: Int = R.string.app_common_customize_label, + bottomSheetContent: @Composable ColumnScope.() -> Unit, + content: @Composable BoxScope.() -> Unit +) { + val coroutineScope = rememberCoroutineScope() + val bottomSheetHeaderStateDescription = when (bottomSheetScaffoldState.bottomSheetState.currentValue) { + SheetValue.Hidden, SheetValue.PartiallyExpanded -> stringResource(R.string.app_common_bottomSheetCollapsed_a11y) + SheetValue.Expanded -> stringResource(R.string.app_common_bottomSheetExpanded_a11y) + } + BackHandler(bottomSheetScaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) { + coroutineScope.launch { + bottomSheetScaffoldState.bottomSheetState.partialExpand() + } + } + BottomSheetScaffold( + scaffoldState = bottomSheetScaffoldState, + sheetSwipeEnabled = false, + sheetDragHandle = null, + sheetContent = { + Row( + modifier = Modifier + .clickable { + coroutineScope.launch { + if (bottomSheetScaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) { + bottomSheetScaffoldState.bottomSheetState.partialExpand() + } else { + bottomSheetScaffoldState.bottomSheetState.expand() + } + } + } + .semantics { + stateDescription = bottomSheetHeaderStateDescription + } + .fillMaxWidth() + .height(BottomSheetDefaults.SheetPeekHeight) + .padding(horizontal = OudsSpaceKeyToken.Fixed.Medium.value), + verticalAlignment = Alignment.CenterVertically + ) { + val degrees = if (bottomSheetScaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) 0f else -180f + val angle by animateFloatAsState(targetValue = degrees, label = "ComponentCustomizationBottomSheetScaffoldIconRotation") + Icon( + modifier = Modifier.rotate(angle), + painter = painterResource(id = R.drawable.ic_chevron_down), + contentDescription = null, + tint = OudsColorKeyToken.Content.Default.value + ) + Text( + modifier = Modifier.padding(start = OudsSpaceKeyToken.Fixed.Medium.value), + text = stringResource(id = titleResId), + style = OudsTypographyKeyToken.Heading.Medium.value + ) + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + bottomSheetContent() + } + } + ) { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .background(OudsColorKeyToken.Background.Primary.value), + content = content + ) + } + + LifecycleResumeEffect(Unit) { + tryExpandBottomSheet(coroutineScope, bottomSheetScaffoldState.bottomSheetState) + onPauseOrDispose {} + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private fun tryExpandBottomSheet(coroutineScope: CoroutineScope, sheetState: SheetState, retryCount: Int = 0) { + coroutineScope.launch { + try { + sheetState.expand() + } catch (exception: CancellationException) { + // Retry up to 3 times if animation was interrupted by a composition + if (retryCount < 3) { + tryExpandBottomSheet(coroutineScope, sheetState, retryCount + 1) + } + } + } +} diff --git a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/CustomizationElements.kt b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/CustomizationElements.kt new file mode 100644 index 00000000..de4e17d3 --- /dev/null +++ b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/CustomizationElements.kt @@ -0,0 +1,89 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.app.ui.utilities.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.orange.ouds.core.theme.value +import com.orange.ouds.theme.tokens.OudsSpaceKeyToken +import com.orange.ouds.theme.tokens.OudsTypographyKeyToken + +@Composable +fun CustomizationSwitchListItem(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true) { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = enabled) { onCheckedChange(!checked) }, + headlineContent = { Text(text = label, style = OudsTypographyKeyToken.Heading.Medium.value) }, + trailingContent = { Switch(checked = checked, onCheckedChange = null, enabled = enabled) } + ) +} + +@Composable +fun CustomizationChoiceChipsColumn( + label: String, + chipsLabels: List, + selectedChipIndex: Int, + onSelectionChange: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier.fillMaxWidth()) { + Text(modifier = Modifier.padding(horizontal = OudsSpaceKeyToken.Fixed.Medium.value), text = label, style = OudsTypographyKeyToken.Heading.Medium.value) + Row( + Modifier + .fillMaxWidth() + .horizontalScroll(state = rememberScrollState()) + .selectableGroup() + .padding(horizontal = OudsSpaceKeyToken.Fixed.Medium.value, vertical = OudsSpaceKeyToken.Fixed.Shorter.value), + horizontalArrangement = Arrangement.spacedBy(OudsSpaceKeyToken.Fixed.Shorter.value) + ) { + chipsLabels.forEachIndexed { id, label -> + val isSelected = selectedChipIndex == id + FilterChip( + selected = isSelected, + leadingIcon = if (isSelected) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + }, + onClick = { onSelectionChange(id) }, + label = { Text(text = label) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/DetailScreenHeader.kt b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/DetailScreenHeader.kt index 57d00845..b96f7d08 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/DetailScreenHeader.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/DetailScreenHeader.kt @@ -65,7 +65,7 @@ fun DetailScreenHeader( } @Composable -private fun DetailScreenDescription( +fun DetailScreenDescription( modifier: Modifier = Modifier, @StringRes descriptionRes: Int ) { diff --git a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/LargeCard.kt b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/LargeCard.kt index 1fe8483e..eb95f079 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/LargeCard.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/LargeCard.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.CardDefaults.cardElevation import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale @@ -47,12 +48,13 @@ fun LargeCard( title: String, @DrawableRes imageRes: Int, onClick: () -> Unit, + imageTint: Color? = null ) { Card(shape = RectangleShape, elevation = cardElevation(defaultElevation = OudsElevationKeyToken.Raised.value), onClick = onClick) { Column(modifier = Modifier.background(OudsColorKeyToken.Background.Primary.value)) { Image( painter = painterResource(imageRes), - colorFilter = ColorFilter.tint(OudsColorKeyToken.Always.White.value), + colorFilter = imageTint?.let { ColorFilter.tint(imageTint) }, contentDescription = null, modifier = Modifier .fillMaxWidth() @@ -85,6 +87,7 @@ private fun PreviewLargeCard() = OudsPreview { LargeCard( title = "Title", imageRes = R.drawable.ic_filter_effects, - onClick = {} + onClick = {}, + imageTint = Color.White ) } diff --git a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/Screen.kt b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/Screen.kt index fef5b794..069fa347 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/Screen.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/utilities/composable/Screen.kt @@ -33,6 +33,9 @@ fun Screen(content: @Composable () -> Unit) { } } +@Composable +fun DemoScreen(demoState: T, content: @Composable T.() -> Unit) = Screen { demoState.content() } + @UiModePreviews.Default @Composable private fun PreviewScreen() = OudsPreview { diff --git a/app/src/main/res/drawable-hdpi/il_components_button.png b/app/src/main/res/drawable-hdpi/il_components_button.png new file mode 100644 index 00000000..61920b7c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/il_components_button.png differ diff --git a/app/src/main/res/drawable-mdpi/il_components_button.png b/app/src/main/res/drawable-mdpi/il_components_button.png new file mode 100644 index 00000000..84369b70 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/il_components_button.png differ diff --git a/app/src/main/res/drawable-xhdpi/il_components_button.png b/app/src/main/res/drawable-xhdpi/il_components_button.png new file mode 100644 index 00000000..6b0d8aad Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/il_components_button.png differ diff --git a/app/src/main/res/drawable-xxhdpi/il_components_button.png b/app/src/main/res/drawable-xxhdpi/il_components_button.png new file mode 100644 index 00000000..b955b719 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/il_components_button.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/il_components_button.png b/app/src/main/res/drawable-xxxhdpi/il_components_button.png new file mode 100644 index 00000000..f9a41e01 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/il_components_button.png differ diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..6b8719b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6646f923..c9808f47 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - ChangeTheme @@ -104,6 +108,19 @@ Typography Typography is our system of fonts and text styles. They enhance communication and reinforce the brand style. + + + + Button + Buttons allow users to make choices or perform an action. They have multiple styles for various needs. + Hierarchy + Style + Layout + Text only + Icon + text + Icon only + Icon + Privacy policy Legal information diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e17eab22..b9f6bc80 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,6 +12,7 @@ plugins { id("library") + id(libs.plugins.kotlin.parcelize.get().pluginId) // https://github.com/gradle/gradle/issues/20084#issuecomment-1060822638 alias(libs.plugins.compose.compiler) alias(libs.plugins.paparazzi) } diff --git a/core/src/androidTest/java/com/orange/ouds/core/component/button/OudsButtonTest.kt b/core/src/androidTest/java/com/orange/ouds/core/component/button/OudsButtonTest.kt index 46287403..76be700b 100644 --- a/core/src/androidTest/java/com/orange/ouds/core/component/button/OudsButtonTest.kt +++ b/core/src/androidTest/java/com/orange/ouds/core/component/button/OudsButtonTest.kt @@ -12,12 +12,14 @@ package com.orange.ouds.core.component.button -import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import com.orange.ouds.core.extension.setOudsContent import org.junit.Rule import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify internal class OudsButtonTest { @@ -25,19 +27,20 @@ internal class OudsButtonTest { val composeTestRule = createComposeRule() @Test - fun oudsButtonRendersAsExpected() { + fun oudsButtonClickSucceeds() { with(composeTestRule) { - val text = "Click" - val onClick = {} + val text = "Text" + val onClick = mock<() -> Unit>() setOudsContent { OudsButton( text = text, - onClick = onClick, + onClick = onClick ) } - onNodeWithText(text).assertIsDisplayed() + onNodeWithText(text).performClick() + verify(onClick).invoke() } } } \ No newline at end of file diff --git a/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt b/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt index a0e40ee9..e719652a 100644 --- a/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt +++ b/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt @@ -12,80 +12,650 @@ package com.orange.ouds.core.component.button +import android.os.Parcelable +import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.orange.ouds.core.R +import com.orange.ouds.core.component.content.OudsComponentContent +import com.orange.ouds.core.component.content.OudsComponentIcon +import com.orange.ouds.core.extensions.InteractionState +import com.orange.ouds.core.extensions.collectInteractionStateAsState import com.orange.ouds.core.theme.OudsTheme import com.orange.ouds.core.theme.value import com.orange.ouds.core.utilities.OudsPreview +import com.orange.ouds.foundation.extensions.orElse import com.orange.ouds.foundation.utilities.BasicPreviewParameterProvider import com.orange.ouds.foundation.utilities.UiModePreviews +import com.orange.ouds.theme.outerBorder +import com.orange.ouds.theme.tokens.OudsBorderKeyToken +import com.orange.ouds.theme.tokens.OudsColorKeyToken +import com.orange.ouds.theme.tokens.OudsTypographyKeyToken +import kotlinx.parcelize.Parcelize +/** + * An OUDS button which displays only text. + * + * @param text Text displayed in the button. + * @param onClick Callback invoked when the button is clicked. + * @param modifier [Modifier] applied to the button. + * @param enabled Controls the enabled state of the button when [style] is equal to [OudsButton.Style.Default]. + * When `false`, this button will not be clickable. + * Has no effect when [style] is equal to [OudsButton.Style.Loading] or [OudsButton.Style.Skeleton]. + * @param style The button style. + * @param hierarchy The button hierarchy. + */ @Composable fun OudsButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - enabled: Boolean = true + enabled: Boolean = true, + style: OudsButton.Style = OudsButtonDefaults.Style, + hierarchy: OudsButton.Hierarchy = OudsButtonDefaults.Hierarchy +) { + OudsButton( + icon = null, + text = text, + onClick = onClick, + previewState = null, + modifier = modifier, + enabled = enabled, + style = style, + hierarchy = hierarchy + ) +} + +/** + * An OUDS button which displays only an icon. + * + * @param icon Icon displayed in the button. + * @param onClick Callback invoked when the button is clicked. + * @param modifier [Modifier] applied to the button. + * @param enabled Controls the enabled state of the button when [style] is equal to [OudsButton.Style.Default]. + * When `false`, this button will not be clickable. + * Has no effect when [style] is equal to [OudsButton.Style.Loading] or [OudsButton.Style.Skeleton]. + * @param style The button style. + * @param hierarchy The button hierarchy. + */ +@Composable +fun OudsButton( + icon: OudsButton.Icon, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + style: OudsButton.Style = OudsButtonDefaults.Style, + hierarchy: OudsButton.Hierarchy = OudsButtonDefaults.Hierarchy +) { + OudsButton( + icon = icon, + text = null, + onClick = onClick, + previewState = null, + modifier = modifier, + enabled = enabled, + style = style, + hierarchy = hierarchy + ) +} + +/** + * An OUDS button which displays an icon and text. + * + * @param icon Icon displayed in the button. + * @param text Text displayed in the button. + * @param onClick Callback invoked when the button is clicked. + * @param modifier [Modifier] applied to the button. + * @param enabled Controls the enabled state of the button when [style] is equal to [OudsButton.Style.Default]. + * When `false`, this button will not be clickable. + * Has no effect when [style] is equal to [OudsButton.Style.Loading] or [OudsButton.Style.Skeleton]. + * @param style The button style. + * @param hierarchy The button hierarchy. + */ +@Composable +fun OudsButton( + icon: OudsButton.Icon, + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + style: OudsButton.Style = OudsButtonDefaults.Style, + hierarchy: OudsButton.Hierarchy = OudsButtonDefaults.Hierarchy +) { + OudsButton( + icon = icon, + text = text, + onClick = onClick, + previewState = null, + modifier = modifier, + enabled = enabled, + style = style, + hierarchy = hierarchy + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun OudsButton( + icon: OudsButton.Icon?, + text: String?, + onClick: () -> Unit, + previewState: OudsButton.State?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + style: OudsButton.Style = OudsButton.Style.Default, + hierarchy: OudsButton.Hierarchy = OudsButtonDefaults.Hierarchy ) { - with(OudsTheme.componentsTokens.button) { + val buttonTokens = OudsTheme.componentsTokens.button + val interactionSource = remember { MutableInteractionSource() } + val interactionState by interactionSource.collectInteractionStateAsState() + val state = previewState.orElse { rememberOudsButtonState(enabled = enabled, style = style, interactionState = interactionState) } + val maxHeight = if (icon != null && text == null) buttonTokens.sizeMaxHeight.dp else Dp.Unspecified + val shape = RoundedCornerShape(buttonTokens.borderRadius.value) + + CompositionLocalProvider(LocalRippleConfiguration provides null) { + val stateDescription = if (state == OudsButton.State.Loading) stringResource(id = R.string.core_button_loading_a11y) else "" Button( onClick = onClick, - enabled = enabled, - shape = RoundedCornerShape(borderRadius.value), - modifier = modifier, - contentPadding = PaddingValues( - vertical = spacePaddingBlock.value, - horizontal = spacePaddingBlock.value - ), - interactionSource = remember { MutableInteractionSource() }, - colors = ButtonDefaults.buttonColors( - containerColor = colorBgDefaultEnabled.value, - contentColor = colorContentDefaultEnabled.value, - disabledContainerColor = colorBgDefaultDisabled.value, - disabledContentColor = colorContentDefaultDisabled.value, - ) + modifier = modifier + .widthIn(min = buttonTokens.sizeMinWidth.dp) + .heightIn(min = buttonTokens.sizeMinHeight.dp, max = maxHeight) + .border(hierarchy = hierarchy, state = state, shape = shape) + .outerBorder(state = state, shape = shape) + .semantics { + this.stateDescription = stateDescription + }, + enabled = state !in remember { listOf(OudsButton.State.Disabled, OudsButton.State.Loading, OudsButton.State.Skeleton) }, + shape = shape, + colors = buttonColors(hierarchy = hierarchy, buttonState = state), + elevation = null, + contentPadding = contentPadding(icon = icon, text = text), + interactionSource = interactionSource ) { - Text( - modifier = modifier, - text = text + Box(contentAlignment = Alignment.Center) { + if (state == OudsButton.State.Loading) { + val loadingStyle = style as? OudsButton.Style.Loading + val progress = if (previewState == OudsButton.State.Loading) 0.75f else loadingStyle?.progress + LoadingIndicator(hierarchy = hierarchy, progress) + } + + val alpha = if (state in remember { listOf(OudsButton.State.Loading, OudsButton.State.Skeleton) }) 0f else 1f + Row( + modifier = Modifier.alpha(alpha = alpha), + horizontalArrangement = Arrangement.spacedBy(buttonTokens.spaceColumnGapIcon.value), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + val size = if (text == null) buttonTokens.sizeIconOnly else buttonTokens.sizeIcon + val tint = contentColor(hierarchy = hierarchy, state = state) + icon.Content( + modifier = Modifier + .size(size.value) + .semantics { + contentDescription = if (text == null) icon.contentDescription else "" + }, + extraParameters = OudsButton.Icon.ExtraParameters(tint = tint) + ) + } + if (text != null) { + Text( + modifier = modifier, + text = text, + style = OudsTypographyKeyToken.Label.Strong.Large.value + ) + } + } + } + } + } +} + +@Composable +private fun rememberOudsButtonState( + enabled: Boolean, + style: OudsButton.Style, + interactionState: InteractionState +): OudsButton.State = remember(enabled, style, interactionState) { + when (style) { + OudsButton.Style.Default -> when { + !enabled -> OudsButton.State.Disabled + interactionState == InteractionState.Hovered -> OudsButton.State.Hovered + interactionState == InteractionState.Pressed -> OudsButton.State.Pressed + interactionState == InteractionState.Focused -> OudsButton.State.Focused + else -> OudsButton.State.Enabled + } + is OudsButton.Style.Loading -> OudsButton.State.Loading + OudsButton.Style.Skeleton -> OudsButton.State.Skeleton + } +} + +@Composable +private fun Modifier.border(hierarchy: OudsButton.Hierarchy, state: OudsButton.State, shape: Shape): Modifier { + val borderWidth = borderWidth(hierarchy = hierarchy, state = state) + val borderColor = borderColor(hierarchy = hierarchy, state = state) + + return if (borderWidth != null && borderColor != null) { + border(width = borderWidth, color = borderColor, shape = shape) + } else { + this + } +} + +@Composable +private fun Modifier.outerBorder(state: OudsButton.State, shape: Shape): Modifier { + return if (state == OudsButton.State.Focused) { + outerBorder( + width = OudsBorderKeyToken.Width.Focus.value, + color = OudsColorKeyToken.Border.Focus.value, + shape = shape, + insetWidth = OudsBorderKeyToken.Width.FocusInset.value, + insetColor = OudsColorKeyToken.Border.FocusInset.value + ) + } else { + this + } +} + +@Composable +private fun borderWidth(hierarchy: OudsButton.Hierarchy, state: OudsButton.State): Dp? { + return with(OudsTheme.componentsTokens.button) { + when (hierarchy) { + OudsButton.Hierarchy.Default -> when (state) { + OudsButton.State.Enabled, + OudsButton.State.Disabled -> borderWidthDefault + OudsButton.State.Hovered, + OudsButton.State.Pressed, + OudsButton.State.Loading -> borderWidthDefaultInteraction + OudsButton.State.Focused -> OudsBorderKeyToken.Width.FocusInset + OudsButton.State.Skeleton -> null + } + OudsButton.Hierarchy.Minimal -> when (state) { + OudsButton.State.Enabled, + OudsButton.State.Disabled -> borderWidthMinimal + OudsButton.State.Hovered, + OudsButton.State.Pressed, + OudsButton.State.Loading -> borderWidthMinimalInteraction + OudsButton.State.Focused -> OudsBorderKeyToken.Width.FocusInset + OudsButton.State.Skeleton -> null + } + OudsButton.Hierarchy.Strong, + OudsButton.Hierarchy.Negative -> if (state == OudsButton.State.Focused) OudsBorderKeyToken.Width.FocusInset else null + }?.value + } +} + +@Composable +private fun borderColor(hierarchy: OudsButton.Hierarchy, state: OudsButton.State): Color? { + return with(OudsTheme.componentsTokens.button) { + when (hierarchy) { + OudsButton.Hierarchy.Default -> when (state) { + OudsButton.State.Enabled -> colorBorderDefaultEnabled + OudsButton.State.Hovered -> colorBorderDefaultHover + OudsButton.State.Pressed -> colorBorderDefaultPressed + OudsButton.State.Loading -> colorBorderDefaultLoading + OudsButton.State.Disabled -> colorBorderDefaultDisabled + OudsButton.State.Focused -> colorBorderDefaultFocus + OudsButton.State.Skeleton -> null + } + OudsButton.Hierarchy.Minimal -> when (state) { + OudsButton.State.Enabled -> colorBorderMinimalEnabled + OudsButton.State.Hovered -> colorBorderMinimalHover + OudsButton.State.Pressed -> colorBorderMinimalPressed + OudsButton.State.Loading -> colorBorderMinimalLoading + OudsButton.State.Disabled -> colorBorderMinimalDisabled + OudsButton.State.Focused -> colorBorderMinimalFocus + OudsButton.State.Skeleton -> null + } + OudsButton.Hierarchy.Strong, + OudsButton.Hierarchy.Negative -> null + }?.value + } +} + +@Composable +private fun buttonColors(hierarchy: OudsButton.Hierarchy, buttonState: OudsButton.State): ButtonColors { + return ButtonDefaults.buttonColors( + containerColor = containerColor(hierarchy = hierarchy, state = buttonState), + contentColor = contentColor(hierarchy = hierarchy, state = buttonState), + disabledContainerColor = containerColor(hierarchy = hierarchy, state = buttonState), + disabledContentColor = contentColor(hierarchy = hierarchy, state = buttonState) + ) +} + +@Composable +private fun containerColor(hierarchy: OudsButton.Hierarchy, state: OudsButton.State): Color { + return with(OudsTheme.componentsTokens.button) { + when (hierarchy) { + OudsButton.Hierarchy.Default -> when (state) { + OudsButton.State.Enabled -> colorBgDefaultEnabled + OudsButton.State.Focused -> colorBgDefaultFocus + OudsButton.State.Hovered -> colorBgDefaultHover + OudsButton.State.Pressed -> colorBgDefaultPressed + OudsButton.State.Loading -> colorBgDefaultLoading + OudsButton.State.Disabled -> colorBgDefaultDisabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + OudsButton.Hierarchy.Minimal -> when (state) { + OudsButton.State.Enabled -> colorBgMinimalEnabled + OudsButton.State.Focused -> colorBgMinimalFocus + OudsButton.State.Hovered -> colorBgMinimalHover + OudsButton.State.Pressed -> colorBgMinimalPressed + OudsButton.State.Loading -> colorBgMinimalLoading + OudsButton.State.Disabled -> colorBgMinimalDisabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + OudsButton.Hierarchy.Strong -> when (state) { + OudsButton.State.Enabled -> OudsColorKeyToken.Action.Enabled + OudsButton.State.Focused -> OudsColorKeyToken.Action.Focus + OudsButton.State.Hovered -> OudsColorKeyToken.Action.Hover + OudsButton.State.Pressed -> OudsColorKeyToken.Action.Pressed + OudsButton.State.Loading -> OudsColorKeyToken.Action.Loading + OudsButton.State.Disabled -> OudsColorKeyToken.Action.Disabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + OudsButton.Hierarchy.Negative -> when (state) { + OudsButton.State.Enabled -> OudsColorKeyToken.Action.Negative.Enabled + OudsButton.State.Focused -> OudsColorKeyToken.Action.Negative.Focus + OudsButton.State.Hovered -> OudsColorKeyToken.Action.Negative.Hover + OudsButton.State.Pressed -> OudsColorKeyToken.Action.Negative.Pressed + OudsButton.State.Loading -> OudsColorKeyToken.Action.Negative.Loading + OudsButton.State.Disabled -> OudsColorKeyToken.Action.Disabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + }.value + } +} + +@Composable +private fun contentColor(hierarchy: OudsButton.Hierarchy, state: OudsButton.State): Color { + return with(OudsTheme.componentsTokens.button) { + when (hierarchy) { + OudsButton.Hierarchy.Default -> when (state) { + OudsButton.State.Enabled -> colorContentDefaultEnabled + OudsButton.State.Focused -> colorContentDefaultFocus + OudsButton.State.Hovered -> colorContentDefaultHover + OudsButton.State.Pressed -> colorContentDefaultPressed + OudsButton.State.Loading -> colorContentDefaultLoading + OudsButton.State.Disabled -> colorContentDefaultDisabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + OudsButton.Hierarchy.Minimal -> when (state) { + OudsButton.State.Enabled -> colorContentMinimalEnabled + OudsButton.State.Focused -> colorContentMinimalFocus + OudsButton.State.Hovered -> colorContentMinimalHover + OudsButton.State.Pressed -> colorContentMinimalPressed + OudsButton.State.Loading -> colorContentMinimalLoading + OudsButton.State.Disabled -> colorContentMinimalDisabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + OudsButton.Hierarchy.Strong -> when (state) { + OudsButton.State.Enabled -> OudsColorKeyToken.Content.OnAction.Enabled + OudsButton.State.Focused -> OudsColorKeyToken.Content.OnAction.Focus + OudsButton.State.Hovered -> OudsColorKeyToken.Content.OnAction.Hover + OudsButton.State.Pressed -> OudsColorKeyToken.Content.OnAction.Pressed + OudsButton.State.Loading -> OudsColorKeyToken.Content.OnAction.Loading + OudsButton.State.Disabled -> OudsColorKeyToken.Content.OnAction.Disabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + OudsButton.Hierarchy.Negative -> when (state) { + OudsButton.State.Enabled, + OudsButton.State.Hovered, + OudsButton.State.Pressed, + OudsButton.State.Loading, + OudsButton.State.Focused -> OudsColorKeyToken.Content.OnAction.Negative + OudsButton.State.Disabled -> OudsColorKeyToken.Content.OnAction.Disabled + OudsButton.State.Skeleton -> OudsTheme.componentsTokens.skeleton.colorBg + } + }.value + } +} + +@Composable +private fun contentPadding(icon: OudsButton.Icon?, text: String?): PaddingValues { + return with(OudsTheme.componentsTokens.button) { + when { + icon != null && text != null -> PaddingValues( + start = spacePaddingInlineIconStart.value, + top = spacePaddingBlock.value, + end = spacePaddingInlineEndIconStart.value, + bottom = spacePaddingBlock.value + ) + icon != null && text == null -> PaddingValues( + horizontal = spaceInsetIconAlone.value, + vertical = spacePaddingBlock.value + ) + else -> PaddingValues( + horizontal = spacePaddingInlineIconNone.value, + vertical = spacePaddingBlock.value ) } } } -@Suppress("PreviewShouldNotBeCalledRecursively") -@UiModePreviews.Button +@OptIn(ExperimentalComposeUiApi::class) @Composable -private fun PreviewOudsButton(@PreviewParameter(OudsButtonPreviewParameterProvider::class) parameter: OudsButtonPreviewParameter) = OudsPreview { +private fun LoadingIndicator(hierarchy: OudsButton.Hierarchy, progress: Float?) { + val modifier = Modifier + .size(OudsTheme.componentsTokens.button.sizeLoader.value) + .semantics { invisibleToUser() } + val color = contentColor(hierarchy = hierarchy, state = OudsButton.State.Loading) + val strokeWidth = 3.dp + val trackColor = Color.Transparent + val strokeCap = StrokeCap.Square + if (progress != null) { + CircularProgressIndicator( + progress = { progress }, + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) + } else { + CircularProgressIndicator( + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) + } +} + +/** + * Contains the default values used by OUDS buttons. + */ +object OudsButtonDefaults { + + /** + * The default hierarchy. + */ + val Hierarchy = OudsButton.Hierarchy.Default + + /** + * The default style. + */ + val Style = OudsButton.Style.Default +} + +/** + * Contains classes to build an [com.orange.ouds.core.component.button.OudsButton]. + */ +object OudsButton { + + /** + * A button icon in an [OudsButton]. + * It is non-clickable and no content description is needed cause a button label is always present. + */ + class Icon private constructor( + graphicsObject: Any, + val contentDescription: String + ) : OudsComponentIcon(ExtraParameters::class.java, graphicsObject, contentDescription) { + + @ConsistentCopyVisibility + data class ExtraParameters internal constructor( + internal val tint: Color + ) : OudsComponentContent.ExtraParameters() + + /** + * Creates an instance of [OudsButton.Icon]. + * + * @param painter Painter of the icon. + * @param contentDescription The content description associated with this [OudsButton.Icon]. This value is ignored if the button also contains text. + */ + constructor(painter: Painter, contentDescription: String) : this(painter as Any, contentDescription) + + /** + * Creates an instance of [OudsButton.Icon]. + * + * @param imageVector Image vector of the icon. + * @param contentDescription The content description associated with this [OudsButton.Icon]. This value is ignored if the button also contains text. + */ + constructor(imageVector: ImageVector, contentDescription: String) : this(imageVector as Any, contentDescription) + + /** + * Creates an instance of [OudsButton.Icon]. + * + * @param bitmap Image bitmap of the icon. + * @param contentDescription The content description associated with this [OudsButton.Icon]. This value is ignored if the button also contains text. + */ + constructor(bitmap: ImageBitmap, contentDescription: String) : this(bitmap as Any, contentDescription) + + override val tint: Color? + @Composable + get() = extraParameters.tint + } + + /** + * Represents the hierarchy of an OUDS button. + */ + enum class Hierarchy { + Default, Strong, Minimal, Negative + } + + /** + * Represents the different styles of an OUDS button. + */ + sealed class Style : Parcelable { + + /** + * The button displays an icon and/or a text and supports user interactions if it is enabled. + */ + @Parcelize + data object Default : Style() + + /** + * The button displays a circular loading indicator. + * + * @param progress The loading progress, where 0.0 represents no progress and 1.0 represents full progress. + * Values outside of this range are coerced into the range. + * Set this value to `null` to display a circular indeterminate progress indicator. + */ + @Parcelize + data class Loading(val progress: Float?) : Style() + + /** + * The button displays a skeleton. + */ + @Parcelize + data object Skeleton : Style() + } + + internal enum class State { + Enabled, Hovered, Pressed, Loading, Disabled, Focused, Skeleton + } +} + +@UiModePreviews.Default +@Composable +@Suppress("PreviewShouldNotBeCalledRecursively") +private fun PreviewOudsButton(@PreviewParameter(OudsButtonPreviewParameterProvider::class) parameter: OudsButtonPreviewParameter) { PreviewOudsButton(darkThemeEnabled = isSystemInDarkTheme(), parameter = parameter) } @Composable -internal fun PreviewOudsButton(darkThemeEnabled: Boolean, parameter: OudsButtonPreviewParameter) = OudsPreview(darkThemeEnabled) { +internal fun PreviewOudsButton( + darkThemeEnabled: Boolean, + parameter: OudsButtonPreviewParameter +) = OudsPreview(modifier = Modifier.padding(16.dp), darkThemeEnabled = darkThemeEnabled) { with(parameter) { - OudsButton(text = "Text", onClick = {}, enabled = enabled) + val text = if (hasText) hierarchy.name else null + val icon = if (hasIcon) OudsButton.Icon(painterResource(id = android.R.drawable.star_on), "") else null + val chunkedStates = states.chunked(2) + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + chunkedStates.forEach { states -> + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + states.forEach { state -> + OudsButton(icon = icon, text = text, onClick = {}, hierarchy = hierarchy, previewState = state) + } + } + } + } } } internal data class OudsButtonPreviewParameter( - val enabled: Boolean = true + val hierarchy: OudsButton.Hierarchy, + val hasText: Boolean, + val hasIcon: Boolean, + val states: List = listOf( + OudsButton.State.Enabled, + OudsButton.State.Hovered, + OudsButton.State.Pressed, + OudsButton.State.Loading, + OudsButton.State.Disabled, + OudsButton.State.Focused, + OudsButton.State.Skeleton + ) ) -internal class OudsButtonPreviewParameterProvider : - BasicPreviewParameterProvider(*previewParameterValues.toTypedArray()) +internal class OudsButtonPreviewParameterProvider : BasicPreviewParameterProvider(*previewParameterValues.toTypedArray()) private val previewParameterValues: List - get() = mutableListOf().apply { - add(OudsButtonPreviewParameter()) - add(OudsButtonPreviewParameter(enabled = false)) + get() = buildList { + OudsButton.Hierarchy.entries.forEach { hierarchy -> + add(OudsButtonPreviewParameter(hierarchy, hasText = true, hasIcon = false)) + add(OudsButtonPreviewParameter(hierarchy, hasText = true, hasIcon = true)) + add(OudsButtonPreviewParameter(hierarchy, hasText = false, hasIcon = true)) + } } - diff --git a/core/src/main/java/com/orange/ouds/core/component/content/OudsComponentContent.kt b/core/src/main/java/com/orange/ouds/core/component/content/OudsComponentContent.kt new file mode 100644 index 00000000..00529d1b --- /dev/null +++ b/core/src/main/java/com/orange/ouds/core/component/content/OudsComponentContent.kt @@ -0,0 +1,99 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.core.component.content + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import com.orange.ouds.foundation.extensions.asOrNull +import com.orange.ouds.foundation.extensions.orElse + + +internal val localExtraParametersByClass = + mutableMapOf, ProvidableCompositionLocal>() + +internal fun getLocalExtraParameters(clazz: Class): ProvidableCompositionLocal where T : OudsComponentContent.ExtraParameters { + return localExtraParametersByClass[clazz]?.asOrNull>().orElse { + staticCompositionLocalOf { error("CompositionLocal LocalExtraParameters for class ${clazz.name} not present") }.also { compositionLocal -> + localExtraParametersByClass[clazz] = compositionLocal + } + } +} + +/** + * The content of a component. + * + * Subclasses of [OudsComponentContent] should be used instead of composable methods when passing parameters to components. + * This prevents using generic composable methods that can encapsulate any kind of views and thus helps developers to follow UI guidelines more easily. + * This also allows to group parameters that are related to the same content inside a component. + * For instance it is possible to create an `Icon` subclass to replace both `icon: @Composable () -> Unit` and `onIconClick: () -> Unit` parameters with a single `icon: Icon` parameter. + * + * @param extraParametersClass The extra parameters class. + * @param T The type of extra parameters. + */ +abstract class OudsComponentContent internal constructor(private val extraParametersClass: Class) where T : OudsComponentContent.ExtraParameters { + + /** + * Extra parameters that can be passed to the `Content` method when other parameters than those provided by the user are needed to layout the component. + */ + abstract class ExtraParameters + + /** + * The extra parameters. + */ + protected val extraParameters: T + @Composable + get() = getLocalExtraParameters(extraParametersClass).current + + /** + * The Jetpack Compose UI for this component content. + * + * Calls `Content(Modifier)` with the default `Modifier`. + */ + @Composable + internal fun Content() = Content(modifier = Modifier) + + /** + * The Jetpack Compose UI for this component content. + * + * Calls `Content(Modifier, T)` with the default `Modifier`. + * + * @param extraParameters The extra parameters for this content. + */ + @Composable + internal fun Content(extraParameters: T) = Content(modifier = Modifier, extraParameters = extraParameters) + + /** + * The Jetpack Compose UI for this component content. + * + * @param modifier The modifier for this content. + * @param extraParameters The extra parameters for this content. + */ + @Composable + internal fun Content(modifier: Modifier, extraParameters: T) { + CompositionLocalProvider(getLocalExtraParameters(extraParametersClass) provides extraParameters) { + Content(modifier = modifier) + } + } + + /** + * The Jetpack Compose UI for this component content. + * Subclasses must implement this method to provide content. + * + * @param modifier The modifier for this content. + */ + @Composable + internal abstract fun Content(modifier: Modifier) +} diff --git a/core/src/main/java/com/orange/ouds/core/component/content/OudsComponentIcon.kt b/core/src/main/java/com/orange/ouds/core/component/content/OudsComponentIcon.kt new file mode 100644 index 00000000..d4c44a62 --- /dev/null +++ b/core/src/main/java/com/orange/ouds/core/component/content/OudsComponentIcon.kt @@ -0,0 +1,69 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.core.component.content + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import com.orange.ouds.core.component.icon.OudsIcon +import com.orange.ouds.core.component.icon.OudsIconDefaults +import com.orange.ouds.foundation.extensions.orElse + +/** + * An icon in a component. + */ +abstract class OudsComponentIcon protected constructor( + extraParametersClass: Class, + private val graphicsObject: Any, + private val contentDescription: String +) : OudsComponentContent(extraParametersClass) where T : OudsComponentContent.ExtraParameters { + + val painter: Painter? = graphicsObject as? Painter + val imageVector: ImageVector? = graphicsObject as? ImageVector + val bitmap: ImageBitmap? = graphicsObject as? ImageBitmap + + protected open val tint: Color? + @Composable + get() = null + + protected constructor( + extraParametersClass: Class, + painter: Painter, + contentDescription: String + ) : this(extraParametersClass, painter as Any, contentDescription) + + protected constructor( + extraParametersClass: Class, + imageVector: ImageVector, + contentDescription: String + ) : this(extraParametersClass, imageVector as Any, contentDescription) + + protected constructor( + extraParametersClass: Class, + bitmap: ImageBitmap, + contentDescription: String + ) : this(extraParametersClass, bitmap as Any, contentDescription) + + @Composable + override fun Content(modifier: Modifier) { + OudsIcon( + graphicsObject = graphicsObject, + contentDescription = contentDescription, + modifier = modifier, + tint = tint.orElse { OudsIconDefaults.tint } + ) + } +} diff --git a/core/src/main/java/com/orange/ouds/core/component/icon/OudsIcon.kt b/core/src/main/java/com/orange/ouds/core/component/icon/OudsIcon.kt new file mode 100644 index 00000000..a87272a3 --- /dev/null +++ b/core/src/main/java/com/orange/ouds/core/component/icon/OudsIcon.kt @@ -0,0 +1,44 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.core.component.icon + +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +internal fun OudsIcon( + graphicsObject: Any, + contentDescription: String, + modifier: Modifier = Modifier, + tint: Color = OudsIconDefaults.tint +) { + when (graphicsObject) { + is Painter -> Icon(painter = graphicsObject, contentDescription = contentDescription, modifier = modifier, tint = tint) + is ImageVector -> Icon(imageVector = graphicsObject, contentDescription = contentDescription, modifier = modifier, tint = tint) + is ImageBitmap -> Icon(bitmap = graphicsObject, contentDescription = contentDescription, modifier = modifier, tint = tint) + else -> {} + } +} + +internal object OudsIconDefaults { + + val tint: Color + @Composable + get() = LocalContentColor.current +} diff --git a/core/src/main/java/com/orange/ouds/core/extensions/InteractionSourceExt.kt b/core/src/main/java/com/orange/ouds/core/extensions/InteractionSourceExt.kt new file mode 100644 index 00000000..14e567b1 --- /dev/null +++ b/core/src/main/java/com/orange/ouds/core/extensions/InteractionSourceExt.kt @@ -0,0 +1,45 @@ +/* + * Software Name: OUDS Android + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT license, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Android library of reusable graphical components + */ + +package com.orange.ouds.core.extensions + +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember + +internal enum class InteractionState { + None, Focused, Hovered, Pressed +} + +@Composable +internal fun InteractionSource.collectInteractionStateAsState(): State { + val isFocused by collectIsFocusedAsState() + val isHovered by collectIsHoveredAsState() + val isPressed by collectIsPressedAsState() + + return remember { + derivedStateOf { + when { + isFocused -> InteractionState.Focused + isHovered -> InteractionState.Hovered + isPressed -> InteractionState.Pressed + else -> InteractionState.None + } + } + } +} diff --git a/core/src/main/java/com/orange/ouds/core/theme/OudsTheme.kt b/core/src/main/java/com/orange/ouds/core/theme/OudsTheme.kt index 168099a5..fea18327 100644 --- a/core/src/main/java/com/orange/ouds/core/theme/OudsTheme.kt +++ b/core/src/main/java/com/orange/ouds/core/theme/OudsTheme.kt @@ -26,12 +26,12 @@ import com.orange.ouds.theme.tokens.components.OudsComponentsTokens private fun missingCompositionLocalError(compositionLocalName: String): Nothing = error("OudsTheme not found. $compositionLocalName CompositionLocal not present.") -private val LocalDarkThemeEnabled = staticCompositionLocalOf { missingCompositionLocalError("LocalDarkThemeEnabled.") } +private val LocalDarkThemeEnabled = staticCompositionLocalOf { missingCompositionLocalError("LocalDarkThemeEnabled") } private val LocalColorScheme = staticCompositionLocalOf { missingCompositionLocalError("LocalColorScheme") } -private val LocalLightColorScheme = compositionLocalOf { missingCompositionLocalError("LocalLightThemeColorScheme") } -private val LocalDarkColorScheme = compositionLocalOf { missingCompositionLocalError("LocalDarkThemeColorScheme") } -private val LocalMaterialLightColorScheme = compositionLocalOf { missingCompositionLocalError("LocalMaterialLightThemeColorScheme") } -private val LocalMaterialDarkColorScheme = compositionLocalOf { missingCompositionLocalError("LocalMaterialDarkThemeColorScheme") } +private val LocalLightColorScheme = compositionLocalOf { missingCompositionLocalError("LocalLightColorScheme") } +private val LocalDarkColorScheme = compositionLocalOf { missingCompositionLocalError("LocalDarkColorScheme") } +private val LocalMaterialLightColorScheme = compositionLocalOf { missingCompositionLocalError("LocalMaterialLightColorScheme") } +private val LocalMaterialDarkColorScheme = compositionLocalOf { missingCompositionLocalError("LocalMaterialDarkColorScheme") } private val LocalBorders = staticCompositionLocalOf { missingCompositionLocalError("LocalBorders") } private val LocalElevations = staticCompositionLocalOf { missingCompositionLocalError("LocalElevations") } private val LocalTypography = staticCompositionLocalOf { missingCompositionLocalError("LocalTypography") } diff --git a/core/src/main/java/com/orange/ouds/core/utilities/OudsPreview.kt b/core/src/main/java/com/orange/ouds/core/utilities/OudsPreview.kt index 2869283b..73acd256 100644 --- a/core/src/main/java/com/orange/ouds/core/utilities/OudsPreview.kt +++ b/core/src/main/java/com/orange/ouds/core/utilities/OudsPreview.kt @@ -12,9 +12,11 @@ package com.orange.ouds.core.utilities +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.Surface +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.orange.ouds.core.BuildConfig import com.orange.ouds.core.theme.OudsTheme import com.orange.ouds.core.theme.value @@ -23,12 +25,21 @@ import com.orange.ouds.theme.tokens.OudsColorKeyToken /** * Configures the Compose OUDS preview environment in Android Studio. * + * @param modifier The modifier for the preview content. * @param darkThemeEnabled Indicates whether the dark theme is enabled or not. * @param content The content of the preview. */ @Composable -fun OudsPreview(darkThemeEnabled: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { +fun OudsPreview(modifier: Modifier = Modifier, darkThemeEnabled: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { OudsTheme(themeContract = BuildConfig.PREVIEW_THEME, darkThemeEnabled) { - Surface(color = OudsColorKeyToken.Background.Primary.value, content = content) // Add a surface to be able to see components + // Add a box to be able to see components + // Use a box instead of a surface to avoid clipping children in cases where something is drawn outside of the component to preview + Box( + modifier = Modifier + .background(OudsColorKeyToken.Background.Primary.value) + .then(modifier) + ) { + content() + } } } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml new file mode 100644 index 00000000..f39011ff --- /dev/null +++ b/core/src/main/res/values/strings.xml @@ -0,0 +1,16 @@ + + + + + Loading + diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[0].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[0].png index e67d5993..d40c5c01 100644 Binary files a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[0].png and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[0].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[10].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[10].png new file mode 100644 index 00000000..958dfc21 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[10].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[11].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[11].png new file mode 100644 index 00000000..7d108a43 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[11].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[1].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[1].png index b95b4e28..ca366dd6 100644 Binary files a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[1].png and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[1].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[2].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[2].png new file mode 100644 index 00000000..f2ca871c Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[2].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[3].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[3].png new file mode 100644 index 00000000..692a210b Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[3].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[4].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[4].png new file mode 100644 index 00000000..b792c892 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[4].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[5].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[5].png new file mode 100644 index 00000000..c7f76dfb Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[5].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[6].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[6].png new file mode 100644 index 00000000..83ebdd33 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[6].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[7].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[7].png new file mode 100644 index 00000000..527b0bdc Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[7].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[8].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[8].png new file mode 100644 index 00000000..5472f22e Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[8].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[9].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[9].png new file mode 100644 index 00000000..113730eb Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonDarkThemeSnapshot[9].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[0].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[0].png index e85828dd..64ac28e8 100644 Binary files a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[0].png and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[0].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[10].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[10].png new file mode 100644 index 00000000..04e04a20 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[10].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[11].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[11].png new file mode 100644 index 00000000..49ce468c Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[11].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[1].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[1].png index a9d8e936..805e3e0e 100644 Binary files a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[1].png and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[1].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[2].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[2].png new file mode 100644 index 00000000..71247345 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[2].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[3].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[3].png new file mode 100644 index 00000000..9ebfd616 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[3].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[4].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[4].png new file mode 100644 index 00000000..ffcbcb6e Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[4].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[5].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[5].png new file mode 100644 index 00000000..5cb5519f Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[5].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[6].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[6].png new file mode 100644 index 00000000..7596919a Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[6].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[7].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[7].png new file mode 100644 index 00000000..78783cc2 Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[7].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[8].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[8].png new file mode 100644 index 00000000..50493bee Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[8].png differ diff --git a/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[9].png b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[9].png new file mode 100644 index 00000000..bcf71b9a Binary files /dev/null and b/core/src/test/snapshots/images/com.orange.ouds.core.component.button_OudsButtonTest_takeOudsButtonLightThemeSnapshot[9].png differ diff --git a/foundation/src/main/java/com/orange/ouds/foundation/utilities/Preview.kt b/foundation/src/main/java/com/orange/ouds/foundation/utilities/Preview.kt index d81ec731..68304b9f 100644 --- a/foundation/src/main/java/com/orange/ouds/foundation/utilities/Preview.kt +++ b/foundation/src/main/java/com/orange/ouds/foundation/utilities/Preview.kt @@ -46,14 +46,9 @@ annotation class UiModePreviews { companion object { private const val LightName = "Light" private const val DarkName = "Dark" - private const val ButtonWidthDp = 200 } @Preview(name = LightName) @Preview(name = DarkName, uiMode = Configuration.UI_MODE_NIGHT_YES) annotation class Default - - @Preview(name = LightName, widthDp = ButtonWidthDp) - @Preview(name = DarkName, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = ButtonWidthDp) - annotation class Button } diff --git a/theme-contract/src/main/java/com/orange/ouds/theme/OudsBorderModifier.kt b/theme-contract/src/main/java/com/orange/ouds/theme/OudsBorderModifier.kt index dc3851fa..be50f937 100644 --- a/theme-contract/src/main/java/com/orange/ouds/theme/OudsBorderModifier.kt +++ b/theme-contract/src/main/java/com/orange/ouds/theme/OudsBorderModifier.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StampedPathEffectStyle import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.unit.Dp @@ -101,25 +102,33 @@ fun Modifier.dottedBorder( /** * Modify element to add an outer border (drawn outside the element) with appearance specified with a [width], a [color] and a [shape]. * - * @param width Thickness of the border in dp - * @param color Color to paint the border with - * @param shape Shape of the border + * @param width The width of the border in dp. Use [Dp.Hairline] for a hairline border. + * @param color The color to paint the border with. + * @param shape The shape of the border. + * @param insetWidth The width of the border inset in dp. Use [Dp.Hairline] for a hairline border inset. + * @param insetColor The color to paint the border inset with. */ fun Modifier.outerBorder( width: Dp, color: Color, - shape: Shape = RectangleShape + shape: Shape = RectangleShape, + insetWidth: Dp = Dp.Unspecified, + insetColor: Color = Color.Unspecified ) = drawWithContent { - val outerSize = Size(size.width + width.toPx(), size.height + width.toPx()) - val outline = shape.createOutline(outerSize, layoutDirection, density = this) - val stroke = Stroke(width = width.toPx()) - drawContent() - translate(-width.toPx() / 2f, -width.toPx() / 2f) { - drawOutline( - outline = outline, - style = stroke, - brush = SolidColor(color) - ) + drawOuterBorder(width, color, shape) + drawOuterBorder(insetWidth, insetColor, shape) +} + +private fun DrawScope.drawOuterBorder(width: Dp, color: Color, shape: Shape) { + if (width != Dp.Unspecified) { + val outerSize = Size(size.width + width.toPx(), size.height + width.toPx()) + translate(-width.toPx() / 2f, -width.toPx() / 2f) { + drawOutline( + outline = shape.createOutline(outerSize, layoutDirection, this), + style = Stroke(width.toPx()), + brush = SolidColor(color) + ) + } } } diff --git a/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsButtonTokens.kt b/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsButtonTokens.kt index 28497502..c114e8cb 100644 --- a/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsButtonTokens.kt +++ b/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsButtonTokens.kt @@ -131,4 +131,4 @@ open class OudsButtonTokens( val spacePaddingInlineIconNone: OudsSpaceKeyToken.PaddingInline = OudsSpaceKeyToken.PaddingInline.Huge, val spacePaddingInlineIconStart: OudsSpaceKeyToken.PaddingInline = OudsSpaceKeyToken.PaddingInline.Taller, val spacePaddingInlineStartIconEnd: OudsSpaceKeyToken.PaddingInline = OudsSpaceKeyToken.PaddingInline.Spacious -) +) \ No newline at end of file diff --git a/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsComponentsTokens.kt b/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsComponentsTokens.kt index 159530d7..0b460262 100644 --- a/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsComponentsTokens.kt +++ b/theme-contract/src/main/java/com/orange/ouds/theme/tokens/components/OudsComponentsTokens.kt @@ -14,4 +14,5 @@ package com.orange.ouds.theme.tokens.components class OudsComponentsTokens( val button: OudsButtonTokens = OudsButtonTokens(), + val skeleton: OudsSkeletonTokens = OudsSkeletonTokens() ) \ No newline at end of file