Răsfoiți Sursa

Extended NSUIView to abstract Platform accessibility. (#1060)

Added accessibilityChildren to ChartViewBase which is a layer over both UIAccessibilityContainer and NSAccessibilityGroup protocols. Updated PieChartRenderer to use the platform agnostic NSUIAccessibilityElement. Added init() overrides in NSUIView declaration for macOS to add .list NSAccessibilityRole. Added Platform+Accessibility.swift which extends NSUIView with accessibility container and group protocols and also declares NSUIAccessibilityElement, which acts as an abstraction over NSAccessibilityElement and UIAccessibilityElement.
Adi 7 ani în urmă
părinte
comite
4adff8954e

+ 4 - 0
Charts.xcodeproj/project.pbxproj

@@ -98,6 +98,7 @@
 		9400725714D0DA707DDECD2E /* ViewPortJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BDB22C97F39A4B33E38A7 /* ViewPortJob.swift */; };
 		95B6D6F35684292A62DBEA74 /* LineChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A75AA73C5AA381DA517959 /* LineChartDataSet.swift */; };
 		967EE2EDDE3337C5C4337C59 /* IndexAxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10DD0A02E3CF611BD11EBA9B /* IndexAxisValueFormatter.swift */; };
+		970221AD20ADFA85007410E5 /* Platform+Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970221AC20ADFA85007410E5 /* Platform+Accessibility.swift */; };
 		97E033CC0ABEF0F448DAFA8E /* DataApproximator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EF9709CF635BEE70D1ABC5 /* DataApproximator.swift */; };
 		98E2EEF45E8933E4AD182D58 /* ChartViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFAD7920F76360ADB3B5F5 /* ChartViewBase.swift */; };
 		9A26C8DB1F87B01700367599 /* DataApproximator+N.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A26C8DA1F87B01700367599 /* DataApproximator+N.swift */; };
@@ -257,6 +258,7 @@
 		923206233CA89FD03565FF87 /* LineScatterCandleRadarRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarRenderer.swift; path = Source/Charts/Renderers/LineScatterCandleRadarRenderer.swift; sourceTree = "<group>"; };
 		9249AD9AEC8C85772365A128 /* ILineScatterCandleRadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ILineScatterCandleRadarChartDataSet.swift; path = Source/Charts/Data/Interfaces/ILineScatterCandleRadarChartDataSet.swift; sourceTree = "<group>"; };
 		93EF9709CF635BEE70D1ABC5 /* DataApproximator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DataApproximator.swift; path = Source/Charts/Filters/DataApproximator.swift; sourceTree = "<group>"; };
+		970221AC20ADFA85007410E5 /* Platform+Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Platform+Accessibility.swift"; sourceTree = "<group>"; };
 		998F2BFE318471AFC05B50AC /* IHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IHighlighter.swift; path = Source/Charts/Highlight/IHighlighter.swift; sourceTree = "<group>"; };
 		9A26C8DA1F87B01700367599 /* DataApproximator+N.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "DataApproximator+N.swift"; path = "Source/Charts/Filters/DataApproximator+N.swift"; sourceTree = "<group>"; };
 		9D7184C8A5A60A3522AB9B05 /* BarChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataProvider.swift; path = Source/Charts/Interfaces/BarChartDataProvider.swift; sourceTree = "<group>"; };
@@ -584,6 +586,7 @@
 				3FDA09EF973925A110506799 /* ChartUtils.swift */,
 				5A4CFFFB65819121595F06F1 /* Fill.swift */,
 				3ED23C354AFE81818D78E645 /* Platform.swift */,
+				970221AC20ADFA85007410E5 /* Platform+Accessibility.swift */,
 				FF475B9593B9898853814340 /* Transformer.swift */,
 				324C9127B53A8D39C8B49277 /* TransformerHorizontalBarChart.swift */,
 				72EAEBB7CF73E33565FC2896 /* ViewPortHandler.swift */,
@@ -923,6 +926,7 @@
 				24151B0729D77251A8494D70 /* LineRadarRenderer.swift in Sources */,
 				B6DCC229615EFE706F64A37D /* LineScatterCandleRadarRenderer.swift in Sources */,
 				795E100895C24049509F1EDE /* PieChartRenderer.swift in Sources */,
+				970221AD20ADFA85007410E5 /* Platform+Accessibility.swift in Sources */,
 				69EA073EDF75D49ABE1715D6 /* RadarChartRenderer.swift in Sources */,
 				CEF68F42A5390A73113F3663 /* Renderer.swift in Sources */,
 				796D3E63A37A95FD9D1AB9A1 /* ChevronDownShapeRenderer.swift in Sources */,

+ 207 - 0
Platform+Accessibility.swift

@@ -0,0 +1,207 @@
+import Foundation
+
+#if os(iOS) || os(tvOS)
+
+internal func accessibilityPostLayoutChangedNotification(withElement element: Any? = nil)
+{
+    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, element)
+}
+
+internal func accessibilityPostScreenChangedNotification(withElement element: Any? = nil)
+{
+    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, element)
+}
+
+open class NSUIAccessibilityElement: UIAccessibilityElement
+{
+    private let containerView: UIView
+
+    final var isHeader: Bool = false
+    {
+        didSet
+        {
+            accessibilityTraits = isHeader ? UIAccessibilityTraitHeader : UIAccessibilityTraitNone
+        }
+    }
+
+    final var isSelected: Bool = false
+        {
+        didSet
+        {
+            accessibilityTraits = isSelected ? UIAccessibilityTraitSelected : UIAccessibilityTraitNone
+        }
+    }
+
+    override init(accessibilityContainer container: Any)
+    {
+        // We can force unwrap since all chart views are subclasses of UIView
+        containerView = container as! UIView
+        super.init(accessibilityContainer: container)
+    }
+
+    override open var accessibilityFrame: CGRect
+    {
+        get
+        {
+            return super.accessibilityFrame
+        }
+
+        set
+        {
+            super.accessibilityFrame = containerView.convert(newValue, to: UIScreen.main.coordinateSpace)
+        }
+    }
+}
+
+extension NSUIView
+{
+    /// An array of accessibilityElements that is used to implement UIAccessibilityContainer internally.
+    /// Subclasses **MUST** override this with an array of such elements.
+    @objc open func accessibilityChildren() -> [Any]?
+    {
+        return nil
+    }
+
+    public final override var isAccessibilityElement: Bool
+    {
+        get { return false } // Return false here, so we can make individual elements accessible
+        set { }
+    }
+
+    open override func accessibilityElementCount() -> Int
+    {
+        return accessibilityChildren()?.count ?? 0
+    }
+
+    open override func accessibilityElement(at index: Int) -> Any?
+    {
+        return accessibilityChildren()?[index]
+    }
+
+    open override func index(ofAccessibilityElement element: Any) -> Int
+    {
+        guard let axElement = element as? NSUIAccessibilityElement else { return -1 }
+        return (accessibilityChildren() as? [NSUIAccessibilityElement])?.index(of: axElement) ?? -1
+    }
+}
+
+#endif
+
+#if os(OSX)
+
+internal func accessibilityPostLayoutChangedNotification(withElement element: Any? = nil)
+{
+    guard let validElement = element else { return }
+    NSAccessibilityPostNotification(validElement, .layoutChanged)
+}
+
+internal func accessibilityPostScreenChangedNotification(withElement element: Any? = nil)
+{
+    // Placeholder
+}
+
+open class NSUIAccessibilityElement: NSAccessibilityElement
+{
+    private let containerView: NSView
+
+    final var isHeader: Bool = false
+    {
+        didSet
+        {
+            setAccessibilityRole(isHeader ? .staticText : .none)
+        }
+    }
+
+    // TODO: Make isSelected toggle a selected state in conjunction with a .valueChanged notification
+    /// A placeholder for parity with iOS. Has no effect.
+    final var isSelected: Bool = false
+
+    open var accessibilityLabel: String
+    {
+        get
+        {
+            return accessibilityLabel() ?? ""
+        }
+
+        set
+        {
+            setAccessibilityLabel(newValue)
+        }
+    }
+
+    open var accessibilityFrame: NSRect
+    {
+        get
+        {
+            return accessibilityFrame()
+        }
+
+        set
+        {
+            let bounds = NSAccessibilityFrameInView(containerView, newValue)
+            setAccessibilityFrame(bounds)
+        }
+    }
+
+    init(accessibilityContainer container: Any)
+    {
+        // We can force unwrap since all chart views are subclasses of NSView
+        containerView = container as! NSView
+
+        super.init()
+
+        setAccessibilityParent(containerView)
+        setAccessibilityRole(.row)
+    }
+
+    open override func accessibilityParent() -> Any?
+    {
+        return super.accessibilityParent()
+    }
+}
+
+/*
+/// This would have been needed if the NSAccessibilityList protocol worked.
+extension NSUIAccessibilityElement: NSAccessibilityRow
+{
+    open override func accessibilityChildren() -> [Any]?
+    {
+        return nil
+    }
+
+    open override func accessibilityIdentifier() -> String
+    {
+        return super.accessibilityIdentifier() ?? ""
+    }
+
+    open override func accessibilityIndex() -> Int
+    {
+        guard let parentChartView = containerView as? ChartViewBase else { return -1 }
+        return (parentChartView.accessibilityChildren() as? [NSUIAccessibilityElement])?.index(of: self) ?? -1
+    }
+}
+*/
+
+/// NOTE: Using Swift makes all NSAccessibility methods required
+/// Since the method signatures for accessibilityRows() differ between the NSAccessibilityTable and NSAccessibility protocols,
+/// trying to override or create either causes a compiler error. Hence we resort to calling setAccessibilityRole(.list)
+/// while making NSUIView an NSAccessibilityGroup.
+extension NSUIView: NSAccessibilityGroup
+{
+    open override func accessibilityChildren() -> [Any]?
+    {
+        return nil
+    }
+
+    open override func accessibilityLabel() -> String?
+    {
+        return "Chart View"
+    }
+
+    open override func accessibilityRows() -> [Any]?
+    {
+        return accessibilityChildren()
+    }
+}
+
+#endif

+ 2 - 16
Source/Charts/Charts/ChartViewBase.swift

@@ -369,22 +369,8 @@ open class ChartViewBase: NSUIView, ChartDataProvider, AnimatorDelegate
     
     // MARK: - Accessibility
 
-    open override var isAccessibilityElement: Bool {
-        get { return false }
-        set { }
-    }
-
-    open override func accessibilityElementCount() -> Int {
-        return renderer?.accessibleChartElements.count ?? 0
-    }
-
-    open override func accessibilityElement(at index: Int) -> Any? {
-        return renderer?.accessibleChartElements[index]
-    }
-
-    open override func index(ofAccessibilityElement element: Any) -> Int {
-        guard let axElement: UIAccessibilityElement = element as? UIAccessibilityElement else { return -1 }
-        return renderer?.accessibleChartElements.index(of: axElement) ?? -1
+    open override func accessibilityChildren() -> [Any]? {
+        return renderer?.accessibleChartElements
     }
 
     // MARK: - Highlighting

+ 3 - 2
Source/Charts/Renderers/ChartDataRendererBase.swift

@@ -15,8 +15,9 @@ import CoreGraphics
 @objc(ChartDataRendererBase)
 open class DataRenderer: Renderer
 {
-    /// An array of elements that, when populated are presented to ChartViewBase accessibility methods
-    @objc open var accessibleChartElements: [UIAccessibilityElement] = []
+    /// An array of elements that are presented to the ChartViewBase accessibility methods.
+    /// Subclasses should populate this array in drawData() or drawDataSet() to make the chart accessible.
+    @objc open var accessibleChartElements: [NSUIAccessibilityElement] = []
 
     @objc open let animator: Animator
     

+ 10 - 12
Source/Charts/Renderers/PieChartRenderer.swift

@@ -148,10 +148,10 @@ open class PieChartRenderer: DataRenderer
         let description = chart.chartDescription?.text ?? dataSet.label ?? chart.centerText ??  ""
 
         let
-        element: UIAccessibilityElement = UIAccessibilityElement(accessibilityContainer: chart)
+        element = NSUIAccessibilityElement(accessibilityContainer: chart)
         element.accessibilityLabel = description + ". \(entryCount) \(prefix + (entryCount == 1 ? "" : "s"))"
-        element.accessibilityFrame = chart.convert(chart.bounds, to: UIScreen.main.fixedCoordinateSpace)
-        element.accessibilityTraits = UIAccessibilityTraitHeader
+        element.accessibilityFrame = chart.bounds
+        element.isHeader = true
         accessibleChartElements.append(element)
 
         for j in 0 ..< entryCount
@@ -268,8 +268,7 @@ open class PieChartRenderer: DataRenderer
                                                             container: chart,
                                                             dataSet: dataSet)
                     { (element) in
-                        element.accessibilityFrame = chart.convert(path.boundingBoxOfPath,
-                                                                       to: UIScreen.main.coordinateSpace)
+                        element.accessibilityFrame = path.boundingBoxOfPath
                     }
 
                     accessibleChartElements.append(axElement)
@@ -280,7 +279,7 @@ open class PieChartRenderer: DataRenderer
         }
 
         // Post this notification to let VoiceOver account for the redrawn frames
-        UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil)
+        accessibilityPostLayoutChangedNotification()
 
         context.restoreGState()
     }
@@ -710,7 +709,7 @@ open class PieChartRenderer: DataRenderer
         let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0
 
         // Append highlighted accessibility slices into this array, so we can prioritize them over unselected slices
-        var highlightedAccessibleElements: [UIAccessibilityElement] = []
+        var highlightedAccessibleElements: [NSUIAccessibilityElement] = []
 
         for i in 0 ..< indices.count
         {
@@ -866,9 +865,8 @@ open class PieChartRenderer: DataRenderer
                                                     container: chart,
                                                     dataSet: set)
             { (element) in
-                element.accessibilityFrame = chart.convert(path.boundingBoxOfPath,
-                                                               to: UIScreen.main.coordinateSpace)
-                element.accessibilityTraits = UIAccessibilityTraitSelected
+                element.accessibilityFrame = path.boundingBoxOfPath
+                element.isSelected = true
             }
 
             highlightedAccessibleElements.append(axElement)
@@ -886,9 +884,9 @@ open class PieChartRenderer: DataRenderer
     private func createAccessibleElement(withIndex idx: Int,
                                          container: PieChartView,
                                          dataSet: IPieChartDataSet,
-                                         modifier: (UIAccessibilityElement) -> ()) -> UIAccessibilityElement {
+                                         modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement {
 
-        let element: UIAccessibilityElement = UIAccessibilityElement(accessibilityContainer: container)
+        let element = NSUIAccessibilityElement(accessibilityContainer: container)
 
         guard let e = dataSet.entryForIndex(idx) else { return element }
         guard let formatter = dataSet.valueFormatter else { return element }

+ 16 - 1
Source/Charts/Utils/Platform.swift

@@ -394,6 +394,22 @@ types are aliased to either their UI* implementation (on iOS) or their NS* imple
     
 	open class NSUIView: NSView
     {
+        /// A private constant to set the accessibility role during initialization
+        /// (See Platform+Accessibility for details)
+        private let role: NSAccessibilityRole = .list
+
+        public override init(frame frameRect: NSRect)
+        {
+            super.init(frame: frameRect)
+            setAccessibilityRole(role)
+        }
+
+        required public init?(coder decoder: NSCoder)
+        {
+            super.init(coder: decoder)
+            setAccessibilityRole(role)
+        }
+
 		public final override var isFlipped: Bool
         {
 			return true
@@ -403,7 +419,6 @@ types are aliased to either their UI* implementation (on iOS) or their NS* imple
         {
 			self.setNeedsDisplay(self.bounds)
 		}
-
         
 		public final override func touchesBegan(with event: NSEvent)
         {