diff --git a/.mapping.json b/.mapping.json index f0ce217c2..fd800432e 100644 --- a/.mapping.json +++ b/.mapping.json @@ -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", diff --git a/client/ios/DivKit/Extensions/DivAccessibilityExtensions.swift b/client/ios/DivKit/Extensions/DivAccessibilityExtensions.swift index d2cd76dbf..bd11dd6c7 100644 --- a/client/ios/DivKit/Extensions/DivAccessibilityExtensions.swift +++ b/client/ios/DivKit/Extensions/DivAccessibilityExtensions.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 = "" } diff --git a/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift b/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift index fee235c71..6df059ec4 100644 --- a/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift +++ b/client/ios/DivKit/Extensions/DivBase/DivBaseBlockBuilder.swift @@ -187,6 +187,7 @@ final class DivBaseBlockBuilder { let accessibilityElement = (div.accessibility ?? DivAccessibility()).resolve( expressionResolver, id: context.currentDivId, + block: block, customParams: customAccessibilityParams ) diff --git a/client/ios/DivKit/Extensions/DivContainer/DivContainer+Accessibility.swift b/client/ios/DivKit/Extensions/DivContainer/DivContainer+Accessibility.swift deleted file mode 100644 index e5413d635..000000000 --- a/client/ios/DivKit/Extensions/DivContainer/DivContainer+Accessibility.swift +++ /dev/null @@ -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) - } - } -} diff --git a/client/ios/DivKit/Extensions/DivContainer/DivContainerExtensions.swift b/client/ios/DivKit/Extensions/DivContainer/DivContainerExtensions.swift index cd016c624..e409c538b 100644 --- a/client/ios/DivKit/Extensions/DivContainer/DivContainerExtensions.swift +++ b/client/ios/DivKit/Extensions/DivContainer/DivContainerExtensions.swift @@ -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) ) diff --git a/client/ios/DivKitTests/Extensions/DivAccessibilityExtensionsTests.swift b/client/ios/DivKitTests/Extensions/DivAccessibilityExtensionsTests.swift index 157821a0f..90ef1ec74 100644 --- a/client/ios/DivKitTests/Extensions/DivAccessibilityExtensionsTests.swift +++ b/client/ios/DivKitTests/Extensions/DivAccessibilityExtensionsTests.swift @@ -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 diff --git a/client/ios/DivKitTests/Extensions/DivContainerAccessibilityDescriptionTests.swift b/client/ios/DivKitTests/Extensions/DivContainerAccessibilityDescriptionTests.swift index cbb71b639..666c69c75 100644 --- a/client/ios/DivKitTests/Extensions/DivContainerAccessibilityDescriptionTests.swift +++ b/client/ios/DivKitTests/Extensions/DivContainerAccessibilityDescriptionTests.swift @@ -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 + } } } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/AccessibilityContaining.swift b/client/ios/LayoutKit/LayoutKit/Blocks/AccessibilityContaining.swift index e94087d61..d20f855bb 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/AccessibilityContaining.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/AccessibilityContaining.swift @@ -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] { [] } } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Block+AccessibilityMerge.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Block+AccessibilityMerge.swift new file mode 100644 index 000000000..89acfc83d --- /dev/null +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Block+AccessibilityMerge.swift @@ -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: " ") + } +} diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift index faba81a6f..c3bc74b5b 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift @@ -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 } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift index b05ad243e..d5aed8c0e 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/GalleryBlock.swift @@ -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): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift index 58aaf0c6a..a4acb6ce7 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Grid/GridBlock.swift @@ -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 diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift index 37cd2f4c2..13e0589cc 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/LayeredBlock.swift @@ -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): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift index a4d1df274..c8c72d0da 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/PagerBlock.swift @@ -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): diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift index a177ede8f..679ab6914 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/TabsBlock.swift @@ -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 diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/WrapperBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/WrapperBlock.swift index 6de0a1d03..94b14e948 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/WrapperBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/WrapperBlock.swift @@ -13,6 +13,8 @@ extension WrapperBlock { public var isEmpty: Bool { child.isEmpty } + public var accessibilityChildren: [AccessibilityContaining] { [child] } + public var reuseId: String { child.reuseId }