⚠ Archived content — this site is no longer maintained.   Current WebKit documentation is at docs.webkit.org.

Changeset 295299 in webkit


Ignore:
Timestamp:
Jun 6, 2022, 3:08:33 PM (4 years ago)
Author:
Wenson Hsieh
Message:

[iOS] Keyboard obscures file upload context menu when trying to attach a file in gmail.com
​https://bugs.webkit.org/show_bug.cgi?id=241320
rdar://84667910

Reviewed by Tim Horton.

When composing a message on gmail.com, if a user taps the "Attach file" button in the bottom
toolbar, Gmail's script programmatically clicks a hidden file input and then immediately focuses the
editable body field. The context menu presents at the interaction location near the bottom of the
viewport; however, if no hardware keyboard is attached, the software keyboard appears right
afterwards, obscuring the context menu completely.

To address this, teach WKFileUploadPanel to reposition its context menu when the keyboard is
shown, if it might overlap with the bounds of the input view.

  • LayoutTests/fast/forms/ios/show-file-upload-context-menu-above-keyboard-expected.txt: Added.
  • LayoutTests/fast/forms/ios/show-file-upload-context-menu-above-keyboard.html: Added.

Add a layout test to verify that focusing a text field with the software keyboard while presenting a
file picker doesn't result in the file picker's context menu being obscured behind the keyboard.

  • LayoutTests/resources/ui-helper.js:

(window.UIHelper.dismissMenu):
(window.UIHelper.contextMenuRect):

Add a couple of new UIHelper methods.

  • Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm:

(-[WKContentView _keyboardDidShow]):

If the keyboard appears while the file upload panel is active, reposition the context menu if it
overlaps with the new keyboard bounds.

(-[WKContentView _zoomToFocusRectAfterShowingKeyboardIfNeeded]):

Factor this existing logic out into a helper method.

  • Source/WebKit/UIProcess/ios/forms/WKFileUploadPanel.h:
  • Source/WebKit/UIProcess/ios/forms/WKFileUploadPanel.mm:

(-[WKFileUploadPanel contextMenuInteraction:willEndForConfiguration:animator:]):

If the _isRepositioningContextMenu flag is set, avoid removing and destroying the context menu
interaction.

(-[WKFileUploadPanel ensureContextMenuInteraction]):

Drive-by fix: adjust -ensureContextMenuInteraction so that it returns the either the existing or
newly created context menu interaction.

(-[WKFileUploadPanel repositionContextMenuIfNeeded]):

Add a helper method to reposition the context menu by removing and re-presenting the context menu
interaction without animation, only if it overlaps the input view bounds. While removing and
presenting the context menu again, set a _isRepositioningContextMenu flag, such that

(-[WKFileUploadPanel showDocumentPickerMenu]):

  • Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
  • Tools/TestRunnerShared/UIScriptContext/UIScriptController.h:

(WTR::UIScriptController::contextMenuRect const):

  • Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm:

(-[TestRunnerWKWebView _dismissAllContextMenuInteractions]):

Adjust this helper to immediately cancel all context menu interactions without animation, so that
context menu removal delegate methods don't bleed into subsequent tests.

  • Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h:
  • Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm:

(WTR::UIScriptControllerIOS::menuRect const):

Use the new toObject helper method.

(WTR::UIScriptControllerIOS::contextMenuRect const):

Add support for a new script controller method to return the context menu container view's bounds
in window coordinates. Note that I'm using window coordinates here as opposed to root view
coordinates, since the new test that uses this method needs to compare the context menu's position
against the input view bounds in window coordinates.

(WTR::UIScriptControllerIOS::toObject const):

Add a helper method to convert the given CGRect into a JavaScript object reference.

Canonical link: ​https://commits.webkit.org/251344@main

Location:
trunk
Files:
2 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/resources/ui-helper.js

    r292812 r295299  
    779779    }
    780780
     781    static dismissMenu()
     782    {
     783        if (!this.isWebKit2())
     784            return Promise.resolve();
     785
     786        return new Promise(resolve => {
     787            testRunner.runUIScript("uiController.dismissMenu()", resolve);
     788        });
     789    }
     790
    781791    static waitForKeyboardToHide()
    782792    {
    … …  
    13531363        return new Promise(resolve => {
    13541364            testRunner.runUIScript("JSON.stringify(uiController.menuRect)", result => resolve(JSON.parse(result)));
     1365        });
     1366    }
     1367
     1368    static contextMenuRect()
     1369    {
     1370        return new Promise(resolve => {
     1371            testRunner.runUIScript("JSON.stringify(uiController.contextMenuRect)", result => resolve(JSON.parse(result)));
    13551372        });
    13561373    }
  • trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm

    r295289 r295299  
    23782378- (void)_keyboardDidShow
    23792379{
     2380    [self _zoomToFocusRectAfterShowingKeyboardIfNeeded];
     2381
     2382#if USE(UICONTEXTMENU)
     2383    [_fileUploadPanel repositionContextMenuIfNeeded];
     2384#endif
     2385}
     2386
     2387- (void)_zoomToFocusRectAfterShowingKeyboardIfNeeded
     2388{
    23802389    if (!_shouldZoomToFocusRectAfterShowingKeyboard)
    23812390        return;
  • trunk/Source/WebKit/UIProcess/ios/forms/WKFileUploadPanel.h

    r284558 r295299  
    4545- (void)dismiss;
    4646
     47#if USE(UICONTEXTMENU)
     48- (void)repositionContextMenuIfNeeded;
     49#endif
     50
    4751- (NSArray<NSString *> *)currentAvailableActionTitles;
    4852- (NSArray<NSString *> *)acceptedTypeIdentifiers;
  • trunk/Source/WebKit/UIProcess/ios/forms/WKFileUploadPanel.mm

    r294882 r295299  
    4040#import "WKStringCF.h"
    4141#import "WKURLCF.h"
     42#import "WKWebViewInternal.h"
    4243#import "WebIconUtilities.h"
    4344#import "WebOpenPanelResultListenerProxy.h"
    … …  
    5051#import <wtf/OptionSet.h>
    5152#import <wtf/RetainPtr.h>
     53#import <wtf/SetForScope.h>
    5254#import <wtf/WeakObjCPtr.h>
    5355#import <wtf/text/StringView.h>
    … …  
    357359    ALLOW_DEPRECATED_DECLARATIONS_END
    358360#if USE(UICONTEXTMENU)
     361    BOOL _isRepositioningContextMenu;
    359362    RetainPtr<UIContextMenuInteraction> _documentContextMenuInteraction;
    360363#endif
    … …  
    683686- (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willEndForConfiguration:(UIContextMenuConfiguration *)configuration animator:(id<UIContextMenuInteractionAnimating>)animator
    684687{
     688    if (_isRepositioningContextMenu)
     689        return;
     690
    685691    [animator addCompletion:^{
    686692        [self removeContextMenuInteraction];
    … …  
    699705}
    700706
    701 - (void)ensureContextMenuInteraction
     707- (UIContextMenuInteraction *)ensureContextMenuInteraction
    702708{
    703709    if (!_documentContextMenuInteraction) {
    … …  
    706712        self->_isPresentingSubMenu = NO;
    707713    }
    708 }
    709 
    710 #endif
     714    return _documentContextMenuInteraction.get();
     715}
     716
     717- (void)repositionContextMenuIfNeeded
     718{
     719    if (!_documentContextMenuInteraction)
     720        return;
     721
     722    auto *webView = [_view webView];
     723    if (!webView)
     724        return;
     725
     726    auto inputViewBoundsInWindow = webView->_inputViewBoundsInWindow;
     727    if (CGRectIsEmpty(inputViewBoundsInWindow))
     728        return;
     729
     730    // The exact bounds of the context menu container itself isn't exposed through any UIKit API or SPI,
     731    // and would require traversing the view hierarchy in search of internal UIKit views. For now, just
     732    // reposition the context menu if its presentation location is covered by the input view.
     733    if (!CGRectContainsPoint(inputViewBoundsInWindow, [_view convertPoint:_interactionPoint toView:webView.window]))
     734        return;
     735
     736    SetForScope repositioningContextMenuScope { _isRepositioningContextMenu, YES };
     737    [UIView performWithoutAnimation:^{
     738        [_documentContextMenuInteraction dismissMenu];
     739        [_view presentContextMenu:_documentContextMenuInteraction.get() atLocation:_interactionPoint];
     740    }];
     741}
     742
     743#endif // USE(UICONTEXTMENU)
    711744
    712745- (void)showFilePickerMenu
    … …  
    728761    // FIXME 49961589: Support picking media with UIImagePickerController
    729762#if HAVE(UICONTEXTMENU_LOCATION)
    730     if (_allowedImagePickerTypes.containsAny({ WKFileUploadPanelImagePickerType::Image, WKFileUploadPanelImagePickerType::Video })) {
    731         [self ensureContextMenuInteraction];
    732         [_view presentContextMenu:_documentContextMenuInteraction.get() atLocation:_interactionPoint];
    733     } else // Image and Video types are not accepted so bypass the menu and open the file picker directly.
     763    if (_allowedImagePickerTypes.containsAny({ WKFileUploadPanelImagePickerType::Image, WKFileUploadPanelImagePickerType::Video }))
     764        [_view presentContextMenu:self.ensureContextMenuInteraction atLocation:_interactionPoint];
     765    else // Image and Video types are not accepted so bypass the menu and open the file picker directly.
    734766#endif
    735767        [self showFilePickerMenu];
  • trunk/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl

    r290098 r295299  
    267267    readonly attribute boolean isShowingMenu;
    268268    readonly attribute object menuRect;
     269    readonly attribute object contextMenuRect;
    269270    object rectForMenuAction(DOMString action);
    270271    undefined chooseMenuAction(DOMString action, object callback);
  • trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h

    r290098 r295299  
    286286    virtual JSObjectRef rectForMenuAction(JSStringRef) const { notImplemented(); return nullptr; }
    287287    virtual JSObjectRef menuRect() const { notImplemented(); return nullptr; }
     288    virtual JSObjectRef contextMenuRect() const { notImplemented(); return nullptr; }
    288289    virtual bool isShowingContextMenu() const { notImplemented(); return false; }
    289290
  • trunk/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.mm

    r293117 r295299  
    262262- (void)_dismissAllContextMenuInteractions
    263263{
    264 #if PLATFORM(IOS)
     264#if USE(UICONTEXTMENU)
    265265    for (id <UIInteraction> interaction in self.contentView.interactions) {
    266         if ([interaction isKindOfClass:UIContextMenuInteraction.class])
    267             [(UIContextMenuInteraction *)interaction dismissMenu];
     266        if (auto contextMenuInteraction = dynamic_objc_cast<UIContextMenuInteraction>(interaction)) {
     267            [UIView performWithoutAnimation:^{
     268                [contextMenuInteraction dismissMenu];
     269            }];
     270        }
    268271    }
    269272#endif
  • trunk/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h

    r288178 r295299  
    3030#import "UIScriptControllerCocoa.h"
    3131#import <wtf/BlockPtr.h>
     32
     33typedef struct CGRect CGRect;
    3234
    3335namespace WebCore {
    … …  
    124126    bool isShowingPopover() const override;
    125127    JSObjectRef rectForMenuAction(JSStringRef) const override;
     128    JSObjectRef contextMenuRect() const override;
    126129    JSObjectRef menuRect() const override;
    127130    bool isDismissingMenu() const override;
    … …  
    171174    void singleTapAtPointWithModifiers(WebCore::FloatPoint location, Vector<String>&& modifierFlags, BlockPtr<void()>&&);
    172175
     176    JSObjectRef toObject(CGRect) const;
     177
    173178    bool isWebContentFirstResponder() const override;
    174179};
  • trunk/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm

    r288178 r295299  
    11061106        return nullptr;
    11071107
    1108     CGRect rectInRootViewCoordinates = [calloutBar convertRect:calloutBar.bounds toView:platformContentView()];
    1109     return m_context->objectFromRect(WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height));
     1108    return toObject([calloutBar convertRect:calloutBar.bounds toView:platformContentView()]);
     1109}
     1110
     1111JSObjectRef UIScriptControllerIOS::contextMenuRect() const
     1112{
     1113    auto *window = webView().window;
     1114    auto *contextMenuView = [findAllViewsInHierarchyOfType(window, NSClassFromString(@"_UIContextMenuView")) firstObject];
     1115    if (!contextMenuView)
     1116        return nullptr;
     1117
     1118    return toObject([contextMenuView convertRect:contextMenuView.bounds toView:nil]);
     1119}
     1120
     1121JSObjectRef UIScriptControllerIOS::toObject(CGRect rect) const
     1122{
     1123    return m_context->objectFromRect(WebCore::FloatRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height));
    11101124}
    11111125
Note: See TracChangeset for help on using the changeset viewer.