diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AccessibilityScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AccessibilityScreen.kt index beb02633e..b8b08ec02 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AccessibilityScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AccessibilityScreen.kt @@ -64,6 +64,7 @@ import ee.ria.DigiDoc.ui.component.shared.DynamicText import ee.ria.DigiDoc.ui.component.shared.HrefDynamicText import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.TopBar +import ee.ria.DigiDoc.ui.component.shared.keyboard.keyboardScrollable import ee.ria.DigiDoc.ui.theme.Dimensions.LINE_HEIGHT import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding @@ -82,6 +83,8 @@ fun AccessibilityScreen( val snackBarHostState = remember { SnackbarHostState() } val snackBarScope = rememberCoroutineScope() + val screenScrollState = rememberScrollState() + val messages by SnackBarManager.messages.collectAsState(emptyList()) val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) } @@ -140,7 +143,8 @@ fun AccessibilityScreen( testTagsAsResourceId = true }.testTag("scrollView") .fillMaxWidth() - .verticalScroll(rememberScrollState()), + .verticalScroll(screenScrollState) + .keyboardScrollable(screenScrollState), horizontalAlignment = Alignment.Start, ) { DynamicText( diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt index 738c260ab..eb2cccdee 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/DiagnosticsScreen.kt @@ -29,6 +29,8 @@ import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -59,6 +61,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -80,6 +83,7 @@ import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryOutlinedButton import ee.ria.DigiDoc.ui.component.shared.SpannableBoldText import ee.ria.DigiDoc.ui.component.shared.TopBar +import ee.ria.DigiDoc.ui.component.shared.keyboard.keyboardScrollable import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme @@ -113,6 +117,9 @@ fun DiagnosticsScreen( val snackBarHostState = remember { SnackbarHostState() } val snackBarScope = rememberCoroutineScope() + val screenScrollState = rememberScrollState() + val loggingDialogScrollState = rememberScrollState() + val messages by SnackBarManager.messages.collectAsState(emptyList()) val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) } @@ -213,7 +220,8 @@ fun DiagnosticsScreen( Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .verticalScroll(screenScrollState) + .keyboardScrollable(screenScrollState) .fillMaxWidth() .testTag("scrollView"), horizontalAlignment = Alignment.Start, @@ -652,14 +660,16 @@ fun DiagnosticsScreen( .padding(SPadding) .wrapContentHeight() .wrapContentWidth() - .verticalScroll(rememberScrollState()), + .verticalScroll(loggingDialogScrollState), ) { Column( modifier = modifier .semantics { testTagsAsResourceId = true - }.testTag("diagnosticsActivateLoggingContainer"), + } + .keyboardScrollable(loggingDialogScrollState) + .testTag("diagnosticsActivateLoggingContainer"), ) { HrefMessageDialog( text1 = R.string.main_diagnostics_restart_message, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/InfoScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/InfoScreen.kt index c7ee977b0..b632a9138 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/InfoScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/InfoScreen.kt @@ -83,6 +83,7 @@ import ee.ria.DigiDoc.ui.component.info.InfoComponentItem import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.TopBar +import ee.ria.DigiDoc.ui.component.shared.keyboard.keyboardScrollable import ee.ria.DigiDoc.ui.theme.Dimensions.MCornerRadius import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding @@ -108,6 +109,8 @@ fun InfoScreen( val snackBarHostState = remember { SnackbarHostState() } val snackBarScope = rememberCoroutineScope() + val screenScrollState = rememberScrollState() + val messages by SnackBarManager.messages.collectAsState(emptyList()) val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) } @@ -171,7 +174,8 @@ fun InfoScreen( Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .verticalScroll(screenScrollState) + .keyboardScrollable(screenScrollState) .fillMaxWidth() .semantics { testTagsAsResourceId = true diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryOutlinedButton.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryOutlinedButton.kt index 3f4fa8960..6e21fec48 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryOutlinedButton.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryOutlinedButton.kt @@ -24,7 +24,6 @@ package ee.ria.DigiDoc.ui.component.shared import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -40,6 +39,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -86,7 +86,8 @@ fun PrimaryOutlinedButton( }.fillMaxWidth() .padding( horizontal = XSPadding, - ).focusable(enabled = isFocusable), + ) + .focusProperties { canFocus = isFocusable }, shape = buttonRoundCornerShape, contentPadding = PaddingValues(zeroPadding), enabled = enabled, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/TopBar.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/TopBar.kt index 18f3bc0dc..28bd7d59d 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/TopBar.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/TopBar.kt @@ -174,9 +174,6 @@ fun TopBar( modifier .semantics { heading() } .focusRequester(headingFocusRequester) - .focusable(enabled = true) - .focusTarget() - .focusProperties { canFocus = true } .onGloballyPositioned { if (!headingTextLoaded) { CoroutineScope(Main).launch { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/keyboard/KeyboardScrollable.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/keyboard/KeyboardScrollable.kt new file mode 100644 index 000000000..44de97f99 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/keyboard/KeyboardScrollable.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.ui.component.shared.keyboard + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.onSizeChanged +import kotlinx.coroutines.launch + +fun interface KeyboardScrollAdapter { + suspend fun scrollBy(delta: Float) +} + +fun ScrollState.asKeyboardAdapter() = KeyboardScrollAdapter { delta -> + scrollBy(delta) +} + +fun LazyListState.asKeyboardAdapter() = KeyboardScrollAdapter { delta -> + scrollBy(delta) +} + +fun Modifier.keyboardScrollable( + adapter: KeyboardScrollAdapter, + step: Float = 100f +): Modifier = composed { + + var heightPx by remember { mutableFloatStateOf(0f) } + val scope = rememberCoroutineScope() + + this + .onSizeChanged { heightPx = it.height.toFloat() } + .onPreviewKeyEvent { event -> + if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false + + when (event.key) { + + Key.DirectionDown -> { + scope.launch { adapter.scrollBy(step) } + true + } + + Key.DirectionUp -> { + scope.launch { adapter.scrollBy(-step) } + true + } + + Key.PageDown -> { + scope.launch { adapter.scrollBy(heightPx) } + true + } + + Key.PageUp -> { + scope.launch { adapter.scrollBy(-heightPx) } + true + } + + else -> false + } + } +} + +fun Modifier.keyboardScrollable( + scrollState: ScrollState, + step: Float = 100f +): Modifier = keyboardScrollable(scrollState.asKeyboardAdapter(), step) + +fun Modifier.keyboardScrollable( + listState: LazyListState, + step: Float = 100f +): Modifier = keyboardScrollable(listState.asKeyboardAdapter(), step) \ No newline at end of file