Merge pull request #924 from j-f1/poll-compose-a11y
IOS-72: Improve accessibility for the poll composer UI
This commit is contained in:
commit
70d939c3ad
|
@ -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",
|
||||||
|
|
|
@ -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..."
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue