Merge pull request #924 from j-f1/poll-compose-a11y

IOS-72: Improve accessibility for the poll composer UI
This commit is contained in:
Marcus Kida 2023-02-08 15:29:10 +01:00 committed by GitHub
commit 70d939c3ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 97 additions and 19 deletions

View File

@ -444,6 +444,7 @@
"server_processing_state": "Server Processing..." "server_processing_state": "Server Processing..."
}, },
"poll": { "poll": {
"title": "Poll",
"duration_time": "Duration: %s", "duration_time": "Duration: %s",
"thirty_minutes": "30 minutes", "thirty_minutes": "30 minutes",
"one_hour": "1 Hour", "one_hour": "1 Hour",

View File

@ -461,6 +461,7 @@
"server_processing_state": "Server Processing..." "server_processing_state": "Server Processing..."
}, },
"poll": { "poll": {
"title": "Poll",
"duration_time": "Duration: %s", "duration_time": "Duration: %s",
"thirty_minutes": "30 minutes", "thirty_minutes": "30 minutes",
"one_hour": "1 Hour", "one_hour": "1 Hour",
@ -470,7 +471,11 @@
"seven_days": "7 Days", "seven_days": "7 Days",
"option_number": "Option %ld", "option_number": "Option %ld",
"the_poll_is_invalid": "The poll is invalid", "the_poll_is_invalid": "The poll is invalid",
"the_poll_has_empty_option": "The poll has empty option" "the_poll_has_empty_option": "The poll has empty option",
"add_option": "Add Option",
"remove_option": "Remove Option",
"move_up": "Move Up",
"move_down": "Move Down"
}, },
"content_warning": { "content_warning": {
"placeholder": "Write an accurate warning here..." "placeholder": "Write an accurate warning here..."

View File

@ -593,10 +593,16 @@ public enum L10n {
public static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary", fallback: "Photo Library") public static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary", fallback: "Photo Library")
} }
public enum Poll { public enum Poll {
/// Add Option
public static let addOption = L10n.tr("Localizable", "Scene.Compose.Poll.AddOption", fallback: "Add Option")
/// Duration: %@ /// Duration: %@
public static func durationTime(_ p1: Any) -> String { public static func durationTime(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Compose.Poll.DurationTime", String(describing: p1), fallback: "Duration: %@") return L10n.tr("Localizable", "Scene.Compose.Poll.DurationTime", String(describing: p1), fallback: "Duration: %@")
} }
/// Move Down
public static let moveDown = L10n.tr("Localizable", "Scene.Compose.Poll.MoveDown", fallback: "Move Down")
/// Move Up
public static let moveUp = L10n.tr("Localizable", "Scene.Compose.Poll.MoveUp", fallback: "Move Up")
/// 1 Day /// 1 Day
public static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay", fallback: "1 Day") public static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay", fallback: "1 Day")
/// 1 Hour /// 1 Hour
@ -605,6 +611,8 @@ public enum L10n {
public static func optionNumber(_ p1: Int) -> String { public static func optionNumber(_ p1: Int) -> String {
return L10n.tr("Localizable", "Scene.Compose.Poll.OptionNumber", p1, fallback: "Option %ld") return L10n.tr("Localizable", "Scene.Compose.Poll.OptionNumber", p1, fallback: "Option %ld")
} }
/// Remove Option
public static let removeOption = L10n.tr("Localizable", "Scene.Compose.Poll.RemoveOption", fallback: "Remove Option")
/// 7 Days /// 7 Days
public static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays", fallback: "7 Days") public static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays", fallback: "7 Days")
/// 6 Hours /// 6 Hours
@ -617,6 +625,8 @@ public enum L10n {
public static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes", fallback: "30 minutes") public static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes", fallback: "30 minutes")
/// 3 Days /// 3 Days
public static let threeDays = L10n.tr("Localizable", "Scene.Compose.Poll.ThreeDays", fallback: "3 Days") public static let threeDays = L10n.tr("Localizable", "Scene.Compose.Poll.ThreeDays", fallback: "3 Days")
/// Poll
public static let title = L10n.tr("Localizable", "Scene.Compose.Poll.Title", fallback: "Poll")
} }
public enum Title { public enum Title {
/// New Post /// New Post

View File

@ -211,16 +211,21 @@ uploaded to Mastodon.";
"Scene.Compose.MediaSelection.Browse" = "Browse"; "Scene.Compose.MediaSelection.Browse" = "Browse";
"Scene.Compose.MediaSelection.Camera" = "Take Photo"; "Scene.Compose.MediaSelection.Camera" = "Take Photo";
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library"; "Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
"Scene.Compose.Poll.AddOption" = "Add Option";
"Scene.Compose.Poll.DurationTime" = "Duration: %@"; "Scene.Compose.Poll.DurationTime" = "Duration: %@";
"Scene.Compose.Poll.MoveDown" = "Move Down";
"Scene.Compose.Poll.MoveUp" = "Move Up";
"Scene.Compose.Poll.OneDay" = "1 Day"; "Scene.Compose.Poll.OneDay" = "1 Day";
"Scene.Compose.Poll.OneHour" = "1 Hour"; "Scene.Compose.Poll.OneHour" = "1 Hour";
"Scene.Compose.Poll.OptionNumber" = "Option %ld"; "Scene.Compose.Poll.OptionNumber" = "Option %ld";
"Scene.Compose.Poll.RemoveOption" = "Remove Option";
"Scene.Compose.Poll.SevenDays" = "7 Days"; "Scene.Compose.Poll.SevenDays" = "7 Days";
"Scene.Compose.Poll.SixHours" = "6 Hours"; "Scene.Compose.Poll.SixHours" = "6 Hours";
"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option"; "Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option";
"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid"; "Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid";
"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes"; "Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
"Scene.Compose.Poll.ThreeDays" = "3 Days"; "Scene.Compose.Poll.ThreeDays" = "3 Days";
"Scene.Compose.Poll.Title" = "Poll";
"Scene.Compose.ReplyingToUser" = "replying to %@"; "Scene.Compose.ReplyingToUser" = "replying to %@";
"Scene.Compose.Title.NewPost" = "New Post"; "Scene.Compose.Title.NewPost" = "New Post";
"Scene.Compose.Title.NewReply" = "New Reply"; "Scene.Compose.Title.NewReply" = "New Reply";

View File

@ -22,7 +22,7 @@ public struct PollAddOptionRow: View {
.padding(.trailing, 16 - 10) // 8pt for TextField leading .padding(.trailing, 16 - 10) // 8pt for TextField leading
.font(.system(size: 17)) .font(.system(size: 17))
PollOptionTextField( PollOptionTextField(
text: $viewModel.text, text: .constant(""),
index: 999, index: 999,
delegate: nil delegate: nil
) { textField in ) { textField in
@ -44,9 +44,6 @@ public struct PollAddOptionRow: View {
extension PollAddOptionRow { extension PollAddOptionRow {
public class ViewModel: ObservableObject { public class ViewModel: ObservableObject {
// input
@Published public var text: String = ""
// output // output
@Published public var backgroundColor = ThemeService.shared.currentTheme.value.composePollRowBackgroundColor @Published public var backgroundColor = ThemeService.shared.currentTheme.value.composePollRowBackgroundColor

View File

@ -8,12 +8,16 @@
import SwiftUI import SwiftUI
import MastodonAsset import MastodonAsset
import MastodonCore import MastodonCore
import MastodonLocalization
public struct PollOptionRow: View { public struct PollOptionRow: View {
@ObservedObject var viewModel: PollComposeItem.Option @ObservedObject var viewModel: PollComposeItem.Option
let index: Int? let index: Int
let moveUp: (() -> Void)?
let moveDown: (() -> Void)?
let removeOption: (() -> Void)?
let deleteBackwardResponseTextFieldRelayDelegate: DeleteBackwardResponseTextFieldRelayDelegate? let deleteBackwardResponseTextFieldRelayDelegate: DeleteBackwardResponseTextFieldRelayDelegate?
let configurationHandler: (DeleteBackwardResponseTextField) -> Void let configurationHandler: (DeleteBackwardResponseTextField) -> Void
@ -25,9 +29,10 @@ public struct PollOptionRow: View {
.padding(.leading, 16) .padding(.leading, 16)
.padding(.trailing, 16 - 10) // 8pt for TextField leading .padding(.trailing, 16 - 10) // 8pt for TextField leading
.font(.system(size: 17)) .font(.system(size: 17))
PollOptionTextField( .accessibilityHidden(true)
let field = PollOptionTextField(
text: $viewModel.text, text: $viewModel.text,
index: index ?? -1, index: index,
delegate: deleteBackwardResponseTextFieldRelayDelegate delegate: deleteBackwardResponseTextFieldRelayDelegate
) { textField in ) { textField in
viewModel.textField = textField viewModel.textField = textField
@ -38,12 +43,55 @@ public struct PollOptionRow: View {
viewModel.shouldBecomeFirstResponder = false viewModel.shouldBecomeFirstResponder = false
viewModel.textField?.becomeFirstResponder() viewModel.textField?.becomeFirstResponder()
} }
if #available(iOS 16.0, *) {
field.accessibilityActions {
if let moveUp {
Button(L10n.Scene.Compose.Poll.moveUp, action: moveUp)
}
if let moveDown {
Button(L10n.Scene.Compose.Poll.moveDown, action: moveDown)
}
if let removeOption {
Button(L10n.Scene.Compose.Poll.removeOption, action: removeOption)
}
}
} else {
switch (moveUp, moveDown, removeOption) {
case let (.some(up), .some(down), .some(remove)):
field
.accessibilityAction(named: L10n.Scene.Compose.Poll.moveUp, up)
.accessibilityAction(named: L10n.Scene.Compose.Poll.moveDown, down)
.accessibilityAction(named: L10n.Scene.Compose.Poll.removeOption, remove)
case let (.some(up), .some(down), .none):
field
.accessibilityAction(named: L10n.Scene.Compose.Poll.moveUp, up)
.accessibilityAction(named: L10n.Scene.Compose.Poll.moveDown, down)
case let (.some(up), .none, .some(remove)):
field
.accessibilityAction(named: L10n.Scene.Compose.Poll.moveUp, up)
.accessibilityAction(named: L10n.Scene.Compose.Poll.removeOption, remove)
case let (.some(up), .none, .none):
field.accessibilityAction(named: L10n.Scene.Compose.Poll.moveUp, up)
case let (.none, .some(down), .some(remove)):
field
.accessibilityAction(named: L10n.Scene.Compose.Poll.moveDown, down)
.accessibilityAction(named: L10n.Scene.Compose.Poll.removeOption, remove)
case let (.none, .some(down), .none):
field.accessibilityAction(named: L10n.Scene.Compose.Poll.moveDown, down)
case let (.none, .none, .some(remove)):
field.accessibilityAction(named: L10n.Scene.Compose.Poll.removeOption, remove)
case (.none, .none, .none):
field
}
}
} }
.background(Color(viewModel.backgroundColor)) .background(Color(viewModel.backgroundColor))
.cornerRadius(10) .cornerRadius(10)
.shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) .shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1)
Image(uiImage: Asset.Scene.Compose.reorderDot.image.withRenderingMode(.alwaysTemplate)) Image(uiImage: Asset.Scene.Compose.reorderDot.image.withRenderingMode(.alwaysTemplate))
.foregroundColor(Color(UIColor.label)) .foregroundColor(Color(UIColor.label))
.accessibilityHidden(true)
} }
.background(Color.clear) .background(Color.clear)
} }

View File

@ -180,20 +180,30 @@ extension ComposeContentView {
ReorderableForEach( ReorderableForEach(
items: $viewModel.pollOptions items: $viewModel.pollOptions
) { $pollOption in ) { $pollOption in
let _index = viewModel.pollOptions.firstIndex(of: pollOption) if let _index = viewModel.pollOptions.firstIndex(of: pollOption) {
PollOptionRow( PollOptionRow(
viewModel: pollOption, viewModel: pollOption,
index: _index, index: _index,
deleteBackwardResponseTextFieldRelayDelegate: viewModel moveUp: _index == 0 ? nil : {
) { textField in viewModel.pollOptions.swapAt(_index, _index - 1)
viewModel.customEmojiPickerInputViewModel.configure(textInput: textField) },
moveDown: _index == viewModel.pollOptions.count - 1 ? nil : {
viewModel.pollOptions.swapAt(_index, _index + 1)
},
removeOption: viewModel.pollOptions.count <= 2 ? nil : {
viewModel.pollOptions.remove(at: _index)
},
deleteBackwardResponseTextFieldRelayDelegate: viewModel
) { textField in
viewModel.customEmojiPickerInputViewModel.configure(textInput: textField)
}
} }
} }
if viewModel.maxPollOptionLimit != viewModel.pollOptions.count { if viewModel.maxPollOptionLimit != viewModel.pollOptions.count {
PollAddOptionRow() Button(action: viewModel.createNewPollOptionIfCould) {
.onTapGesture { PollAddOptionRow()
viewModel.createNewPollOptionIfCould() .accessibilityLabel(L10n.Scene.Compose.Poll.addOption)
} }
} }
Menu { Menu {
Picker(selection: $viewModel.pollExpireConfigurationOption) { Picker(selection: $viewModel.pollExpireConfigurationOption) {
@ -214,6 +224,8 @@ extension ComposeContentView {
} }
} }
} // end VStack } // end VStack
.accessibilityElement(children: .contain)
.accessibilityLabel(L10n.Scene.Compose.Poll.title)
} }
// MARK: - media // MARK: - media