[Extra zoom mode] Programmatically changing focus when an element already has focus...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 18 Apr 2018 06:03:06 +0000 (06:03 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 18 Apr 2018 06:03:06 +0000 (06:03 +0000)
https://bugs.webkit.org/show_bug.cgi?id=184635
<rdar://problem/39440642>

Reviewed by Tim Horton.

Source/WebKit:

Currently on iOS, we allow element focus to present UI if the keyboard is already shown. In extra zoom mode,
this would lead to a confusing experience when the focus form control overlay is disabled, since fullscreen
input view controllers are swapped out from underneath the user. Currently, this also puts the UI process into a
bad state where the focused form control overlay is active, but still hidden. This patch makes some tweaks to
input view controller handling in the UI process to address these issues, and also adds WebKitTestRunner support
for simulating interactions with select menus in extra zoom mode. See comments below for more detail.

Test: fast/events/extrazoom/change-focus-during-change-event.html

* UIProcess/API/Cocoa/WKUIDelegatePrivate.h:

Add new SPI delegate hooks to notify the UI delegate when view controllers are presented or dismissed in extra
zoom mode. See -presentViewControllerForCurrentAssistedNode and -dismissAllInputViewControllers.

* UIProcess/WebProcessProxy.cpp:
(WebKit::WebProcessProxy::takeBackgroundActivityTokenForFullscreenInput):
(WebKit::WebProcessProxy::releaseBackgroundActivityTokenForFullscreenInput):

See the comment below -dismissAllInputViewControllers.

* UIProcess/WebProcessProxy.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView _startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:]):

In extra zoom mode, when changing focus from one assisted node to another, only allow the second node to be
assisted if the focused form control overlay is being shown. Otherwise, (i.e. when a fullscreen input view
controller is being presented), don't allow focus to start an input session.

Additionally, make a minor tweak to allow the previous node to blur, even if we are not showing the keyboard for
the new focused element. Without this adjustment, in the case where the page has programmatically focused
another element while a fullscreen input view controller is presented, we'll show the old view controller for
the new focused element.

(-[WKContentView presentViewControllerForCurrentAssistedNode]):
(-[WKContentView dismissAllInputViewControllers:]):

Currently, when a fullscreen input view controller is presented, the web process gets backgrounded. This
prevents event handlers from executing, which leads to strange behaviors in many cases (for instance: if we
have a multiple select, and the "change" event handler blurs the select, the user may check or uncheck multiple
items, but only the first change will actually take effect).

To fix this, we maintain a background activity token while presenting an input view controller.

(-[WKContentView focusedFormControlViewDidBeginEditing:]):

Start hiding the focused form overlay when re-presenting an input view controller. This allows us to bail from
showing fullscreen input UI for another focused element if focus programmatically changes while the current
fullscreen input view controller is presented, due to the -isHidden check in -_startAssistingNode:.

(-[WKContentView selectFormAccessoryPickerRow:]):

Simulate tapping a given row in select menu UI in extra zoom mode.

Tools:

Add plumbing to support invoking `didHideKeyboardCallback` and `didShowKeyboardCallback` when (respectively)
dismissing or presenting fullscreen input view controllers in extra zoom mode.

* WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:
(-[TestRunnerWKWebView initWithFrame:configuration:]):
(-[TestRunnerWKWebView dealloc]):
(-[TestRunnerWKWebView _invokeShowKeyboardCallbackIfNecessary]):
(-[TestRunnerWKWebView _invokeHideKeyboardCallbackIfNecessary]):
(-[TestRunnerWKWebView _keyboardDidShow:]):
(-[TestRunnerWKWebView _keyboardDidHide:]):
(-[TestRunnerWKWebView _webView:didPresentFocusedElementViewController:]):
(-[TestRunnerWKWebView _webView:didDismissFocusedElementViewController:]):

LayoutTests:

Add a new layout test to exercise the following sequence of events in extra zoom mode:

1. Focus select element #1.
2. Choose an unselected option.
3. Programmatically focus select element #2 in the "change" event handler.
4. Choose an unselected option.
5. Programmatically blur select element #2 in the "change" event handler.

* fast/events/extrazoom/change-focus-during-change-event-expected.txt: Added.
* fast/events/extrazoom/change-focus-during-change-event.html: Added.
* resources/ui-helper.js:
(window.UIHelper.waitForKeyboardToHide.return.new.Promise):
(window.UIHelper.waitForKeyboardToHide):

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

LayoutTests/ChangeLog
LayoutTests/fast/events/extrazoom/change-focus-during-change-event-expected.txt [new file with mode: 0644]
LayoutTests/fast/events/extrazoom/change-focus-during-change-event.html [new file with mode: 0644]
LayoutTests/resources/ui-helper.js
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/Cocoa/WKUIDelegatePrivate.h
Source/WebKit/UIProcess/WebProcessProxy.cpp
Source/WebKit/UIProcess/WebProcessProxy.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Tools/ChangeLog
Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm

index 8e65ed8..b32e485 100644 (file)
@@ -1,5 +1,27 @@
 2018-04-17  Wenson Hsieh  <wenson_hsieh@apple.com>
 
+        [Extra zoom mode] Programmatically changing focus when an element already has focus is a confusing experience
+        https://bugs.webkit.org/show_bug.cgi?id=184635
+        <rdar://problem/39440642>
+
+        Reviewed by Tim Horton.
+
+        Add a new layout test to exercise the following sequence of events in extra zoom mode:
+
+        1. Focus select element #1.
+        2. Choose an unselected option.
+        3. Programmatically focus select element #2 in the "change" event handler.
+        4. Choose an unselected option.
+        5. Programmatically blur select element #2 in the "change" event handler.
+
+        * fast/events/extrazoom/change-focus-during-change-event-expected.txt: Added.
+        * fast/events/extrazoom/change-focus-during-change-event.html: Added.
+        * resources/ui-helper.js:
+        (window.UIHelper.waitForKeyboardToHide.return.new.Promise):
+        (window.UIHelper.waitForKeyboardToHide):
+
+2018-04-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
         [Extra zoom mode] Double tap to zoom should account for text legibility in extra zoom mode
         https://bugs.webkit.org/show_bug.cgi?id=184631
         <rdar://problem/39303706>
diff --git a/LayoutTests/fast/events/extrazoom/change-focus-during-change-event-expected.txt b/LayoutTests/fast/events/extrazoom/change-focus-during-change-event-expected.txt
new file mode 100644 (file)
index 0000000..6b75009
--- /dev/null
@@ -0,0 +1,9 @@
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+Choosing second option in top select...
+Option two should be selected: true
+Choosing second option in bottom select...
+Option four should be selected: true
+
diff --git a/LayoutTests/fast/events/extrazoom/change-focus-during-change-event.html b/LayoutTests/fast/events/extrazoom/change-focus-during-change-event.html
new file mode 100644 (file)
index 0000000..8eb820f
--- /dev/null
@@ -0,0 +1,60 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<meta name="viewport" content="width=device-width">
+<head>
+    <script src="../../../resources/js-test.js"></script>
+    <script src="../../../resources/ui-helper.js"></script>
+    <style>
+        body, html {
+            margin: 0;
+            width: 100%;
+            height: 100%;
+            position: absolute;
+        }
+        select {
+            width: 100%;
+            height: 80px;
+            display: block;
+        }
+    </style>
+    <script>
+        jsTestIsAsync = true;
+
+        function selectOptionAtIndexAndWaitForKeyboardToHide(index) {
+            return new Promise(resolve => {
+                UIHelper.waitForKeyboardToHide().then(resolve);
+                UIHelper.selectFormAccessoryPickerRow(index);
+            });
+        }
+
+        async function runTest() {
+            // First, tap on the top select and choose the second option. Changing the value of the select causes the
+            // bottom select to be focused.
+            await UIHelper.activateAndWaitForInputSessionAt(50, 40);
+            output.innerHTML += "Choosing second option in top select...<br>";
+            await selectOptionAtIndexAndWaitForKeyboardToHide(1);
+            output.innerHTML += `Option two should be selected: ${two.selected}<br>`;
+
+            // Now tap on the bottom select and choose the second option. Changing the value of this select causes the
+            // select to blur, dismissing the input view.
+            await UIHelper.activateAndWaitForInputSessionAt(50, 120);
+            output.innerHTML += "Choosing second option in bottom select...<br>";
+            await selectOptionAtIndexAndWaitForKeyboardToHide(1);
+            output.innerHTML += `Option four should be selected: ${four.selected}<br>`;
+
+            finishJSTest();
+        }
+    </script>
+</head>
+<body onload="runTest()">
+<select id="top" onchange="document.getElementById('bottom').focus()">
+    <option selected>One</option>
+    <option id="two">Two</option>
+</select>
+<select id="bottom" onchange="this.blur()">
+    <option selected>Three</option>
+    <option id="four">Four</option>
+</select>
+<pre id="output"></pre>
+</body>
+</html>
index 22adaeb..099df83 100644 (file)
@@ -100,6 +100,16 @@ window.UIHelper = class UIHelper {
         });
     }
 
+    static waitForKeyboardToHide()
+    {
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                (function() {
+                    uiController.didHideKeyboardCallback = () => uiController.uiScriptComplete();
+                })()`, resolve);
+        });
+    }
+
     static getUICaretRect()
     {
         if (!this.isWebKit2() || !this.isIOS())
index 7b3e2ce..bc8ada3 100644 (file)
@@ -1,3 +1,64 @@
+2018-04-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Extra zoom mode] Programmatically changing focus when an element already has focus is a confusing experience
+        https://bugs.webkit.org/show_bug.cgi?id=184635
+        <rdar://problem/39440642>
+
+        Reviewed by Tim Horton.
+
+        Currently on iOS, we allow element focus to present UI if the keyboard is already shown. In extra zoom mode,
+        this would lead to a confusing experience when the focus form control overlay is disabled, since fullscreen
+        input view controllers are swapped out from underneath the user. Currently, this also puts the UI process into a
+        bad state where the focused form control overlay is active, but still hidden. This patch makes some tweaks to
+        input view controller handling in the UI process to address these issues, and also adds WebKitTestRunner support
+        for simulating interactions with select menus in extra zoom mode. See comments below for more detail.
+
+        Test: fast/events/extrazoom/change-focus-during-change-event.html
+
+        * UIProcess/API/Cocoa/WKUIDelegatePrivate.h:
+
+        Add new SPI delegate hooks to notify the UI delegate when view controllers are presented or dismissed in extra
+        zoom mode. See -presentViewControllerForCurrentAssistedNode and -dismissAllInputViewControllers.
+
+        * UIProcess/WebProcessProxy.cpp:
+        (WebKit::WebProcessProxy::takeBackgroundActivityTokenForFullscreenInput):
+        (WebKit::WebProcessProxy::releaseBackgroundActivityTokenForFullscreenInput):
+
+        See the comment below -dismissAllInputViewControllers.
+
+        * UIProcess/WebProcessProxy.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView _startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:]):
+
+        In extra zoom mode, when changing focus from one assisted node to another, only allow the second node to be
+        assisted if the focused form control overlay is being shown. Otherwise, (i.e. when a fullscreen input view
+        controller is being presented), don't allow focus to start an input session.
+
+        Additionally, make a minor tweak to allow the previous node to blur, even if we are not showing the keyboard for
+        the new focused element. Without this adjustment, in the case where the page has programmatically focused
+        another element while a fullscreen input view controller is presented, we'll show the old view controller for
+        the new focused element.
+
+        (-[WKContentView presentViewControllerForCurrentAssistedNode]):
+        (-[WKContentView dismissAllInputViewControllers:]):
+
+        Currently, when a fullscreen input view controller is presented, the web process gets backgrounded. This
+        prevents event handlers from executing, which leads to strange behaviors in many cases (for instance: if we
+        have a multiple select, and the "change" event handler blurs the select, the user may check or uncheck multiple
+        items, but only the first change will actually take effect).
+
+        To fix this, we maintain a background activity token while presenting an input view controller.
+
+        (-[WKContentView focusedFormControlViewDidBeginEditing:]):
+
+        Start hiding the focused form overlay when re-presenting an input view controller. This allows us to bail from
+        showing fullscreen input UI for another focused element if focus programmatically changes while the current
+        fullscreen input view controller is presented, due to the -isHidden check in -_startAssistingNode:.
+
+        (-[WKContentView selectFormAccessoryPickerRow:]):
+
+        Simulate tapping a given row in select menu UI in extra zoom mode.
+
 2018-04-17  Conrad Shultz  <conrad_shultz@apple.com>
 
         WebKit::DisplayLink maintains a strong reference to WebPageProxy, creating a reference cycle
index d12aa20..f7b5efd 100644 (file)
@@ -153,6 +153,8 @@ struct UIEdgeInsets;
 - (NSInteger)_webView:(WKWebView *)webView dataOwnerForDragSession:(id <UIDragSession>)session WK_API_AVAILABLE(ios(11.0));
 #endif
 - (void)_webView:(WKWebView *)webView didChangeSafeAreaShouldAffectObscuredInsets:(BOOL)safeAreaShouldAffectObscuredInsets WK_API_AVAILABLE(ios(11.0));
+- (void)_webView:(WKWebView *)webView didPresentFocusedElementViewController:(UIViewController *)controller WK_API_AVAILABLE(ios(WK_IOS_TBA));
+- (void)_webView:(WKWebView *)webView didDismissFocusedElementViewController:(UIViewController *)controller WK_API_AVAILABLE(ios(WK_IOS_TBA));
 #else // TARGET_OS_IPHONE
 - (void)_prepareForImmediateActionAnimationForWebView:(WKWebView *)webView WK_API_AVAILABLE(macosx(10.13.4));
 - (void)_cancelImmediateActionAnimationForWebView:(WKWebView *)webView WK_API_AVAILABLE(macosx(10.13.4));
index bf5fe4a..9a01d5b 100644 (file)
@@ -1407,4 +1407,26 @@ void WebProcessProxy::didCheckProcessLocalPortForActivity(uint64_t callbackIdent
     callback(isLocallyReachable ? MessagePortChannelProvider::HasActivity::Yes : MessagePortChannelProvider::HasActivity::No);
 }
 
+#if ENABLE(EXTRA_ZOOM_MODE)
+
+void WebProcessProxy::takeBackgroundActivityTokenForFullscreenInput()
+{
+    if (m_backgroundActivityTokenForFullscreenFormControls)
+        return;
+
+    m_backgroundActivityTokenForFullscreenFormControls = m_throttler.backgroundActivityToken();
+    RELEASE_LOG(ProcessSuspension, "UIProcess is taking a background assertion because it is presenting fullscreen UI for form controls.");
+}
+
+void WebProcessProxy::releaseBackgroundActivityTokenForFullscreenInput()
+{
+    if (!m_backgroundActivityTokenForFullscreenFormControls)
+        return;
+
+    m_backgroundActivityTokenForFullscreenFormControls = nullptr;
+    RELEASE_LOG(ProcessSuspension, "UIProcess is releasing a background assertion because it has dismissed fullscreen UI for form controls.");
+}
+
+#endif
+
 } // namespace WebKit
index c6770ea..607b9b2 100644 (file)
@@ -212,6 +212,11 @@ public:
     void suspendWebPageProxy(WebPageProxy&);
     void suspendedPageWasDestroyed(SuspendedPageProxy&);
 
+#if ENABLE(EXTRA_ZOOM_MODE)
+    void takeBackgroundActivityTokenForFullscreenInput();
+    void releaseBackgroundActivityTokenForFullscreenInput();
+#endif
+
 protected:
     static uint64_t generatePageID();
     WebProcessProxy(WebProcessPool&, WebsiteDataStore&);
@@ -340,6 +345,10 @@ private:
     HashMap<uint64_t, CompletionHandler<void(WebCore::MessagePortChannelProvider::HasActivity)>> m_localPortActivityCompletionHandlers;
 
     bool m_hasCommittedAnyProvisionalLoads { false };
+
+#if ENABLE(EXTRA_ZOOM_MODE)
+    ProcessThrottler::BackgroundActivityToken m_backgroundActivityTokenForFullscreenFormControls;
+#endif
 };
 
 } // namespace WebKit
index dd7b643..0200563 100644 (file)
@@ -4011,18 +4011,26 @@ static bool isAssistableInputType(InputType type)
     if ([inputDelegate respondsToSelector:@selector(_webView:focusShouldStartInputSession:)])
         shouldShowKeyboard = [inputDelegate _webView:_webView focusShouldStartInputSession:focusedElementInfo.get()];
     else {
-        // The default behavior is to allow node assistance if the user is interacting or the keyboard is already active.
-        shouldShowKeyboard = userIsInteracting || _isChangingFocus || changingActivityState;
-#if ENABLE(DATA_INTERACTION)
-        shouldShowKeyboard |= _dragDropInteractionState.isPerformingDrop();
+        // The default behavior is to allow node assistance if the user is interacting.
+        // We also allow node assistance if the keyboard already is showing, unless we're in extra zoom mode.
+        shouldShowKeyboard = userIsInteracting
+#if ENABLE(EXTRA_ZOOM_MODE)
+            || (_isChangingFocus && ![_focusedFormControlView isHidden])
+#else
+            || _isChangingFocus
 #endif
+#if ENABLE(DRAG_SUPPORT)
+            || _dragDropInteractionState.isPerformingDrop()
+#endif
+            || changingActivityState;
     }
-    if (!shouldShowKeyboard)
-        return;
 
     if (blurPreviousNode)
         [self _stopAssistingNode];
 
+    if (!shouldShowKeyboard)
+        return;
+
     if (!isAssistableInputType(information.elementType))
         return;
 
@@ -4216,6 +4224,16 @@ static bool isAssistableInputType(InputType type)
         [_inputNavigationViewControllerForFullScreenInputs pushViewController:_presentedFullScreenInputViewController.get() animated:YES];
     else
         [presentingViewController presentViewController:_presentedFullScreenInputViewController.get() animated:YES completion:nil];
+
+    // Presenting a fullscreen input view controller fully obscures the web view. Without taking this token, the web content process will get backgrounded.
+    _page->process().takeBackgroundActivityTokenForFullscreenInput();
+
+    [presentingViewController.transitionCoordinator animateAlongsideTransition:nil completion:[weakWebView = WeakObjCPtr<WKWebView>(_webView), controller = _presentedFullScreenInputViewController] (id <UIViewControllerTransitionCoordinatorContext>) {
+        auto strongWebView = weakWebView.get();
+        id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([strongWebView UIDelegate]);
+        if ([uiDelegate respondsToSelector:@selector(_webView:didPresentFocusedElementViewController:)])
+            [uiDelegate _webView:strongWebView.get() didPresentFocusedElementViewController:controller.get()];
+    }];
 }
 
 - (void)dismissAllInputViewControllers:(BOOL)animated
@@ -4223,16 +4241,28 @@ static bool isAssistableInputType(InputType type)
     auto navigationController = WTFMove(_inputNavigationViewControllerForFullScreenInputs);
     auto presentedController = WTFMove(_presentedFullScreenInputViewController);
 
+    if (!presentedController)
+        return;
+
     if ([navigationController viewControllers].lastObject == presentedController.get())
         [navigationController popViewControllerAnimated:animated];
     else
         [presentedController dismissViewControllerAnimated:animated completion:nil];
 
+    [[presentedController transitionCoordinator] animateAlongsideTransition:nil completion:[weakWebView = WeakObjCPtr<WKWebView>(_webView), controller = presentedController] (id <UIViewControllerTransitionCoordinatorContext>) {
+        auto strongWebView = weakWebView.get();
+        id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([strongWebView UIDelegate]);
+        if ([uiDelegate respondsToSelector:@selector(_webView:didDismissFocusedElementViewController:)])
+            [uiDelegate _webView:strongWebView.get() didDismissFocusedElementViewController:controller.get()];
+    }];
+
     if (_shouldRestoreFirstResponderStatusAfterLosingFocus) {
         _shouldRestoreFirstResponderStatusAfterLosingFocus = NO;
         if (!self.isFirstResponder)
             [self becomeFirstResponder];
     }
+
+    _page->process().releaseBackgroundActivityTokenForFullscreenInput();
 }
 
 - (void)focusedFormControlViewDidSubmit:(WKFocusedFormControlView *)view
@@ -4249,8 +4279,12 @@ static bool isAssistableInputType(InputType type)
 - (void)focusedFormControlViewDidBeginEditing:(WKFocusedFormControlView *)view
 {
     [self updateCurrentAssistedNodeInformation:[weakSelf = WeakObjCPtr<WKContentView>(self)] (bool didUpdate) {
-        if (didUpdate)
-            [weakSelf presentViewControllerForCurrentAssistedNode];
+        if (!didUpdate)
+            return;
+
+        auto strongSelf = weakSelf.get();
+        [strongSelf presentViewControllerForCurrentAssistedNode];
+        [strongSelf->_focusedFormControlView hide:YES];
     }];
 }
 
@@ -5401,8 +5435,13 @@ static NSArray<UIItemProvider *> *extractItemProvidersFromDropSession(id <UIDrop
 
 - (void)selectFormAccessoryPickerRow:(NSInteger)rowIndex
 {
+#if ENABLE(EXTRA_ZOOM_MODE)
+    if ([_presentedFullScreenInputViewController isKindOfClass:[WKSelectMenuListViewController class]])
+        [(WKSelectMenuListViewController *)_presentedFullScreenInputViewController.get() selectItemAtIndex:rowIndex];
+#else
     if ([_inputPeripheral isKindOfClass:[WKFormSelectControl self]])
         [(WKFormSelectControl *)_inputPeripheral selectRow:rowIndex inComponent:0 extendingSelection:NO];
+#endif
 }
 
 - (NSString *)selectFormPopoverTitle
index 4670ac4..ae08f3b 100644 (file)
@@ -1,3 +1,24 @@
+2018-04-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Extra zoom mode] Programmatically changing focus when an element already has focus is a confusing experience
+        https://bugs.webkit.org/show_bug.cgi?id=184635
+        <rdar://problem/39440642>
+
+        Reviewed by Tim Horton.
+
+        Add plumbing to support invoking `didHideKeyboardCallback` and `didShowKeyboardCallback` when (respectively)
+        dismissing or presenting fullscreen input view controllers in extra zoom mode.
+
+        * WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:
+        (-[TestRunnerWKWebView initWithFrame:configuration:]):
+        (-[TestRunnerWKWebView dealloc]):
+        (-[TestRunnerWKWebView _invokeShowKeyboardCallbackIfNecessary]):
+        (-[TestRunnerWKWebView _invokeHideKeyboardCallbackIfNecessary]):
+        (-[TestRunnerWKWebView _keyboardDidShow:]):
+        (-[TestRunnerWKWebView _keyboardDidHide:]):
+        (-[TestRunnerWKWebView _webView:didPresentFocusedElementViewController:]):
+        (-[TestRunnerWKWebView _webView:didDismissFocusedElementViewController:]):
+
 2018-04-17  Michael Catanzaro  <mcatanzaro@igalia.com>
 
         [GTK] Webkit should spoof as Safari on a Mac for Outlook.com
index c6cd4ed..7ff7fd5 100644 (file)
@@ -27,6 +27,7 @@
 #import "TestRunnerWKWebView.h"
 
 #import "WebKitTestRunnerDraggingInfo.h"
+#import <WebKit/WKUIDelegatePrivate.h>
 #import <wtf/Assertions.h>
 #import <wtf/RetainPtr.h>
 
 
 #if WK_API_ENABLED
 
-@interface TestRunnerWKWebView () {
+@interface TestRunnerWKWebView () <WKUIDelegatePrivate> {
     RetainPtr<NSNumber *> m_stableStateOverride;
 }
 
 @property (nonatomic, copy) void (^zoomToScaleCompletionHandler)(void);
-@property (nonatomic, copy) void (^showKeyboardCompletionHandler)(void);
 @property (nonatomic, copy) void (^retrieveSpeakSelectionContentCompletionHandler)(void);
 @property (nonatomic) BOOL isShowingKeyboard;
 
 {
     if (self = [super initWithFrame:frame configuration:configuration]) {
         NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
-        [center addObserver:self selector:@selector(_keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
-        [center addObserver:self selector:@selector(_keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
+        [center addObserver:self selector:@selector(_invokeShowKeyboardCallbackIfNecessary) name:UIKeyboardDidShowNotification object:nil];
+        [center addObserver:self selector:@selector(_invokeHideKeyboardCallbackIfNecessary) name:UIKeyboardDidHideNotification object:nil];
+
+        self.UIDelegate = self;
     }
     return self;
 }
@@ -94,7 +96,6 @@
     self.rotationDidEndCallback = nil;
 
     self.zoomToScaleCompletionHandler = nil;
-    self.showKeyboardCompletionHandler = nil;
     self.retrieveSpeakSelectionContentCompletionHandler = nil;
 
     [super dealloc];
     [self.scrollView setZoomScale:scale animated:animated];
 }
 
-- (void)_keyboardDidShow:(NSNotification *)notification
+- (void)_invokeShowKeyboardCallbackIfNecessary
 {
     if (self.isShowingKeyboard)
         return;
         self.didShowKeyboardCallback();
 }
 
-- (void)_keyboardDidHide:(NSNotification *)notification
+- (void)_invokeHideKeyboardCallbackIfNecessary
 {
     if (!self.isShowingKeyboard)
         return;
     return _overrideSafeAreaInsets;
 }
 
-#endif
+#pragma mark - WKUIDelegatePrivate
+
+// In extra zoom mode, fullscreen form control UI takes on the same role as keyboards and input view controllers
+// in UIKit. As such, we allow keyboard presentation and dismissal callbacks to work in extra zoom mode as well.
+- (void)_webView:(WKWebView *)webView didPresentFocusedElementViewController:(UIViewController *)controller
+{
+    [self _invokeShowKeyboardCallbackIfNecessary];
+}
+
+- (void)_webView:(WKWebView *)webView didDismissFocusedElementViewController:(UIViewController *)controller
+{
+    [self _invokeHideKeyboardCallbackIfNecessary];
+}
+
+#endif // PLATFORM(IOS)
 
 @end