From fa7bdf80f8f347ee4f558159edbbf7c413a1d168 Mon Sep 17 00:00:00 2001 From: babaevmm Date: Tue, 11 Feb 2025 12:33:44 +0300 Subject: [PATCH] support div-tooltip.mode commit_hash:949be67575b7f3da8a6d40d3341bb8c681123d76 --- .../Extensions/DivTooltipExtensions.swift | 10 +- .../SnapshotTestRunner.swift | 2 +- .../LayoutKit/Blocks/BlockTooltip.swift | 13 ++- .../LayoutKit/Tooltips/TooltipManager.swift | 73 +++++++++------ .../UI/Blocks/TooltipContainerView.swift | 37 ++++++-- .../Blocks/BlockTooltipTests.swift | 1 + schema/div-tooltip-mode-non-modal.json | 4 +- test_data/regression_test_data/index.json | 42 +++++---- .../tooltips-with-tap-outside-properties.json | 92 +++++++++++++++++++ 9 files changed, 214 insertions(+), 60 deletions(-) diff --git a/client/ios/DivKit/Extensions/DivTooltipExtensions.swift b/client/ios/DivKit/Extensions/DivTooltipExtensions.swift index 67897487a..dfad607bf 100644 --- a/client/ios/DivKit/Extensions/DivTooltipExtensions.swift +++ b/client/ios/DivKit/Extensions/DivTooltipExtensions.swift @@ -18,6 +18,13 @@ extension DivTooltip { return await tooltipViewFactory.makeView(div: self.div, tooltipId: self.id) } + let mode: BlockTooltip.Mode = switch mode { + case .divTooltipModeModal: + .modal + case .divTooltipModeNonModal: + .nonModal + } + return try BlockTooltip( id: id, // Legacy behavior. Views should be created with tooltipViewFactory. @@ -28,7 +35,8 @@ extension DivTooltip { useLegacyWidth: context.flagsInfo.useTooltipLegacyWidth, tooltipViewFactory: tooltipViewFactory, closeByTapOutside: resolveCloseByTapOutside(expressionResolver), - tapOutsideActions: tapOutsideActions?.uiActions(context: context) ?? [] + tapOutsideActions: tapOutsideActions?.uiActions(context: context) ?? [], + mode: mode ) } } diff --git a/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift b/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift index 0430af2fd..1cfad50f6 100644 --- a/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift +++ b/client/ios/DivKitSnapshotTests/SnapshotTestRunner.swift @@ -82,7 +82,7 @@ final class SnapshotTestRunner { manager: DefaultTooltipManager, check: CheckAction ) async throws { - guard let tooltipWindow = manager.tooltipWindow else { + guard let tooltipWindow = manager.modalTooltipWindow else { return try check(nil) } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift b/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift index 1726aebc2..203e175bb 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift @@ -21,6 +21,11 @@ public struct BlockTooltip: Equatable { case center } + public enum Mode: Equatable { + case modal + case nonModal + } + public let id: String public let block: Block public let duration: TimeInterval @@ -30,6 +35,7 @@ public struct BlockTooltip: Equatable { public let tooltipViewFactory: TooltipViewFactory? public let closeByTapOutside: Bool public let tapOutsideActions: [UserInterfaceAction] + public let mode: Mode public init( id: String, @@ -40,7 +46,8 @@ public struct BlockTooltip: Equatable { useLegacyWidth: Bool = true, tooltipViewFactory: TooltipViewFactory? = nil, closeByTapOutside: Bool = true, - tapOutsideActions: [UserInterfaceAction] = [] + tapOutsideActions: [UserInterfaceAction] = [], + mode: Mode = .modal ) { self.id = id self.block = block @@ -51,6 +58,7 @@ public struct BlockTooltip: Equatable { self.tooltipViewFactory = tooltipViewFactory self.closeByTapOutside = closeByTapOutside self.tapOutsideActions = tapOutsideActions + self.mode = mode } public static func ==(lhs: BlockTooltip, rhs: BlockTooltip) -> Bool { @@ -61,6 +69,7 @@ public struct BlockTooltip: Equatable { lhs.useLegacyWidth == rhs.useLegacyWidth && lhs.block.equals(rhs.block) && lhs.closeByTapOutside == rhs.closeByTapOutside && - lhs.tapOutsideActions == rhs.tapOutsideActions + lhs.tapOutsideActions == rhs.tapOutsideActions && + lhs.mode == rhs.mode } } diff --git a/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift b/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift index 5cc929aca..20dd00a4e 100644 --- a/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift +++ b/client/ios/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift @@ -68,6 +68,7 @@ public class DefaultTooltipManager: TooltipManager { public let duration: TimeInterval public let closeByTapOutside: Bool public let tapOutsideActions: [UserInterfaceAction] + public let isModal: Bool public let view: VisibleBoundsTrackingView } @@ -76,9 +77,14 @@ public class DefaultTooltipManager: TooltipManager { private var handleAction: (UIActionEvent) -> Void private var existingAnchorViews = WeakCollection() private var showingTooltips = [String: TooltipContainerView]() - private(set) var tooltipWindow: UIWindow? + private(set) var modalTooltipWindow: UIWindow? private var previousOrientation = UIDevice.current.orientation + private var foregroundWindowScene: UIWindowScene? { + let scenes = UIApplication.shared.connectedScenes + return scenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene + } + public init( shownTooltips: Property> = Property(), handleAction: @escaping (UIActionEvent) -> Void = { _ in } @@ -95,13 +101,14 @@ public class DefaultTooltipManager: TooltipManager { } public func showTooltip(info: TooltipInfo) { - setupTooltipWindow() - - guard let tooltipWindow, + setupModalTooltipWindow() + guard let modalTooltipWindow, + let foregroundWindowScene, + let currentKeyWindow = foregroundWindowScene.windows.first(where: { $0.isKeyWindow }), !showingTooltips.keys.contains(info.id) else { return } - let windowBounds = tooltipWindow.bounds.inset(by: tooltipWindow.safeAreaInsets) + let windowBounds = modalTooltipWindow.bounds.inset(by: modalTooltipWindow.safeAreaInsets) Task { @MainActor in guard let tooltip = await existingAnchorViews.compactMap( @@ -112,23 +119,34 @@ public class DefaultTooltipManager: TooltipManager { tooltipView: tooltip.view, closeByTapOutside: tooltip.closeByTapOutside, tapOutsideActions: tooltip.tapOutsideActions, + isModal: tooltip.isModal, handleAction: handleAction, onCloseAction: { [weak self] in - self?.showingTooltips.removeValue(forKey: tooltip.id) - self?.tooltipWindow?.isHidden = true + guard let self else { return } + showingTooltips.removeValue(forKey: tooltip.id) + if !showingTooltips.contains(where: { $1.isModal }) { + modalTooltipWindow.isHidden = true + } } ) - // Passing the statusBarStyle control to `rootViewController` of the main window - let vc = ProxyViewController( - viewController: UIApplication.shared.delegate?.window?? - .rootViewController ?? UIViewController() - ) - vc.view = view - // Window won't rotate if `rootViewController` is not set - tooltipWindow.rootViewController = vc - tooltipWindow.isHidden = false - tooltipWindow.makeKeyAndVisible() - view.frame = tooltipWindow.bounds + + if tooltip.isModal { + // Passing the statusBarStyle control to `rootViewController` of the main window + let vc = ProxyViewController( + viewController: UIApplication.shared.delegate?.window?? + .rootViewController ?? UIViewController() + ) + vc.view = view + // Window won't rotate if `rootViewController` is not set + modalTooltipWindow.rootViewController = vc + modalTooltipWindow.isHidden = false + modalTooltipWindow.makeKeyAndVisible() + view.frame = modalTooltipWindow.bounds + } else { + currentKeyWindow.addSubview(view) + view.frame = currentKeyWindow.bounds + } + showingTooltips[info.id] = view if !tooltip.duration.isZero { try await Task.sleep(nanoseconds: UInt64(tooltip.duration.nanoseconds)) @@ -152,7 +170,7 @@ public class DefaultTooltipManager: TooltipManager { public func reset() { showingTooltips = [:] - tooltipWindow = nil + modalTooltipWindow = nil } public func setHandler(_ handler: @escaping (UIActionEvent) -> Void) { @@ -176,15 +194,13 @@ public class DefaultTooltipManager: TooltipManager { previousOrientation = orientation } - private func setupTooltipWindow() { - if tooltipWindow == nil { - guard let windowScene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene - else { return } - tooltipWindow = UIWindow(windowScene: windowScene) - tooltipWindow?.windowLevel = UIWindow.Level.alert + 1 - tooltipWindow?.isHidden = true - } + private func setupModalTooltipWindow() { + guard modalTooltipWindow == nil, + let windowScene = foregroundWindowScene else { return } + + modalTooltipWindow = UIWindow(windowScene: windowScene) + modalTooltipWindow?.windowLevel = UIWindow.Level.alert + 1 + modalTooltipWindow?.isHidden = true } } @@ -215,6 +231,7 @@ extension TooltipAnchorView { duration: tooltip.duration, closeByTapOutside: tooltip.closeByTapOutside, tapOutsideActions: tooltip.tapOutsideActions, + isModal: tooltip.mode == .modal, view: tooltipView ) } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift index 9730defff..f1212d6f3 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift @@ -3,12 +3,14 @@ import UIKit import VGSL public final class TooltipContainerView: UIView, UIActionEventPerforming { + let isModal: Bool private let tooltipView: VisibleBoundsTrackingView private let closeByTapOutside: Bool private let tapOutsideActions: [UserInterfaceAction] private let handleAction: (LayoutKit.UIActionEvent) -> Void private let onCloseAction: Action + private var isClosing = false private var lastNonZeroBounds: CGRect? private var onVisibleBoundsChanged: Action? @@ -16,12 +18,14 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { tooltipView: VisibleBoundsTrackingView, closeByTapOutside: Bool, tapOutsideActions: [UserInterfaceAction], + isModal: Bool, handleAction: @escaping (LayoutKit.UIActionEvent) -> Void, onCloseAction: @escaping Action ) { self.tooltipView = tooltipView self.closeByTapOutside = closeByTapOutside self.tapOutsideActions = tapOutsideActions + self.isModal = isModal self.handleAction = handleAction self.onCloseAction = onCloseAction let tooltipBounds = tooltipView.bounds @@ -31,8 +35,10 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { super.init(frame: .zero) - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - addGestureRecognizer(tapRecognizer) + if isModal { + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapRecognizer) + } addSubview(tooltipView) } @@ -44,16 +50,21 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { @objc private func handleTap(_ sender: UITapGestureRecognizer) { let point = sender.location(in: self) - let isPointInsideTooltip = tooltipView.point( - inside: tooltipView.convert(point, from: self), - with: nil - ) - if !isPointInsideTooltip { + if !isPointInsideTooltip(point) { performTapOutsideActions() + } + } + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !isPointInsideTooltip(point, event: event), !isModal { if closeByTapOutside { - close() + DispatchQueue.main.async { + self.close() + } } + return nil + } else { + return super.hitTest(point, with: event) } } @@ -78,6 +89,8 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { } public func close() { + guard !isClosing else { return } + isClosing = true self.tooltipView.onVisibleBoundsChanged(from: tooltipView.bounds, to: .zero) removeFromParentAnimated(completion: { self.onCloseAction() @@ -89,5 +102,13 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { UIActionEvent(uiAction: $0, originalSender: self) } perform(uiActionEvents: uiActionEvents, from: self) + + if closeByTapOutside { + close() + } + } + + private func isPointInsideTooltip(_ point: CGPoint, event: UIEvent? = nil) -> Bool { + tooltipView.point(inside: tooltipView.convert(point, from: self), with: event) } } diff --git a/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTooltipTests.swift b/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTooltipTests.swift index c8ef4c5ba..9f983e5f5 100644 --- a/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTooltipTests.swift +++ b/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTooltipTests.swift @@ -43,6 +43,7 @@ final class BlockTooltipTests: XCTestCase { tooltipView: TestView(), closeByTapOutside: true, tapOutsideActions: [], + isModal: true, handleAction: { _ in }, onCloseAction: {} ) diff --git a/schema/div-tooltip-mode-non-modal.json b/schema/div-tooltip-mode-non-modal.json index ff6f4f847..2f4db0f16 100644 --- a/schema/div-tooltip-mode-non-modal.json +++ b/schema/div-tooltip-mode-non-modal.json @@ -1,7 +1,9 @@ { "type": "object", "$description": "translations.json#/div_tooltip_mode_non_modal", - "platforms": [], + "platforms": [ + "ios" + ], "properties": { "type": { "type": "string", diff --git a/test_data/regression_test_data/index.json b/test_data/regression_test_data/index.json index 06694670a..dd33722c5 100644 --- a/test_data/regression_test_data/index.json +++ b/test_data/regression_test_data/index.json @@ -565,19 +565,19 @@ ], "steps": [ "Set russian language on device", - "Turn on Screen reader and set order of elements description to '\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435,\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435,\u0442\u0438\u043f'", - "Set focus on '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 1'", + "Turn on Screen reader and set order of elements description to 'Состояние,название,тип'", + "Set focus on 'Элемент 1'", "Listen to Screen reader", "Double tap on screen", - "Set focus on '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 2'", + "Set focus on 'Элемент 2'", "Listen to Screen reader", - "Set focus on '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 3'" + "Set focus on 'Элемент 3'" ], "expected_results": [ - "On '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 1' Screen reader should say 'C\u0435\u0439\u0447\u0430\u0441 \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 0; \u042d\u043b\u0435\u043c\u0435\u043d\u0442 1; \u041a\u043d\u043e\u043f\u043a\u0430; \u041a\u043e\u0441\u043d\u0438\u0442\u0435\u0441\u044c \u0434\u0432\u0430\u0436\u0434\u044b, \u0447\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c'", - "After double tap Screen reader should say '\u0421\u0435\u0439\u0447\u0430\u0441 \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 1'", - "On '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 2' Screen reader should say '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 2; \u041a\u043d\u043e\u043f\u043a\u0430; \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443, \u0447\u0442\u043e\u0431\u044b \u0447\u0442\u043e-\u0442\u043e \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e'", - "On '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 3' Screen reader should say '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 3; \u041a\u043d\u043e\u043f\u043a\u0430'" + "On 'Элемент 1' Screen reader should say 'Cейчас в состоянии 0; Элемент 1; Кнопка; Коснитесь дважды, чтобы активировать'", + "After double tap Screen reader should say 'Сейчас в состоянии 1'", + "On 'Элемент 2' Screen reader should say 'Элемент 2; Кнопка; Нажмите на кнопку, чтобы что-то произошло'", + "On 'Элемент 3' Screen reader should say 'Элемент 3; Кнопка'" ], "file": "accessibility/descriptions.json" }, @@ -651,8 +651,8 @@ "Focus on items from top to bottom" ], "expected_results": [ - "When focusing on 1 item you'll hear '\u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 1'", - "When focusing on group of 2 and 3 items you will hear '\u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 2, \u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 3'", + "When focusing on 1 item you'll hear 'Это Элемент 1'", + "When focusing on group of 2 and 3 items you will hear 'Это Элемент 2, Это Элемент 3'", "Can't focus 4 element" ], "file": "accessibility/mode/default.json" @@ -672,9 +672,9 @@ ], "expected_results": [ "Focusing on all card", - "You will hear '\u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 1, \u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 2, \u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 3'", - "On activation menu with text '\u042d\u043b\u0435\u043c\u0435\u043d\u0442 1, \u042d\u043b\u0435\u043c\u0435\u043d\u0442 2, \u042d\u043b\u0435\u043c\u0435\u043d\u0442 3' appears", - "On focus Screen reader should not say '\u041a\u043e\u0441\u043d\u0438\u0442\u0435\u0441\u044c \u0434\u0432\u0430\u0436\u0434\u044b \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435'" + "You will hear 'Это Элемент 1, Это Элемент 2, Это Элемент 3'", + "On activation menu with text 'Элемент 1, Элемент 2, Элемент 3' appears", + "On focus Screen reader should not say 'Коснитесь дважды и удерживайте'" ], "file": "accessibility/mode/merge.json" }, @@ -693,8 +693,8 @@ ], "expected_results": [ "Focus on 5-th item", - "\u0423\u0441\u043b\u044b\u0448\u0438\u043c '\u042d\u0442\u043e \u042d\u043b\u0435\u043c\u0435\u043d\u0442 5'", - "\u041f\u0440\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c Screen reader \u043d\u0430 \u043a\u0430\u0436\u0434\u043e\u0439 \u043a\u043d\u043e\u043f\u043a\u0435 \u043f\u0440\u0438 \u0442\u0430\u043f\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043c\u0435\u043d\u044e \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u044d\u0442\u043e\u0439 \u043a\u043d\u043e\u043f\u043a\u0438" + "Услышим 'Это Элемент 5'", + "При выключенном Screen reader на каждой кнопке при тапе должно появляться меню с названием этой кнопки" ], "file": "accessibility/mode/exclude.json" }, @@ -2082,7 +2082,7 @@ }, { "title": "Custom card", - "case_id": 93, + "case_id": 93, "platforms": [ "android", "web" @@ -2599,14 +2599,18 @@ "Try to tap outside of tooltip", "Close tooltip by cross button", "Tap on second button", - "Tap outside of tooltip" + "Tap outside of tooltip", + "Tap on third button", + "Tap on other buttons" ], "expected_results": [ "First tooltip is shown", "First tooltip isn't closed by tapping outside of the tooltip", "First tooltip is closed", "Second tooltip is shown", - "Label text changed to `Outside actions called: true`, label text color is green" + "Label text changed to `Outside actions called: true`, label text color is green", + "Third tooltip is shown", + "First and second buttons are clickable during showing of third non modal tooltip" ], "file": "tooltips-with-tap-outside-properties.json" }, @@ -2764,7 +2768,7 @@ { "title": "Patch gallery with typed action", "case_id": 192, - "platforms": [ + "platforms": [ "android", "web" ], diff --git a/test_data/regression_test_data/tooltips-with-tap-outside-properties.json b/test_data/regression_test_data/tooltips-with-tap-outside-properties.json index 4ca2d28fa..23ab8f790 100644 --- a/test_data/regression_test_data/tooltips-with-tap-outside-properties.json +++ b/test_data/regression_test_data/tooltips-with-tap-outside-properties.json @@ -250,6 +250,98 @@ "type": "text", "text": "Outside actions called: @{outside_actions_called}", "text_color": "@{(outside_actions_called == 'true') ? '#00FF00' : '#000000'}" + }, + { + "type": "button", + "text": "tooltip mode = non_modal", + "tooltips": [ + { + "id": "tooltip_3", + "position": "bottom", + "duration": 0, + "offset": { + "x": { + "value": 0 + }, + "y": { + "value": 50 + } + }, + "mode": { + "type": "non_modal" + }, + "close_by_tap_outside": true, + "div": { + "type": "container", + "orientation": "overlap", + "items": [ + { + "type": "container", + "paddings": { + "left": 16, + "top": 16, + "right": 16, + "bottom": 16 + }, + "items": [ + { + "type": "text", + "width": { + "type": "wrap_content" + }, + "paddings": { + "left": 16, + "top": 16, + "right": 16, + "bottom": 16 + }, + "background": [ + { + "type": "solid", + "color": "#799eb7" + } + ], + "border": { + "corner_radius": 6 + }, + "text_color": "#2C3E50", + "text": "Non modal tooltip" + } + ] + }, + { + "type": "image", + "width": { + "type": "fixed", + "value": 20 + }, + "height": { + "type": "fixed", + "value": 20 + }, + "margins": { + "left": 8, + "top": 8 + }, + "action": { + "log_id": "hide_tooltip", + "url": "div-action://hide_tooltip?id=tooltip_3" + }, + "image_url": "https://yastatic.net/s3/home/div/div_fullscreens/cross2.3.png", + "tint_color": "#000000", + "alignment_horizontal": "left", + "alignment_vertical": "top" + } + ] + } + } + ], + "actions": [ + { + "log_id": "show_tooltip", + "url": "div-action://show_tooltip?id=tooltip_3&multiple=true" + } + ] } ] }