Edit poll UX (#2151)

* Add edit poll on room proxy

* Add CreatePollMode

* Add “edit poll” presentation flow

* Add delete poll section

* Inject editing poll

* Add submit action

* Refactor validation logic

* Add edit/delete actions

* Fix bubble timestamp for polls

* Update localisations

* Refactor CreatePoll -> PollForm

* Refactor tests

* Update rust sdk to 0.0.5-november23

* Update confirmation alerts

* Add edit support in TimelineItem menu

* Refactor a11y id

* Cleanup

* Fix failing tests

* Add tests

* Refine isEditable workaround

* Refactor timestamp in TimelineItemBubbledStylerView
This commit is contained in:
Alfonso Grillo
2023-11-23 12:19:15 +01:00
committed by GitHub
parent 4d011b9116
commit e583f52c65
36 changed files with 670 additions and 335 deletions
@@ -17,35 +17,36 @@
import Compound
import SwiftUI
struct CreatePollScreen: View {
@ObservedObject var context: CreatePollScreenViewModel.Context
struct PollFormScreen: View {
@ObservedObject var context: PollFormScreenViewModel.Context
@FocusState var focus: Focus?
enum Focus: Hashable {
case question
case option(index: Int)
}
var body: some View {
Form {
questionSection
optionsSection
showResultsSection
deletePollSection
}
.track(screen: .createPoll)
.trackAnalyticsIfNeeded(context: context)
.compoundForm()
.scrollDismissesKeyboard(.immediately)
.environment(\.editMode, .constant(.active))
.navigationTitle(L10n.screenCreatePollTitle)
.navigationTitle(context.viewState.navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.animation(.elementDefault, value: context.options)
.interactiveDismissDisabled(context.viewState.bindings.hasContent)
.interactiveDismissDisabled(context.viewState.formContentHasChanged)
.alert(item: $context.alertInfo)
}
// MARK: - Private
private var questionSection: some View {
Section(L10n.screenCreatePollQuestionDesc) {
TextField(text: $context.question) {
@@ -57,7 +58,7 @@ struct CreatePollScreen: View {
}
.textFieldStyle(.compoundForm)
.focused($focus, equals: .question)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.question)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.question)
.onSubmit {
focus = context.options.indices.first.map { .option(index: $0) }
}
@@ -65,22 +66,22 @@ struct CreatePollScreen: View {
}
.compoundFormSection()
}
private var optionsSection: some View {
Section {
ForEach(context.options) { option in
if let index = context.options.firstIndex(of: option) {
CreatePollOptionView(text: $context.options[index].text.limited(to: 240),
placeholder: L10n.screenCreatePollAnswerHint(index + 1),
canDeleteItem: context.options.count > 2) {
PollFormOptionView(text: $context.options[index].text.limited(to: 240),
placeholder: L10n.screenCreatePollAnswerHint(index + 1),
canDeleteItem: context.options.count > 2) {
if case .option(let focusedIndex) = focus, focusedIndex == index {
focus = nil
}
context.send(viewAction: .deleteOption(index: index))
}
.focused($focus, equals: .option(index: index))
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.optionID(index))
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.optionID(index))
.onSubmit {
let nextOptionIndex = index == context.options.endIndex - 1 ? nil : index + 1
focus = nextOptionIndex.map { .option(index: $0) }
@@ -91,26 +92,43 @@ struct CreatePollScreen: View {
.onMove { offsets, toOffset in
context.options.move(fromOffsets: offsets, toOffset: toOffset)
}
if context.options.count < context.viewState.maxNumberOfOptions {
Button(L10n.screenCreatePollAddOptionBtn) {
context.send(viewAction: .addOption)
focus = context.options.indices.last.map { .option(index: $0) }
}
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.addOption)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.addOption)
}
}
.compoundFormSection()
}
private var showResultsSection: some View {
Section {
Toggle(L10n.screenCreatePollAnonymousDesc, isOn: $context.isUndisclosed)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.pollKind)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.pollKind)
}
.compoundFormSection()
}
@ViewBuilder
private var deletePollSection: some View {
switch context.viewState.mode {
case .edit:
Section {
Button(role: .destructive) {
context.send(viewAction: .delete)
} label: {
Text(L10n.actionDeletePoll)
}
}
.compoundFormSection()
case .new:
EmptyView()
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
@@ -118,24 +136,36 @@ struct CreatePollScreen: View {
context.send(viewAction: .cancel)
}
}
ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionCreate) {
context.send(viewAction: .create)
Button(context.viewState.submitButtonTitle) {
context.send(viewAction: .submit)
}
.disabled(context.viewState.bindings.isCreateButtonDisabled)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.create)
.disabled(context.viewState.isSubmitButtonDisabled)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.submit)
}
}
}
private struct CreatePollOptionView: View {
private extension View {
@MainActor @ViewBuilder
func trackAnalyticsIfNeeded(context: PollFormScreenViewModel.Context) -> some View {
switch context.viewState.mode {
case .edit:
self
case .new:
track(screen: .createPoll)
}
}
}
private struct PollFormOptionView: View {
@Environment(\.editMode) var editMode
@Binding var text: String
let placeholder: String
let canDeleteItem: Bool
let deleteAction: () -> Void
var body: some View {
HStack(spacing: 8) {
if editMode?.wrappedValue == .active {
@@ -160,11 +190,11 @@ private struct CreatePollOptionView: View {
// MARK: - Previews
struct CreatePollScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = CreatePollScreenViewModel()
struct PollFormScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = PollFormScreenViewModel(mode: .new)
static var previews: some View {
NavigationStack {
CreatePollScreen(context: viewModel.context)
PollFormScreen(context: viewModel.context)
}
}
}