diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/App.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/App.kt index 94d79aa..f576c25 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/App.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/App.kt @@ -511,7 +511,7 @@ fun App( composable { com.tt.honeyDue.ui.screens.residence.JoinResidenceScreen( - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onJoined = { residenceId -> navController.popBackStack() navController.navigate(ResidenceDetailRoute(residenceId)) @@ -674,7 +674,7 @@ fun App( composable { // P2 Stream E — full-screen Free vs. Pro comparison. FeatureComparisonScreen( - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onNavigateToUpgrade = { navController.popBackStack() navController.navigate(UpgradeRoute) @@ -687,7 +687,7 @@ fun App( val route = backStackEntry.toRoute() TaskSuggestionsScreen( residenceId = route.residenceId, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, ) } @@ -696,7 +696,7 @@ fun App( val route = backStackEntry.toRoute() AddTaskWithResidenceScreen( residenceId = route.residenceId, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onCreated = { navController.popBackStack() }, ) } @@ -707,7 +707,7 @@ fun App( TaskTemplatesBrowserScreen( residenceId = route.residenceId, fromOnboarding = route.fromOnboarding, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, ) } @@ -734,7 +734,7 @@ fun App( updatedAt = route.updatedAt, completions = emptyList() ), - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onTaskUpdated = { navController.popBackStack() } ) } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/ErrorCard.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/ErrorCard.kt index a061782..0393863 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/ErrorCard.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/ErrorCard.kt @@ -6,6 +6,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.error +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.tt.honeyDue.ui.haptics.Haptics import org.jetbrains.compose.ui.tooling.preview.Preview @@ -22,7 +26,12 @@ fun ErrorCard( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.errorContainer ), - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .semantics { + liveRegion = LiveRegionMode.Polite + error(message) + }, shape = RoundedCornerShape(12.dp) ) { Text( diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/LoginScreen.kt index 2f51707..b14bb08 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/LoginScreen.kt @@ -8,12 +8,17 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId @@ -51,6 +56,12 @@ fun LoginScreen( var googleSignInError by remember { mutableStateOf(null) } val loginState by viewModel.loginState.collectAsStateWithLifecycle() val googleSignInState by viewModel.googleSignInState.collectAsStateWithLifecycle() + val focusManager = LocalFocusManager.current + val usernameFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + usernameFocusRequester.requestFocus() + } // Handle errors for login loginState.HandleErrors( @@ -139,12 +150,16 @@ fun LoginScreen( }, modifier = Modifier .fillMaxWidth() + .focusRequester(usernameFocusRequester) .testTag(AccessibilityIds.Authentication.usernameField), singleLine = true, shape = RoundedCornerShape(OrganicRadius.md), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } ) ) @@ -171,7 +186,19 @@ fun LoginScreen( .testTag(AccessibilityIds.Authentication.passwordField), singleLine = true, visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - shape = RoundedCornerShape(OrganicRadius.md) + shape = RoundedCornerShape(OrganicRadius.md), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + if (username.isNotEmpty() && password.isNotEmpty()) { + viewModel.login(username, password) + } + } + ) ) ErrorCard(message = errorMessage) diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/MainScreen.kt index 58a34ac..a733d6d 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/MainScreen.kt @@ -155,7 +155,7 @@ fun MainScreen( composable { Box(modifier = Modifier.fillMaxSize()) { com.tt.honeyDue.ui.screens.residence.JoinResidenceScreen( - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onJoined = { residenceId -> // Pop the join screen and hand off to the // parent nav graph (ResidenceDetailRoute lives @@ -242,7 +242,7 @@ fun MainScreen( AddDocumentScreen( residenceId = route.residenceId, initialDocumentType = route.initialDocumentType, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onDocumentCreated = { navController.popBackStack() } @@ -253,7 +253,7 @@ fun MainScreen( val route = backStackEntry.toRoute() DocumentDetailScreen( documentId = route.documentId, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController.navigateUp() }, onNavigateToEdit = { documentId -> navController.navigate(EditDocumentRoute(documentId)) } @@ -264,7 +264,7 @@ fun MainScreen( val route = backStackEntry.toRoute() EditDocumentScreen( documentId = route.documentId, - onNavigateBack = { navController.popBackStack() } + onNavigateBack = { navController.navigateUp() } ) } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/RegisterScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/RegisterScreen.kt index 1c6b3b0..dfcd331 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/RegisterScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/RegisterScreen.kt @@ -3,6 +3,8 @@ package com.tt.honeyDue.ui.screens import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -11,10 +13,16 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -47,6 +55,12 @@ fun RegisterScreen( var isLoading by remember { mutableStateOf(false) } val createState by viewModel.registerState.collectAsStateWithLifecycle() + val focusManager = LocalFocusManager.current + val usernameFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + usernameFocusRequester.requestFocus() + } // Handle errors for registration createState.HandleErrors( @@ -130,9 +144,14 @@ fun RegisterScreen( }, modifier = Modifier .fillMaxWidth() + .focusRequester(usernameFocusRequester) .testTag(AccessibilityIds.Authentication.registerUsernameField), singleLine = true, - shape = RoundedCornerShape(AppRadius.md) + shape = RoundedCornerShape(AppRadius.md), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) ) OutlinedTextField( @@ -146,7 +165,14 @@ fun RegisterScreen( .fillMaxWidth() .testTag(AccessibilityIds.Authentication.registerEmailField), singleLine = true, - shape = RoundedCornerShape(AppRadius.md) + shape = RoundedCornerShape(AppRadius.md), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) ) OrganicDivider() @@ -163,7 +189,14 @@ fun RegisterScreen( .testTag(AccessibilityIds.Authentication.registerPasswordField), singleLine = true, visualTransformation = PasswordVisualTransformation(), - shape = RoundedCornerShape(AppRadius.md) + shape = RoundedCornerShape(AppRadius.md), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) ) OutlinedTextField( @@ -178,7 +211,14 @@ fun RegisterScreen( .testTag(AccessibilityIds.Authentication.registerConfirmPasswordField), singleLine = true, visualTransformation = PasswordVisualTransformation(), - shape = RoundedCornerShape(AppRadius.md) + shape = RoundedCornerShape(AppRadius.md), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() } + ) ) } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/onboarding/OnboardingCreateAccountContent.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/onboarding/OnboardingCreateAccountContent.kt index 214d1bc..630b86f 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/onboarding/OnboardingCreateAccountContent.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/onboarding/OnboardingCreateAccountContent.kt @@ -15,6 +15,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.error +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -240,7 +244,12 @@ fun OnboardingCreateAccountContent( // Error message if (localErrorMessage != null) { OrganicCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .semantics { + liveRegion = LiveRegionMode.Polite + error(localErrorMessage ?: "") + }, accentColor = MaterialTheme.colorScheme.error, showBlob = false ) { diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/residence/JoinResidenceScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/residence/JoinResidenceScreen.kt index 3ffcdfc..77b3345 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/residence/JoinResidenceScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/residence/JoinResidenceScreen.kt @@ -35,6 +35,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.error +import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.font.FontWeight @@ -160,7 +163,11 @@ fun JoinResidenceScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.sm), + .padding(AppSpacing.sm) + .semantics { + liveRegion = LiveRegionMode.Polite + error(error ?: "") + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), ) {