[iOS] Open element actions sometimes result in a hover instead of a click
authoraestes@apple.com <aestes@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 24 Jun 2020 20:52:29 +0000 (20:52 +0000)
committeraestes@apple.com <aestes@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 24 Jun 2020 20:52:29 +0000 (20:52 +0000)
https://bugs.webkit.org/show_bug.cgi?id=213530
<rdar://problem/64176707>

Reviewed by Darin Adler.

Source/WebKit:

_WKElementAction implements a standard action handler for _WKElementActionTypeOpen that
attempts a synthetic click at the interaction location, expecting this click to start a
navigation to the interaction node's target URL. Synthetic clicks are subject to content
change observing, however, and if ContentChangeObserver detects a meaningful visible change
after dispatching a mousemove event at the interaction location then WebPage will refrain
from dispatching the mousedown and mouseup events that would have started a navigation.

While this behavior is necessary to support hover menus, it doesn't make sense when the user
is interacting with a context menu. Since the system might display a preview of the target
URL found at the interaction location, users expect the "Open" action to navigate to that
same URL rather than merely display a hover menu.

This change opts synthetic clicks from the open action out of content change observing by
ensuring that an interaction is started when handling _WKElementActionTypeOpen (like we do
for "Copy" and "Save Image"), then checking if WebPage::m_interactionNode matches the node
found in WebPage::attemptSyntheticClick. If it does, WebPage now bypasses content change
observing and completes a synthetic click.

New API test: ElementActionTests.OpenLinkWithHoverMenu

* UIProcess/API/Cocoa/_WKElementAction.mm:
(+[_WKElementAction _elementActionWithType:customTitle:assistant:]): Changed to call
-[WKActionSheetAssistant handleElementActionWithType:element:needsInteraction:] in the
handlers for _WKElementActionTypeCopy, _WKElementActionTypeOpen,
_WKElementActionTypeSaveImage, and _WKElementActionTypeShare.

* UIProcess/ios/WKActionSheetAssistant.h:
* UIProcess/ios/WKActionSheetAssistant.mm:
(-[WKActionSheetAssistant handleElementActionWithType:element:needsInteraction:]): Added to
handle the subset of standard element actions that flow through
WKActionSheetAssistantDelegate. Used a boolean parameter to decide whether the delegate
should start and stop an interaction when handling the action.

* UIProcess/ios/WKContentViewInteraction.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView _highlightLongPressRecognized:]):
(-[WKContentView actionSheetAssistant:openElementAtLocation:]): Renamed
-_attemptClickAtLocation:modifierFlags: to -_attemptSyntheticClickAtLocation:modifierFlags:.
(-[WKContentView _attemptSyntheticClickAtLocation:modifierFlags:]): Changed to call
WebPageProxy::attemptSyntheticClick instead of WebPageProxy::handleTap.
(-[WKContentView _simulateElementAction:atLocation:]): Set
_layerTreeTransactionIdAtLastInteractionStart to better simulate context menu interactions.

* UIProcess/WebPageProxy.h:
* UIProcess/ios/WebPageProxyIOS.mm:
(WebKit::WebPageProxy::attemptSyntheticClick):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::attemptSyntheticClick): Renamed handleTap to attemptSyntheticClick. If the
node responding to clicks matches the node found on interaction start (m_interactionNode),
call WebPage::completeSyntheticClick directly.

Tools:

Added an API test.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/ios/ElementActionTests.mm: Added.
* TestWebKitAPI/Tests/ios/link-with-hover-menu.html: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@263472 268f45cc-cd09-0410-ab3c-d52691b4dbfc

15 files changed:
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/Cocoa/_WKElementAction.mm
Source/WebKit/UIProcess/WebPageProxy.h
Source/WebKit/UIProcess/ios/WKActionSheetAssistant.h
Source/WebKit/UIProcess/ios/WKActionSheetAssistant.mm
Source/WebKit/UIProcess/ios/WKContentViewInteraction.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm
Source/WebKit/WebProcess/WebPage/WebPage.h
Source/WebKit/WebProcess/WebPage/WebPage.messages.in
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/ios/ElementActionTests.mm [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/ios/link-with-hover-menu.html [new file with mode: 0644]

index 72a794f..4e165da 100644 (file)
@@ -1,3 +1,64 @@
+2020-06-24  Andy Estes  <aestes@apple.com>
+
+        [iOS] Open element actions sometimes result in a hover instead of a click
+        https://bugs.webkit.org/show_bug.cgi?id=213530
+        <rdar://problem/64176707>
+
+        Reviewed by Darin Adler.
+
+        _WKElementAction implements a standard action handler for _WKElementActionTypeOpen that
+        attempts a synthetic click at the interaction location, expecting this click to start a
+        navigation to the interaction node's target URL. Synthetic clicks are subject to content
+        change observing, however, and if ContentChangeObserver detects a meaningful visible change
+        after dispatching a mousemove event at the interaction location then WebPage will refrain
+        from dispatching the mousedown and mouseup events that would have started a navigation.
+
+        While this behavior is necessary to support hover menus, it doesn't make sense when the user
+        is interacting with a context menu. Since the system might display a preview of the target
+        URL found at the interaction location, users expect the "Open" action to navigate to that
+        same URL rather than merely display a hover menu.
+
+        This change opts synthetic clicks from the open action out of content change observing by
+        ensuring that an interaction is started when handling _WKElementActionTypeOpen (like we do
+        for "Copy" and "Save Image"), then checking if WebPage::m_interactionNode matches the node
+        found in WebPage::attemptSyntheticClick. If it does, WebPage now bypasses content change
+        observing and completes a synthetic click.
+
+        New API test: ElementActionTests.OpenLinkWithHoverMenu
+
+        * UIProcess/API/Cocoa/_WKElementAction.mm:
+        (+[_WKElementAction _elementActionWithType:customTitle:assistant:]): Changed to call
+        -[WKActionSheetAssistant handleElementActionWithType:element:needsInteraction:] in the
+        handlers for _WKElementActionTypeCopy, _WKElementActionTypeOpen,
+        _WKElementActionTypeSaveImage, and _WKElementActionTypeShare.
+
+        * UIProcess/ios/WKActionSheetAssistant.h:
+        * UIProcess/ios/WKActionSheetAssistant.mm:
+        (-[WKActionSheetAssistant handleElementActionWithType:element:needsInteraction:]): Added to
+        handle the subset of standard element actions that flow through
+        WKActionSheetAssistantDelegate. Used a boolean parameter to decide whether the delegate
+        should start and stop an interaction when handling the action.
+
+        * UIProcess/ios/WKContentViewInteraction.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView _highlightLongPressRecognized:]):
+        (-[WKContentView actionSheetAssistant:openElementAtLocation:]): Renamed
+        -_attemptClickAtLocation:modifierFlags: to -_attemptSyntheticClickAtLocation:modifierFlags:.
+        (-[WKContentView _attemptSyntheticClickAtLocation:modifierFlags:]): Changed to call
+        WebPageProxy::attemptSyntheticClick instead of WebPageProxy::handleTap.
+        (-[WKContentView _simulateElementAction:atLocation:]): Set
+        _layerTreeTransactionIdAtLastInteractionStart to better simulate context menu interactions.
+
+        * UIProcess/WebPageProxy.h:
+        * UIProcess/ios/WebPageProxyIOS.mm:
+        (WebKit::WebPageProxy::attemptSyntheticClick):
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/WebPage.messages.in:
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::attemptSyntheticClick): Renamed handleTap to attemptSyntheticClick. If the
+        node responding to clicks matches the node found on interaction start (m_interactionNode),
+        call WebPage::completeSyntheticClick directly.
+
 2020-06-24  Tim Horton  <timothy_horton@apple.com>
 
         Fix the build once RBSProcessLimitations.h is introduced
index 31ec821..6da5760 100644 (file)
@@ -127,27 +127,19 @@ static void addToReadingList(NSURL *targetURL, NSString *title)
     case _WKElementActionTypeCopy:
         title = WEB_UI_STRING_KEY("Copy", "Copy (ActionSheet)", "Title for Copy Link or Image action button");
         handler = ^(WKActionSheetAssistant *assistant, _WKActivatedElementInfo *actionInfo) {
-            if ([assistant.delegate respondsToSelector:@selector(actionSheetAssistant:willStartInteractionWithElement:)])
-                [assistant.delegate actionSheetAssistant:assistant willStartInteractionWithElement:actionInfo];
-            [assistant.delegate actionSheetAssistant:assistant performAction:WebKit::SheetAction::Copy];
-            if ([assistant.delegate respondsToSelector:@selector(actionSheetAssistantDidStopInteraction:)])
-                [assistant.delegate actionSheetAssistantDidStopInteraction:assistant];
+            [assistant handleElementActionWithType:type element:actionInfo needsInteraction:YES];
         };
         break;
     case _WKElementActionTypeOpen:
         title = WEB_UI_STRING("Open", "Title for Open Link action button");
         handler = ^(WKActionSheetAssistant *assistant, _WKActivatedElementInfo *actionInfo) {
-            [assistant.delegate actionSheetAssistant:assistant openElementAtLocation:actionInfo._interactionLocation];
+            [assistant handleElementActionWithType:type element:actionInfo needsInteraction:YES];
         };
         break;
     case _WKElementActionTypeSaveImage:
         title = WEB_UI_STRING("Add to Photos", "Title for Add to Photos action button");
         handler = ^(WKActionSheetAssistant *assistant, _WKActivatedElementInfo *actionInfo) {
-            if ([assistant.delegate respondsToSelector:@selector(actionSheetAssistant:willStartInteractionWithElement:)])
-                [assistant.delegate actionSheetAssistant:assistant willStartInteractionWithElement:actionInfo];
-            [assistant.delegate actionSheetAssistant:assistant performAction:WebKit::SheetAction::SaveImage];
-            if ([assistant.delegate respondsToSelector:@selector(actionSheetAssistantDidStopInteraction:)])
-                [assistant.delegate actionSheetAssistantDidStopInteraction:assistant];
+            [assistant handleElementActionWithType:type element:actionInfo needsInteraction:YES];
         };
         break;
 #if HAVE(SAFARI_SERVICES_FRAMEWORK)
@@ -161,7 +153,7 @@ static void addToReadingList(NSURL *targetURL, NSString *title)
     case _WKElementActionTypeShare:
         title = WEB_UI_STRING("Share…", "Title for Share action button");
         handler = ^(WKActionSheetAssistant *assistant, _WKActivatedElementInfo *actionInfo) {
-            [assistant.delegate actionSheetAssistant:assistant shareElementWithURL:actionInfo.URL ?: actionInfo.imageURL rect:actionInfo.boundingRect];
+            [assistant handleElementActionWithType:type element:actionInfo needsInteraction:NO];
         };
         break;
     case _WKElementActionToggleShowLinkPreviews:
index 1298c55..878e571 100644 (file)
@@ -1352,7 +1352,7 @@ public:
     void commitPotentialTap(OptionSet<WebKit::WebEvent::Modifier>, TransactionID layerTreeTransactionIdAtLastTouchStart, WebCore::PointerID);
     void cancelPotentialTap();
     void tapHighlightAtPosition(const WebCore::FloatPoint&, uint64_t& requestID);
-    void handleTap(const WebCore::FloatPoint&, OptionSet<WebKit::WebEvent::Modifier>, TransactionID layerTreeTransactionIdAtLastTouchStart);
+    void attemptSyntheticClick(const WebCore::FloatPoint&, OptionSet<WebKit::WebEvent::Modifier>, TransactionID layerTreeTransactionIdAtLastTouchStart);
     void didRecognizeLongPress();
     void handleDoubleTapForDoubleClickAtPoint(const WebCore::IntPoint&, OptionSet<WebEvent::Modifier>, TransactionID layerTreeTransactionIdAtLastTouchStart);
 
index f757094..15687c4 100644 (file)
@@ -44,6 +44,8 @@ struct InteractionInformationAtPosition;
 @protocol WKActionSheetDelegate;
 @protocol UIContextMenuInteractionDelegate;
 
+typedef NS_ENUM(NSInteger, _WKElementActionType);
+
 @protocol WKActionSheetAssistantDelegate <NSObject>
 @required
 - (Optional<WebKit::InteractionInformationAtPosition>)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant;
@@ -90,6 +92,7 @@ struct InteractionInformationAtPosition;
 - (BOOL)isShowingSheet;
 - (void)interactionDidStartWithPositionInformation:(const WebKit::InteractionInformationAtPosition&)information;
 - (NSArray *)currentAvailableActionTitles;
+- (void)handleElementActionWithType:(_WKElementActionType)type element:(_WKActivatedElementInfo *)element needsInteraction:(BOOL)needsInteraction;
 #if USE(UICONTEXTMENU)
 - (NSArray<UIMenuElement *> *)suggestedActionsForContextMenuWithPositionInformation:(const WebKit::InteractionInformationAtPosition&)positionInformation;
 #endif
index 0620db7..9b110e2 100644 (file)
@@ -810,6 +810,35 @@ static NSArray<UIMenuElement *> *menuElementsFromDefaultActions(RetainPtr<NSArra
 
 #endif // USE(UICONTEXTMENU)
 
+- (void)handleElementActionWithType:(_WKElementActionType)type element:(_WKActivatedElementInfo *)element needsInteraction:(BOOL)needsInteraction
+{
+    auto delegate = _delegate.get();
+
+    if (needsInteraction && [delegate respondsToSelector:@selector(actionSheetAssistant:willStartInteractionWithElement:)])
+        [delegate actionSheetAssistant:self willStartInteractionWithElement:element];
+
+    switch (type) {
+    case _WKElementActionTypeCopy:
+        [delegate actionSheetAssistant:self performAction:WebKit::SheetAction::Copy];
+        break;
+    case _WKElementActionTypeOpen:
+        [delegate actionSheetAssistant:self openElementAtLocation:element._interactionLocation];
+        break;
+    case _WKElementActionTypeSaveImage:
+        [delegate actionSheetAssistant:self performAction:WebKit::SheetAction::SaveImage];
+        break;
+    case _WKElementActionTypeShare:
+        [delegate actionSheetAssistant:self shareElementWithURL:element.URL ?: element.imageURL rect:element.boundingRect];
+        break;
+    default:
+        ASSERT_NOT_REACHED();
+        break;
+    }
+
+    if (needsInteraction && [delegate respondsToSelector:@selector(actionSheetAssistantDidStopInteraction:)])
+        [delegate actionSheetAssistantDidStopInteraction:self];
+}
+
 - (void)cleanupSheet
 {
     auto delegate = _delegate.get();
index 0a106d2..808024b 100644 (file)
@@ -510,7 +510,7 @@ FOR_EACH_PRIVATE_WKCONTENTVIEW_ACTION(DECLARE_WKCONTENTVIEW_ACTION_FOR_WEB_VIEW)
 - (BOOL)_interpretKeyEvent:(::WebEvent *)theEvent isCharEvent:(BOOL)isCharEvent;
 - (void)_positionInformationDidChange:(const WebKit::InteractionInformationAtPosition&)info;
 - (BOOL)_currentPositionInformationIsValidForRequest:(const WebKit::InteractionInformationRequest&)request;
-- (void)_attemptClickAtLocation:(CGPoint)location modifierFlags:(UIKeyModifierFlags)modifierFlags;
+- (void)_attemptSyntheticClickAtLocation:(CGPoint)location modifierFlags:(UIKeyModifierFlags)modifierFlags;
 - (void)_willStartScrollingOrZooming;
 - (void)_didScroll;
 - (void)_didEndScrollingOrZooming;
index 3eb7372..3736260 100644 (file)
@@ -2642,7 +2642,7 @@ static Class tapAndAHalfRecognizerClass()
         break;
     case UIGestureRecognizerStateEnded:
         if (_longPressCanClick && _positionInformation.isElement) {
-            [self _attemptClickAtLocation:gestureRecognizer.startPoint modifierFlags:gestureRecognizerModifierFlags(gestureRecognizer)];
+            [self _attemptSyntheticClickAtLocation:gestureRecognizer.startPoint modifierFlags:gestureRecognizerModifierFlags(gestureRecognizer)];
             [self _finishInteraction];
         } else
             [self _cancelInteraction];
@@ -2844,13 +2844,13 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
     _smartMagnificationController->handleResetMagnificationGesture(gestureRecognizer.location);
 }
 
-- (void)_attemptClickAtLocation:(CGPoint)location modifierFlags:(UIKeyModifierFlags)modifierFlags
+- (void)_attemptSyntheticClickAtLocation:(CGPoint)location modifierFlags:(UIKeyModifierFlags)modifierFlags
 {
     if (![self isFirstResponder])
         [self becomeFirstResponder];
 
     [_inputPeripheral endEditing];
-    _page->handleTap(location, WebKit::webEventModifierFlags(modifierFlags), _layerTreeTransactionIdAtLastInteractionStart);
+    _page->attemptSyntheticClick(location, WebKit::webEventModifierFlags(modifierFlags), _layerTreeTransactionIdAtLastInteractionStart);
 }
 
 - (void)setUpTextSelectionAssistant
@@ -6904,7 +6904,7 @@ static BOOL allPasteboardItemOriginsMatchOrigin(UIPasteboard *pasteboard, const
 
 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location
 {
-    [self _attemptClickAtLocation:location modifierFlags:0];
+    [self _attemptSyntheticClickAtLocation:location modifierFlags:0];
 }
 
 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithURL:(NSURL *)url rect:(CGRect)boundingRect
@@ -8788,9 +8788,9 @@ static RetainPtr<NSItemProvider> createItemProvider(const WebKit::WebPageProxy&
 
 - (void)_simulateElementAction:(_WKElementActionType)actionType atLocation:(CGPoint)location
 {
-    RetainPtr<WKContentView> protectedSelf = self;
-    [self doAfterPositionInformationUpdate:[actionType, protectedSelf] (WebKit::InteractionInformationAtPosition info) {
-        _WKElementAction *action = [_WKElementAction _elementActionWithType:actionType assistant:protectedSelf->_actionSheetAssistant.get()];
+    _layerTreeTransactionIdAtLastInteractionStart = downcast<WebKit::RemoteLayerTreeDrawingAreaProxy>(*_page->drawingArea()).lastCommittedLayerTreeTransactionID();
+    [self doAfterPositionInformationUpdate:[actionType, self, protectedSelf = retainPtr(self)] (WebKit::InteractionInformationAtPosition info) {
+        _WKElementAction *action = [_WKElementAction _elementActionWithType:actionType assistant:_actionSheetAssistant.get()];
         _WKActivatedElementInfo *elementInfo = [_WKActivatedElementInfo activatedElementInfoWithInteractionInformationAtPosition:info userInfo:nil];
         [action runActionWithElementInfo:elementInfo];
     } forRequest:WebKit::InteractionInformationRequest(WebCore::roundedIntPoint(location))];
index 1aab8f5..0509cec 100644 (file)
@@ -905,9 +905,9 @@ void WebPageProxy::tapHighlightAtPosition(const WebCore::FloatPoint& position, u
     send(Messages::WebPage::TapHighlightAtPosition(requestID, position));
 }
 
-void WebPageProxy::handleTap(const FloatPoint& location, OptionSet<WebEvent::Modifier> modifiers, TransactionID layerTreeTransactionIdAtLastTouchStart)
+void WebPageProxy::attemptSyntheticClick(const FloatPoint& location, OptionSet<WebEvent::Modifier> modifiers, TransactionID layerTreeTransactionIdAtLastTouchStart)
 {
-    send(Messages::WebPage::HandleTap(roundedIntPoint(location), modifiers, layerTreeTransactionIdAtLastTouchStart));
+    send(Messages::WebPage::AttemptSyntheticClick(roundedIntPoint(location), modifiers, layerTreeTransactionIdAtLastTouchStart));
 }
 
 void WebPageProxy::didRecognizeLongPress()
index 6a7f688..4240198 100644 (file)
@@ -676,7 +676,7 @@ public:
     bool allowsUserScaling() const;
     bool hasStablePageScaleFactor() const { return m_hasStablePageScaleFactor; }
 
-    void handleTap(const WebCore::IntPoint&, OptionSet<WebKit::WebEvent::Modifier>, TransactionID lastLayerTreeTransactionId);
+    void attemptSyntheticClick(const WebCore::IntPoint&, OptionSet<WebKit::WebEvent::Modifier>, TransactionID lastLayerTreeTransactionId);
     void potentialTapAtPosition(uint64_t requestID, const WebCore::FloatPoint&, bool shouldRequestMagnificationInformation);
     void commitPotentialTap(OptionSet<WebKit::WebEvent::Modifier>, TransactionID lastLayerTreeTransactionId, WebCore::PointerID);
     void commitPotentialTapFailed();
index 16af317..62b909a 100644 (file)
@@ -55,7 +55,7 @@ messages -> WebPage LegacyReceiver {
     
     SetScreenIsBeingCaptured(bool captured)
 
-    HandleTap(WebCore::IntPoint point, OptionSet<WebKit::WebEvent::Modifier> modifiers, WebKit::TransactionID lastLayerTreeTransactionId)
+    AttemptSyntheticClick(WebCore::IntPoint point, OptionSet<WebKit::WebEvent::Modifier> modifiers, WebKit::TransactionID lastLayerTreeTransactionId)
     PotentialTapAtPosition(uint64_t requestID, WebCore::FloatPoint point, bool shouldRequestMagnificationInformation)
     CommitPotentialTap(OptionSet<WebKit::WebEvent::Modifier> modifiers, WebKit::TransactionID lastLayerTreeTransactionId, WebCore::PointerID pointerId)
     CancelPotentialTap()
index fe37437..f6e92b5 100644 (file)
@@ -867,7 +867,7 @@ void WebPage::completeSyntheticClick(Node& nodeRespondingToClick, const WebCore:
     send(Messages::WebPageProxy::DidCompleteSyntheticClick());
 }
 
-void WebPage::handleTap(const IntPoint& point, OptionSet<WebEvent::Modifier> modifiers, TransactionID lastLayerTreeTransactionId)
+void WebPage::attemptSyntheticClick(const IntPoint& point, OptionSet<WebEvent::Modifier> modifiers, TransactionID lastLayerTreeTransactionId)
 {
     FloatPoint adjustedPoint;
     Node* nodeRespondingToClick = m_page->mainFrame().nodeRespondingToClickEvents(point, adjustedPoint);
@@ -876,6 +876,8 @@ void WebPage::handleTap(const IntPoint& point, OptionSet<WebEvent::Modifier> mod
 
     if (!frameRespondingToClick || lastLayerTreeTransactionId < WebFrame::fromCoreFrame(*frameRespondingToClick)->firstLayerTreeTransactionIDAfterDidCommitLoad())
         send(Messages::WebPageProxy::DidNotHandleTapAsClick(adjustedIntPoint));
+    else if (m_interactionNode == nodeRespondingToClick)
+        completeSyntheticClick(*nodeRespondingToClick, adjustedPoint, modifiers, WebCore::OneFingerTap);
     else
         handleSyntheticClick(*nodeRespondingToClick, adjustedPoint, modifiers);
 }
index e899021..929da15 100644 (file)
@@ -1,3 +1,17 @@
+2020-06-24  Andy Estes  <aestes@apple.com>
+
+        [iOS] Open element actions sometimes result in a hover instead of a click
+        https://bugs.webkit.org/show_bug.cgi?id=213530
+        <rdar://problem/64176707>
+
+        Reviewed by Darin Adler.
+
+        Added an API test.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/ios/ElementActionTests.mm: Added.
+        * TestWebKitAPI/Tests/ios/link-with-hover-menu.html: Added.
+
 2020-06-24  Caitlin Potter  <caitp@igalia.com>
 
         [JSC] add microbenchmark for op_get_private_name
index 289a070..f1a9949 100644 (file)
                9C64DC321D76198A004B598E /* YouTubePluginReplacement.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9C64DC311D76198A004B598E /* YouTubePluginReplacement.cpp */; };
                A10F047E1E3AD29C00C95E19 /* NSFileManagerExtras.mm in Sources */ = {isa = PBXBuildFile; fileRef = A10F047C1E3AD29C00C95E19 /* NSFileManagerExtras.mm */; };
                A1146A8D1D2D7115000FE710 /* ContentFiltering.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1146A8A1D2D704F000FE710 /* ContentFiltering.mm */; };
+               A11E7DA024A169F900026745 /* link-with-hover-menu.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = A11E7D9F24A169E200026745 /* link-with-hover-menu.html */; };
+               A11E7DA324A17D2500026745 /* ElementActionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = A11E7DA224A17C7D00026745 /* ElementActionTests.mm */; };
                A125478F1DB18B9400358564 /* LoadDataWithNilMIMEType.mm in Sources */ = {isa = PBXBuildFile; fileRef = A125478D1DB18B9400358564 /* LoadDataWithNilMIMEType.mm */; };
                A12DDBFB1E836F0700CF6CAE /* RenderedImageWithOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = A12DDBF91E836F0700CF6CAE /* RenderedImageWithOptions.mm */; };
                A12DDC001E8373E700CF6CAE /* rendered-image-excluding-overflow.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = A12DDBFF1E8373C100CF6CAE /* rendered-image-excluding-overflow.html */; };
                        dstPath = TestWebKitAPI.resources;
                        dstSubfolderSpec = 7;
                        files = (
-                               5751B28A249D5BC500664C2A /* web-authentication-make-credential-hid-pin-get-pin-token-fake-pin-invalid-error-retry.html in Copy Resources */,
                                55A817FF2181021A0004A39A /* 100x100-red.tga in Copy Resources */,
                                1A9E52C913E65EF4006917F5 /* 18-characters.html in Copy Resources */,
                                55A81800218102210004A39A /* 400x400-green.png in Copy Resources */,
                                F46128D4211E40FD00D9FADB /* link-in-iframe-and-input.html in Copy Resources */,
                                8361F1781E610B4E00759B25 /* link-with-download-attribute-with-slashes.html in Copy Resources */,
                                8349D3C41DB9728E004A9F65 /* link-with-download-attribute.html in Copy Resources */,
+                               A11E7DA024A169F900026745 /* link-with-hover-menu.html in Copy Resources */,
                                3128A81323763FAC00D90D40 /* link-with-image.html in Copy Resources */,
                                378E64791632707400B6C676 /* link-with-title.html in Copy Resources */,
                                573255A622139BC700396AE8 /* load-web-archive-1.html in Copy Resources */,
                                57C624502346C21E00383FE7 /* web-authentication-get-assertion.html in Copy Resources */,
                                578DA44823ECD09B00246010 /* web-authentication-make-credential-hid-pin-auth-blocked-error.html in Copy Resources */,
                                570D26F423C3CA6A00D5CF67 /* web-authentication-make-credential-hid-pin-get-key-agreement-error.html in Copy Resources */,
+                               5751B28A249D5BC500664C2A /* web-authentication-make-credential-hid-pin-get-pin-token-fake-pin-invalid-error-retry.html in Copy Resources */,
                                578DA44223ECC7A000246010 /* web-authentication-make-credential-hid-pin-get-pin-token-pin-auth-blocked-error.html in Copy Resources */,
                                578DA44623ECCC0A00246010 /* web-authentication-make-credential-hid-pin-get-pin-token-pin-auth-invalid-error-retry.html in Copy Resources */,
                                570D26FA23C3F25100D5CF67 /* web-authentication-make-credential-hid-pin-get-pin-token-pin-blocked-error.html in Copy Resources */,
                9C64DC311D76198A004B598E /* YouTubePluginReplacement.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = YouTubePluginReplacement.cpp; sourceTree = "<group>"; };
                A10F047C1E3AD29C00C95E19 /* NSFileManagerExtras.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NSFileManagerExtras.mm; sourceTree = "<group>"; };
                A1146A8A1D2D704F000FE710 /* ContentFiltering.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContentFiltering.mm; sourceTree = "<group>"; };
+               A11E7D9F24A169E200026745 /* link-with-hover-menu.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "link-with-hover-menu.html"; sourceTree = "<group>"; };
+               A11E7DA224A17C7D00026745 /* ElementActionTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ElementActionTests.mm; sourceTree = "<group>"; };
                A125478D1DB18B9400358564 /* LoadDataWithNilMIMEType.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = LoadDataWithNilMIMEType.mm; sourceTree = "<group>"; };
                A12DDBF91E836F0700CF6CAE /* RenderedImageWithOptions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RenderedImageWithOptions.mm; sourceTree = "<group>"; };
                A12DDBFC1E836FF100CF6CAE /* RenderedImageWithOptionsPlugIn.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RenderedImageWithOptionsPlugIn.mm; sourceTree = "<group>"; };
                                F42D634322A1729F00D2FB3A /* AutocorrectionTestsIOS.mm */,
                                44077BB0231449D200179E2D /* DataDetectorsTestIOS.mm */,
                                F4D4F3B71E4E36E400BB2767 /* DragAndDropTestsIOS.mm */,
+                               A11E7DA224A17C7D00026745 /* ElementActionTests.mm */,
                                F4CF327F2366552200D3AD07 /* EnterKeyHintTests.mm */,
                                F4BC0B132146C849002A0478 /* FocusPreservationTests.mm */,
                                CDA93DAC22F4EC2200490A69 /* FullscreenTouchSecheuristicTests.cpp */,
                                0F16BED72304A1D100B4A167 /* composited.html */,
                                CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */,
                                CE6D0EE22426B8ED002AD901 /* insert-text.html */,
+                               A11E7D9F24A169E200026745 /* link-with-hover-menu.html */,
                                0F340777230382540060A1A0 /* overflow-scroll.html */,
                                A1C4FB721BACD1B7003742D0 /* pages.pages */,
                                CE6E81A320A933B800E2C80F /* set-timeout-function.html */,
                                834138C7203261CA00F26960 /* AsyncPolicyForNavigationResponse.mm in Sources */,
                                7CCE7EB41A411A7E00447C4C /* AttributedString.mm in Sources */,
                                3760C4F1211249AF00233ACC /* AttrStyle.mm in Sources */,
+                               CDED342F249DDE0E0002AE7A /* AudioRoutingArbitration.mm in Sources */,
                                CDC8E48D1BC5CB4500594FEC /* AudioSessionCategoryIOS.mm in Sources */,
                                F42D634422A1729F00D2FB3A /* AutocorrectionTestsIOS.mm in Sources */,
                                7C83E0B91D0A64F100FEBCF3 /* AutoLayoutIntegration.mm in Sources */,
                                1C2B81801C891E7C00A5529F /* CancelFontSubresource.mm in Sources */,
                                7CCE7EB61A411A7E00447C4C /* CancelLoadFromResourceLoadDelegate.mm in Sources */,
                                7C83E0411D0A63F200FEBCF3 /* CandidateTests.mm in Sources */,
-                               CDED342F249DDE0E0002AE7A /* AudioRoutingArbitration.mm in Sources */,
                                7CCE7EE71A411AE600447C4C /* CanHandleRequest.cpp in Sources */,
                                07C046CA1E4262A8007201E7 /* CARingBuffer.cpp in Sources */,
                                57303BCB2008376500355965 /* CBORReaderTest.cpp in Sources */,
                                5C0BF8921DD599B600B00328 /* EarlyKVOCrash.mm in Sources */,
                                7CCE7EE01A411A9A00447C4C /* EditorCommands.mm in Sources */,
                                F44D06471F39627A001A0E29 /* EditorStateTests.mm in Sources */,
+                               A11E7DA324A17D2500026745 /* ElementActionTests.mm in Sources */,
                                7CCE7EBF1A411A7E00447C4C /* ElementAtPointInWebFrame.mm in Sources */,
                                C13D82D92416F13200A62793 /* EnableAccessibility.mm in Sources */,
                                F4CF32802366552200D3AD07 /* EnterKeyHintTests.mm in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/ios/ElementActionTests.mm b/Tools/TestWebKitAPI/Tests/ios/ElementActionTests.mm
new file mode 100644 (file)
index 0000000..2b5731d
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "config.h"
+#import "TestCocoa.h"
+
+#if PLATFORM(IOS_FAMILY)
+
+#import "TestNavigationDelegate.h"
+#import "TestWKWebView.h"
+#import <WebKit/WKWebViewPrivateForTesting.h>
+
+namespace TestWebKitAPI {
+
+static void runTest(TestWKWebView *webView, TestNavigationDelegate *navigationDelegate, NSString *selector)
+{
+    __block bool mouseMoved = false;
+    __block bool navigationAttempted = false;
+    __block bool done = false;
+
+    navigationDelegate.decidePolicyForNavigationAction = ^(WKNavigationAction *navigationAction, void (^decisionHandler)(WKNavigationActionPolicy)) {
+        decisionHandler(WKNavigationActionPolicyCancel);
+        EXPECT_FALSE(mouseMoved);
+        EXPECT_FALSE(navigationAttempted);
+        EXPECT_NS_EQUAL(@"https://www.apple.com/", navigationAction.request.URL.absoluteString);
+        navigationAttempted = true;
+        done = true;
+    };
+
+    [webView performAfterReceivingMessage:@"mousemove" action:^{
+        if (mouseMoved)
+            done = true;
+        mouseMoved = true;
+    }];
+
+    id array = [webView objectByEvaluatingJavaScript:[NSString stringWithFormat:@"boundingClientRectArray('%@')", selector]];
+    auto boundingRect = CGRectMake([array[0] floatValue], [array[1] floatValue], [array[2] floatValue], [array[3] floatValue]);
+    [webView _simulateElementAction:_WKElementActionTypeOpen atLocation:CGPointMake(CGRectGetMidX(boundingRect), CGRectGetMidY(boundingRect))];
+
+    Util::run(&done);
+
+    EXPECT_FALSE(mouseMoved);
+    EXPECT_TRUE(navigationAttempted);
+
+    navigationDelegate.decidePolicyForNavigationAction = nil;
+    [webView clearMessageHandlers:@[ @"mousemove" ]];
+}
+
+TEST(ElementActionTests, OpenLinkWithHoverMenu)
+{
+    auto frame = CGRectMake(0, 0, 320, 500);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:frame]);
+    [webView synchronouslyLoadTestPageNamed:@"link-with-hover-menu"];
+
+    auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]);
+    [webView setNavigationDelegate:navigationDelegate.get()];
+
+    runTest(webView.get(), navigationDelegate.get(), @"#link");
+    runTest(webView.get(), navigationDelegate.get(), @"#link");
+}
+
+} // namespace TestWebKitAPI
+
+#endif // PLATFORM(IOS_FAMILY)
diff --git a/Tools/TestWebKitAPI/Tests/ios/link-with-hover-menu.html b/Tools/TestWebKitAPI/Tests/ios/link-with-hover-menu.html
new file mode 100644 (file)
index 0000000..0b9e5f3
--- /dev/null
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name='viewport' content='width=device-width'>
+    <style>
+        body {
+            height: 100vh;
+        }
+
+        ul {
+            list-style: none;
+        }
+
+        li > ul {
+            visibility: hidden;
+            padding: 0;
+        }
+
+        li:hover > ul {
+            visibility: visible;
+        }
+    </style>
+    <script>
+        const boundingClientRectArray = (selector) => {
+            const rect = document.querySelector(selector).getBoundingClientRect();
+            return [rect.left, rect.top, rect.width, rect.height];
+        };
+
+        addEventListener('mousemove', (event) => webkit.messageHandlers.testHandler.postMessage(event.type));
+    </script>
+</head>
+<body>
+    <ul>
+        <li>
+            <a id='link' href='https://www.apple.com/'>link</a>
+            <ul>
+                <li><a href='#'>hover menu</a></li>
+            </ul>
+        </li>
+    </ul>
+</body>
+</html>