REGRESSION (iOS 16): System controls overlap website controls (affects SquareSpace, medium.com, and others)
https://bugs.webkit.org/show_bug.cgi?id=241837
rdar://95658478

Reviewed by Aditya Keerthi.

On iOS 16, the callout bar (which is now built on top of `UIEditMenuInteraction`) no longer respects
evasion rects, passed in through the `UIWKInteractionViewProtocol` delegate method
`-requestRectsToEvadeForSelectionCommandsWithCompletionHandler:`. This was previously used to avoid
overlapping interactable controls on the page when presenting the callout bar, which the layout
test `editing/selection/ios/avoid-showing-callout-menu-over-controls.html` exercises.

To fix this, we're adding a replacement SPI in UIKit, allowing WebKit to vend a preferred
`UIEditMenuArrowDirection` which will be consulted when presenting the edit menu interaction in
order to show the callout bar.

For more details, see: rdar://95652872.

* Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView requestPreferredArrowDirectionForEditMenuWithCompletionHandler:]):

In the case where there are clickable controls above the selection, use `UIEditMenuArrowDirectionUp`
to make the callout bar present _below_ the selection instead of above; otherwise, simply go with
the default behavior, which puts the callout bar above the selection.

(-[WKContentView requestRectsToEvadeForSelectionCommandsWithCompletionHandler:]):

Pull out common logic for requesting a list of rects to evade into a separate internal helper
method, and use this common helper method to implement both the legacy delegate method (which no
longer works on iOS 16), as well as the new delegate method in iOS 16. For now, I opted to still
implement both methods, such that this test will pass on both iOS 15 and iOS 16 (but only with the
changes in rdar://95652872).

(-[WKContentView _requestEvasionRectsAboveSelectionIfNeeded:]):
* Tools/TestWebKitAPI/Tests/WebKitCocoa/ImageAnalysisTests.mm:

Also, adopt the new SPI in an API test that simulates callout bar appearance on iOS 16.

(TestWebKitAPI::simulateCalloutBarAppearance):
* Tools/TestWebKitAPI/cocoa/TestWKWebView.h:
* Tools/TestWebKitAPI/ios/UIKitSPI.h:
* Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::internalClassNamed):
(WTR::UIScriptControllerIOS::menuRect const):
(WTR::UIScriptControllerIOS::contextMenuRect const):

Additionally tweak a couple of script controller hooks, to work with the new `UIEditMenuInteraction`
-based callout bars on iOS 16.

Canonical link: https://commits.webkit.org/251756@main


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@295751 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm b/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
index a29569e..6e50e6a 100644
--- a/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
+++ b/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
@@ -4624,53 +4624,64 @@
     });
 }
 
-- (void)requestRectsToEvadeForSelectionCommandsWithCompletionHandler:(void(^)(NSArray<NSValue *> *rects))completionHandler
+#if HAVE(UI_EDIT_MENU_INTERACTION)
+
+- (void)requestPreferredArrowDirectionForEditMenuWithCompletionHandler:(void(^)(UIEditMenuArrowDirection))completion
 {
-    if (!completionHandler) {
-        [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__];
-        return;
-    }
+    [self _requestEvasionRectsAboveSelectionIfNeeded:[completion = makeBlockPtr(completion)](const Vector<WebCore::FloatRect>& rects) {
+        completion(rects.isEmpty() ? UIEditMenuArrowDirectionAutomatic : UIEditMenuArrowDirectionUp);
+    }];
+}
 
+#endif
+
+- (void)requestRectsToEvadeForSelectionCommandsWithCompletionHandler:(void(^)(NSArray<NSValue *> *rects))completion
+{
+    [self _requestEvasionRectsAboveSelectionIfNeeded:[completion = makeBlockPtr(completion)](const Vector<WebCore::FloatRect>& rects) {
+        completion(createNSArray(rects).get());
+    }];
+}
+
+- (void)_requestEvasionRectsAboveSelectionIfNeeded:(CompletionHandler<void(const Vector<WebCore::FloatRect>&)>&&)completion
+{
     if ([self _shouldSuppressSelectionCommands]) {
-        completionHandler(@[ ]);
+        completion({ });
         return;
     }
 
-    auto requestRectsToEvadeIfNeeded = [startTime = ApproximateTime::now(), weakSelf = WeakObjCPtr<WKContentView>(self), completion = makeBlockPtr(completionHandler)] {
+    auto requestRectsToEvadeIfNeeded = [startTime = ApproximateTime::now(), weakSelf = WeakObjCPtr<WKContentView>(self), completion = WTFMove(completion)]() mutable {
         auto strongSelf = weakSelf.get();
         if (!strongSelf) {
-            completion(@[ ]);
+            completion({ });
             return;
         }
 
         if ([strongSelf webView]._editable) {
-            completion(@[ ]);
+            completion({ });
             return;
         }
 
         auto focusedElementType = strongSelf->_focusedElementInformation.elementType;
         if (focusedElementType != WebKit::InputType::ContentEditable && focusedElementType != WebKit::InputType::TextArea) {
-            completion(@[ ]);
+            completion({ });
             return;
         }
 
         // Give the page some time to present custom editing UI before attempting to detect and evade it.
         auto delayBeforeShowingCalloutBar = std::max(0_s, 0.25_s - (ApproximateTime::now() - startTime));
-        WorkQueue::main().dispatchAfter(delayBeforeShowingCalloutBar, [completion, weakSelf] () mutable {
+        WorkQueue::main().dispatchAfter(delayBeforeShowingCalloutBar, [completion = WTFMove(completion), weakSelf]() mutable {
             auto strongSelf = weakSelf.get();
             if (!strongSelf) {
-                completion(@[ ]);
+                completion({ });
                 return;
             }
 
             if (!strongSelf->_page) {
-                completion(@[ ]);
+                completion({ });
                 return;
             }
 
-            strongSelf->_page->requestEvasionRectsAboveSelection([completion = WTFMove(completion)] (auto& rects) {
-                completion(createNSArray(rects).get());
-            });
+            strongSelf->_page->requestEvasionRectsAboveSelection(WTFMove(completion));
         });
     };
 
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/ImageAnalysisTests.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/ImageAnalysisTests.mm
index 1a52753..f214b9b 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/ImageAnalysisTests.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/ImageAnalysisTests.mm
@@ -376,7 +376,7 @@
 static void simulateCalloutBarAppearance(TestWKWebView *webView)
 {
     __block bool done = false;
-    [webView.textInputContentView requestRectsToEvadeForSelectionCommandsWithCompletionHandler:^(NSArray<NSValue *> *) {
+    [webView.textInputContentView requestPreferredArrowDirectionForEditMenuWithCompletionHandler:^(UIEditMenuArrowDirection) {
         done = true;
     }];
     Util::run(&done);
diff --git a/Tools/TestWebKitAPI/cocoa/TestWKWebView.h b/Tools/TestWebKitAPI/cocoa/TestWKWebView.h
index 593902c..06c6a7b 100644
--- a/Tools/TestWebKitAPI/cocoa/TestWKWebView.h
+++ b/Tools/TestWebKitAPI/cocoa/TestWKWebView.h
@@ -34,7 +34,7 @@
 @protocol UITextInputInternal;
 @protocol UITextInputMultiDocument;
 @protocol UITextInputPrivate;
-@protocol UIWKInteractionViewProtocol_Staging_91919121;
+@protocol UIWKInteractionViewProtocol_Staging_95652872;
 #endif
 
 @interface WKWebView (AdditionalDeclarations)
@@ -50,7 +50,7 @@
 
 @interface WKWebView (TestWebKitAPI)
 #if PLATFORM(IOS_FAMILY)
-@property (nonatomic, readonly) UIView <UITextInputPrivate, UITextInputInternal, UITextInputMultiDocument, UIWKInteractionViewProtocol_Staging_91919121, UITextInputTokenizer> *textInputContentView;
+@property (nonatomic, readonly) UIView <UITextInputPrivate, UITextInputInternal, UITextInputMultiDocument, UIWKInteractionViewProtocol_Staging_95652872, UITextInputTokenizer> *textInputContentView;
 - (NSArray<_WKTextInputContext *> *)synchronouslyRequestTextInputContextsInRect:(CGRect)rect;
 #endif
 @property (nonatomic, readonly) NSUInteger gpuToWebProcessConnectionCount;
diff --git a/Tools/TestWebKitAPI/ios/UIKitSPI.h b/Tools/TestWebKitAPI/ios/UIKitSPI.h
index 601d2d4..5d7f5bb 100644
--- a/Tools/TestWebKitAPI/ios/UIKitSPI.h
+++ b/Tools/TestWebKitAPI/ios/UIKitSPI.h
@@ -214,7 +214,6 @@
 
 @protocol UIWKInteractionViewProtocol
 - (void)pasteWithCompletionHandler:(void (^)(void))completionHandler;
-- (void)requestRectsToEvadeForSelectionCommandsWithCompletionHandler:(void(^)(NSArray<NSValue *> *rects))completionHandler;
 - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHandler:(void (^)(UIWKAutocorrectionRects *rectsForInput))completionHandler;
 - (void)requestAutocorrectionContextWithCompletionHandler:(void (^)(UIWKAutocorrectionContext *autocorrectionContext))completionHandler;
 - (void)selectWordBackward;
@@ -358,6 +357,12 @@
 - (void)didInsertFinalDictationResult;
 @end
 
+@protocol UIWKInteractionViewProtocol_Staging_95652872 <UIWKInteractionViewProtocol_Staging_91919121>
+#if HAVE(UI_EDIT_MENU_INTERACTION)
+- (void)requestPreferredArrowDirectionForEditMenuWithCompletionHandler:(void (^)(UIEditMenuArrowDirection))completionHandler;
+#endif
+@end
+
 #if HAVE(UIFINDINTERACTION)
 @interface UITextSearchOptions ()
 @property (nonatomic, readwrite) UITextSearchMatchMethod wordMatchMethod;
diff --git a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
index 08d84f0..b2e4e02 100644
--- a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
+++ b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
@@ -100,6 +100,14 @@
     return modifiers;
 }
 
+static Class internalClassNamed(NSString *className)
+{
+    auto result = NSClassFromString(className);
+    if (!result)
+        NSLog(@"Warning: an internal class named '%@' does not exist.", className);
+    return result;
+}
+
 Ref<UIScriptController> UIScriptController::create(UIScriptContext& context)
 {
     return adoptRef(*new UIScriptControllerIOS(context));
@@ -1101,17 +1109,18 @@
 
 JSObjectRef UIScriptControllerIOS::menuRect() const
 {
-    UIView *calloutBar = UICalloutBar.activeCalloutBar;
-    if (!calloutBar.window)
-        return nullptr;
-
-    return toObject([calloutBar convertRect:calloutBar.bounds toView:platformContentView()]);
+    UIView *containerView = nil;
+    if (auto *calloutBar = UICalloutBar.activeCalloutBar; calloutBar.window)
+        containerView = calloutBar;
+    else
+        containerView = findAllViewsInHierarchyOfType(webView().textEffectsWindow, internalClassNamed(@"_UIEditMenuListView")).firstObject;
+    return containerView ? toObject([containerView convertRect:containerView.bounds toView:platformContentView()]) : nullptr;
 }
 
 JSObjectRef UIScriptControllerIOS::contextMenuRect() const
 {
     auto *window = webView().window;
-    auto *contextMenuView = [findAllViewsInHierarchyOfType(window, NSClassFromString(@"_UIContextMenuView")) firstObject];
+    auto *contextMenuView = findAllViewsInHierarchyOfType(window, internalClassNamed(@"_UIContextMenuView")).firstObject;
     if (!contextMenuView)
         return nullptr;