-
Notifications
You must be signed in to change notification settings - Fork 6k
Copied Apple's semantics for switches, made checkboxes the same. #16211
Changes from all commits
430eeea
ef4dc40
c517767
9f74d5b
c3e136b
1e33b4c
949d57a
6e8c07b
5f28b12
b4703d0
649e9b0
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 |
|---|---|---|
|
|
@@ -45,6 +45,75 @@ | |
|
|
||
| } // namespace | ||
|
|
||
| /// A proxy class for SemanticsObject and UISwitch. For most Accessibility and | ||
| /// SemanticsObject methods it delegates to the semantics object, otherwise it | ||
| /// sends messages to the UISwitch. | ||
| @interface FlutterSwitchSemanticsObject : UISwitch | ||
| @end | ||
|
|
||
| @implementation FlutterSwitchSemanticsObject { | ||
| SemanticsObject* _semanticsObject; | ||
| } | ||
|
|
||
| - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject { | ||
| self = [super init]; | ||
| if (self) { | ||
| _semanticsObject = [semanticsObject retain]; | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| - (void)dealloc { | ||
| [_semanticsObject release]; | ||
| [super dealloc]; | ||
| } | ||
|
|
||
| - (NSMethodSignature*)methodSignatureForSelector:(SEL)sel { | ||
| NSMethodSignature* result = [super methodSignatureForSelector:sel]; | ||
| if (!result) { | ||
| result = [_semanticsObject methodSignatureForSelector:sel]; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| - (void)forwardInvocation:(NSInvocation*)anInvocation { | ||
| [anInvocation setTarget:_semanticsObject]; | ||
| [anInvocation invoke]; | ||
| } | ||
|
|
||
| - (CGRect)accessibilityFrame { | ||
| return [_semanticsObject accessibilityFrame]; | ||
| } | ||
|
|
||
| - (id)accessibilityContainer { | ||
| return [_semanticsObject accessibilityContainer]; | ||
| } | ||
|
|
||
| - (NSString*)accessibilityLabel { | ||
| return [_semanticsObject accessibilityLabel]; | ||
| } | ||
|
|
||
| - (NSString*)accessibilityHint { | ||
| return [_semanticsObject accessibilityHint]; | ||
| } | ||
|
|
||
| - (NSString*)accessibilityValue { | ||
| if ([_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsToggled) || | ||
| [_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { | ||
| self.on = YES; | ||
| } else { | ||
| self.on = NO; | ||
| } | ||
|
|
||
| if (![_semanticsObject isAccessibilityBridgeAlive]) { | ||
| return nil; | ||
| } else { | ||
| return [super accessibilityValue]; | ||
| } | ||
| } | ||
|
|
||
| @end // FlutterSwitchSemanticsObject | ||
|
|
||
| @implementation FlutterCustomAccessibilityAction { | ||
| } | ||
| @end | ||
|
|
@@ -265,9 +334,23 @@ - (NSString*)accessibilityHint { | |
| - (NSString*)accessibilityValue { | ||
| if (![self isAccessibilityBridgeAlive]) | ||
| return nil; | ||
| if ([self node].value.empty()) | ||
| return nil; | ||
| return @([self node].value.data()); | ||
|
|
||
| if (![self node].value.empty()) { | ||
| return @([self node].value.data()); | ||
| } | ||
|
|
||
| // FlutterSwitchSemanticsObject should supercede these conditionals. | ||
| if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) || | ||
|
Member
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. Can we remove this code and just continue to use the
Member
Author
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. What was there was wrong for 3 reasons:
The way things are setup this should never happen, but
Member
Author
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. Added comments. |
||
| [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { | ||
| if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) || | ||
| [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { | ||
| return @"1"; | ||
| } else { | ||
| return @"0"; | ||
| } | ||
| } | ||
|
|
||
| return nil; | ||
| } | ||
|
|
||
| - (CGRect)accessibilityFrame { | ||
|
|
@@ -423,10 +506,12 @@ - (UIAccessibilityTraits)accessibilityTraits { | |
| [self node].HasAction(flutter::SemanticsAction::kDecrease)) { | ||
| traits |= UIAccessibilityTraitAdjustable; | ||
| } | ||
| // TODO(jonahwilliams): switches should have a value of "on" or "off" | ||
| if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected) || | ||
| [self node].HasFlag(flutter::SemanticsFlags::kIsToggled) || | ||
| [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { | ||
| // FlutterSwitchSemanticsObject should supercede these conditionals. | ||
| if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) || | ||
| [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { | ||
| traits |= UIAccessibilityTraitButton; | ||
| } | ||
| if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) { | ||
| traits |= UIAccessibilityTraitSelected; | ||
| } | ||
| if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) { | ||
|
|
@@ -755,49 +840,66 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { | |
| platform_view_->DispatchSemanticsAction(uid, action, std::move(args)); | ||
| } | ||
|
|
||
| static void ReplaceSemanticsObject(SemanticsObject* oldObject, | ||
| SemanticsObject* newObject, | ||
| NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) { | ||
| // `newObject` should represent the same id as `oldObject`. | ||
| assert(oldObject.node.id == newObject.node.id); | ||
| NSNumber* nodeId = @(oldObject.node.id); | ||
| NSUInteger positionInChildlist = [oldObject.parent.children indexOfObject:oldObject]; | ||
| SemanticsObject* parent = oldObject.parent; | ||
| [objects removeObjectForKey:nodeId]; | ||
| newObject.parent = parent; | ||
| [newObject.parent.children replaceObjectAtIndex:positionInChildlist withObject:newObject]; | ||
| objects[nodeId] = newObject; | ||
| } | ||
|
|
||
| static SemanticsObject* CreateObject(const flutter::SemanticsNode& node, | ||
| fml::WeakPtr<AccessibilityBridge> weak_ptr) { | ||
| if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) && | ||
| !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) { | ||
| // Text fields are backed by objects that implement UITextInput. | ||
| return [[[TextInputSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease]; | ||
| } else if (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) || | ||
| node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) { | ||
| SemanticsObject* delegateObject = [[FlutterSemanticsObject alloc] initWithBridge:weak_ptr | ||
| uid:node.id]; | ||
| return (SemanticsObject*)[[[FlutterSwitchSemanticsObject alloc] | ||
| initWithSemanticsObject:delegateObject] autorelease]; | ||
| [delegateObject release]; | ||
| } else { | ||
| return [[[FlutterSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease]; | ||
| } | ||
| } | ||
|
|
||
| static bool DidFlagChange(const flutter::SemanticsNode& oldNode, | ||
| const flutter::SemanticsNode& newNode, | ||
| SemanticsFlags flag) { | ||
| return oldNode.HasFlag(flag) != newNode.HasFlag(flag); | ||
| } | ||
|
|
||
| SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid, | ||
| flutter::SemanticsNodeUpdates& updates) { | ||
| SemanticsObject* object = objects_.get()[@(uid)]; | ||
| if (!object) { | ||
| // New node case: simply create a new SemanticsObject. | ||
| flutter::SemanticsNode node = updates[uid]; | ||
| if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) && | ||
| !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) { | ||
| // Text fields are backed by objects that implement UITextInput. | ||
| object = [[[TextInputSemanticsObject alloc] initWithBridge:GetWeakPtr() uid:uid] autorelease]; | ||
| } else { | ||
| object = [[[FlutterSemanticsObject alloc] initWithBridge:GetWeakPtr() uid:uid] autorelease]; | ||
| } | ||
|
|
||
| object = CreateObject(updates[uid], GetWeakPtr()); | ||
| objects_.get()[@(uid)] = object; | ||
| } else { | ||
| // Existing node case | ||
| auto nodeEntry = updates.find(object.node.id); | ||
| if (nodeEntry != updates.end()) { | ||
| // There's an update for this node | ||
| flutter::SemanticsNode node = nodeEntry->second; | ||
| BOOL isTextField = node.HasFlag(flutter::SemanticsFlags::kIsTextField); | ||
| BOOL wasTextField = object.node.HasFlag(flutter::SemanticsFlags::kIsTextField); | ||
| BOOL isReadOnly = node.HasFlag(flutter::SemanticsFlags::kIsReadOnly); | ||
| BOOL wasReadOnly = object.node.HasFlag(flutter::SemanticsFlags::kIsReadOnly); | ||
| if (wasTextField != isTextField || isReadOnly != wasReadOnly) { | ||
| // The node changed its type from text field to something else, or vice versa. In this | ||
| // case, we cannot reuse the existing SemanticsObject implementation. Instead, we replace | ||
| // it with a new instance. | ||
| NSUInteger positionInChildlist = [object.parent.children indexOfObject:object]; | ||
| SemanticsObject* parent = object.parent; | ||
| [objects_ removeObjectForKey:@(node.id)]; | ||
| if (isTextField && !isReadOnly) { | ||
| // Text fields are backed by objects that implement UITextInput. | ||
| object = [[[TextInputSemanticsObject alloc] initWithBridge:GetWeakPtr() | ||
| uid:uid] autorelease]; | ||
| } else { | ||
| object = [[[FlutterSemanticsObject alloc] initWithBridge:GetWeakPtr() | ||
| uid:uid] autorelease]; | ||
| } | ||
| object.parent = parent; | ||
| [object.parent.children replaceObjectAtIndex:positionInChildlist withObject:object]; | ||
| objects_.get()[@(node.id)] = object; | ||
| if (DidFlagChange(object.node, node, flutter::SemanticsFlags::kIsTextField) || | ||
| DidFlagChange(object.node, node, flutter::SemanticsFlags::kIsReadOnly) || | ||
| DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasCheckedState) || | ||
| DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasToggledState)) { | ||
| // The node changed its type. In this case, we cannot reuse the existing | ||
| // SemanticsObject implementation. Instead, we replace it with a new | ||
| // instance. | ||
| SemanticsObject* newSemanticsObject = CreateObject(node, GetWeakPtr()); | ||
| ReplaceSemanticsObject(object, newSemanticsObject, objects_.get()); | ||
| object = newSemanticsObject; | ||
| } | ||
| } | ||
| } | ||
|
|
||
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.
I definitely lack some context here which is making it hard for me to review this. Why is this object a switch subclass? It is not going to be ever used as a switch. That is, it is not in the view hierarchy and you have already overridden most of ally methods. If you wanted to delegate to the base classes implementation of the a11y methods, the responses will be based on a switch in its default state (and not how it appears in the Flutter view hierarchy). Towards that end, shouldn't it be on the implementation to override all the a11y methods as necessary here?
Again. I could be missing something but the base class and the forwarding I don't get the point of.
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.
No problem, I can explain. There is magic that happens on iOS for UISwitch with respect to VoiceOver. If you look at the results of a UISwitch's accessibility methods, it doesn't match up to what VoiceOver actually says. In order to get the magic the SemanticsObject has to be a UISwitch (it has undocumented methods or there is some reflection happening). We do something similar for text fields.