mirror of
https://github.com/divkit/divkit.git
synced 2026-05-07 20:02:32 +00:00
Fixed accessibility merging
commit_hash:bd1ab1f35f3c7f568d0355c82eb0f0ad2fa12e50
This commit is contained in:
+1
-1
@@ -19572,7 +19572,6 @@
|
||||
"client/ios/DivKit/Extensions/DivBlendModeExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivBlendModeExtensions.swift",
|
||||
"client/ios/DivKit/Extensions/DivChangeTransitionExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivChangeTransitionExtensions.swift",
|
||||
"client/ios/DivKit/Extensions/DivContainer/DivCollectionItemsBuilderExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivContainer/DivCollectionItemsBuilderExtensions.swift",
|
||||
"client/ios/DivKit/Extensions/DivContainer/DivContainer+Accessibility.swift":"divkit/public/client/ios/DivKit/Extensions/DivContainer/DivContainer+Accessibility.swift",
|
||||
"client/ios/DivKit/Extensions/DivContainer/DivContainerExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivContainer/DivContainerExtensions.swift",
|
||||
"client/ios/DivKit/Extensions/DivContainer/DivContainerItemsExtensions.swift":"divkit/public/client/ios/DivKit/Extensions/DivContainer/DivContainerItemsExtensions.swift",
|
||||
"client/ios/DivKit/Extensions/DivContainer/DivContainerSizeModifier.swift":"divkit/public/client/ios/DivKit/Extensions/DivContainer/DivContainerSizeModifier.swift",
|
||||
@@ -20396,6 +20395,7 @@
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/AspectRatioConstrainedSize.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/AspectRatioConstrainedSize.swift",
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/BackgroundBlock.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/BackgroundBlock.swift",
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/Block+Accessibility.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/Block+Accessibility.swift",
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/Block+AccessibilityMerge.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/Block+AccessibilityMerge.swift",
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/Block+Animation.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/Block+Animation.swift",
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/Block+CAAnimationRendering.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/Block+CAAnimationRendering.swift",
|
||||
"client/ios/LayoutKit/LayoutKit/Blocks/Block+Debugging.swift":"divkit/public/client/ios/LayoutKit/LayoutKit/Blocks/Block+Debugging.swift",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import LayoutKit
|
||||
import VGSL
|
||||
|
||||
extension DivAccessibility {
|
||||
func resolve(
|
||||
_ expressionResolver: ExpressionResolver,
|
||||
id: String?,
|
||||
block: AccessibilityContaining,
|
||||
customParams: CustomAccessibilityParams
|
||||
) -> AccessibilityElement {
|
||||
if resolveMode(expressionResolver) == .exclude {
|
||||
let mode = resolveMode(expressionResolver)
|
||||
if mode == .exclude {
|
||||
return AccessibilityElement(
|
||||
traits: .none,
|
||||
strings: AccessibilityElement.Strings(label: nil),
|
||||
@@ -20,6 +23,10 @@ extension DivAccessibility {
|
||||
} else if let description = resolveDescription(expressionResolver) {
|
||||
label = description
|
||||
}
|
||||
if mode == .merge, (label ?? "").isEmpty,
|
||||
let merged = block.mergedAccessibilityLabel {
|
||||
label = merged
|
||||
}
|
||||
if label == nil, type != .auto {
|
||||
label = ""
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ final class DivBaseBlockBuilder {
|
||||
let accessibilityElement = (div.accessibility ?? DivAccessibility()).resolve(
|
||||
expressionResolver,
|
||||
id: context.currentDivId,
|
||||
block: block,
|
||||
customParams: customAccessibilityParams
|
||||
)
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension DivContainer {
|
||||
func resolveAccessibilityDescription(_ context: DivBlockModelingContext) -> String? {
|
||||
guard let accessibility else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let expressionResolver = context.expressionResolver
|
||||
switch accessibility.resolveMode(expressionResolver) {
|
||||
case .default:
|
||||
return accessibility.resolveDescription(expressionResolver)
|
||||
case .merge:
|
||||
if let description = accessibility.resolveDescription(expressionResolver) {
|
||||
return description
|
||||
}
|
||||
return nonNilItems.resolveMergedDescription(context)
|
||||
case .exclude:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension [Div] {
|
||||
fileprivate func resolveMergedDescription(_ context: DivBlockModelingContext) -> String? {
|
||||
var result = ""
|
||||
func traverse(div: Div) {
|
||||
if let descritpion = div.resolveDescription(context), !descritpion.isEmpty {
|
||||
result = result.isEmpty ? descritpion : result + " " + descritpion
|
||||
}
|
||||
div.children.forEach(traverse)
|
||||
}
|
||||
forEach(traverse)
|
||||
return result.isEmpty ? nil : result
|
||||
}
|
||||
}
|
||||
|
||||
extension Div {
|
||||
fileprivate func resolveDescription(_ context: DivBlockModelingContext) -> String? {
|
||||
let expressionResolver = context.expressionResolver
|
||||
let accessibility = value.accessibility
|
||||
guard accessibility?.resolveMode(expressionResolver) != .exclude else {
|
||||
return nil
|
||||
}
|
||||
switch self {
|
||||
case .divContainer,
|
||||
.divCustom,
|
||||
.divGallery,
|
||||
.divGifImage,
|
||||
.divGrid,
|
||||
.divImage,
|
||||
.divIndicator,
|
||||
.divInput,
|
||||
.divPager,
|
||||
.divSelect,
|
||||
.divSeparator,
|
||||
.divSlider,
|
||||
.divState,
|
||||
.divSwitch,
|
||||
.divTabs,
|
||||
.divVideo:
|
||||
return accessibility?.resolveDescription(expressionResolver)
|
||||
case let .divText(divText):
|
||||
let extensionDescription = context
|
||||
.getExtensionHandlers(for: divText)
|
||||
.compactMap { $0.accessibilityElement?.strings.label }
|
||||
.reduce(nil) { $0?.appending(" " + $1) ?? $1 }
|
||||
return extensionDescription ??
|
||||
accessibility?.resolveDescription(expressionResolver) ??
|
||||
divText.resolveText(expressionResolver)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ extension DivContainer: DivBlockModeling {
|
||||
to: { try makeBaseBlock(context: context) },
|
||||
context: context,
|
||||
actionsHolder: self,
|
||||
customAccessibilityParams: CustomAccessibilityParams { [unowned self] in
|
||||
resolveAccessibilityDescription(context)
|
||||
},
|
||||
applyPaddings: false,
|
||||
clipToBounds: resolveClipToBounds(context.expressionResolver)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@testable import DivKit
|
||||
import DivKitTestsSupport
|
||||
import LayoutKit
|
||||
import VGSL
|
||||
import XCTest
|
||||
|
||||
@@ -159,6 +160,7 @@ extension DivAccessibility {
|
||||
resolve(
|
||||
DivBlockModelingContext.default.expressionResolver,
|
||||
id: id,
|
||||
block: EmptyBlock.zeroSized,
|
||||
customParams: CustomAccessibilityParams(
|
||||
defaultTraits: defaultTraits,
|
||||
descriptionProvider: customDescriptionProvider
|
||||
|
||||
+129
-76
@@ -1,105 +1,158 @@
|
||||
@testable import DivKit
|
||||
import DivKitTestsSupport
|
||||
@testable import LayoutKit
|
||||
import XCTest
|
||||
|
||||
final class DivContainerAccessibilityDescriptionTests: XCTestCase {
|
||||
func test_Nil_ForContainerWithoutAccessibility() {
|
||||
let description = divContainer()
|
||||
.resolveAccessibilityDescription()
|
||||
func test_Nil_ForContainerWithoutAccessibility() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer()
|
||||
)
|
||||
|
||||
XCTAssertNil(description)
|
||||
XCTAssertNil(label)
|
||||
}
|
||||
|
||||
func test_Nil_ForExcludeMode() {
|
||||
let description = divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Container description"),
|
||||
mode: .value(.exclude)
|
||||
func test_Hidden_ForExcludeMode() throws {
|
||||
let block = try makeBlock(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Container description"),
|
||||
mode: .value(.exclude)
|
||||
)
|
||||
)
|
||||
).resolveAccessibilityDescription()
|
||||
)
|
||||
|
||||
XCTAssertNil(description)
|
||||
XCTAssertEqual(block.accessibilityElement?.hideElementWithChildren, true)
|
||||
}
|
||||
|
||||
func test_Description_ForContainerWithDescription() {
|
||||
let description = divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Container description")
|
||||
func test_Description_ForContainerWithDescription() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Container description")
|
||||
)
|
||||
)
|
||||
).resolveAccessibilityDescription()
|
||||
)
|
||||
|
||||
XCTAssertEqual(description, "Container description")
|
||||
XCTAssertEqual(label, "Container description")
|
||||
}
|
||||
|
||||
func test_MergedDescription_ForMergeMode() {
|
||||
let description = divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Nested container description")
|
||||
)
|
||||
),
|
||||
]
|
||||
).resolveAccessibilityDescription()
|
||||
func test_MergedDescription_ForMergeMode() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Nested container description")
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(description, "Hello! Nested container description")
|
||||
XCTAssertEqual(label, "Hello! Nested container description")
|
||||
}
|
||||
|
||||
func test_MergedDescription_NotContainsExcludedItem() {
|
||||
let description = divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divText(
|
||||
accessibility: DivAccessibility(mode: .value(.exclude)),
|
||||
text: "Excluded"
|
||||
),
|
||||
]
|
||||
).resolveAccessibilityDescription()
|
||||
func test_MergedDescription_NotContainsExcludedItem() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divText(
|
||||
accessibility: DivAccessibility(mode: .value(.exclude)),
|
||||
text: "Excluded"
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(description, "Hello!")
|
||||
XCTAssertEqual(label, "Hello!")
|
||||
}
|
||||
|
||||
func test_MergedDescription_ContainsDescriptionFromNestedContainer() {
|
||||
let description = divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divContainer(
|
||||
items: [
|
||||
divText(text: "Nested item"),
|
||||
]
|
||||
),
|
||||
]
|
||||
).resolveAccessibilityDescription()
|
||||
func test_MergedDescription_ContainsTextFromNestedContainer() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divContainer(
|
||||
items: [
|
||||
divText(text: "Nested item"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(description, "Hello! Nested item")
|
||||
XCTAssertEqual(label, "Hello! Nested item")
|
||||
}
|
||||
|
||||
func test_Description_HasHigherPriorityThanMergedDescription() {
|
||||
let description = divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Container description"),
|
||||
mode: .value(.merge)
|
||||
),
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Nested container description")
|
||||
)
|
||||
func test_Description_HasHigherPriorityThanMergedDescription() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Container description"),
|
||||
mode: .value(.merge)
|
||||
),
|
||||
]
|
||||
).resolveAccessibilityDescription()
|
||||
items: [
|
||||
divText(text: "Hello!"),
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(
|
||||
description: .value("Nested container description")
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(description, "Container description")
|
||||
}
|
||||
}
|
||||
|
||||
extension Div {
|
||||
fileprivate func resolveAccessibilityDescription() -> String? {
|
||||
(value as! DivContainer).resolveAccessibilityDescription(.default)
|
||||
XCTAssertEqual(label, "Container description")
|
||||
}
|
||||
|
||||
func test_MergedDescription_OnlyContainsVisibleContent() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divText(text: "One"),
|
||||
divText(text: "Two"),
|
||||
divText(text: "Three", visibility: .value(.gone)),
|
||||
divText(text: "Four", visibility: .value(.invisible)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(label, "One Two")
|
||||
}
|
||||
|
||||
func test_MergedDescription_OnlyContainsActiveStateContent() throws {
|
||||
let label = try resolveLabel(
|
||||
divContainer(
|
||||
accessibility: DivAccessibility(mode: .value(.merge)),
|
||||
items: [
|
||||
divState(
|
||||
divId: "switcher",
|
||||
states: [
|
||||
divStateState(div: divText(text: "Active"), stateId: "a"),
|
||||
divStateState(div: divText(text: "Inactive"), stateId: "b"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(label, "Active")
|
||||
}
|
||||
|
||||
private func makeBlock(_ div: Div) throws -> Block {
|
||||
try div.value.makeBlock(context: DivBlockModelingContext())
|
||||
}
|
||||
|
||||
private func resolveLabel(_ div: Div) throws -> String? {
|
||||
try makeBlock(div).accessibilityElement?.strings.label.flatMap {
|
||||
$0.isEmpty ? nil : $0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import VGSL
|
||||
|
||||
public protocol AccessibilityContaining {
|
||||
var accessibilityElement: AccessibilityElement? { get }
|
||||
var accessibilityChildren: [AccessibilityContaining] { get }
|
||||
}
|
||||
|
||||
extension AccessibilityContaining {
|
||||
public var accessibilityElement: AccessibilityElement? { nil }
|
||||
public var accessibilityChildren: [AccessibilityContaining] { [] }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import VGSL
|
||||
|
||||
extension AccessibilityContaining {
|
||||
public var mergedAccessibilityLabel: String? {
|
||||
if let element = accessibilityElement {
|
||||
if element.hideElementWithChildren {
|
||||
return nil
|
||||
}
|
||||
if let label = element.strings.label, !label.isEmpty {
|
||||
return label
|
||||
}
|
||||
}
|
||||
let parts = accessibilityChildren
|
||||
.compactMap(\.mergedAccessibilityLabel)
|
||||
.filter { !$0.isEmpty }
|
||||
return parts.isEmpty ? nil : parts.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,8 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
|
||||
private var cached = CachedSizes()
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] { children.map(\.content) }
|
||||
|
||||
public var isVerticallyResizable: Bool { heightTrait.isResizable }
|
||||
public var isHorizontallyResizable: Bool { widthTrait.isResizable }
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ public final class GalleryBlock: BlockWithTraits {
|
||||
model.path
|
||||
}
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] { model.items.map(\.content) }
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(value):
|
||||
|
||||
@@ -79,6 +79,8 @@ public final class GridBlock: BlockWithTraits, BlockWithLayout {
|
||||
private var cachedIntrinsicWidth: CGFloat?
|
||||
private var cachedIntrinsicHeight: (width: CGFloat, height: CGFloat)?
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] { items.map(\.contents) }
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
if case let .fixed(value) = widthTrait {
|
||||
return value
|
||||
|
||||
@@ -18,6 +18,8 @@ public final class LayeredBlock: BlockWithTraits, BlockWithLayout {
|
||||
public let heightTrait: LayoutTrait
|
||||
public let children: [Child]
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] { children.map(\.content) }
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(width):
|
||||
|
||||
@@ -23,6 +23,8 @@ public final class PagerBlock: BlockWithTraits {
|
||||
gallery.path
|
||||
}
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] { gallery.items.map(\.content) }
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(value):
|
||||
|
||||
@@ -12,6 +12,10 @@ public final class TabsBlock: BlockWithTraits {
|
||||
model.contentsModel.path
|
||||
}
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] {
|
||||
model.contentsModel.pages.map(\.block)
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
if case let .fixed(value) = widthTrait {
|
||||
return value
|
||||
|
||||
@@ -13,6 +13,8 @@ extension WrapperBlock {
|
||||
|
||||
public var isEmpty: Bool { child.isEmpty }
|
||||
|
||||
public var accessibilityChildren: [AccessibilityContaining] { [child] }
|
||||
|
||||
public var reuseId: String {
|
||||
child.reuseId
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user