Audit: form-error TalkBack + focus management + navigateUp polish
(a) liveRegion + error semantics on form error surfaces so TalkBack
announces them when they appear:
- Shared ErrorCard (used by LoginScreen, RegisterScreen,
VerifyEmail/ResetCode, ForgotPassword, ResetPassword)
- OnboardingCreateAccountContent inline error row
- JoinResidenceScreen inline error row
(b) focusRequester + ImeAction.Next on multi-field forms:
- LoginScreen: auto-focus username, Next→password, Done→submit
- RegisterScreen: auto-focus username, Next chain through
email/password/confirm, Done on last
(c) navigateUp() replaces navController.popBackStack() for simple back
actions in App.kt (6 screens) and MainScreen.kt (3 screens), where
the back behavior is purely navigation-controlled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -511,7 +511,7 @@ fun App(
|
||||
|
||||
composable<JoinResidenceRoute> {
|
||||
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<FeatureComparisonRoute> {
|
||||
// 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<TaskSuggestionsRoute>()
|
||||
TaskSuggestionsScreen(
|
||||
residenceId = route.residenceId,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateBack = { navController.navigateUp() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -696,7 +696,7 @@ fun App(
|
||||
val route = backStackEntry.toRoute<AddTaskWithResidenceRoute>()
|
||||
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() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<String?>(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)
|
||||
|
||||
@@ -155,7 +155,7 @@ fun MainScreen(
|
||||
composable<JoinResidenceRoute> {
|
||||
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<DocumentDetailRoute>()
|
||||
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<EditDocumentRoute>()
|
||||
EditDocumentScreen(
|
||||
documentId = route.documentId,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
onNavigateBack = { navController.navigateUp() }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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),
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user