mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
+61
-31
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user