[iOS WK2] Presenting an action sheet on an image map prevents selection UI from updating
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 16 Jul 2017 04:01:30 +0000 (04:01 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 16 Jul 2017 04:01:30 +0000 (04:01 +0000)
https://bugs.webkit.org/show_bug.cgi?id=174539
<rdar://problem/33307395>

Reviewed by Darin Adler.

Source/WebCore:

Currently, if TextIndicator fails to take a snapshot in TextIndicator::createWithRange, we will enter an
inconsistent state in the web process where Editor will continue to ignore selection changes until the next time
Editor::setIgnoreSelectionChanges(false) is called. This causes us to indefinitely defer EditorState updates to
the UI process, which leads to selection UI appearing unresponsive.

To fix this, we introduce a new TemporarySelectionChange object to simplify selection changes and/or
EditorState-update-ignoring behaviors within the scope of a single function. The constructor applies these
temporary changes, and the destructor reverts them as needed to their prior values.

This patch only adopts TemporarySelectionChange in order to fix this bug, but future patches will replace the
remaining places where we temporarily change selection and/or ignore selection with this helper.

Test: ActionSheetTests.ImageMapDoesNotDestroySelection.

* editing/Editor.cpp:
(WebCore::TemporarySelectionChange::TemporarySelectionChange):
(WebCore::TemporarySelectionChange::~TemporarySelectionChange):
* editing/Editor.h:
* editing/FrameSelection.h:
(WebCore::FrameSelection::isUpdateAppearanceEnabled):
* page/TextIndicator.cpp:
(WebCore::TextIndicator::createWithRange):

Source/WebKit:

Small tweak to avoid presenting at the element rect or text rect if the interaction information failed to
capture valid bounds for the element. We instead fall back to presenting at the touch location. This addresses
problems when presenting the action sheet popover on image maps on iPad, where GetPositionInformation fails to
capture correct data about for the <area>.

* UIProcess/ios/WKActionSheetAssistant.mm:
(presentationStyleForView):

Tools:

Adds a new unit test suite to cover action sheet popover presentation.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKit2Cocoa/image-map.html: Added.
* TestWebKitAPI/Tests/ios/ActionSheetTests.mm: Added.
(-[ActionSheetObserver waitForActionSheetAfterBlock:]):

Runs the given block and waits until the UI process has indicated that it will present an action sheet.

(-[ActionSheetObserver _webView:actionsForElement:defaultActions:]):
(TestWebKitAPI::IPadUserInterfaceSwizzler::IPadUserInterfaceSwizzler):

Helper class to alter the behavior of [[UIDevice currentDevice] userInterfaceIdiom] for testing.

(TestWebKitAPI::IPadUserInterfaceSwizzler::padUserInterfaceIdiom):
(TestWebKitAPI::TEST):

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

Source/WebCore/ChangeLog
Source/WebCore/editing/Editor.cpp
Source/WebCore/editing/Editor.h
Source/WebCore/editing/FrameSelection.h
Source/WebCore/page/TextIndicator.cpp
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/ios/WKActionSheetAssistant.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKit2Cocoa/image-map.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/ios/ActionSheetTests.mm [new file with mode: 0644]

index 4037fcb..21291d4 100644 (file)
@@ -1,3 +1,34 @@
+2017-07-15  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] Presenting an action sheet on an image map prevents selection UI from updating
+        https://bugs.webkit.org/show_bug.cgi?id=174539
+        <rdar://problem/33307395>
+
+        Reviewed by Darin Adler.
+
+        Currently, if TextIndicator fails to take a snapshot in TextIndicator::createWithRange, we will enter an
+        inconsistent state in the web process where Editor will continue to ignore selection changes until the next time
+        Editor::setIgnoreSelectionChanges(false) is called. This causes us to indefinitely defer EditorState updates to
+        the UI process, which leads to selection UI appearing unresponsive.
+
+        To fix this, we introduce a new TemporarySelectionChange object to simplify selection changes and/or
+        EditorState-update-ignoring behaviors within the scope of a single function. The constructor applies these
+        temporary changes, and the destructor reverts them as needed to their prior values.
+
+        This patch only adopts TemporarySelectionChange in order to fix this bug, but future patches will replace the
+        remaining places where we temporarily change selection and/or ignore selection with this helper.
+
+        Test: ActionSheetTests.ImageMapDoesNotDestroySelection.
+
+        * editing/Editor.cpp:
+        (WebCore::TemporarySelectionChange::TemporarySelectionChange):
+        (WebCore::TemporarySelectionChange::~TemporarySelectionChange):
+        * editing/Editor.h:
+        * editing/FrameSelection.h:
+        (WebCore::FrameSelection::isUpdateAppearanceEnabled):
+        * page/TextIndicator.cpp:
+        (WebCore::TextIndicator::createWithRange):
+
 2017-07-15  Myles C. Maxfield  <mmaxfield@apple.com>
 
         Clean up line-height and minimumFontSize functions
index 0e60605..4f290e2 100644 (file)
@@ -184,6 +184,44 @@ using namespace HTMLNames;
 using namespace WTF;
 using namespace Unicode;
 
+TemporarySelectionChange::TemporarySelectionChange(Frame& frame, std::optional<VisibleSelection> temporarySelection, TemporarySelectionOptions options)
+    : m_frame(frame)
+    , m_options(options)
+    , m_wasIgnoringSelectionChanges(frame.editor().ignoreSelectionChanges())
+#if PLATFORM(IOS)
+    , m_appearanceUpdatesWereEnabled(frame.selection().isUpdateAppearanceEnabled())
+#endif
+{
+#if PLATFORM(IOS)
+    if (options & TemporarySelectionOptionEnableAppearanceUpdates)
+        frame.selection().setUpdateAppearanceEnabled(true);
+#endif
+
+    if (options & TemporarySelectionOptionIgnoreSelectionChanges)
+        frame.editor().setIgnoreSelectionChanges(true);
+
+    if (temporarySelection) {
+        m_selectionToRestore = frame.selection().selection();
+        frame.selection().setSelection(temporarySelection.value());
+    }
+}
+
+TemporarySelectionChange::~TemporarySelectionChange()
+{
+    if (m_selectionToRestore)
+        m_frame->selection().setSelection(m_selectionToRestore.value());
+
+    if (m_options & TemporarySelectionOptionIgnoreSelectionChanges) {
+        auto revealSelection = m_options & TemporarySelectionOptionRevealSelection ? Editor::RevealSelection::Yes : Editor::RevealSelection::No;
+        m_frame->editor().setIgnoreSelectionChanges(m_wasIgnoringSelectionChanges, revealSelection);
+    }
+
+#if PLATFORM(IOS)
+    if (m_options & TemporarySelectionOptionEnableAppearanceUpdates)
+        m_frame->selection().setUpdateAppearanceEnabled(m_appearanceUpdatesWereEnabled);
+#endif
+}
+
 // When an event handler has moved the selection outside of a text control
 // we should use the target control's selection for this editing operation.
 VisibleSelection Editor::selectionForCommand(Event* event)
index 6f38430..c829fd1 100644 (file)
@@ -102,6 +102,37 @@ struct FragmentAndResources {
 
 #endif
 
+enum TemporarySelectionOption : uint8_t {
+    // By default, no additional options are enabled.
+    TemporarySelectionOptionDefault = 0,
+
+    // Scroll to reveal the selection.
+    TemporarySelectionOptionRevealSelection = 1 << 0,
+
+    // Don't propagate selection changes to the UI process.
+    TemporarySelectionOptionIgnoreSelectionChanges = 1 << 1,
+
+    // Force the render tree to update selection state. Only respected on iOS.
+    TemporarySelectionOptionEnableAppearanceUpdates = 1 << 2
+};
+
+using TemporarySelectionOptions = uint8_t;
+
+class TemporarySelectionChange {
+public:
+    TemporarySelectionChange(Frame&, std::optional<VisibleSelection> = std::nullopt, TemporarySelectionOptions = TemporarySelectionOptionDefault);
+    ~TemporarySelectionChange();
+
+private:
+    Ref<Frame> m_frame;
+    TemporarySelectionOptions m_options;
+    bool m_wasIgnoringSelectionChanges;
+#if PLATFORM(IOS)
+    bool m_appearanceUpdatesWereEnabled;
+#endif
+    std::optional<VisibleSelection> m_selectionToRestore;
+};
+
 class Editor {
     WTF_MAKE_FAST_ALLOCATED;
 public:
index 4b500f8..d35ef7c 100644 (file)
@@ -236,6 +236,7 @@ public:
     void setCaretBlinks(bool caretBlinks = true);
     WEBCORE_EXPORT void setCaretColor(const Color&);
     WEBCORE_EXPORT static VisibleSelection wordSelectionContainingCaretSelection(const VisibleSelection&);
+    bool isUpdateAppearanceEnabled() const { return m_updateAppearanceEnabled; }
     void setUpdateAppearanceEnabled(bool enabled) { m_updateAppearanceEnabled = enabled; }
     void suppressScrolling() { ++m_scrollingSuppressCount; }
     void restoreScrolling()
index 0498fd2..aca9918 100644 (file)
@@ -78,13 +78,13 @@ RefPtr<TextIndicator> TextIndicator::createWithRange(const Range& range, TextInd
 
     Ref<Frame> protector(*frame);
 
+    VisibleSelection oldSelection = frame->selection().selection();
+    TemporarySelectionOptions temporarySelectionOptions = TemporarySelectionOptionDefault;
 #if PLATFORM(IOS)
-    frame->editor().setIgnoreSelectionChanges(true);
-    frame->selection().setUpdateAppearanceEnabled(true);
+    temporarySelectionOptions |= TemporarySelectionOptionIgnoreSelectionChanges;
+    temporarySelectionOptions |= TemporarySelectionOptionEnableAppearanceUpdates;
 #endif
-
-    VisibleSelection oldSelection = frame->selection().selection();
-    frame->selection().setSelection(range);
+    TemporarySelectionChange selectionChange(*frame, { range }, temporarySelectionOptions);
 
     TextIndicatorData data;
 
@@ -96,16 +96,7 @@ RefPtr<TextIndicator> TextIndicator::createWithRange(const Range& range, TextInd
     if (!initializeIndicator(data, *frame, range, margin, indicatesCurrentSelection))
         return nullptr;
 
-    RefPtr<TextIndicator> indicator = TextIndicator::create(data);
-
-    frame->selection().setSelection(oldSelection);
-
-#if PLATFORM(IOS)
-    frame->editor().setIgnoreSelectionChanges(false, Editor::RevealSelection::No);
-    frame->selection().setUpdateAppearanceEnabled(false);
-#endif
-
-    return indicator;
+    return TextIndicator::create(data);
 }
 
 RefPtr<TextIndicator> TextIndicator::createWithSelectionInFrame(Frame& frame, TextIndicatorOptions options, TextIndicatorPresentationTransition presentationTransition, FloatSize margin)
index 28215f2..5213021 100644 (file)
@@ -1,3 +1,19 @@
+2017-07-15  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] Presenting an action sheet on an image map prevents selection UI from updating
+        https://bugs.webkit.org/show_bug.cgi?id=174539
+        <rdar://problem/33307395>
+
+        Reviewed by Darin Adler.
+
+        Small tweak to avoid presenting at the element rect or text rect if the interaction information failed to
+        capture valid bounds for the element. We instead fall back to presenting at the touch location. This addresses
+        problems when presenting the action sheet popover on image maps on iPad, where GetPositionInformation fails to
+        capture correct data about for the <area>.
+
+        * UIProcess/ios/WKActionSheetAssistant.mm:
+        (presentationStyleForView):
+
 2017-07-14  Jonathan Bedard  <jbedard@apple.com>
 
         Add iOS 11 SPI
index c34861c..f48e88c 100644 (file)
@@ -396,6 +396,9 @@ static const CGFloat presentationElementRectPadding = 15;
 static WKActionSheetPresentationStyle presentationStyleForView(UIView *view, const InteractionInformationAtPosition& positionInfo, _WKActivatedElementInfo *elementInfo)
 {
     auto apparentElementRect = [view convertRect:positionInfo.bounds toView:view.window];
+    if (CGRectIsEmpty(apparentElementRect))
+        return WKActionSheetPresentAtTouchLocation;
+
     auto windowRect = view.window.bounds;
     apparentElementRect = CGRectIntersection(apparentElementRect, windowRect);
 
index 620cd19..4a19c68 100644 (file)
@@ -1,3 +1,28 @@
+2017-07-15  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] Presenting an action sheet on an image map prevents selection UI from updating
+        https://bugs.webkit.org/show_bug.cgi?id=174539
+        <rdar://problem/33307395>
+
+        Reviewed by Darin Adler.
+
+        Adds a new unit test suite to cover action sheet popover presentation.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKit2Cocoa/image-map.html: Added.
+        * TestWebKitAPI/Tests/ios/ActionSheetTests.mm: Added.
+        (-[ActionSheetObserver waitForActionSheetAfterBlock:]):
+
+        Runs the given block and waits until the UI process has indicated that it will present an action sheet.
+
+        (-[ActionSheetObserver _webView:actionsForElement:defaultActions:]):
+        (TestWebKitAPI::IPadUserInterfaceSwizzler::IPadUserInterfaceSwizzler):
+
+        Helper class to alter the behavior of [[UIDevice currentDevice] userInterfaceIdiom] for testing.
+
+        (TestWebKitAPI::IPadUserInterfaceSwizzler::padUserInterfaceIdiom):
+        (TestWebKitAPI::TEST):
+
 2017-07-15  Sam Weinig  <sam@webkit.org>
 
         [Scripts] Make svn-create-patch work better when called in sub directories
index f685060..2edc18c 100644 (file)
                F42DA5161D8CEFE400336F40 /* large-input-field-focus-onload.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F42DA5151D8CEFDB00336F40 /* large-input-field-focus-onload.html */; };
                F4451C761EB8FD890020C5DA /* two-paragraph-contenteditable.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4451C751EB8FD7C0020C5DA /* two-paragraph-contenteditable.html */; };
                F4538EF71E8473E600B5C953 /* large-red-square.png in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4538EF01E846B4100B5C953 /* large-red-square.png */; };
+               F45B63FB1F197F4A009D38B9 /* image-map.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F45B63FA1F197F33009D38B9 /* image-map.html */; };
+               F45B63FE1F19D410009D38B9 /* ActionSheetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */; };
                F46849BE1EEF58E400B937FE /* UIPasteboardTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F46849BD1EEF58E400B937FE /* UIPasteboardTests.mm */; };
                F46849C01EEF5EF300B937FE /* rich-and-plain-text.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F46849BF1EEF5EDC00B937FE /* rich-and-plain-text.html */; };
                F469FB241F01804B00401539 /* contenteditable-and-target.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F469FB231F01803500401539 /* contenteditable-and-target.html */; };
                        dstPath = TestWebKitAPI.resources;
                        dstSubfolderSpec = 7;
                        files = (
+                               F45B63FB1F197F4A009D38B9 /* image-map.html in Copy Resources */,
                                F4D5E4E81F0C5D38008C1A49 /* dragstart-clear-selection.html in Copy Resources */,
                                F4A32EC41F05F3850047C544 /* dragstart-change-selection-offscreen.html in Copy Resources */,
                                F4A32ECB1F0643370047C544 /* contenteditable-in-iframe.html in Copy Resources */,
                F42DA5151D8CEFDB00336F40 /* large-input-field-focus-onload.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = "large-input-field-focus-onload.html"; path = "Tests/WebKit2Cocoa/large-input-field-focus-onload.html"; sourceTree = SOURCE_ROOT; };
                F4451C751EB8FD7C0020C5DA /* two-paragraph-contenteditable.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "two-paragraph-contenteditable.html"; sourceTree = "<group>"; };
                F4538EF01E846B4100B5C953 /* large-red-square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "large-red-square.png"; sourceTree = "<group>"; };
+               F45B63FA1F197F33009D38B9 /* image-map.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "image-map.html"; sourceTree = "<group>"; };
+               F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ActionSheetTests.mm; sourceTree = "<group>"; };
                F46849BD1EEF58E400B937FE /* UIPasteboardTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = UIPasteboardTests.mm; sourceTree = "<group>"; };
                F46849BF1EEF5EDC00B937FE /* rich-and-plain-text.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "rich-and-plain-text.html"; sourceTree = "<group>"; };
                F469FB231F01803500401539 /* contenteditable-and-target.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "contenteditable-and-target.html"; sourceTree = "<group>"; };
                                7560917719259C59009EF06E /* MemoryCacheAddImageToCacheIOS.mm */,
                                F4D7BCD61EA574DD00C421D3 /* PositionInformationTests.mm */,
                                F46849BD1EEF58E400B937FE /* UIPasteboardTests.mm */,
+                               F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */,
                        );
                        path = ios;
                        sourceTree = "<group>";
                                F41AB99B1EF4692C0083FA08 /* file-uploading.html */,
                                F41AB9991EF4692C0083FA08 /* image-and-contenteditable.html */,
                                F41AB9931EF4692C0083FA08 /* image-and-textarea.html */,
+                               F45B63FA1F197F33009D38B9 /* image-map.html */,
                                F41AB9961EF4692C0083FA08 /* link-and-input.html */,
                                F41AB99D1EF4692C0083FA08 /* link-and-target-div.html */,
                                F41AB9941EF4692C0083FA08 /* prevent-operation.html */,
                                07492B3B1DF8B14C00633DE1 /* EnumerateMediaDevices.cpp in Sources */,
                                448D7E471EA6C55500ECC756 /* EnvironmentUtilitiesTest.cpp in Sources */,
                                7CCE7EEF1A411AE600447C4C /* EphemeralSessionPushStateNoHistoryCallback.cpp in Sources */,
+                               F45B63FE1F19D410009D38B9 /* ActionSheetTests.mm in Sources */,
                                7CCE7EF01A411AE600447C4C /* EvaluateJavaScript.cpp in Sources */,
                                315118101DB1AE4000176304 /* ExtendedColor.cpp in Sources */,
                                7CCE7EF11A411AE600447C4C /* FailedLoad.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/image-map.html b/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/image-map.html
new file mode 100644 (file)
index 0000000..5f88bb4
--- /dev/null
@@ -0,0 +1,31 @@
+<head>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+        <style>
+        body {
+            width: 100%;
+            height: 100%;
+            margin: 0;
+        }
+
+        img {
+            width: 320px;
+            height: 320px;
+        }
+
+        h1 {
+            font-size: 50px;
+        }
+        </style>
+</head>
+
+<body>
+    <img src="icon.png" usemap="#imgmap">
+    <map name="imgmap"><area href="https://www.apple.com" coords="0,0,400,400" shape="rect"></map>
+    <h1 id="h1">Hello world</h1>
+    <script>
+    function selectTextNode(text)
+    {
+        getSelection().setBaseAndExtent(text, 0, text, text.data.length);
+    }
+    </script>
+</body>
diff --git a/Tools/TestWebKitAPI/Tests/ios/ActionSheetTests.mm b/Tools/TestWebKitAPI/Tests/ios/ActionSheetTests.mm
new file mode 100644 (file)
index 0000000..62dcf5d
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include "config.h"
+
+#if PLATFORM(IOS)
+
+#import "InstanceMethodSwizzler.h"
+#import "PlatformUtilities.h"
+#import "TestWKWebView.h"
+#import <WebKit/WKUIDelegatePrivate.h>
+#import <WebKit/WKWebViewPrivate.h>
+#import <wtf/RetainPtr.h>
+#import <wtf/SoftLinking.h>
+
+@interface ActionSheetObserver : NSObject<WKUIDelegatePrivate>
+@property (nonatomic) BOOL presentedActionSheet;
+@end
+
+@implementation ActionSheetObserver
+
+- (BOOL)waitForActionSheetAfterBlock:(dispatch_block_t)block
+{
+    _presentedActionSheet = NO;
+    block();
+    while ([[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]]) {
+        if (_presentedActionSheet)
+            break;
+    }
+    return _presentedActionSheet;
+}
+
+- (NSArray *)_webView:(ActionSheetObserver *)webView actionsForElement:(_WKActivatedElementInfo *)element defaultActions:(NSArray<_WKElementAction *> *)defaultActions
+{
+    _presentedActionSheet = YES;
+    return defaultActions;
+}
+
+@end
+
+namespace TestWebKitAPI {
+
+class IPadUserInterfaceSwizzler {
+public:
+    IPadUserInterfaceSwizzler()
+        : m_swizzler([UIDevice class], @selector(userInterfaceIdiom), reinterpret_cast<IMP>(padUserInterfaceIdiom))
+    {
+    }
+private:
+    static UIUserInterfaceIdiom padUserInterfaceIdiom()
+    {
+        return UIUserInterfaceIdiomPad;
+    }
+    InstanceMethodSwizzler m_swizzler;
+};
+
+TEST(ActionSheetTests, ImageMapDoesNotDestroySelection)
+{
+    IPadUserInterfaceSwizzler iPadUserInterface;
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 1024, 768)]);
+    auto observer = adoptNS([[ActionSheetObserver alloc] init]);
+    [webView setUIDelegate:observer.get()];
+    [webView synchronouslyLoadTestPageNamed:@"image-map"];
+    [webView stringByEvaluatingJavaScript:@"selectTextNode(h1.childNodes[0])"];
+
+    EXPECT_WK_STREQ("Hello world", [webView stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+    [observer waitForActionSheetAfterBlock:^() {
+        [webView _simulateLongPressActionAtLocation:CGPointMake(200, 200)];
+    }];
+    EXPECT_WK_STREQ("Hello world", [webView stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+}
+
+} // namespace TestWebKitAPI
+
+#endif // PLATFORM(IOS)