diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt index 60024cbb..7a085e53 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddArtifactScreen.kt @@ -21,12 +21,9 @@ import androidx.compose.material.icons.filled.LocalOffer import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilterChip -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -36,7 +33,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -46,8 +42,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.disabled -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -87,6 +81,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @@ -148,10 +143,6 @@ fun AddArtifactScreen( // about afterwards, and the button says so. val canAddArtifact = selectedDialectId != null && addArtifactUIState.canSign - // M3 gives a FAB no `enabled`, so borrow the disabled colours every - // other button in the app uses rather than inventing a shade here. - val buttonColors = ButtonDefaults.buttonColors() - // imePadding: this screen has a text field, and without it the software // keyboard covers whatever is being typed into. Scaffold( @@ -169,28 +160,11 @@ fun AddArtifactScreen( BottomAppBar( actions = {}, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = if (canAddArtifact) { - Modifier - } else { - // Looking unavailable is not being unavailable: - // without this a screen reader still announces - // a button it is happy to press. - Modifier.semantics { disabled() } - }, - containerColor = if (canAddArtifact) { - FloatingActionButtonDefaults.containerColor - } else { - buttonColors.disabledContainerColor - }, - contentColor = if (canAddArtifact) { - contentColorFor(FloatingActionButtonDefaults.containerColor) - } else { - buttonColors.disabledContentColor - }, + ExtendedFab( + label = stringResource(Res.string.propose_artifact), + icon = { Icon(Icons.Default.Add, contentDescription = "Propose artifact") }, + enabled = canAddArtifact, onClick = { - if (!canAddArtifact) return@ExtendedFloatingActionButton - addArtifactViewModel.addArtifact( localChatRoom = addArtifactUIState.localChatRoom, nameField = nameFieldState, @@ -220,13 +194,7 @@ fun AddArtifactScreen( ) } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Propose artifact" - ) - Text(stringResource(Res.string.propose_artifact)) - } + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt index 4fa54ce0..ccc6a166 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddChapterScreen.kt @@ -13,10 +13,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.DriveFileRenameOutline import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -24,13 +21,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.disabled -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -66,6 +60,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition import press.mantra.compose.ui.composable.widgets.NavigateBackButton +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3Api::class) @@ -133,10 +128,6 @@ fun AddChapterScreen( paragraphCount > 0 && !tooManyChunks - // M3 gives a FAB no `enabled`, so borrow the disabled colours every - // other button in the app uses rather than inventing a shade here. - val buttonColors = ButtonDefaults.buttonColors() - // imePadding: this screen has a text field, and without it the software // keyboard covers whatever is being typed into. Scaffold( @@ -152,27 +143,12 @@ fun AddChapterScreen( BottomAppBar( actions = {}, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = if (canProposeChapter) { - Modifier - } else { - // Looking unavailable is not being unavailable. - Modifier.semantics { disabled() } - }, - containerColor = if (canProposeChapter) { - FloatingActionButtonDefaults.containerColor - } else { - buttonColors.disabledContainerColor - }, - contentColor = if (canProposeChapter) { - contentColorFor(FloatingActionButtonDefaults.containerColor) - } else { - buttonColors.disabledContentColor - }, + ExtendedFab( + label = stringResource(Res.string.propose_chapter), + icon = { Icon(Icons.Default.Add, contentDescription = "Propose chapter") }, + enabled = canProposeChapter, onClick = { - if (artifactVersion == null || !canProposeChapter) { - return@ExtendedFloatingActionButton - } + if (artifactVersion == null) return@ExtendedFab addChapterViewModel.addChapter( localChatRoom = addChapterUIState.localChatRoom, @@ -200,13 +176,7 @@ fun AddChapterScreen( } ) } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Propose chapter" - ) - Text(stringResource(Res.string.propose_chapter)) - } + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt index ebd8d934..97e8e4d7 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddDialectScreen.kt @@ -17,11 +17,8 @@ import androidx.compose.material.icons.filled.Title import androidx.compose.material.icons.filled.Translate import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -30,14 +27,11 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.disabled -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -71,6 +65,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @@ -119,10 +114,6 @@ fun AddDialectScreen( val countryFieldState = rememberTextFieldState() val languageFieldState = rememberTextFieldState() - // M3 gives a FAB no `enabled`, so borrow the disabled colours every - // other button in the app uses rather than inventing a shade here. - val buttonColors = ButtonDefaults.buttonColors() - // imePadding: this screen has a text field, and without it the software // keyboard covers whatever is being typed into. Scaffold( @@ -140,26 +131,11 @@ fun AddDialectScreen( BottomAppBar( actions = {}, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = if (addDialectUIState.canSign) { - Modifier - } else { - // Looking unavailable is not being unavailable. - Modifier.semantics { disabled() } - }, - containerColor = if (addDialectUIState.canSign) { - FloatingActionButtonDefaults.containerColor - } else { - buttonColors.disabledContainerColor - }, - contentColor = if (addDialectUIState.canSign) { - contentColorFor(FloatingActionButtonDefaults.containerColor) - } else { - buttonColors.disabledContentColor - }, + ExtendedFab( + label = stringResource(Res.string.propose_dialect), + icon = { Icon(Icons.Default.Add, contentDescription = "Propose dialect") }, + enabled = addDialectUIState.canSign, onClick = { - if (!addDialectUIState.canSign) return@ExtendedFloatingActionButton - addDialectViewModel.addDialect( localChatRoom = addDialectUIState.localChatRoom, nameField = nameFieldState, @@ -186,13 +162,7 @@ fun AddDialectScreen( } ) } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Propose dialect" - ) - Text(stringResource(Res.string.propose_dialect)) - } + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt index 21f5dffc..6f08fd13 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -52,6 +51,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @@ -109,7 +109,12 @@ fun AddMemberToChatRoomConfirmationScreen( bottomBar = { BottomAppBar( floatingActionButton = { - ExtendedFloatingActionButton( + ExtendedFab( + label = stringResource( + Res.string.invite_2, + addMemberToChatRoomConfirmationUIState.profile.humanReadableNameOrPubkey() + ), + icon = { Icon(Icons.Default.Add, contentDescription = "Invite new member") }, onClick = { addMemberToChatRoomConfirmationViewModel.inviteToChatRoom( localChatRoom = addMemberToChatRoomConfirmationUIState.localChatRoom, @@ -118,14 +123,7 @@ fun AddMemberToChatRoomConfirmationScreen( onInviteSent = onInviteSent ) } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Invite new member" - ) - - Text(stringResource(Res.string.invite_2, addMemberToChatRoomConfirmationUIState.profile.humanReadableNameOrPubkey())) - } + ) }, actions = { diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt index 2dfc9a4a..dbd2592a 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/AddTranslationArtifactVersionScreen.kt @@ -12,18 +12,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Translate import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilterChip -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,8 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.disabled -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -67,6 +61,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition import press.mantra.compose.ui.composable.widgets.NavigateBackButton +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @@ -135,10 +130,6 @@ fun AddTranslationArtifactVersionScreen( selectedDialect != null && addTranslationUIState.canSign - // M3 gives a FAB no `enabled`, so borrow the disabled colours every - // other button in the app uses rather than inventing a shade here. - val buttonColors = ButtonDefaults.buttonColors() - Scaffold( snackbarHost = { SnackbarHost(LocalSnackbarHostState.current) }, topBar = { @@ -151,27 +142,12 @@ fun AddTranslationArtifactVersionScreen( BottomAppBar( actions = {}, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = if (canProposeTranslation) { - Modifier - } else { - // Looking unavailable is not being unavailable. - Modifier.semantics { disabled() } - }, - containerColor = if (canProposeTranslation) { - FloatingActionButtonDefaults.containerColor - } else { - buttonColors.disabledContainerColor - }, - contentColor = if (canProposeTranslation) { - contentColorFor(FloatingActionButtonDefaults.containerColor) - } else { - buttonColors.disabledContentColor - }, + ExtendedFab( + label = stringResource(Res.string.propose_translation), + icon = { Icon(Icons.Default.Translate, contentDescription = "Propose translation") }, + enabled = canProposeTranslation, onClick = { - if (artifactVersion == null || selectedDialect == null || !canProposeTranslation) { - return@ExtendedFloatingActionButton - } + if (artifactVersion == null || selectedDialect == null) return@ExtendedFab addTranslationArtifactVersionViewModel.addTranslation( localChatRoom = addTranslationUIState.localChatRoom, @@ -201,13 +177,7 @@ fun AddTranslationArtifactVersionScreen( } ) } - ) { - Icon( - Icons.Default.Translate, - contentDescription = "Propose translation" - ) - Text(stringResource(Res.string.propose_translation)) - } + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/BroadcastGroupSignedEventScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/BroadcastGroupSignedEventScreen.kt index 80bfd17c..99fe9a19 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/BroadcastGroupSignedEventScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/BroadcastGroupSignedEventScreen.kt @@ -24,12 +24,9 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Dns import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor @@ -41,7 +38,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -101,6 +97,7 @@ import press.mantra.compose.ui.view.model.BroadcastGroupSignedEventViewModel import press.mantra.compose.ui.view.model.BroadcastGroupSignedEventViewModel.RelayOutcome import press.mantra.compose.ui.view.state.BroadcastGroupSignedEventUIState import press.mantra.compose.extensions.toFormattedTimeAndDateString +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlin.time.Instant @@ -193,9 +190,6 @@ fun BroadcastGroupSignedEventScreen( val json = signedEvent.toEvent().toJson() val summary = ProposedEvent.summarize(signedEvent.toEvent()) - // M3 gives a FAB no `enabled`, so borrow the disabled colours every - // other button in the app uses rather than inventing a shade here. - val buttonColors = ButtonDefaults.buttonColors() val canSend = !isPending Scaffold( @@ -213,26 +207,11 @@ fun BroadcastGroupSignedEventScreen( BottomAppBar( actions = {}, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = if (canSend) { - Modifier - } else { - // Looking unavailable is not being unavailable. - Modifier.semantics { disabled() } - }, - containerColor = if (canSend) { - FloatingActionButtonDefaults.containerColor - } else { - buttonColors.disabledContainerColor - }, - contentColor = if (canSend) { - contentColorFor(FloatingActionButtonDefaults.containerColor) - } else { - buttonColors.disabledContentColor - }, + ExtendedFab( + label = stringResource(Res.string.broadcast), + icon = { Icon(Icons.Default.CellTower, contentDescription = "Broadcast to these relays") }, + enabled = canSend, onClick = { - if (!canSend) return@ExtendedFloatingActionButton - broadcastGroupSignedEventViewModel.broadcast( signedEvent = signedEvent, // Stays on the screen: an empty list is @@ -240,13 +219,7 @@ fun BroadcastGroupSignedEventScreen( onNoRelays = { notify(noRelays) } ) } - ) { - Icon( - Icons.Default.CellTower, - contentDescription = "Broadcast to these relays" - ) - Text(stringResource(Res.string.broadcast)) - } + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt index 64ce688d..f669285c 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/DkgRitualScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator @@ -117,6 +116,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews /** @@ -194,7 +194,13 @@ fun DkgRitualScreen( if (pending != null) { BottomAppBar( floatingActionButton = { - ExtendedFloatingActionButton( + ExtendedFab( + label = when (pending) { + DkgApprovalStep.HOST_KEY -> stringResource(Res.string.review_and_join) + DkgApprovalStep.ROUND_1 -> stringResource(Res.string.review_and_contribute) + DkgApprovalStep.ROUND_2 -> stringResource(Res.string.review_and_confirm) + }, + icon = { Icon(Icons.Default.Key, contentDescription = Decorative) }, onClick = { onNavigateToRoute( when (pending) { @@ -216,16 +222,7 @@ fun DkgRitualScreen( } ) } - ) { - Icon(Icons.Default.Key, contentDescription = Decorative) - Text( - text = when (pending) { - DkgApprovalStep.HOST_KEY -> stringResource(Res.string.review_and_join) - DkgApprovalStep.ROUND_1 -> stringResource(Res.string.review_and_contribute) - DkgApprovalStep.ROUND_2 -> stringResource(Res.string.review_and_confirm) - } - ) - } + ) }, actions = {} ) @@ -238,19 +235,21 @@ fun DkgRitualScreen( if (canStartRitual) { BottomAppBar( floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { dkgRitualViewModel.startRitual() } - ) { - if (isActionPending) { - CircularProgressIndicator(modifier = Modifier.size(20.dp)) + ExtendedFab( + label = if (session == null) { + stringResource(Res.string.start_key_ceremony) } else { - Icon(Icons.Default.Key, contentDescription = "Start") - } - - Text( - text = if (session == null) stringResource(Res.string.start_key_ceremony) else stringResource(Res.string.try_again) - ) - } + stringResource(Res.string.try_again) + }, + icon = { + if (isActionPending) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + Icon(Icons.Default.Key, contentDescription = "Start") + } + }, + onClick = { dkgRitualViewModel.startRitual() } + ) }, actions = { Text( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt index 7b2ba46a..9cf1ec42 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Icon import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryScrollableTabRow @@ -81,6 +80,7 @@ import mantra.composeapp.generated.resources.messages_need_the_secret_key_this_p import mantra.composeapp.generated.resources.sign_in_with_the_nsec import press.mantra.compose.ui.composable.widgets.LocalCanSign import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -456,8 +456,8 @@ private fun ReadOnlyInbox(onSignInWithTheNsec: () -> Unit) { */ @Composable private fun NewChatButton(onClick: () -> Unit) { - ExtendedFloatingActionButton( - onClick = onClick, + ExtendedFab( + label = stringResource(Res.string.new_chat), icon = { Icon( Icons.Default.Add, @@ -465,7 +465,7 @@ private fun NewChatButton(onClick: () -> Unit) { contentDescription = Decorative, ) }, - text = { Text(stringResource(Res.string.new_chat)) }, + onClick = onClick, ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt index 772a2ce9..c9e888fc 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomMembersScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -57,6 +56,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews /** @@ -123,26 +123,21 @@ fun SelectChatRoomMembersScreen( bottomBar = { BottomAppBar( floatingActionButton = { - ExtendedFloatingActionButton( + ExtendedFab( + label = if (selectedCount > 0) { + stringResource(Res.string.next_with, selectedCount) + } else { + stringResource(Res.string.next) + }, + icon = { + Icon(Icons.AutoMirrored.Filled.NavigateNext, contentDescription = "Next") + }, onClick = { selectChatRoomMembersViewModel.selectChatRoomType( onNavigateToRoute = onNavigateToRoute ) } - ) { - Icon( - Icons.AutoMirrored.Filled.NavigateNext, - contentDescription = "Next" - ) - - Text( - text = if (selectedCount > 0) { - stringResource(Res.string.next_with, selectedCount) - } else { - stringResource(Res.string.next) - } - ) - } + ) }, actions = { Text( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt index 5f5399fd..c0705763 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectChatRoomTypeScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -74,6 +73,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews /** @@ -151,32 +151,25 @@ fun SelectChatRoomTypeScreen( bottomBar = { BottomAppBar( floatingActionButton = { - ExtendedFloatingActionButton( + ExtendedFab( + label = if (selectChatRoomTypeViewModel.createdChatRoomId.value != null) { + stringResource(Res.string.open_chat) + } else { + stringResource(Res.string.create_chat) + }, + icon = { + if (isActionPending) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + Icon(Icons.Default.Check, contentDescription = "Create chat") + } + }, onClick = { selectChatRoomTypeViewModel.createChatRoom( onNavigateToRoute = onNavigateToRoute ) } - ) { - if (isActionPending) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp) - ) - } else { - Icon( - Icons.Default.Check, - contentDescription = "Create chat" - ) - } - - Text( - text = if (selectChatRoomTypeViewModel.createdChatRoomId.value != null) { - stringResource(Res.string.open_chat) - } else { - stringResource(Res.string.create_chat) - } - ) - } + ) }, actions = { Text( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectSubgroupAdminsScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectSubgroupAdminsScreen.kt index 44110cbf..a104bc1e 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectSubgroupAdminsScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SelectSubgroupAdminsScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -61,6 +60,7 @@ import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.composable.widgets.NavigateBackButton import press.mantra.compose.ui.composable.widgets.ScreenStateTransition +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.composable.widgets.profile.ProfileAvatar import press.mantra.compose.ui.theme.ConformancePreviews import press.mantra.compose.ui.theme.MantraTheme @@ -123,7 +123,9 @@ fun SelectSubgroupAdminsScreen( ) }, floatingActionButton = { - ExtendedFloatingActionButton( + ExtendedFab( + label = stringResource(Res.string.start_the_key_ceremony), + icon = { Icon(Icons.Default.AccountTree, contentDescription = Decorative) }, onClick = { scope.launch { // The refusal is recomputed rather than cached on @@ -137,9 +139,7 @@ fun SelectSubgroupAdminsScreen( viewModel.confirm(onNavigateToRoute) } } - }, - icon = { Icon(Icons.Default.AccountTree, contentDescription = Decorative) }, - text = { Text(stringResource(Res.string.start_the_key_ceremony)) } + } ) } ) { innerPadding -> diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SocialPreconditionScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SocialPreconditionScreen.kt index 936fb60d..b221d50a 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SocialPreconditionScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SocialPreconditionScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Button -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -33,6 +32,7 @@ import mantra.composeapp.generated.resources.view_invites import mantra.composeapp.generated.resources.who_will_you_be_passing_the_aux_to import androidx.compose.material3.SnackbarHost import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.theme.ConformancePreviews @@ -51,17 +51,9 @@ fun SocialPreconditionScreen( }, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = Modifier, - icon = { - Icon( - Icons.Default.SkipNext, - contentDescription = "Skip for now" - ) - }, - text = { - Text(stringResource(Res.string.skip_for_now)) - }, + ExtendedFab( + label = stringResource(Res.string.skip_for_now), + icon = { Icon(Icons.Default.SkipNext, contentDescription = "Skip for now") }, onClick = { onNavigateToSkipForNow.invoke() } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt index bd9bd0af..6af370c2 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslateChunkScreen.kt @@ -14,11 +14,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -26,13 +23,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.disabled -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -64,6 +58,7 @@ import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import press.mantra.compose.ui.theme.readableContent import press.mantra.compose.ui.composable.widgets.ScreenStateTransition import press.mantra.compose.ui.composable.widgets.NavigateBackButton +import press.mantra.compose.ui.composable.widgets.buttons.ExtendedFab import press.mantra.compose.ui.theme.ConformancePreviews @OptIn(ExperimentalMaterial3Api::class) @@ -111,10 +106,6 @@ fun TranslateChunkScreen( is TranslateChunkUIState.Loaded -> { val translationFieldState = rememberTextFieldState(translateChunkUIState.existingTranslationText) - // M3 gives a FAB no `enabled`, so borrow the disabled colours every - // other button in the app uses rather than inventing a shade here. - val buttonColors = ButtonDefaults.buttonColors() - // imePadding: this screen has a text field, and without it the software // keyboard covers whatever is being typed into. Scaffold( @@ -130,28 +121,11 @@ fun TranslateChunkScreen( BottomAppBar( actions = {}, floatingActionButton = { - ExtendedFloatingActionButton( - modifier = if (translateChunkUIState.canSign) { - Modifier - } else { - // Looking unavailable is not being unavailable: - // without this a screen reader still announces - // a button it is happy to press. - Modifier.semantics { disabled() } - }, - containerColor = if (translateChunkUIState.canSign) { - FloatingActionButtonDefaults.containerColor - } else { - buttonColors.disabledContainerColor - }, - contentColor = if (translateChunkUIState.canSign) { - contentColorFor(FloatingActionButtonDefaults.containerColor) - } else { - buttonColors.disabledContentColor - }, + ExtendedFab( + label = stringResource(Res.string.propose_translation), + icon = { Icon(Icons.Default.Add, contentDescription = "Propose translation") }, + enabled = translateChunkUIState.canSign, onClick = { - if (!translateChunkUIState.canSign) return@ExtendedFloatingActionButton - translateChunkViewModel.proposeTranslation( localChatRoom = translateChunkUIState.localChatRoom, originalChunk = translateChunkUIState.originalChunk, @@ -178,13 +152,7 @@ fun TranslateChunkScreen( } ) } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Propose translation" - ) - Text(stringResource(Res.string.propose_translation)) - } + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/buttons/ExtendedFab.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/buttons/ExtendedFab.kt new file mode 100644 index 00000000..b2ffc81b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/buttons/ExtendedFab.kt @@ -0,0 +1,80 @@ +package press.mantra.compose.ui.composable.widgets.buttons + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics +import press.mantra.compose.ui.theme.spacing + +/** + * A screen's one primary action, as an extended FAB with a label beside its icon. + * + * Every bottom bar in the app ends in one of these -- propose, broadcast, next, + * create -- and this is the one place their shape is decided. Two decisions: + * + * - **The gap between icon and label is drawn here.** M3's `ExtendedFloatingActionButton` + * comes in two overloads. The `text`/`icon` one spaces them 12dp apart, but wraps the + * label in `clearAndSetSemantics`, so a screen reader is told only what the icon says + * and a test cannot find the button by its label; the content-lambda one keeps the + * label in the tree and puts nothing between the two. This uses the second and adds + * the first's gap, `space150`, which is the 12dp M3 names `ExtendedFabEndIconPadding`. + * The label is what the button says, so it has to be what the button *announces*. + * - **A FAB has no `enabled`, so unavailable is a look and a refusal, not a state.** + * M3 gives the FAB no such parameter. Here [enabled] false borrows the disabled + * colours every other button in the app uses rather than inventing a shade, marks the + * node disabled so a screen reader does not announce a button it is happy to press, + * and drops the press -- looking unavailable is not being unavailable, and each of + * those three has to be done for the look to be honest. + * + * @param label what the button says. Text, so from the catalogue; drawn after the icon. + * @param icon what sits before the label. Usually an `Icon` -- whose description is the + * caller's decision, `Decorative` when the label says it all and a real one when a test + * reaches for the button by it -- and sometimes a progress indicator in its place while + * the action it started is out. + * @param enabled whether a press does anything. False also changes the look, as above. + */ +@Composable +fun ExtendedFab( + label: String, + icon: @Composable () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + val buttonColors = ButtonDefaults.buttonColors() + + ExtendedFloatingActionButton( + modifier = if (enabled) { + modifier + } else { + modifier.semantics { disabled() } + }, + containerColor = if (enabled) { + FloatingActionButtonDefaults.containerColor + } else { + buttonColors.disabledContainerColor + }, + contentColor = if (enabled) { + contentColorFor(FloatingActionButtonDefaults.containerColor) + } else { + buttonColors.disabledContentColor + }, + onClick = { + if (enabled) onClick() + } + ) { + icon() + + Spacer(modifier = Modifier.width(MaterialTheme.spacing.space150)) + + Text(label) + } +} diff --git a/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/widgets/buttons/ExtendedFabJvmTest.kt b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/widgets/buttons/ExtendedFabJvmTest.kt new file mode 100644 index 00000000..38fe09f1 --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/widgets/buttons/ExtendedFabJvmTest.kt @@ -0,0 +1,111 @@ +package press.mantra.compose.ui.composable.widgets.buttons + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Icon +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.getBoundsInRoot +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runDesktopComposeUiTest +import press.mantra.compose.ui.composable.widgets.Decorative +import press.mantra.compose.ui.theme.MantraTheme +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * The two things the shared FAB decides for every screen's primary action. + * + * **The label is what the button announces.** M3's `text`/`icon` overload clears the + * label from the merged semantics tree, so a screen reader hears only the icon and a + * test cannot find the button by what it says. Every screen test in the app that + * reaches a FAB by `onNodeWithText` depends on this one staying the other way, and + * a FAB with a decorative icon has no name at all without it. + * + * **Unavailable is a refusal, not only a look.** M3 gives a FAB no `enabled`, so the + * widget has to both mark the node disabled and drop the press; a button that only + * looked disabled would be announced, and pressed, as one that works. + */ +@OptIn(ExperimentalTestApi::class) +class ExtendedFabJvmTest { + + @Test + fun `the button is found by its label in the merged tree, with the icon beside it`() = + runDesktopComposeUiTest(400, 200) { + setContent { + MantraTheme { + ExtendedFab( + label = "Propose post", + icon = { Icon(Icons.Default.Add, contentDescription = "Propose post icon") }, + onClick = {}, + ) + } + } + + // No `useUnmergedTree`: this is the tree a screen reader is given. + onNodeWithText("Propose post").assertIsDisplayed().assertIsEnabled() + + // The icon first, then a gap, then the label -- not one touching the other. + val icon = onNodeWithContentDescription("Propose post icon", useUnmergedTree = true) + .getBoundsInRoot() + val label = onNodeWithText("Propose post", useUnmergedTree = true).getBoundsInRoot() + assertTrue(icon.right < label.left, "the label is not after the icon") + } + + @Test + fun `a decorative icon leaves the label as the button's only name, which is enough`() = + runDesktopComposeUiTest(400, 200) { + setContent { + MantraTheme { + ExtendedFab( + label = "Broadcast all", + icon = { Icon(Icons.Default.Add, contentDescription = Decorative) }, + onClick = {}, + ) + } + } + + onNodeWithText("Broadcast all").assertIsDisplayed() + } + + @Test + fun `disabled is announced as disabled and drops the press, and enabled takes it`() { + var presses = 0 + + runDesktopComposeUiTest(400, 200) { + setContent { + MantraTheme { + ExtendedFab( + label = "Propose post", + icon = { Icon(Icons.Default.Add, contentDescription = Decorative) }, + enabled = false, + onClick = { presses++ }, + ) + } + } + + onNodeWithText("Propose post").assertIsNotEnabled().performClick() + } + assertEquals(0, presses) + + runDesktopComposeUiTest(400, 200) { + setContent { + MantraTheme { + ExtendedFab( + label = "Propose post", + icon = { Icon(Icons.Default.Add, contentDescription = Decorative) }, + onClick = { presses++ }, + ) + } + } + + onNodeWithText("Propose post").assertIsEnabled().performClick() + } + assertEquals(1, presses) + } +} diff --git a/docs/material-design-conformance.md b/docs/material-design-conformance.md index 50a2e1c0..13301d2d 100644 --- a/docs/material-design-conformance.md +++ b/docs/material-design-conformance.md @@ -696,7 +696,10 @@ let the component apply the 38% state layer. No `FloatingActionButton` overload material3 1.10 takes `enabled` — the spec's position is that an unavailable FAB should not appear at all — so hand-computing the colours is the only way to show one, and the existing code already pairs it with `Modifier.semantics { disabled() }` so a screen reader does not -announce a button it is happy to press. Left alone. +announce a button it is happy to press. Left alone at the time; since done once, in +`ExtendedFab`, which every screen's bottom-bar action now goes through and which also +puts M3's own 12dp between the icon and the label — the content overload the screens were +using put nothing there. **Left for a person.** Eight more screens have two or more filled buttons competing: LandingScreen's "Sign in" beside "Create profile", SocialPreconditionScreen's "Invite a diff --git a/docs/npub-sign-in.md b/docs/npub-sign-in.md index 13672418..68193b62 100644 --- a/docs/npub-sign-in.md +++ b/docs/npub-sign-in.md @@ -36,6 +36,9 @@ floating action button's label by text has to search the unmerged tree, because `ExtendedFloatingActionButton` merges its label into the button's semantics — and on the merged tree an `assertDoesNotExist` for a hidden control is vacuously true. `ReadOnlyEntrancesJvmTest` uses the unmerged tree for every lookup for that reason. +(More precisely, the `text`/`icon` overload *clears* the label's semantics, so the +merged tree never had it; the shared `ExtendedFab` that replaced it keeps the label +in the merged tree, and the test's habit is now only a habit.) The library commit is on `claude/nostr-credentials` in the submodule, at `84cc44c`, one commit ahead of the library's `master` and bumped into the app by the Phase 2