Skip to content
Permalink
Browse files
REGRESSION (iOS 16): System controls overlap website controls (affect…
…s 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: https://svn.webkit.org/repository/webkit/trunk@295751 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
whsieh committed Jun 22, 2022
1 parent b308d25 commit 6a51016a7e54873d82429eb2148c1da79036bbeb
Showing 5 changed files with 51 additions and 26 deletions.
@@ -4624,53 +4624,64 @@ - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHand
});
}

- (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));
});
};

@@ -376,7 +376,7 @@ static void processRequestWithResults(id, SEL, VKImageAnalyzerRequest *request,
static void simulateCalloutBarAppearance(TestWKWebView *webView)
{
__block bool done = false;
[webView.textInputContentView requestRectsToEvadeForSelectionCommandsWithCompletionHandler:^(NSArray<NSValue *> *) {
[webView.textInputContentView requestPreferredArrowDirectionForEditMenuWithCompletionHandler:^(UIEditMenuArrowDirection) {
done = true;
}];
Util::run(&done);
@@ -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;
@@ -214,7 +214,6 @@ typedef NS_ENUM(NSInteger, UIWKGestureType) {

@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 @@ typedef NS_ENUM(NSUInteger, _UIClickInteractionEvent) {
- (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;
@@ -100,6 +100,14 @@ static BOOL returnNo()
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 @@ static UIDeviceOrientation toUIDeviceOrientation(DeviceOrientation* orientation)

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;

0 comments on commit 6a51016

Please sign in to comment.