Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,12 @@ extension AccessibilityDeferral {
// Reset source views.
sources.forEach { $0.accessibilityElementsHidden = false }


/* Content is bound to a Receiver by containment, and a container is modeled around a single receiver. Multiple Receivers is still valid, but they're separate accessibility elements with no single target, so we clear all deferral content and bail rather than duplicate it across all of them. */
guard receivers.count <= 1 else {
Comment thread
robmaceachern marked this conversation as resolved.
assertionFailure("AccessibilityDeferral.ParentContainer must contain at most one Receiver; found \(receivers.count).")
receivers.forEach { $0.apply(content: nil, frameProvider: nil) }
return
}


if let receiver = receivers.first {

// Inherit accessibility content and disable source element.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,29 @@ class AccessibilityDeferralTests: XCTestCase {
XCTAssertEqual(receiver.accessibilityCustomActions?.first?.name, "Test Action")
}

func test_parentContainerWithDuplicateReceiversClearsDeferralContent() {
let view = BlueprintView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let content = AccessibilityDeferral.Content(kind: .inherited(), identifier: "source")

view.element = Column {
AccessibilityTestElement(label: "Source")
.deferredAccessibilitySource(identifier: "source")
AccessibilityTestElement(label: "First receiver")
.deferredAccessibilityReceiver()
AccessibilityTestElement(label: "Second receiver")
.deferredAccessibilityReceiver()
}
.deferAccessibilityToChildren(content: [content])

view.layoutIfNeeded()

let receivers = view.recursiveSubviews(matching: { $0 is AccessibilityDeferral.Receiver })
.compactMap { $0 as? AccessibilityDeferral.Receiver }

XCTAssertEqual(receivers.count, 2)
XCTAssertTrue(receivers.allSatisfy { ($0.deferredAccessibilityContent ?? []).isEmpty })
}

// MARK: - Content customContent generation

func test_content_inherited_customContent() {
Expand Down Expand Up @@ -193,3 +216,20 @@ private final class TestReceiver: UIView, AccessibilityDeferral.Receiver, AXCust
}

extension TestReceiver: AccessibilityDeferral.DeferralView {}

private struct AccessibilityTestElement: Element {
var label: String

var content: ElementContent {
ElementContent(intrinsicSize: CGSize(width: 10, height: 10))
}

func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? {
UIView.describe { config in
config.apply { view in
view.isAccessibilityElement = true
view.accessibilityLabel = label
}
}
}
}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Duplicate accessibility deferral receivers in a parent container are now recovered by clearing deferred content without triggering a debug assertion.

### Added

### Removed
Expand Down
8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ let package = Package(
.process("Resources"),
],
),
.testTarget(
name: "BlueprintUIAccessibilityCoreTests",
dependencies: [
"BlueprintUI",
"BlueprintUIAccessibilityCore",
],
path: "BlueprintUIAccessibilityCore/Tests"
),
],
swiftLanguageModes: [.v5]
)
Loading