refactor(ui): one extended FAB for every screen's primary action, with a gap in it
Twenty screens end their bottom bar in an `ExtendedFloatingActionButton` --
twenty-one buttons, one screen having two -- and eighteen of those were built
from the content-lambda overload with an `Icon` directly followed by a `Text`.
That overload puts nothing between the two, so the icon touched the label on
every one of them: visible in any render, and unnoticed because every FAB in
the app looked the same. Thirteen of the eighteen also carried the same
twenty-line block for looking disabled, because M3 gives a FAB no `enabled`.
**`ExtendedFab`, once, in widgets/buttons.** A label, an icon slot, `enabled`
and `onClick`. It draws `space150` between icon and label, which is the 12dp
M3 names `ExtendedFabEndIconPadding`; it borrows the disabled colours every
other button in the app uses, marks the node disabled so a screen reader does
not announce a button it is happy to press, and drops the press -- so the
thirteen call sites lose their `if (!canX) return@…` guards along with the
block and the `buttonColors` each of them declared. An icon slot rather than
an image, because two screens put a progress indicator where the icon was
while the action they started is out, and they still do.
**Not M3's `text`/`icon` overload, which would have spaced them for free.**
Its source wraps the label in `clearAndSetSemantics {}`: the label leaves the
merged semantics tree, a screen reader is told only what the icon says, and a
test cannot find the button by what it says -- five screen tests do. The
three FABs that were on that overload move to the widget too, and two of them,
Home's "New chat" and "Start the key ceremony", had a decorative icon beside
the cleared label and so no accessible name at all until now.
`ReadOnlyEntrancesJvmTest` had gone to the unmerged tree to find "New chat"
for this reason; the habit is harmless and stays. Every icon's description is
as it was, since several tests reach their FAB by it.
`ExtendedFabJvmTest` pins the decision: found by its label in the merged tree,
the icon before the label with a gap between, and disabled both announced and
refusing the press. The two docs that recorded the old state -- the
conformance account's "left alone" and the npub plan's unmerged-tree note --
each say what superseded them.
Verified with :composeApp:m3Audit (all budgets met, 0 dp literals in spacing
positions), :composeApp:compileDebugKotlinAndroid and :composeApp:jvmTest
(1088 tests, 3 new).
Replayed onto Mantra by docs/curated-to-mantra.md: AcceptCuratedSuggestionScreen.kt: deleted, as this commit deletes it upstream; AddGroupPostScreen.kt: deleted, as this commit deletes it upstream; EditGroupCuratedSchemaScreen.kt: deleted, as this commit deletes it upstream; EditGroupNostrProfileScreen.kt: deleted, as this commit deletes it upstream; EditGroupRelaysScreen.kt: deleted, as this commit deletes it upstream; GroupCuratedEntriesScreen.kt: deleted, as this commit deletes it upstream; ProposeGroupEventScreen.kt: deleted, as this commit deletes it upstream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulled-From: curated/curated@ca6c16beb0
This commit is contained in:
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user