-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
reorder control now proxies accessibility into a seperate element #533
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
// Created by Kyle Van Essen on 11/14/19. | ||
// | ||
|
||
import Accessibility | ||
import BlueprintUI | ||
import ListableUI | ||
import UIKit | ||
|
@@ -38,6 +39,7 @@ import UIKit | |
/// ``` | ||
public struct ListReorderGesture : Element | ||
{ | ||
|
||
public enum Begins { | ||
case onTap | ||
case onLongPress | ||
|
@@ -54,9 +56,8 @@ public struct ListReorderGesture : Element | |
|
||
let actions : ReorderingActions | ||
|
||
/// The acccessibility Label of the item that will be reordered. | ||
/// This will be set as the gesture's accessibilityValue to provide a richer VoiceOver utterance. | ||
public var reorderItemAccessibilityLabel : String? = nil | ||
/// The acccessibility label for the reorder element. Defaults to "Reorder". | ||
public var accessibilityLabel : String? | ||
|
||
/// Creates a new re-order gesture which wraps the provided element. | ||
/// | ||
|
@@ -66,6 +67,7 @@ public struct ListReorderGesture : Element | |
isEnabled : Bool = true, | ||
actions : ReorderingActions, | ||
begins: Begins = .onTap, | ||
accessibilityLabel: String? = nil, | ||
wrapping element : Element | ||
) { | ||
self.isEnabled = isEnabled | ||
|
@@ -74,6 +76,8 @@ public struct ListReorderGesture : Element | |
|
||
self.begins = begins | ||
|
||
self.accessibilityLabel = accessibilityLabel | ||
|
||
self.element = element | ||
} | ||
|
||
|
@@ -88,24 +92,16 @@ public struct ListReorderGesture : Element | |
public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? | ||
{ | ||
return ViewDescription(View.self) { config in | ||
|
||
config.builder = { | ||
View(frame: context.bounds, wrapping: self) | ||
} | ||
config.contentView = { $0.containerView } | ||
|
||
config.apply { view in | ||
view.isAccessibilityElement = true | ||
view.accessibilityLabel = ListableLocalizedStrings.ReorderGesture.accessibilityLabel | ||
view.accessibilityValue = reorderItemAccessibilityLabel | ||
view.accessibilityHint = ListableLocalizedStrings.ReorderGesture.accessibilityHint | ||
view.accessibilityTraits.formUnion(.button) | ||
view.accessibilityCustomActions = accessibilityActions() | ||
|
||
view.recognizer.isEnabled = self.isEnabled | ||
|
||
view.recognizer.apply(actions: self.actions) | ||
|
||
view.recognizer.minimumPressDuration = begins == .onLongPress ? 0.5 : 0.0 | ||
view.apply(self) | ||
} | ||
|
||
} | ||
} | ||
|
||
|
@@ -118,9 +114,14 @@ public extension Element | |
func listReorderGesture( | ||
with actions : ReorderingActions, | ||
isEnabled : Bool = true, | ||
begins: ListReorderGesture.Begins = .onTap | ||
begins: ListReorderGesture.Begins = .onTap, | ||
accessibilityLabel: String? = nil | ||
) -> Element { | ||
ListReorderGesture(isEnabled: isEnabled, actions: actions, begins: begins, wrapping: self) | ||
ListReorderGesture(isEnabled: isEnabled, | ||
actions: actions, | ||
begins: begins, | ||
accessibilityLabel: accessibilityLabel, | ||
wrapping: self) | ||
} | ||
} | ||
|
||
|
@@ -129,25 +130,84 @@ fileprivate extension ListReorderGesture | |
{ | ||
private final class View : UIView | ||
{ | ||
|
||
let containerView = UIView() | ||
let recognizer : ItemReordering.GestureRecognizer | ||
private lazy var proxyElement = UIAccessibilityElement(accessibilityContainer: self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, TIL, I had no idea you could do this! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same, although seems obvious in retrospect. |
||
private var minimumPressDuration: TimeInterval = 0.0 { | ||
didSet { | ||
updateGesturePressDuration() | ||
} | ||
} | ||
|
||
@objc private func updateGesturePressDuration() { | ||
self.recognizer.minimumPressDuration = UIAccessibility.isVoiceOverRunning ? 0.0 : self.minimumPressDuration | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not for this PR, but wondering how the behaviour would be with full keyboard access, Switch Control, and Voice Control. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested this out using full keyboard access. It kind of works, but I observed that Listable reordering is overall broken for full keyboard access. Then again, even the iOS home screen felt broken for full keyboard access, so there's no prior art to even copy from. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldnt expect the gesture recognizer itself to work well with keyboard access. do the accessibility actions work with full keyboard access? |
||
} | ||
|
||
init(frame: CGRect, wrapping : ListReorderGesture) | ||
{ | ||
self.recognizer = .init() | ||
|
||
super.init(frame: frame) | ||
|
||
recognizer.accessibilityProxy = proxyElement | ||
NotificationCenter.default.addObserver(self, selector: #selector(updateGesturePressDuration) , name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil) | ||
|
||
self.isOpaque = false | ||
self.clipsToBounds = false | ||
self.backgroundColor = .clear | ||
|
||
self.addGestureRecognizer(self.recognizer) | ||
|
||
self.isAccessibilityElement = false | ||
|
||
containerView.isOpaque = false | ||
containerView.backgroundColor = .clear | ||
addSubview(containerView) | ||
} | ||
|
||
@available(*, unavailable) | ||
required init?(coder aDecoder: NSCoder) { | ||
listableInternalFatal() | ||
} | ||
|
||
func apply(_ model: ListReorderGesture) { | ||
proxyElement.accessibilityLabel = model.accessibilityLabel ?? ListableLocalizedStrings.ReorderGesture.accessibilityLabel | ||
proxyElement.accessibilityHint = ListableLocalizedStrings.ReorderGesture.accessibilityHint | ||
proxyElement.accessibilityTraits.formUnion(.button) | ||
proxyElement.accessibilityCustomActions = model.accessibilityActions() | ||
|
||
recognizer.isEnabled = model.isEnabled | ||
|
||
recognizer.apply(actions: model.actions) | ||
minimumPressDuration = model.begins == .onLongPress ? 0.5 : 0.0 | ||
} | ||
|
||
override func layoutSubviews() { | ||
super.layoutSubviews() | ||
containerView.frame = bounds | ||
} | ||
|
||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | ||
if UIAccessibility.isVoiceOverRunning, | ||
UIAccessibility.focusedElement(using: .notificationVoiceOver) as? NSObject == proxyElement { | ||
// Intercept touch events to avoid activating contained elements. | ||
return self | ||
} | ||
|
||
return super.hitTest(point, with: event) | ||
} | ||
|
||
override var accessibilityElements: [Any]? { | ||
get { | ||
guard recognizer.isEnabled else { return super.accessibilityElements } | ||
proxyElement.accessibilityFrame = self.accessibilityFrame | ||
proxyElement.accessibilityActivationPoint = self.accessibilityActivationPoint | ||
return [containerView, proxyElement] | ||
} | ||
set { | ||
fatalError("Cannot set accessibility elements directly") | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huh, TIL