From 240ec4693948e9123e7e1c060145b26cc836b002 Mon Sep 17 00:00:00 2001 From: Anubhav Agrawal <68989156+Anubhvv@users.noreply.github.com> Date: Fri, 1 Sep 2023 18:04:11 +0530 Subject: [PATCH] Added Functionality to the BottomSheet with TestCases: Enable/Disable Swipe Down Dismiss with more intuitive accessibility controls (#490) * BottomSheet added functionality to not get dismissed when swiped down * added semantics check * To run test on pipeline * applied enableSwipeDismiss check on dismiss * Improved semantics for talkback actions * Added option for accesibility users to have a way to bring bottomSheet in case it's hidden * localized some string constants * moved string under UI labels * typo --------- Co-authored-by: Anubhav Agrawal --- .../com/microsoft/fluentuidemo/UiTestSuite.kt | 1 + .../demos/V2BottomSheetActivityUITest.kt | 46 ++++++++++++++++ FluentUI.Demo/src/main/AndroidManifest.xml | 3 +- .../demos/V2BottomSheetActivity.kt | 29 +++++++++- FluentUI.Demo/src/main/res/values/strings.xml | 1 + .../microsoft/fluentui/compose/Swipeable.kt | 32 +++++++++++ .../tokenized/bottomsheet/BottomSheet.kt | 54 +++++++++++++++---- 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivityUITest.kt diff --git a/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/UiTestSuite.kt b/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/UiTestSuite.kt index db8957f57..e2e937b17 100644 --- a/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/UiTestSuite.kt +++ b/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/UiTestSuite.kt @@ -12,6 +12,7 @@ import org.junit.runners.Suite V2AvatarGroupActivityUITest::class, V2BadgeActivityUITest::class, V2BottomDrawerUITest::class, + V2BottomSheetActivityUITest::class, V2ButtonsActivityUITest::class, V2CardNudgeActivityUITest::class, V2CardUITest::class, diff --git a/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivityUITest.kt b/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivityUITest.kt new file mode 100644 index 000000000..8d5537392 --- /dev/null +++ b/FluentUI.Demo/src/androidTest/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivityUITest.kt @@ -0,0 +1,46 @@ +package com.microsoft.fluentuidemo.demos + +import androidx.compose.ui.test.onNodeWithTag +import org.junit.Before +import com.microsoft.fluentuidemo.BaseTest +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import com.microsoft.fluentui.tokenized.bottomsheet.BOTTOMSHEET_CONTENT_TAG +import com.microsoft.fluentui.tokenized.bottomsheet.BOTTOMSHEET_HANDLE_TAG +import com.microsoft.fluentui.tokenized.bottomsheet.BOTTOMSHEET_SCRIM_TAG +import org.junit.Test + + +class V2BottomSheetActivityUITest(): BaseTest(){ + @Before + fun initialize() { + BaseTest().launchActivity(V2BottomSheetActivity::class.java) + } + //Tag use for testing + private val bottomSheetHandle = composeTestRule.onNodeWithTag(BOTTOMSHEET_HANDLE_TAG, useUnmergedTree = true) + private val bottomSheetContent = composeTestRule.onNodeWithTag(BOTTOMSHEET_CONTENT_TAG, useUnmergedTree = true) + private val bottomSheetScrim = composeTestRule.onNodeWithTag(BOTTOMSHEET_SCRIM_TAG, useUnmergedTree = true) + + private fun openCheckForBottomSheet() { + composeTestRule.waitForIdle() + bottomSheetHandle.assertExists() + bottomSheetScrim.assertExists() + bottomSheetContent.assertExists() + } + @Test + fun testBottomSheetDismissDisabledSwipeDown() { + composeTestRule.onNodeWithTag(BOTTOM_SHEET_ENABLE_SWIPE_DISMISS_TEST_TAG, useUnmergedTree = true).performClick() + //SwipeDown on bottomSheetContent should close it. + bottomSheetContent.performTouchInput { + swipeDown() + } + bottomSheetHandle.performTouchInput { + swipeDown() + } + openCheckForBottomSheet() + } + + + +} \ No newline at end of file diff --git a/FluentUI.Demo/src/main/AndroidManifest.xml b/FluentUI.Demo/src/main/AndroidManifest.xml index 749c1d605..7a118e1d0 100644 --- a/FluentUI.Demo/src/main/AndroidManifest.xml +++ b/FluentUI.Demo/src/main/AndroidManifest.xml @@ -32,7 +32,8 @@ - + diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt index b55d83638..03b2f5cf9 100644 --- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt +++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt @@ -43,6 +43,8 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -68,6 +70,7 @@ import com.microsoft.fluentuidemo.util.createPersonaList import kotlinx.coroutines.delay import kotlinx.coroutines.launch +const val BOTTOM_SHEET_ENABLE_SWIPE_DISMISS_TEST_TAG = "enableSwipeDismiss" class V2BottomSheetActivity : V2DemoActivity() { init { setupActivity(this) @@ -87,6 +90,8 @@ class V2BottomSheetActivity : V2DemoActivity() { @Composable private fun CreateActivityUI() { + var enableSwipeDismiss by remember { mutableStateOf(true) } + var showHandleState by remember { mutableStateOf(true) } var expandableState by remember { mutableStateOf(true) } @@ -137,7 +142,8 @@ private fun CreateActivityUI() { peekHeight = peekHeightState, showHandle = showHandleState, sheetState = bottomSheetState, - slideOver = slideOverState + slideOver = slideOverState, + enableSwipeDismiss = enableSwipeDismiss ) { Column( verticalArrangement = Arrangement.spacedBy(10.dp), @@ -265,7 +271,26 @@ private fun CreateActivityUI() { onValueChange = { slideOverState = it } ) } - + Row( + horizontalArrangement = Arrangement.spacedBy(30.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + BasicText( + text = stringResource(id =R.string.bottom_sheet_text_enable_swipe_dismiss), + modifier = Modifier.weight(1F), + style = TextStyle( + color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value( + themeMode = ThemeMode.Auto + ) + ) + ) + ToggleSwitch( + modifier = Modifier.testTag(BOTTOM_SHEET_ENABLE_SWIPE_DISMISS_TEST_TAG), + checkedState = enableSwipeDismiss, + onValueChange = { enableSwipeDismiss = it } + ) + } Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/FluentUI.Demo/src/main/res/values/strings.xml b/FluentUI.Demo/src/main/res/values/strings.xml index 32c793620..ef74c1ae7 100644 --- a/FluentUI.Demo/src/main/res/values/strings.xml +++ b/FluentUI.Demo/src/main/res/values/strings.xml @@ -138,6 +138,7 @@ BottomSheet BottomSheetDialog + Enable Swipe Down to Dismiss Show with single line items Show with double line items diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt index 8409a1820..c965f7b6f 100644 --- a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt +++ b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt @@ -891,4 +891,36 @@ val SwipeableState.PostDownNestedScrollConnection: NestedScrollConnection private fun Offset.toFloat(): Float = this.y } +val SwipeableState.NonDismissiblePostDownNestedScrollConnection: NestedScrollConnection + get() = object : NestedScrollConnection { + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + performDrag(delta).toOffset() + } else { + Offset.Zero + } + } + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + return if (source == NestedScrollSource.Drag && available.toFloat() < 0) { + performDrag(available.toFloat()).toOffset() + } else { + Offset.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + performFling(velocity = Offset(available.x, available.y).toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset(0f, this) + + private fun Offset.toFloat(): Float = this.y + } diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt index 1fde38952..0b475517f 100644 --- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt +++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -179,9 +180,9 @@ fun rememberBottomSheetState( } //Tag use for testing -private const val BOTTOMSHEET_HANDLE_TAG = "Fluent Bottom Sheet Handle" -private const val BOTTOMSHEET_CONTENT_TAG = "Fluent Bottom Sheet Content" -private const val BOTTOMSHEET_SCRIM_TAG = "Fluent Bottom Sheet Scrim" +const val BOTTOMSHEET_HANDLE_TAG = "Fluent Bottom Sheet Handle" +const val BOTTOMSHEET_CONTENT_TAG = "Fluent Bottom Sheet Content" +const val BOTTOMSHEET_SCRIM_TAG = "Fluent Bottom Sheet Scrim" private const val BottomSheetOpenFraction = 0.5f @@ -222,6 +223,7 @@ fun BottomSheet( scrimVisible: Boolean = true, showHandle: Boolean = true, slideOver: Boolean = true, + enableSwipeDismiss: Boolean = false, bottomSheetTokens: BottomSheetTokens? = null, content: @Composable () -> Unit ) { @@ -250,7 +252,20 @@ fun BottomSheet( val sheetHeightState = remember(sheetContent.hashCode()) { mutableStateOf(null) } - Box(Modifier.fillMaxSize()) { + Box( + Modifier + .fillMaxSize() + .semantics { + if (!sheetState.isVisible) { + expand { + if (sheetState.confirmStateChange(BottomSheetValue.Shown)) { + scope.launch { sheetState.show() } + } + true + } + } + } + ) { content() if (slideOver) { Scrim( @@ -284,7 +299,12 @@ fun BottomSheet( Box( Modifier .fillMaxWidth() - .nestedScroll(if (slideOver) sheetState.PreUpPostDownNestedScrollConnection else sheetState.PostDownNestedScrollConnection) + .nestedScroll( + if(!enableSwipeDismiss && sheetState.offset.value != null && sheetState.offset.value!! >= (fullHeight - dpToPx(peekHeight) ) ) + sheetState.NonDismissiblePostDownNestedScrollConnection + else if (slideOver) sheetState.PreUpPostDownNestedScrollConnection + else sheetState.PostDownNestedScrollConnection + ) .offset { val y = if (sheetState.anchors.isEmpty()) { // if we don't know our anchors yet, render the sheet as hidden @@ -328,11 +348,13 @@ fun BottomSheet( .background(sheetBackgroundColor) .semantics(mergeDescendants = true) { if (sheetState.isVisible) { - dismiss { - if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) { - scope.launch { sheetState.hide() } + if(enableSwipeDismiss) { + dismiss { + if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) { + scope.launch { sheetState.hide() } + } + true } - true } if (sheetState.currentValue == BottomSheetValue.Shown) { expand { @@ -363,13 +385,23 @@ fun BottomSheet( .draggable( orientation = Orientation.Vertical, state = rememberDraggableState { delta -> - sheetState.performDrag(delta) + if(!enableSwipeDismiss && sheetState.offset.value != null && sheetState.offset.value!! >= (fullHeight - dpToPx(peekHeight) ) ){ + if(delta<0){ + sheetState.performDrag(delta) + } + } + else sheetState.performDrag(delta) }, onDragStopped = { velocity -> launch { sheetState.performFling(velocity) if (!sheetState.isVisible) { - scope.launch { sheetState.hide() } + if(enableSwipeDismiss) { + scope.launch { sheetState.hide() } + } + else { + scope.launch { sheetState.show() } + } } } },