Let the login sheet respect its edges and the account footer tell the truth

MAC2/MAC15: the macOS login form is grouped like its Edit Source parent, so
labels sit inside the sheet and the add-flow step stops clipping; footers
wrap instead of truncating.
MAC12: the account section footer consults account statuses before claiming
the user is logged in.
This commit is contained in:
Arkadiusz Fal
2026-09-11 19:36:50 +02:00
parent 94b2dff75b
commit 3281840a8e
2 changed files with 76 additions and 4 deletions

View File

@@ -77,7 +77,7 @@ struct InstanceAccountsSection: View {
} header: {
Text(String(localized: "login.header.account"))
} footer: {
Text(String(localized: accounts.isEmpty ? "login.footer.loginBenefits" : "login.footer.loggedIn"))
Text(footerText)
}
.onReceive(NotificationCenter.default.publisher(for: .accountCredentialDidChange)) { _ in
credentialRevision &+= 1
@@ -216,6 +216,45 @@ struct InstanceAccountsSection: View {
// MARK: - Presentation
/// What the section says under the rows.
///
/// Read from the statuses, not from the count: this is the screen the user
/// comes to in order to fix a login the server turned down, and a footer
/// that only counts accounts tells them, right there, that they are logged
/// in. The worst state on the list is the one that gets named — a rejected
/// credential is something to act on, and it outranks a server that
/// happens to be down.
private var footerText: String {
guard !accounts.isEmpty else {
return String(localized: "login.footer.loginBenefits")
}
// `checking` counts as healthy: it is the absence of an answer, and a
// footer that reacted to it would blink a warning at every launch.
let unhealthy = accounts.map { status(for: $0) }
.filter { $0 != .signedIn && $0 != .checking }
guard !unhealthy.isEmpty else {
return String(localized: "login.footer.loggedIn")
}
if unhealthy.contains(.credentialRejected) {
return String(localized: "login.footer.credentialRejected")
}
if unhealthy.contains(.unreachable) {
return String(localized: "login.footer.unreachable")
}
if unhealthy.contains(.credentialMissing) {
return String(localized: "login.footer.credentialMissing")
}
// A state named after this was written. Saying only that something is
// wrong is worth little, but it is true, and the rows above say what.
return String(localized: "login.footer.accountUnusable")
}
private func displayName(for account: Account) -> String {
appEnvironment?.dataContextValidator.displayName(for: account) ?? instance.displayName
}

View File

@@ -73,7 +73,12 @@ struct InstanceLoginView: View {
.accessibilityIdentifier("instance.login.view")
}
#if os(macOS)
.frame(minWidth: 450, minHeight: 350)
// Sized to what the form actually holds: two fields and a button leave
// most of a taller sheet empty, and the optional step carries two more
// rows than the plain one. The width is what the labels have to fit
// in, which matters most where this is pushed on top of the add flow's
// own sheet.
.frame(minWidth: 480, minHeight: isOptionalStep ? 400 : 300)
#endif
#endif
}
@@ -84,6 +89,19 @@ struct InstanceLoginView: View {
#if os(tvOS)
TVSettingsTextField(title: usernameFieldLabel, text: $username)
TVSettingsTextField(title: String(localized: "login.password"), text: $password, isSecure: true)
#elseif os(macOS)
LabeledContent(usernameFieldLabel) {
TextField("", text: $username)
.textContentType(.username)
.autocorrectionDisabled()
.accessibilityIdentifier("instance.login.usernameField")
}
LabeledContent(String(localized: "login.password")) {
SecureField("", text: $password)
.textContentType(.password)
.accessibilityIdentifier("instance.login.passwordField")
}
#else
TextField(usernameFieldLabel, text: $username)
.textContentType(.username)
@@ -100,7 +118,7 @@ struct InstanceLoginView: View {
} header: {
Text(String(localized: "login.header.credentials"))
} footer: {
Text(footerText)
wrappingFooter(footerText)
}
if let error = errorMessage {
@@ -144,13 +162,17 @@ struct InstanceLoginView: View {
.buttonStyle(TVSettingsButtonStyle())
#endif
} footer: {
Text(String(localized: "login.footer.optionalStep"))
wrappingFooter(String(localized: "login.footer.optionalStep"))
}
}
}
#if os(iOS)
.scrollDismissesKeyboard(.interactively)
#endif
#if os(macOS)
.formStyle(.grouped)
.opaqueSettingsFormBackground()
#endif
.onAppear {
// Only ever fills an untouched field: the sheet's body runs again
// for every keystroke, and a prefill that reasserted itself would
@@ -163,6 +185,17 @@ struct InstanceLoginView: View {
// MARK: - Computed Properties
/// A section footer that takes as many lines as it needs.
///
/// The sentences here explain what the form is for and that skipping it is
/// safe, and the sheet is narrow — narrower still when it is pushed on top
/// of the add flow's sheet. Left to lay itself out, macOS keeps the footer
/// on one line and cuts the end of it off.
private func wrappingFooter(_ text: String) -> some View {
Text(text)
.fixedSize(horizontal: false, vertical: true)
}
/// What the first field is called on this backend.
///
/// Named per backend rather than genericised: every backend that has